namespace WoofWare.PawPrint
open System
open System.Collections.Generic
open System.Collections.Immutable
open System.IO
open System.Reflection
open System.Reflection.Metadata
open System.Reflection.Metadata.Ecma335
open System.Reflection.PortableExecutable
open Microsoft.Extensions.Logging
open Microsoft.FSharp.Core
///
/// Represents a .NET assembly definition.
/// This is a strongly-typed representation of AssemblyDefinition from System.Reflection.Metadata.
///
type AssemblyDefinition =
{
///
/// The fully specified name of the assembly, including name, version, culture, and public key token.
///
Name : AssemblyName
}
[]
module AssemblyDefinition =
let make (assy : System.Reflection.Metadata.AssemblyDefinition) : AssemblyDefinition =
{
Name = assy.GetAssemblyName ()
}
///
/// Represents a fully parsed .NET assembly with all its metadata components.
/// This serves as the main container for accessing assembly information in the PawPrint library.
///
type DumpedAssembly =
{
OriginalPath : string option
/// Logger for recording information about this assembly.
Logger : ILogger
///
/// Dictionary of all type definitions in this assembly, keyed by their handle.
///
TypeDefs :
IReadOnlyDictionary>
///
/// Dictionary of all type references in this assembly, keyed by their handle.
///
TypeRefs : IReadOnlyDictionary
///
/// Dictionary of all type specifications in this assembly, keyed by their handle.
/// Type specifications represent complex types like generic instantiations.
///
TypeSpecs : IReadOnlyDictionary
///
/// Dictionary of all method definitions in this assembly, keyed by their handle.
///
Methods :
IReadOnlyDictionary<
MethodDefinitionHandle,
WoofWare.PawPrint.MethodInfo
>
///
/// Dictionary of all member references in this assembly, keyed by their handle.
///
Members : IReadOnlyDictionary>
///
/// Dictionary of all field definitions in this assembly, keyed by their handle.
///
Fields : IReadOnlyDictionary>
///
/// The entry point method of the assembly, if one exists.
///
MainMethod : MethodDefinitionHandle option
///
/// Dictionary of all method specifications in this assembly, keyed by their handle.
/// Method specifications typically represent generic method instantiations.
///
MethodSpecs : ImmutableDictionary
///
/// Function to resolve string tokens to their actual string values.
///
Strings : StringToken -> string
///
/// Dictionary of all assembly references in this assembly, keyed by their handle.
///
AssemblyReferences : ImmutableDictionary
///
/// Information about this assembly.
///
ThisAssemblyDefinition : AssemblyDefinition
///
/// The root namespace of this assembly.
///
RootNamespace : Namespace
///
/// Dictionary of all non-root namespaces in this assembly, keyed by their name components.
///
NonRootNamespaces : ImmutableDictionary
///
/// The PE reader for the underlying assembly file.
/// TODO: work out how to render all the strings up front, then drop this.
///
PeReader : PEReader
///
/// Dictionary of all custom attributes in this assembly, keyed by their handle.
///
Attributes : ImmutableDictionary
///
/// Dictionary of all exported types in this assembly, keyed by their handle.
///
ExportedTypes : ImmutableDictionary
///
/// Internal lookup for exported types by namespace and name.
///
_ExportedTypesLookup : ImmutableDictionary
///
/// Internal lookup for type references by namespace and name.
///
_TypeRefsLookup : ImmutableDictionary
///
/// Internal lookup for type definitions by namespace and name.
///
_TypeDefsLookup :
ImmutableDictionary>
}
static member internal BuildExportedTypesLookup
(logger : ILogger)
(name : AssemblyName)
(types : WoofWare.PawPrint.ExportedType seq)
: ImmutableDictionary
=
let result = ImmutableDictionary.CreateBuilder ()
let keys = HashSet ()
for ty in types do
let key = ty.Namespace, ty.Name
if keys.Add key then
result.Add (key, ty)
else
logger.LogDebug (
"Duplicate types exported from assembly {ThisAssemblyName}: namespace {DuplicatedTypeNamespace}, type {DuplicatedTypeName}. Ignoring the duplicate.",
name,
ty.Namespace,
ty.Name
)
result.Remove key |> ignore
result.ToImmutable ()
static member internal BuildTypeRefsLookup
(logger : ILogger)
(name : AssemblyName)
(typeRefs : WoofWare.PawPrint.TypeRef seq)
=
let result = ImmutableDictionary.CreateBuilder ()
let keys = HashSet ()
for ty in typeRefs do
let key = (ty.Namespace, ty.Name)
if keys.Add key then
result.Add (key, ty)
else
// TODO: this is all very dubious, the ResolutionScope is supposed to tell us how to disambiguate these
logger.LogDebug (
"Duplicate type refs from assembly {ThisAssemblyName}: namespace {DuplicatedTypeNamespace}, type {DuplicatedTypeName}. Ignoring the duplicate.",
name,
ty.Namespace,
ty.Name
)
result.ToImmutable ()
static member internal BuildTypeDefsLookup
(logger : ILogger)
(name : AssemblyName)
(typeDefs : WoofWare.PawPrint.TypeInfo seq)
=
let result = ImmutableDictionary.CreateBuilder ()
let keys = HashSet ()
for ty in typeDefs do
let key = (ty.Namespace, ty.Name)
if keys.Add key then
result.Add (key, ty)
else
// TODO: this is all very dubious, the ResolutionScope is supposed to tell us how to disambiguate these
logger.LogDebug (
"Duplicate type defs from assembly {ThisAssemblyName}: namespace {DuplicatedTypeNamespace}, type {DuplicatedTypeName}. Ignoring the duplicate.",
name,
ty.Namespace,
ty.Name
)
result.ToImmutable ()
member this.Name = this.ThisAssemblyDefinition.Name
member this.TypeRef (``namespace`` : string) (name : string) : WoofWare.PawPrint.TypeRef option =
match this._TypeRefsLookup.TryGetValue ((``namespace``, name)) with
| false, _ -> None
| true, v -> Some v
member this.TypeDef
(``namespace`` : string)
(name : string)
: WoofWare.PawPrint.TypeInfo option
=
match this._TypeDefsLookup.TryGetValue ((``namespace``, name)) with
| false, _ -> None
| true, v -> Some v
member this.ExportedType (``namespace`` : string option) (name : string) : WoofWare.PawPrint.ExportedType option =
match this._ExportedTypesLookup.TryGetValue ((``namespace``, name)) with
| false, _ -> None
| true, v -> Some v
interface IDisposable with
member this.Dispose () = this.PeReader.Dispose ()
type TypeResolutionResult =
| FirstLoadAssy of WoofWare.PawPrint.AssemblyReference
| Resolved of DumpedAssembly * TypeInfo
override this.ToString () : string =
match this with
| TypeResolutionResult.FirstLoadAssy a -> $"FirstLoadAssy(%s{a.Name.FullName})"
| TypeResolutionResult.Resolved (assy, ty) ->
$"Resolved(%s{assy.Name.FullName}: {string> ty})"
[]
module Assembly =
let read (loggerFactory : ILoggerFactory) (originalPath : string option) (dllBytes : Stream) : DumpedAssembly =
let peReader = new PEReader (dllBytes)
let metadataReader = peReader.GetMetadataReader ()
let assy = metadataReader.GetAssemblyDefinition () |> AssemblyDefinition.make
let entryPoint =
peReader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress
|> fun x -> if x = 0 then None else Some x
let entryPointMethod =
entryPoint |> Option.map MetadataTokens.MethodDefinitionHandle
let assemblyRefs =
let builder = ImmutableDictionary.CreateBuilder ()
for ref in metadataReader.AssemblyReferences do
builder.Add (ref, AssemblyReference.make (ref, assy.Name) (metadataReader.GetAssemblyReference ref))
builder.ToImmutable ()
let typeRefs =
let builder = ImmutableDictionary.CreateBuilder ()
for ty in metadataReader.TypeReferences do
builder.Add (ty, TypeRef.make metadataReader ty)
builder.ToImmutable ()
let typeDefs =
let builder = ImmutableDictionary.CreateBuilder ()
for ty in metadataReader.TypeDefinitions do
builder.Add (ty, TypeInfo.read loggerFactory peReader assy.Name metadataReader ty)
builder.ToImmutable ()
// TODO: this probably misses any methods out which aren't associated with a type definition?
let methods =
typeDefs
|> Seq.collect (fun (KeyValue (_, ty)) -> ty.Methods |> List.map (fun mi -> KeyValuePair (mi.Handle, mi)))
|> ImmutableDictionary.CreateRange
let methodSpecs =
Seq.init
(metadataReader.GetTableRowCount TableIndex.MethodSpec)
(fun i ->
let i = i + 1
let handle = MetadataTokens.MethodSpecificationHandle i
KeyValuePair (handle, MethodSpec.make assy.Name (metadataReader.GetMethodSpecification handle))
)
|> ImmutableDictionary.CreateRange
let typeSpecs =
let result = ImmutableDictionary.CreateBuilder ()
for i = 1 to metadataReader.GetTableRowCount TableIndex.TypeSpec do
let handle = MetadataTokens.TypeSpecificationHandle i
result.Add (handle, metadataReader.GetTypeSpecification handle |> TypeSpec.make assy.Name handle)
result.ToImmutable ()
let memberReferences =
let builder = ImmutableDictionary.CreateBuilder ()
for c in metadataReader.MemberReferences do
builder.Add (
c,
MemberReference.make
metadataReader.GetBlobReader
metadataReader.GetString
MetadataToken.ofEntityHandle
assy.Name
(metadataReader.GetMemberReference c)
)
builder.ToImmutable ()
// TODO: render all this up front
let strings (token : StringToken) =
match token with
| StringToken.String s -> metadataReader.GetString s
| StringToken.UserString s -> metadataReader.GetUserString s
let rootNamespace, nonRootNamespaces =
metadataReader.GetNamespaceDefinitionRoot ()
|> Namespace.make metadataReader.GetString metadataReader.GetNamespaceDefinition
let fields =
let result = ImmutableDictionary.CreateBuilder ()
for field in metadataReader.FieldDefinitions do
let fieldDefn =
metadataReader.GetFieldDefinition field
|> FieldInfo.make metadataReader assy.Name field
result.Add (field, fieldDefn)
result.ToImmutable ()
let exportedTypes =
let result = ImmutableDictionary.CreateBuilder ()
for ty in metadataReader.ExportedTypes do
result.Add (ty, ExportedType.make metadataReader.GetString ty (metadataReader.GetExportedType ty))
result.ToImmutable ()
let attrs =
let result = ImmutableDictionary.CreateBuilder ()
for field in metadataReader.CustomAttributes do
let fieldDefn =
metadataReader.GetCustomAttribute field |> CustomAttribute.make field
result.Add (field, fieldDefn)
result.ToImmutable ()
let logger = loggerFactory.CreateLogger assy.Name.Name
{
Logger = logger
OriginalPath = originalPath
TypeDefs = typeDefs
TypeRefs = typeRefs
TypeSpecs = typeSpecs
MainMethod = entryPointMethod
Methods = methods
MethodSpecs = methodSpecs
Members = memberReferences
Strings = strings
Fields = fields
AssemblyReferences = assemblyRefs
ThisAssemblyDefinition = assy
RootNamespace = rootNamespace
NonRootNamespaces = nonRootNamespaces
PeReader = peReader
Attributes = attrs
ExportedTypes = exportedTypes
_ExportedTypesLookup = DumpedAssembly.BuildExportedTypesLookup logger assy.Name exportedTypes.Values
_TypeRefsLookup = DumpedAssembly.BuildTypeRefsLookup logger assy.Name typeRefs.Values
_TypeDefsLookup = DumpedAssembly.BuildTypeDefsLookup logger assy.Name typeDefs.Values
}
let print (main : MethodDefinitionHandle) (dumped : DumpedAssembly) : unit =
for KeyValue (_, typ) in dumped.TypeDefs do
Console.WriteLine $"\nType: %s{typ.Namespace}.%s{typ.Name}"
for method in typ.Methods do
if method.Handle = main then
Console.WriteLine "Entry point!"
Console.WriteLine $"\nMethod: %s{method.Name}"
match method.Instructions with
| None -> Console.WriteLine ""
| Some instructions ->
instructions.Instructions
|> List.map (fun (op, index) -> IlOp.Format op index)
|> List.iter Console.WriteLine
let rec resolveTypeRef
(assemblies : ImmutableDictionary)
(referencedInAssembly : DumpedAssembly)
(target : TypeRef)
(genericArgs : ImmutableArray option)
: TypeResolutionResult
=
match target.ResolutionScope with
| TypeRefResolutionScope.Assembly r ->
let assemblyRef = referencedInAssembly.AssemblyReferences.[r]
let assemblyName = assemblyRef.Name
match assemblies.TryGetValue assemblyName.FullName with
| false, _ -> TypeResolutionResult.FirstLoadAssy assemblyRef
| true, assy ->
let nsPath = target.Namespace.Split '.' |> Array.toList
let targetNs = assy.NonRootNamespaces.[nsPath]
let targetType =
targetNs.TypeDefinitions
|> Seq.choose (fun td ->
let ty = assy.TypeDefs.[td]
if ty.Name = target.Name && ty.Namespace = target.Namespace then
Some ty
else
None
)
|> Seq.toList
match targetType with
| [ t ] ->
let t =
t
|> TypeInfo.mapGeneric (fun _ param ->
match genericArgs with
| None -> failwith "got a generic TypeRef but no generic args in context"
| Some genericArgs -> genericArgs.[param.SequenceNumber]
)
TypeResolutionResult.Resolved (assy, t)
| _ :: _ :: _ -> failwith $"Multiple matching type definitions! {nsPath} {target.Name}"
| [] ->
match assy.ExportedType (Some target.Namespace) target.Name with
| None -> failwith $"Failed to find type {nsPath} {target.Name} in {assy.Name.FullName}!"
| Some ty -> resolveTypeFromExport assy assemblies ty genericArgs
| k -> failwith $"Unexpected: {k}"
and resolveTypeFromName
(assy : DumpedAssembly)
(assemblies : ImmutableDictionary)
(ns : string option)
(name : string)
(genericArgs : ImmutableArray option)
: TypeResolutionResult
=
match ns with
| None -> failwith "what are the semantics here"
| Some ns ->
match assy.TypeDef ns name with
| Some typeDef ->
let typeDef =
typeDef
|> TypeInfo.mapGeneric (fun _ param ->
match genericArgs with
| None -> failwith $"tried to resolve generic type {ns}.{name} but no generics in scope"
| Some genericArgs -> genericArgs.[param.SequenceNumber]
)
TypeResolutionResult.Resolved (assy, typeDef)
| None ->
match assy.TypeRef ns name with
| Some typeRef -> resolveTypeRef assemblies assy typeRef genericArgs
| None ->
match assy.ExportedType (Some ns) name with
| Some export -> resolveTypeFromExport assy assemblies export genericArgs
| None -> failwith $"TODO: type resolution unimplemented for {ns} {name}"
and resolveTypeFromExport
(fromAssembly : DumpedAssembly)
(assemblies : ImmutableDictionary)
(ty : WoofWare.PawPrint.ExportedType)
(genericArgs : ImmutableArray option)
: TypeResolutionResult
=
match ty.Data with
| NonForwarded _ -> failwith "Somehow didn't find type definition but it is exported"
| ForwardsTo assy ->
let assy = fromAssembly.AssemblyReferences.[assy]
match assemblies.TryGetValue assy.Name.FullName with
| false, _ -> TypeResolutionResult.FirstLoadAssy assy
| true, toAssy -> resolveTypeFromName toAssy assemblies ty.Namespace ty.Name genericArgs
[]
module DumpedAssembly =
let resolveBaseType
(bct : BaseClassTypes)
(loadedAssemblies : ImmutableDictionary)
(source : AssemblyName)
(baseTypeInfo : BaseTypeInfo option)
: ResolvedBaseType
=
let rec go (source : AssemblyName) (baseType : BaseTypeInfo option) =
match baseType with
| Some (BaseTypeInfo.TypeRef r) ->
let assy = loadedAssemblies.[source.FullName]
// TODO: generics
match Assembly.resolveTypeRef loadedAssemblies assy assy.TypeRefs.[r] None with
| TypeResolutionResult.FirstLoadAssy _ ->
failwith
"seems pretty unlikely that we could have constructed this object without loading its base type"
| TypeResolutionResult.Resolved (assy, typeInfo) ->
match TypeInfo.isBaseType bct _.Name assy.Name typeInfo.TypeDefHandle with
| Some v -> v
| None -> go assy.Name typeInfo.BaseType
| Some (BaseTypeInfo.ForeignAssemblyType (assy, ty)) ->
let assy = loadedAssemblies.[assy.FullName]
match TypeInfo.isBaseType bct _.Name assy.Name ty with
| Some v -> v
| None ->
let ty = assy.TypeDefs.[ty]
go assy.Name ty.BaseType
| Some (BaseTypeInfo.TypeSpec _) -> failwith "TODO"
| Some (BaseTypeInfo.TypeDef h) ->
let assy = loadedAssemblies.[source.FullName]
match TypeInfo.isBaseType bct _.Name assy.Name h with
| Some v -> v
| None ->
let ty = assy.TypeDefs.[h]
go assy.Name ty.BaseType
| None -> ResolvedBaseType.Object
go source baseTypeInfo