diff --git a/.editorconfig b/.editorconfig index b1533ed3f..3c5663e27 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,7 @@ [*] charset = utf-8 -end_of_line = lf +end_of_line = crlf trim_trailing_whitespace = true insert_final_newline = true indent_style = space diff --git a/AsmResolver.sln b/AsmResolver.sln index 313cb8c0b..526ac85f6 100644 --- a/AsmResolver.sln +++ b/AsmResolver.sln @@ -71,9 +71,18 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "TlsTest", "test\TestBinarie EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CallManagedExport", "test\TestBinaries\Native\CallManagedExport\CallManagedExport.vcxproj", "{40483E28-C703-4933-BA5B-9512EF6E6A21}" EndProject -Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "HelloWorldVB", "test\TestBinaries\DotNet\HelloWorldVB\HelloWorldVB.vbproj", "{CF6A7E02-37DC-4963-AC14-76D74ADCD87A}" -EndProject -Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "ClassLibraryVB", "test\TestBinaries\DotNet\ClassLibraryVB\ClassLibraryVB.vbproj", "{2D1DF5DA-7367-4490-B3F0-B996348E150B}" +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "HelloWorldVB", "test\TestBinaries\DotNet\HelloWorldVB\HelloWorldVB.vbproj", "{CF6A7E02-37DC-4963-AC14-76D74ADCD87A}" +EndProject +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "ClassLibraryVB", "test\TestBinaries\DotNet\ClassLibraryVB\ClassLibraryVB.vbproj", "{2D1DF5DA-7367-4490-B3F0-B996348E150B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{66C7E95F-0C1A-466E-988A-C84D5542458B}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + CONTRIBUTING.md = CONTRIBUTING.md + LICENSE.md = LICENSE.md + README.md = README.md + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Directory.Build.props b/Directory.Build.props index 09d02e0ef..648c5ce2f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,7 +7,7 @@ https://github.com/Washi1337/AsmResolver git 10 - 4.9.0 + 4.10.0 diff --git a/README.md b/README.md index db28f348a..31582ebfe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ AsmResolver =========== - [![Master branch build status](https://img.shields.io/appveyor/ci/Washi1337/AsmResolver/master.svg)](https://ci.appveyor.com/project/Washi1337/asmresolver/branch/master) [![Nuget feed](https://img.shields.io/nuget/v/AsmResolver.svg)](https://www.nuget.org/packages/AsmResolver/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Documentation Status](https://readthedocs.org/projects/asmresolver/badge/?version=latest)](https://asmresolver.readthedocs.io/en/latest/?badge=latest) + [![Master branch build status](https://img.shields.io/appveyor/ci/Washi1337/AsmResolver/master.svg)](https://ci.appveyor.com/project/Washi1337/asmresolver/branch/master) + [![Nuget feed](https://img.shields.io/nuget/v/AsmResolver.svg)](https://www.nuget.org/packages/AsmResolver/) + [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + [![Documentation Status](https://readthedocs.org/projects/asmresolver/badge/?version=latest)](https://asmresolver.readthedocs.io/en/latest/?badge=latest) + [![Discord](https://img.shields.io/discord/961647807591243796.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/Y7DTBkbhJJ) AsmResolver is a PE inspection library allowing .NET programmers to read, modify and write executable files. This includes .NET as well as native images. The library exposes high-level representations of the PE, while still allowing the user to access low-level structures. @@ -53,6 +57,12 @@ Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on general workflow and code style. +Found a bug or have questions? +------------------------------ +Please use the [issue tracker](https://github.com/Washi1337/AsmResolver/issues). Try to be as descriptive as possible. + +You can also join the [Discord](https://discord.gg/Y7DTBkbhJJ) to engage more directly with the community. + Acknowledgements ---------------- @@ -60,12 +70,7 @@ AsmResolver started out as a hobby project, but has grown into a community proje - Special thanks to all the people who contributed [directly with code commits](https://github.com/Washi1337/AsmResolver/graphs/contributors). -- Another big thank you to all the people that suggested new features, provided feedback on the API design, have done extensive testing, and/or reported bugs on the [issue board](https://github.com/Washi1337/AsmResolver/issues), by e-mail, or through DMs. +- Another big thank you to all the people that suggested new features, provided feedback on the API design, have done extensive testing, and/or reported bugs on the [issue board](https://github.com/Washi1337/AsmResolver/issues), by e-mail, or through DMs. If you feel you have been under-represented in these acknowledgements, feel free to contact me. - -Found a bug or have questions? ------------------------------- -Please use the [issue tracker](https://github.com/Washi1337/AsmResolver/issues). Try to be as descriptive as possible. - diff --git a/appveyor.yml b/appveyor.yml index 6de584d01..fe19b7d43 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ - master image: Visual Studio 2022 - version: 4.9.0-master-build.{build} + version: 4.10.0-master-build.{build} configuration: Release skip_commits: @@ -33,7 +33,7 @@ - development image: Visual Studio 2022 - version: 4.9.0-dev-build.{build} + version: 4.10.0-dev-build.{build} configuration: Release skip_commits: diff --git a/docs/dotnet/bundles.rst b/docs/dotnet/bundles.rst new file mode 100644 index 000000000..670b9eb3d --- /dev/null +++ b/docs/dotnet/bundles.rst @@ -0,0 +1,176 @@ +AppHost / SingleFileHost Bundles +================================ + +Since the release of .NET Core 3.1, it is possible to deploy .NET assemblies as a single binary. These files are executables that do not contain a traditional .NET metadata header, and run natively on the underlying operating system via a platform-specific application host bootstrapper. + +AsmResolver supports extracting the embedded files from these types of binaries. Additionally, given an application host template provided by the .NET SDK, AsmResolver also supports constructing new bundles as well. All relevant code is found in the following namespace: + +.. code-block:: csharp + + using AsmResolver.DotNet.Bundles; + + +Creating Bundles +---------------- + +.NET bundles are represented using the ``BundleManifest`` class. Creating new bundles can be done using any of the constructors: + +.. code-block:: csharp + + var manifest = new BundleManifest(majorVersionNumber: 6); + + +The major version number refers to the file format that should be used when saving the manifest. Below an overview of the values that are recognized by the CLR: + ++----------------------+----------------------------+ +| .NET Version Number | Bundle File Format Version | ++======================+============================+ +| .NET Core 3.1 | 1 | ++----------------------+----------------------------+ +| .NET 5.0 | 2 | ++----------------------+----------------------------+ +| .NET 6.0 | 6 | ++----------------------+----------------------------+ + +To create a new bundle with a specific bundle identifier, use the overloaded constructor + +.. code-block:: csharp + + var manifest = new BundleManifest(6, "MyBundleID"); + + +It is also possible to change the version number as well as the bundle ID later, since these values are exposed as mutable properties ``MajorVersion`` and ``BundleID`` + +.. code-block:: csharp + + manifest.MajorVersion = 6; + manifest.BundleID = manifest.GenerateDeterministicBundleID(); + +.. note:: + + If ``BundleID`` is left unset (``null``), it will be automatically assigned a new one using ``GenerateDeterministicBundleID`` upon writing. + + +Reading Bundles +--------------- + +Reading and extracting existing bundle manifests from an executable can be done by using one of the ``FromXXX`` methods: + +.. code-block:: csharp + + var manifest = BundleManifest.FromFile(@"C:\Path\To\Executable.exe"); + +.. code-block:: csharp + + byte[] contents = ... + var manifest = BundleManifest.FromBytes(contents); + +.. code-block:: csharp + + IDataSource contents = ... + var manifest = BundleManifest.FromDataSource(contents); + + +Similar to the official .NET bundler and extractor, the methods above locate the bundle in the file by looking for a specific signature first. However, official implementations of the application hosting program itself actually do not verify or use this signature in any shape or form. This means that a third party can replace or remove this signature, or write their own implementation of an application host that does not adhere to this standard, and thus throw off static analysis of the file. + +AsmResolver does not provide built-in alternative heuristics for finding the right start address of the bundle header. However, it is possible to implement one yourself and provide the resulting start address in one of the overloads of the ``FromXXX`` methods: + +.. code-block:: csharp + + byte[] contents = ... + ulong bundleAddress = ... + var manifest = BundleManifest.FromBytes(contents, bundleAddress); + +.. code-block:: csharp + + IDataSource contents = ... + ulong bundleAddress = ... + var manifest = BundleManifest.FromDataSource(contents, bundleAddress); + + +Writing Bundles +--------------- + +Constructing new bundled executable files requires a template file that AsmResolver can base the final output on. This is similar how .NET compilers themselves do this as well. By default, the .NET SDK installs template binaries in one of the following directories: + +- ``/sdk//AppHostTemplate`` +- ``/packs/Microsoft.NETCore.App.Host.//runtimes//native`` + +Using this template file, it is then possible to write a new bundled executable file using ``WriteUsingTemplate``: + +.. code-block:: csharp + + BundleManifest manifest = ... + manifest.WriteUsingTemplate( + @"C:\Path\To\Output\File.exe", + new BundlerParameters( + appHostTemplatePath: @"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\6.0.0\runtimes\win-x64\native\apphost.exe", + appBinaryPath: @"HelloWorld.dll")); + + +Typically on Windows, use an ``apphost.exe`` template if you want to construct a native binary that is framework dependent, and ``singlefilehost.exe`` for a fully self-contained binary. On Linux, use the ``apphost`` and ``singlefilehost`` ELF equivalents. + +For bundle executable files targeting Windows, it may be required to copy over some values from the original PE file into the final bundle executable file. Usually these values include fields from the PE headers (such as the executable's sub-system target) and Win32 resources (such as application icons and version information). AsmResolver can automatically update these headers by specifying a source image to pull this data from in the ``BundlerParameters``: + +.. code-block:: csharp + + BundleManifest manifest = ... + manifest.WriteUsingTemplate( + @"C:\Path\To\Output\File.exe", + new BundlerParameters( + appHostTemplatePath: @"C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\6.0.0\runtimes\win-x64\native\apphost.exe", + appBinaryPath: @"HelloWorld.dll", + imagePathToCopyHeadersFrom: @"C:\Path\To\Original\HelloWorld.exe")); + +``BundleManifest`` also defines other ```WriteUsingTemplate`` overloads taking ``byte[]``, ``IDataSource`` or ``IPEImage`` instances instead of paths. + + +Managing Files +-------------- + +Files in a bundle are represented using the ``BundleFile`` class, and are exposed by the ``BundleManifest.Files`` property. Both the class as well as the list itself is fully mutable, and thus can be used to add, remove or modify files in the bundle. + +Creating a new file can be done using the constructors: + +.. code-block:: csharp + + var newFile = new BundleFile( + relativePath: "HelloWorld.dll", + type: BundleFileType.Assembly, + contents: System.IO.File.ReadAllBytes(@"C:\Binaries\HelloWorld.dll")); + + manifest.Files.Add(newFile); + + +It is also possible to iterate over all files and inspect their contents using ``GetData``: + +.. code-block:: csharp + + foreach (var file in manifest.Files) + { + string path = file.RelativePath; + byte[] contents = file.GetData(); + + Console.WriteLine($"Extracting {path}..."); + System.IO.File.WriteAllBytes(path, contents); + } + + +Changing the contents of an existing file can be done using the ``Contents`` property. + +.. code-block:: csharp + + BundleFile file = ... + file.Contents = new DataSegment(new byte[] { 1, 2, 3, 4 }); + + +If the bundle manifest is put into a single-file host template (e.g. ``singlefilehost.exe``), then files can also be compressed or decompressed: + +.. code-block:: csharp + + file.Compress(); + // file.Contents now contains the compressed version of the data and file.IsCompressed = true + + file.Decompress(); + // file.Contents now contains the decompressed version of the data and file.IsCompressed = false + diff --git a/docs/index.rst b/docs/index.rst index 6e56f82d4..89f3b0aeb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -62,4 +62,5 @@ Table of Contents: dotnet/cloning dotnet/token-allocation dotnet/type-memory-layout + dotnet/bundles dotnet/advanced-pe-image-building.rst diff --git a/src/AsmResolver.DotNet/AssemblyDefinition.cs b/src/AsmResolver.DotNet/AssemblyDefinition.cs index 220980bcb..43fe53c77 100644 --- a/src/AsmResolver.DotNet/AssemblyDefinition.cs +++ b/src/AsmResolver.DotNet/AssemblyDefinition.cs @@ -222,6 +222,9 @@ protected virtual IList GetModules() return _publicKeyToken; } + /// + public override bool IsImportedInModule(ModuleDefinition module) => ManifestModule == module; + /// public override AssemblyDefinition Resolve() => this; diff --git a/src/AsmResolver.DotNet/AssemblyDescriptor.cs b/src/AsmResolver.DotNet/AssemblyDescriptor.cs index 248812ba8..87a816029 100644 --- a/src/AsmResolver.DotNet/AssemblyDescriptor.cs +++ b/src/AsmResolver.DotNet/AssemblyDescriptor.cs @@ -2,18 +2,20 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reflection; using System.Security.Cryptography; using System.Threading; using AsmResolver.Collections; using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; +using AssemblyHashAlgorithm = AsmResolver.PE.DotNet.Metadata.Tables.Rows.AssemblyHashAlgorithm; namespace AsmResolver.DotNet { /// /// Provides a base implementation for describing a self-describing .NET assembly hosted by a common language runtime (CLR). /// - public abstract class AssemblyDescriptor : MetadataMember, IHasCustomAttribute, IFullNameProvider + public abstract class AssemblyDescriptor : MetadataMember, IHasCustomAttribute, IFullNameProvider, IImportable { private const int PublicKeyTokenLength = 8; @@ -212,6 +214,9 @@ public IList CustomAttributes /// public override string ToString() => FullName; + /// + public abstract bool IsImportedInModule(ModuleDefinition module); + /// /// Computes the token of a public key using the provided hashing algorithm. /// diff --git a/src/AsmResolver.DotNet/AssemblyReference.cs b/src/AsmResolver.DotNet/AssemblyReference.cs index b5548144e..f37630de6 100644 --- a/src/AsmResolver.DotNet/AssemblyReference.cs +++ b/src/AsmResolver.DotNet/AssemblyReference.cs @@ -147,6 +147,9 @@ public AssemblyReference(AssemblyDescriptor descriptor) /// protected virtual byte[]? GetHashValue() => null; + /// + public override bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// public override AssemblyDefinition? Resolve() => Module?.MetadataResolver.AssemblyResolver.Resolve(this); diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs index b9283f323..691afffa6 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs @@ -14,7 +14,7 @@ namespace AsmResolver.DotNet.Builder.Metadata.Blob public class BlobStreamBuffer : IMetadataStreamBuffer { private readonly MemoryStream _rawStream = new(); - private readonly IBinaryStreamWriter _writer; + private readonly BinaryStreamWriter _writer; private readonly Dictionary _blobs = new(ByteArrayEqualityComparer.Instance); /// diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Guid/GuidStreamBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Guid/GuidStreamBuffer.cs index 710ff651a..aacf2ba7d 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Guid/GuidStreamBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Guid/GuidStreamBuffer.cs @@ -13,7 +13,7 @@ namespace AsmResolver.DotNet.Builder.Metadata.Guid public class GuidStreamBuffer : IMetadataStreamBuffer { private readonly MemoryStream _rawStream = new(); - private readonly IBinaryStreamWriter _writer; + private readonly BinaryStreamWriter _writer; private readonly Dictionary _guids = new(); /// diff --git a/src/AsmResolver.DotNet/Builder/Metadata/UserStrings/UserStringsStreamBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/UserStrings/UserStringsStreamBuffer.cs index 15df10919..c6706b3cf 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/UserStrings/UserStringsStreamBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/UserStrings/UserStringsStreamBuffer.cs @@ -14,7 +14,7 @@ namespace AsmResolver.DotNet.Builder.Metadata.UserStrings public class UserStringsStreamBuffer : IMetadataStreamBuffer { private readonly MemoryStream _rawStream = new(); - private readonly IBinaryStreamWriter _writer; + private readonly BinaryStreamWriter _writer; private readonly Dictionary _strings = new(); /// diff --git a/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs b/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs index 524b55ae2..073df1eec 100644 --- a/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Resources/DotNetResourcesDirectoryBuffer.cs @@ -12,7 +12,7 @@ namespace AsmResolver.DotNet.Builder.Resources public class DotNetResourcesDirectoryBuffer { private readonly MemoryStream _rawStream = new(); - private readonly IBinaryStreamWriter _writer; + private readonly BinaryStreamWriter _writer; private readonly Dictionary _dataOffsets = new(ByteArrayEqualityComparer.Instance); /// diff --git a/src/AsmResolver.DotNet/Bundles/BundleFile.cs b/src/AsmResolver.DotNet/Bundles/BundleFile.cs new file mode 100644 index 000000000..958eeba16 --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/BundleFile.cs @@ -0,0 +1,221 @@ +using System; +using System.IO; +using System.IO.Compression; +using AsmResolver.Collections; +using AsmResolver.IO; + +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Represents a single file in a .NET bundle manifest. + /// + public class BundleFile : IOwnedCollectionElement + { + private readonly LazyVariable _contents; + + /// + /// Creates a new empty bundle file. + /// + /// The path of the file, relative to the root of the bundle. + public BundleFile(string relativePath) + { + RelativePath = relativePath; + _contents = new LazyVariable(GetContents); + } + + /// + /// Creates a new bundle file. + /// + /// The path of the file, relative to the root of the bundle. + /// The type of the file. + /// The contents of the file. + public BundleFile(string relativePath, BundleFileType type, byte[] contents) + : this(relativePath, type, new DataSegment(contents)) + { + } + + /// + /// Creates a new empty bundle file. + /// + /// The path of the file, relative to the root of the bundle. + /// The type of the file. + /// The contents of the file. + public BundleFile(string relativePath, BundleFileType type, ISegment contents) + { + RelativePath = relativePath; + Type = type; + _contents = new LazyVariable(contents); + } + + /// + /// Gets the parent manifest this file was added to. + /// + public BundleManifest? ParentManifest + { + get; + private set; + } + + /// + BundleManifest? IOwnedCollectionElement.Owner + { + get => ParentManifest; + set => ParentManifest = value; + } + + /// + /// Gets or sets the path to the file, relative to the root directory of the bundle. + /// + public string RelativePath + { + get; + set; + } + + /// + /// Gets or sets the type of the file. + /// + public BundleFileType Type + { + get; + set; + } + + /// + /// Gets or sets a value indicating whether the data stored in is compressed or not. + /// + /// + /// The default implementation of the application host by Microsoft only supports compressing files if it is + /// a fully self-contained binary and the file is not the .deps.json nor the .runtmeconfig.json + /// file. This property does not do validation on any of these conditions. As such, if the file is supposed to be + /// compressed with any of these conditions not met, a custom application host template needs to be provided + /// upon serializing the bundle for it to be runnable. + /// + public bool IsCompressed + { + get; + set; + } + + /// + /// Gets or sets the raw contents of the file. + /// + public ISegment Contents + { + get => _contents.Value; + set => _contents.Value = value; + } + + /// + /// Gets a value whether the contents of the file can be read using a . + /// + public bool CanRead => Contents is IReadableSegment; + + /// + /// Obtains the raw contents of the file. + /// + /// The contents. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ISegment? GetContents() => null; + + /// + /// Attempts to create a that points to the start of the raw contents of the file. + /// + /// The reader. + /// true if the reader was constructed successfully, false otherwise. + public bool TryGetReader(out BinaryStreamReader reader) + { + if (Contents is IReadableSegment segment) + { + reader = segment.CreateReader(); + return true; + } + + reader = default; + return false; + } + + /// + /// Reads (and decompresses if necessary) the contents of the file. + /// + /// The contents. + public byte[] GetData() => GetData(true); + + /// + /// Reads the contents of the file. + /// + /// true if the contents should be decompressed or not when necessary. + /// The contents. + public byte[] GetData(bool decompressIfRequired) + { + if (TryGetReader(out var reader)) + { + byte[] contents = reader.ReadToEnd(); + if (decompressIfRequired && IsCompressed) + { + using var outputStream = new MemoryStream(); + + using var inputStream = new MemoryStream(contents); + using (var deflate = new DeflateStream(inputStream, CompressionMode.Decompress)) + { + deflate.CopyTo(outputStream); + } + + contents = outputStream.ToArray(); + } + + return contents; + } + + throw new InvalidOperationException("Contents of file is not readable."); + } + + /// + /// Marks the file as compressed, compresses the file contents, and replaces the value of + /// with the result. + /// + /// Occurs when the file was already compressed. + /// + /// The default implementation of the application host by Microsoft only supports compressing files if it is + /// a fully self-contained binary and the file is not the .deps.json nor the .runtmeconfig.json + /// file. This method does not do validation on any of these conditions. As such, if the file is supposed to be + /// compressed with any of these conditions not met, a custom application host template needs to be provided + /// upon serializing the bundle for it to be runnable. + /// + public void Compress() + { + if (IsCompressed) + throw new InvalidOperationException("File is already compressed."); + + using var inputStream = new MemoryStream(GetData()); + + using var outputStream = new MemoryStream(); + using (var deflate = new DeflateStream(outputStream, CompressionLevel.Optimal)) + { + inputStream.CopyTo(deflate); + } + + Contents = new DataSegment(outputStream.ToArray()); + IsCompressed = true; + } + + /// + /// Marks the file as uncompressed, decompresses the file contents, and replaces the value of + /// with the result. + /// + /// Occurs when the file was not compressed. + public void Decompress() + { + if (!IsCompressed) + throw new InvalidOperationException("File is not compressed."); + + Contents = new DataSegment(GetData(true)); + IsCompressed = false; + } + + /// + public override string ToString() => RelativePath; + } +} diff --git a/src/AsmResolver.DotNet/Bundles/BundleFileType.cs b/src/AsmResolver.DotNet/Bundles/BundleFileType.cs new file mode 100644 index 000000000..8ac16b290 --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/BundleFileType.cs @@ -0,0 +1,38 @@ +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Provides members defining all possible file types that can be stored in a bundled .NET application. + /// + public enum BundleFileType + { + /// + /// Indicates the file type is unknown. + /// + Unknown, + + /// + /// Indicates the file is a .NET assembly. + /// + Assembly, + + /// + /// Indicates the file is a native binary. + /// + NativeBinary, + + /// + /// Indicates the file is the deps.json file associated to a .NET assembly. + /// + DepsJson, + + /// + /// Indicates the file is the runtimeconfig.json file associated to a .NET assembly. + /// + RuntimeConfigJson, + + /// + /// Indicates the file contains symbols. + /// + Symbols + } +} diff --git a/src/AsmResolver.DotNet/Bundles/BundleManifest.cs b/src/AsmResolver.DotNet/Bundles/BundleManifest.cs new file mode 100644 index 000000000..2acc2a57b --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/BundleManifest.cs @@ -0,0 +1,503 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using AsmResolver.Collections; +using AsmResolver.IO; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; +using AsmResolver.PE.Win32Resources.Builder; + +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Represents a set of bundled files embedded in a .NET application host or single-file host. + /// + public class BundleManifest + { + private const int DefaultBundleIDLength = 12; + + private static readonly byte[] BundleSignature = + { + 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38, + 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32, + 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18, + 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae + }; + + private static readonly byte[] AppBinaryPathPlaceholder = + Encoding.UTF8.GetBytes("c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2"); + + private IList? _files; + + /// + /// Initializes an empty bundle manifest. + /// + protected BundleManifest() + { + } + + /// + /// Creates a new bundle manifest. + /// + /// The file format version. + public BundleManifest(uint majorVersionNumber) + { + MajorVersion = majorVersionNumber; + MinorVersion = 0; + } + + /// + /// Creates a new bundle manifest with a specific bundle identifier. + /// + /// The file format version. + /// The unique bundle manifest identifier. + public BundleManifest(uint majorVersionNumber, string bundleId) + { + MajorVersion = majorVersionNumber; + MinorVersion = 0; + BundleID = bundleId; + } + + /// + /// Gets or sets the major file format version of the bundle. + /// + /// + /// Version numbers recognized by the CLR are: + /// + /// 1 for .NET Core 3.1 + /// 2 for .NET 5.0 + /// 6 for .NET 6.0 + /// + /// + public uint MajorVersion + { + get; + set; + } + + /// + /// Gets or sets the minor file format version of the bundle. + /// + /// + /// This value is ignored by the CLR and should be set to 0. + /// + public uint MinorVersion + { + get; + set; + } + + /// + /// Gets or sets the unique identifier for the bundle manifest. + /// + /// + /// When this property is set to null, the bundle identifier will be generated upon writing the manifest + /// based on the contents of the manifest. + /// + public string? BundleID + { + get; + set; + } + + /// + /// Gets or sets flags associated to the bundle. + /// + public BundleManifestFlags Flags + { + get; + set; + } + + /// + /// Gets a collection of files stored in the bundle. + /// + public IList Files + { + get + { + if (_files is null) + Interlocked.CompareExchange(ref _files, GetFiles(), null); + return _files; + } + } + + /// + /// Attempts to automatically locate and parse the bundle header in the provided file. + /// + /// The path to the file to read. + /// The read manifest. + public static BundleManifest FromFile(string filePath) + { + return FromBytes(File.ReadAllBytes(filePath)); + } + + /// + /// Attempts to automatically locate and parse the bundle header in the provided file. + /// + /// The raw contents of the file to read. + /// The read manifest. + public static BundleManifest FromBytes(byte[] data) + { + return FromDataSource(new ByteArrayDataSource(data)); + } + + /// + /// Parses the bundle header in the provided file at the provided address. + /// + /// The raw contents of the file to read. + /// The address within the file to start reading the bundle at. + /// The read manifest. + public static BundleManifest FromBytes(byte[] data, ulong offset) + { + return FromDataSource(new ByteArrayDataSource(data), offset); + } + + /// + /// Attempts to automatically locate and parse the bundle header in the provided file. + /// + /// The raw contents of the file to read. + /// The read manifest. + public static BundleManifest FromDataSource(IDataSource source) + { + long address = FindBundleManifestAddress(source); + if (address == -1) + throw new BadImageFormatException("File does not contain an AppHost bundle signature."); + + return FromDataSource(source, (ulong) address); + } + + /// + /// Parses the bundle header in the provided file at the provided address. + /// + /// The raw contents of the file to read. + /// The address within the file to start reading the bundle at. + /// The read manifest. + public static BundleManifest FromDataSource(IDataSource source, ulong offset) + { + var reader = new BinaryStreamReader(source, 0, 0, (uint) source.Length) + { + Offset = offset + }; + + return FromReader(reader); + } + + /// + /// Parses the bundle header from the provided input stream. + /// + /// The input stream pointing to the start of the bundle to read. + /// The read manifest. + public static BundleManifest FromReader(BinaryStreamReader reader) => new SerializedBundleManifest(reader); + + private static long FindInFile(IDataSource source, byte[] data) + { + // Note: For performance reasons, we read data from the data source in blocks, such that we avoid + // virtual-dispatch calls and do the searching directly on a byte array instead. + + byte[] buffer = new byte[0x1000]; + + ulong start = 0; + while (start < source.Length) + { + int read = source.ReadBytes(start, buffer, 0, buffer.Length); + + for (int i = sizeof(ulong); i < read - data.Length; i++) + { + bool fullMatch = true; + for (int j = 0; fullMatch && j < data.Length; j++) + { + if (buffer[i + j] != data[j]) + fullMatch = false; + } + + if (fullMatch) + return (long) start + i; + } + + start += (ulong) read; + } + + return -1; + } + + private static long ReadBundleManifestAddress(IDataSource source, long signatureAddress) + { + var reader = new BinaryStreamReader(source, (ulong) signatureAddress - sizeof(ulong), 0, 8); + ulong manifestAddress = reader.ReadUInt64(); + + return source.IsValidAddress(manifestAddress) + ? (long) manifestAddress + : -1; + } + + /// + /// Attempts to find the start of the bundle header in the provided file. + /// + /// The file to locate the bundle header in. + /// The offset, or -1 if none was found. + public static long FindBundleManifestAddress(IDataSource source) + { + long signatureAddress = FindInFile(source, BundleSignature); + if (signatureAddress == -1) + return -1; + + return ReadBundleManifestAddress(source, signatureAddress); + } + + /// + /// Gets a value indicating whether the provided data source contains a conventional bundled assembly signature. + /// + /// The file to locate the bundle header in. + /// true if a bundle signature was found, false otherwise. + public static bool IsBundledAssembly(IDataSource source) => FindBundleManifestAddress(source) != -1; + + /// + /// Obtains the list of files stored in the bundle. + /// + /// The files + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetFiles() => new OwnedCollection(this); + + /// + /// Generates a bundle identifier based on the SHA-256 hashes of all files in the manifest. + /// + /// The generated bundle identifier. + public string GenerateDeterministicBundleID() + { + using var manifestHasher = SHA256.Create(); + + for (int i = 0; i < Files.Count; i++) + { + var file = Files[i]; + using var fileHasher = SHA256.Create(); + byte[] fileHash = fileHasher.ComputeHash(file.GetData()); + manifestHasher.TransformBlock(fileHash, 0, fileHash.Length, fileHash, 0); + } + + manifestHasher.TransformFinalBlock(Array.Empty(), 0, 0); + byte[] manifestHash = manifestHasher.Hash!; + + return Convert.ToBase64String(manifestHash) + .Substring(DefaultBundleIDLength) + .Replace('/', '_'); + } + + /// + /// Constructs a new application host file based on the bundle manifest. + /// + /// The path of the file to write to. + /// The parameters to use for bundling all files into a single executable. + public void WriteUsingTemplate(string outputPath, in BundlerParameters parameters) + { + using var fs = File.Create(outputPath); + WriteUsingTemplate(fs, parameters); + } + + /// + /// Constructs a new application host file based on the bundle manifest. + /// + /// The output stream to write to. + /// The parameters to use for bundling all files into a single executable. + public void WriteUsingTemplate(Stream outputStream, in BundlerParameters parameters) + { + WriteUsingTemplate(new BinaryStreamWriter(outputStream), parameters); + } + + /// + /// Constructs a new application host file based on the bundle manifest. + /// + /// The output stream to write to. + /// The parameters to use for bundling all files into a single executable. + public void WriteUsingTemplate(IBinaryStreamWriter writer, BundlerParameters parameters) + { + var appBinaryEntry = Files.FirstOrDefault(f => f.RelativePath == parameters.ApplicationBinaryPath); + if (appBinaryEntry is null) + throw new ArgumentException($"Application {parameters.ApplicationBinaryPath} does not exist within the bundle."); + + byte[] appBinaryPathBytes = Encoding.UTF8.GetBytes(parameters.ApplicationBinaryPath); + if (appBinaryPathBytes.Length > 1024) + throw new ArgumentException("Application binary path cannot exceed 1024 bytes."); + + if (!parameters.IsArm64Linux) + EnsureAppHostPEHeadersAreUpToDate(ref parameters); + + var appHostTemplateSource = new ByteArrayDataSource(parameters.ApplicationHostTemplate); + long signatureAddress = FindInFile(appHostTemplateSource, BundleSignature); + if (signatureAddress == -1) + throw new ArgumentException("AppHost template does not contain the bundle signature."); + + long appBinaryPathAddress = FindInFile(appHostTemplateSource, AppBinaryPathPlaceholder); + if (appBinaryPathAddress == -1) + throw new ArgumentException("AppHost template does not contain the application binary path placeholder."); + + writer.WriteBytes(parameters.ApplicationHostTemplate); + writer.Offset = writer.Length; + ulong headerAddress = WriteManifest(writer, parameters.IsArm64Linux); + + writer.Offset = (ulong) signatureAddress - sizeof(ulong); + writer.WriteUInt64(headerAddress); + + writer.Offset = (ulong) appBinaryPathAddress; + writer.WriteBytes(appBinaryPathBytes); + if (AppBinaryPathPlaceholder.Length > appBinaryPathBytes.Length) + writer.WriteZeroes(AppBinaryPathPlaceholder.Length - appBinaryPathBytes.Length); + } + + private static void EnsureAppHostPEHeadersAreUpToDate(ref BundlerParameters parameters) + { + PEFile file; + try + { + file = PEFile.FromBytes(parameters.ApplicationHostTemplate); + } + catch (BadImageFormatException) + { + // Template is not a PE file. + return; + } + + bool changed = false; + + // Ensure same Windows subsystem is used (typically required for GUI applications). + if (file.OptionalHeader.SubSystem != parameters.SubSystem) + { + file.OptionalHeader.SubSystem = parameters.SubSystem; + changed = true; + } + + // If the app binary has resources (such as an icon or version info), we need to copy it into the + // AppHost template so that they are also visible from the final packed executable. + if (parameters.Resources is { } directory) + { + // Put original resource directory in a new .rsrc section. + var buffer = new ResourceDirectoryBuffer(); + buffer.AddDirectory(directory); + var rsrc = new PESection(".rsrc", SectionFlags.MemoryRead | SectionFlags.ContentInitializedData); + rsrc.Contents = buffer; + + // Find .reloc section, and insert .rsrc before it if it is present. Otherwise just append to the end. + int sectionIndex = file.Sections.Count - 1; + for (int i = file.Sections.Count - 1; i >= 0; i--) + { + if (file.Sections[i].Name == ".reloc") + { + sectionIndex = i; + break; + } + } + + file.Sections.Insert(sectionIndex, rsrc); + + // Update resource data directory va + size. + file.AlignSections(); + file.OptionalHeader.DataDirectories[(int) DataDirectoryIndex.ResourceDirectory] = new DataDirectory( + buffer.Rva, + buffer.GetPhysicalSize()); + + changed = true; + } + + // Rebuild AppHost PE file if necessary. + if (changed) + { + using var stream = new MemoryStream(); + file.Write(stream); + parameters.ApplicationHostTemplate = stream.ToArray(); + } + } + + /// + /// Writes the manifest to an output stream. + /// + /// The output stream to write to. + /// true if the application host is a Linux ELF binary targeting ARM64. + /// The address of the bundle header. + /// + /// This does not necessarily produce a working executable file, it only writes the contents of the entire manifest, + /// without a host application that invokes the manifest. If you want to produce a runnable executable, use one + /// of the WriteUsingTemplate methods instead. + /// + public ulong WriteManifest(IBinaryStreamWriter writer, bool isArm64Linux) + { + WriteFileContents(writer, isArm64Linux + ? 4096u + : 16u); + + ulong headerAddress = writer.Offset; + WriteManifestHeader(writer); + + return headerAddress; + } + + private void WriteFileContents(IBinaryStreamWriter writer, uint alignment) + { + for (int i = 0; i < Files.Count; i++) + { + var file = Files[i]; + + if (file.Type == BundleFileType.Assembly) + writer.Align(alignment); + + file.Contents.UpdateOffsets(writer.Offset, (uint) writer.Offset); + file.Contents.Write(writer); + } + } + + private void WriteManifestHeader(IBinaryStreamWriter writer) + { + writer.WriteUInt32(MajorVersion); + writer.WriteUInt32(MinorVersion); + writer.WriteInt32(Files.Count); + + BundleID ??= GenerateDeterministicBundleID(); + writer.WriteBinaryFormatterString(BundleID); + + if (MajorVersion >= 2) + { + WriteFileOffsetSizePair(writer, Files.FirstOrDefault(f => f.Type == BundleFileType.DepsJson)); + WriteFileOffsetSizePair(writer, Files.FirstOrDefault(f => f.Type == BundleFileType.RuntimeConfigJson)); + writer.WriteUInt64((ulong) Flags); + } + + WriteFileHeaders(writer); + } + + private void WriteFileHeaders(IBinaryStreamWriter writer) + { + for (int i = 0; i < Files.Count; i++) + { + var file = Files[i]; + + WriteFileOffsetSizePair(writer, file); + + if (MajorVersion >= 6) + writer.WriteUInt64(file.IsCompressed ? file.Contents.GetPhysicalSize() : 0); + + writer.WriteByte((byte) file.Type); + writer.WriteBinaryFormatterString(file.RelativePath); + } + } + + private static void WriteFileOffsetSizePair(IBinaryStreamWriter writer, BundleFile? file) + { + if (file is not null) + { + writer.WriteUInt64(file.Contents.Offset); + writer.WriteUInt64((ulong) file.GetData().Length); + } + else + { + writer.WriteUInt64(0); + writer.WriteUInt64(0); + } + } + + } +} diff --git a/src/AsmResolver.DotNet/Bundles/BundleManifestFlags.cs b/src/AsmResolver.DotNet/Bundles/BundleManifestFlags.cs new file mode 100644 index 000000000..0a2a9a277 --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/BundleManifestFlags.cs @@ -0,0 +1,21 @@ +using System; + +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Provides members defining all flags that can be assigned to a bundle manifest. + /// + [Flags] + public enum BundleManifestFlags : ulong + { + /// + /// Indicates no flags were assigned. + /// + None = 0, + + /// + /// Indicates the bundle was compiled in .NET Core 3 compatibility mode. + /// + NetCoreApp3CompatibilityMode = 1 + } +} diff --git a/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs new file mode 100644 index 000000000..2e83c3c16 --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/BundlerParameters.cs @@ -0,0 +1,221 @@ +using System.IO; +using AsmResolver.IO; +using AsmResolver.PE; +using AsmResolver.PE.File.Headers; +using AsmResolver.PE.Win32Resources; + +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Defines parameters for the .NET application bundler. + /// + public struct BundlerParameters + { + /// + /// Initializes new bundler parameters. + /// + /// + /// The path to the application host file template to use. By default this is stored in + /// <DOTNET-INSTALLATION-PATH>/sdk/<version>/AppHostTemplate or + /// <DOTNET-INSTALLATION-PATH>/packs/Microsoft.NETCore.App.Host.<runtime-identifier>/<version>/runtimes/<runtime-identifier>/native. + /// + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + public BundlerParameters(string appHostTemplatePath, string appBinaryPath) + : this(File.ReadAllBytes(appHostTemplatePath), appBinaryPath) + { + } + + /// + /// Initializes new bundler parameters. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + public BundlerParameters(byte[] appHostTemplate, string appBinaryPath) + { + ApplicationHostTemplate = appHostTemplate; + ApplicationBinaryPath = appBinaryPath; + IsArm64Linux = false; + Resources = null; + SubSystem = SubSystem.WindowsCui; + } + + /// + /// Initializes new bundler parameters. + /// + /// + /// The path to the application host file template to use. By default this is stored in + /// <DOTNET-INSTALLATION-PATH>/sdk/<version>/AppHostTemplate or + /// <DOTNET-INSTALLATION-PATH>/packs/Microsoft.NETCore.App.Host.<runtime-identifier>/<version>/runtimes/<runtime-identifier>/native. + /// + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The path to copy the PE headers and Win32 resources from. This is typically the original native executable + /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public BundlerParameters(string appHostTemplatePath, string appBinaryPath, string? imagePathToCopyHeadersFrom) + : this( + File.ReadAllBytes(appHostTemplatePath), + appBinaryPath, + !string.IsNullOrEmpty(imagePathToCopyHeadersFrom) + ? PEImage.FromFile(imagePathToCopyHeadersFrom!) + : null + ) + { + } + + /// + /// Initializes new bundler parameters. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The binary to copy the PE headers and Win32 resources from. This is typically the original native executable + /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, byte[]? imageToCopyHeadersFrom) + : this( + appHostTemplate, + appBinaryPath, + imageToCopyHeadersFrom is not null + ? PEImage.FromBytes(imageToCopyHeadersFrom) + : null + ) + { + } + + /// + /// Initializes new bundler parameters. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The binary to copy the PE headers and Win32 resources from. This is typically the original native executable + /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, IDataSource? imageToCopyHeadersFrom) + : this( + appHostTemplate, + appBinaryPath, + imageToCopyHeadersFrom is not null + ? PEImage.FromDataSource(imageToCopyHeadersFrom) + : null + ) + { + } + + /// + /// Initializes new bundler parameters. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// + /// The PE image to copy the headers and Win32 resources from. This is typically the original native executable + /// file that hosts the CLR, or the original AppHost file the bundle was extracted from. + /// + public BundlerParameters(byte[] appHostTemplate, string appBinaryPath, IPEImage? imageToCopyHeadersFrom) + : this( + appHostTemplate, + appBinaryPath, + imageToCopyHeadersFrom?.SubSystem ?? SubSystem.WindowsCui, + imageToCopyHeadersFrom?.Resources + ) + { + } + + /// + /// Initializes new bundler parameters. + /// + /// The application host template file to use. + /// + /// The name of the file in the bundle that contains the entry point of the application. + /// + /// The subsystem to use in the final Windows PE binary. + /// The resources to copy into the final Windows PE binary. + public BundlerParameters( + byte[] appHostTemplate, + string appBinaryPath, + SubSystem subSystem, + IResourceDirectory? resources) + { + ApplicationHostTemplate = appHostTemplate; + ApplicationBinaryPath = appBinaryPath; + IsArm64Linux = false; + SubSystem = subSystem; + Resources = resources; + } + + /// + /// Gets or sets the template application hosting binary. + /// + /// + /// By default, the official implementations of the application host can be found in one of the following + /// installation directories: + /// + /// <DOTNET-INSTALLATION-PATH>/sdk/<version>/AppHostTemplate + /// <DOTNET-INSTALLATION-PATH>/packs/Microsoft.NETCore.App.Host.<runtime-identifier>/<version>/runtimes/<runtime-identifier>/native + /// + /// It is therefore recommended to use the contents of one of these templates to ensure compatibility. + /// + public byte[] ApplicationHostTemplate + { + get; + set; + } + + /// + /// Gets or sets the path to the binary within the bundle that contains the application's entry point. + /// + public string ApplicationBinaryPath + { + get; + set; + } + + /// + /// Gets a value indicating whether the bundled executable targets the Linux operating system on ARM64. + /// + public bool IsArm64Linux + { + get; + set; + } + + /// + /// Gets or sets the Win32 resources directory to copy into the final PE executable. + /// + /// + /// This field is ignored if is set to true, or + /// does not contain a proper PE image. + /// + public IResourceDirectory? Resources + { + get; + set; + } + + /// + /// Gets or sets the Windows subsystem the final PE executable should target. + /// + /// + /// This field is ignored if is set to true, or + /// does not contain a proper PE image. + /// + public SubSystem SubSystem + { + get; + set; + } + } +} diff --git a/src/AsmResolver.DotNet/Bundles/SerializedBundleFile.cs b/src/AsmResolver.DotNet/Bundles/SerializedBundleFile.cs new file mode 100644 index 000000000..79581a262 --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/SerializedBundleFile.cs @@ -0,0 +1,42 @@ +using AsmResolver.IO; + +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Represents a lazily initialized implementation of that is read from an existing file. + /// + public class SerializedBundleFile : BundleFile + { + private readonly BinaryStreamReader _contentsReader; + + /// + /// Reads a bundle file entry from the provided input stream. + /// + /// The input stream. + /// The file format version of the bundle. + public SerializedBundleFile(ref BinaryStreamReader reader, uint bundleVersionFormat) + : base(string.Empty) + { + ulong offset = reader.ReadUInt64(); + ulong size = reader.ReadUInt64(); + + if (bundleVersionFormat >= 6) + { + ulong compressedSize = reader.ReadUInt64(); + if (compressedSize != 0) + { + size = compressedSize; + IsCompressed = true; + } + } + + Type = (BundleFileType) reader.ReadByte(); + RelativePath = reader.ReadBinaryFormatterString(); + + _contentsReader = reader.ForkAbsolute(offset, (uint) size); + } + + /// + protected override ISegment GetContents() => _contentsReader.ReadSegment(_contentsReader.Length); + } +} diff --git a/src/AsmResolver.DotNet/Bundles/SerializedBundleManifest.cs b/src/AsmResolver.DotNet/Bundles/SerializedBundleManifest.cs new file mode 100644 index 000000000..2fce34fae --- /dev/null +++ b/src/AsmResolver.DotNet/Bundles/SerializedBundleManifest.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using AsmResolver.Collections; +using AsmResolver.IO; + +namespace AsmResolver.DotNet.Bundles +{ + /// + /// Represents a lazily initialized implementation of that is read from an existing file. + /// + public class SerializedBundleManifest : BundleManifest + { + private readonly uint _originalMajorVersion; + private readonly BinaryStreamReader _fileEntriesReader; + private readonly int _originalFileCount; + + /// + /// Reads a bundle manifest from the provided input stream. + /// + /// The input stream. + public SerializedBundleManifest(BinaryStreamReader reader) + { + MajorVersion = _originalMajorVersion = reader.ReadUInt32(); + MinorVersion = reader.ReadUInt32(); + _originalFileCount = reader.ReadInt32(); + BundleID = reader.ReadBinaryFormatterString(); + + if (MajorVersion >= 2) + { + reader.Offset += 4 * sizeof(ulong); + Flags = (BundleManifestFlags) reader.ReadUInt64(); + } + + _fileEntriesReader = reader; + } + + /// + protected override IList GetFiles() + { + var reader = _fileEntriesReader; + var result = new OwnedCollection(this); + + for (int i = 0; i < _originalFileCount; i++) + result.Add(new SerializedBundleFile(ref reader, _originalMajorVersion)); + + return result; + } + } +} diff --git a/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs b/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs index 4fd836cfe..ea1b7d335 100644 --- a/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs +++ b/src/AsmResolver.DotNet/Cloning/CloneContextAwareReferenceImporter.cs @@ -17,6 +17,11 @@ public CloneContextAwareReferenceImporter(MemberCloneContext context) _context = context; } + /// + /// The working space for this member cloning procedure. + /// + protected MemberCloneContext Context => _context; + /// protected override ITypeDefOrRef ImportType(TypeDefinition type) { @@ -40,5 +45,15 @@ public override IMethodDefOrRef ImportMethod(IMethodDefOrRef method) ? (IMethodDefOrRef) clonedMethod : base.ImportMethod(method); } + + /// + protected override ITypeDefOrRef ImportType(TypeReference type) + { + return type.Namespace == "System" + && type.Name == nameof(System.Object) + && (type.Scope?.GetAssembly()?.IsCorLib ?? false) + ? _context.Module.CorLibTypeFactory.Object.Type + : base.ImportType(type); + } } -} \ No newline at end of file +} diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs b/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs index 5f6fa56e9..78c52c8c3 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloneContext.cs @@ -12,10 +12,18 @@ public class MemberCloneContext /// Creates a new instance of the class. /// /// The target module to copy the cloned members into. - public MemberCloneContext(ModuleDefinition module) + public MemberCloneContext(ModuleDefinition module) : this(module, null) { } + + /// + /// Creates a new instance of the class. + /// + /// The target module to copy the cloned members into. + /// The factory for creating the reference importer + public MemberCloneContext(ModuleDefinition module, + Func? importerFactory) { Module = module ?? throw new ArgumentNullException(nameof(module)); - Importer = new CloneContextAwareReferenceImporter(this); + Importer = importerFactory?.Invoke(this) ?? new CloneContextAwareReferenceImporter(this); } /// @@ -42,4 +50,4 @@ public ReferenceImporter Importer get; } = new Dictionary(); } -} \ No newline at end of file +} diff --git a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs index eae611adb..1580d1efa 100644 --- a/src/AsmResolver.DotNet/Cloning/MemberCloner.cs +++ b/src/AsmResolver.DotNet/Cloning/MemberCloner.cs @@ -17,6 +17,7 @@ namespace AsmResolver.DotNet.Cloning /// public partial class MemberCloner { + private readonly Func? _importerFactory; private readonly ModuleDefinition _targetModule; private readonly HashSet _typesToClone = new(); @@ -29,9 +30,18 @@ public partial class MemberCloner /// Creates a new instance of the class. /// /// The target module to copy the members into. - public MemberCloner(ModuleDefinition targetModule) + public MemberCloner(ModuleDefinition targetModule) : this(targetModule, null) { } + + /// + /// Creates a new instance of the class. + /// + /// The target module to copy the members into. + /// The factory for creating the reference importer + public MemberCloner(ModuleDefinition targetModule, + Func? importerFactory) { _targetModule = targetModule ?? throw new ArgumentNullException(nameof(targetModule)); + _importerFactory = importerFactory; } /// @@ -220,7 +230,7 @@ public MemberCloner Include(EventDefinition @event) /// An object representing the result of the cloning process. public MemberCloneResult Clone() { - var context = new MemberCloneContext(_targetModule); + var context = new MemberCloneContext(_targetModule, _importerFactory); CreateMemberStubs(context); DeepCopyMembers(context); diff --git a/src/AsmResolver.DotNet/Code/Cil/CilMethodBody.cs b/src/AsmResolver.DotNet/Code/Cil/CilMethodBody.cs index 633440c24..180e474f8 100644 --- a/src/AsmResolver.DotNet/Code/Cil/CilMethodBody.cs +++ b/src/AsmResolver.DotNet/Code/Cil/CilMethodBody.cs @@ -136,6 +136,7 @@ public bool VerifyLabelsOnBuild /// The method that owns the method body. /// The Dynamic Method/Delegate/DynamicResolver. /// The method body. + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls ResolveDynamicResolver")] public static CilMethodBody FromDynamicMethod(MethodDefinition method, object dynamicMethodObj) { if (!(method.Module is SerializedModuleDefinition module)) diff --git a/src/AsmResolver.DotNet/Collections/ParameterCollection.cs b/src/AsmResolver.DotNet/Collections/ParameterCollection.cs index 1d6639dcc..a2e8b4c5d 100644 --- a/src/AsmResolver.DotNet/Collections/ParameterCollection.cs +++ b/src/AsmResolver.DotNet/Collections/ParameterCollection.cs @@ -15,7 +15,7 @@ namespace AsmResolver.DotNet.Collections [DebuggerDisplay("Count = {" + nameof(Count) + "}")] public class ParameterCollection : IReadOnlyList { - private readonly IList _parameters = new List(); + private readonly List _parameters = new List(); private readonly MethodDefinition _owner; private bool _hasThis; diff --git a/src/AsmResolver.DotNet/Config/Json/RuntimeConfiguration.cs b/src/AsmResolver.DotNet/Config/Json/RuntimeConfiguration.cs index 3a8ef6863..227751398 100644 --- a/src/AsmResolver.DotNet/Config/Json/RuntimeConfiguration.cs +++ b/src/AsmResolver.DotNet/Config/Json/RuntimeConfiguration.cs @@ -1,6 +1,5 @@ using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; namespace AsmResolver.DotNet.Config.Json { @@ -9,13 +8,6 @@ namespace AsmResolver.DotNet.Config.Json /// public class RuntimeConfiguration { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - /// /// Parses runtime configuration from a JSON file. /// @@ -33,7 +25,7 @@ public class RuntimeConfiguration /// The parsed runtime configuration. public static RuntimeConfiguration? FromJson(string json) { - return JsonSerializer.Deserialize(json, JsonSerializerOptions); + return JsonSerializer.Deserialize(json, RuntimeConfigurationSerializerContext.Default.RuntimeConfiguration); } /// @@ -67,7 +59,7 @@ public RuntimeOptions RuntimeOptions /// The JSON string. public string ToJson() { - return JsonSerializer.Serialize(this, JsonSerializerOptions); + return JsonSerializer.Serialize(this, RuntimeConfigurationSerializerContext.Default.RuntimeConfiguration); } /// diff --git a/src/AsmResolver.DotNet/Config/Json/RuntimeConfigurationSerializerContext.cs b/src/AsmResolver.DotNet/Config/Json/RuntimeConfigurationSerializerContext.cs new file mode 100644 index 000000000..7f77c1dc6 --- /dev/null +++ b/src/AsmResolver.DotNet/Config/Json/RuntimeConfigurationSerializerContext.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace AsmResolver.DotNet.Config.Json +{ + [JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + WriteIndented = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(RuntimeConfiguration))] + internal partial class RuntimeConfigurationSerializerContext : JsonSerializerContext + { + } +} diff --git a/src/AsmResolver.DotNet/DefaultMetadataResolver.cs b/src/AsmResolver.DotNet/DefaultMetadataResolver.cs index 839feaa96..30bab5716 100644 --- a/src/AsmResolver.DotNet/DefaultMetadataResolver.cs +++ b/src/AsmResolver.DotNet/DefaultMetadataResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; @@ -11,7 +12,7 @@ namespace AsmResolver.DotNet /// public class DefaultMetadataResolver : IMetadataResolver { - private readonly IDictionary _typeCache; + private readonly ConcurrentDictionary _typeCache; private readonly SignatureComparer _comparer = new() { AcceptNewerAssemblyVersionNumbers = true @@ -24,7 +25,7 @@ public class DefaultMetadataResolver : IMetadataResolver public DefaultMetadataResolver(IAssemblyResolver assemblyResolver) { AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); - _typeCache = new Dictionary(); + _typeCache = new ConcurrentDictionary(); } /// @@ -54,7 +55,7 @@ public IAssemblyResolver AssemblyResolver // Check if type definition has changed since last lookup. if (typeDef.IsTypeOf(type.Namespace, type.Name)) return typeDef; - _typeCache.Remove(type); + _typeCache.TryRemove(type, out _); } return null; diff --git a/src/AsmResolver.DotNet/DotNetCorePathProvider.cs b/src/AsmResolver.DotNet/DotNetCorePathProvider.cs index 63c7cd756..c01e99357 100644 --- a/src/AsmResolver.DotNet/DotNetCorePathProvider.cs +++ b/src/AsmResolver.DotNet/DotNetCorePathProvider.cs @@ -13,8 +13,9 @@ namespace AsmResolver.DotNet public class DotNetCorePathProvider { private static readonly string[] DefaultDotNetUnixPaths = { - "/usr/share/dotnet/shared", - "/opt/dotnet/shared/" + "/usr/share/dotnet/", + "/usr/local/share/dotnet/", + "/opt/dotnet/" }; private static readonly Regex NetCoreRuntimePattern = new(@"\.NET( Core)? \d+\.\d+\.\d+"); @@ -23,7 +24,7 @@ public class DotNetCorePathProvider static DotNetCorePathProvider() { DefaultInstallationPath = FindDotNetPath(); - Default = new(); + Default = new DotNetCorePathProvider(); } /// @@ -153,8 +154,12 @@ public bool HasRuntimeInstalled(string runtimeName, Version runtimeVersion) private void DetectInstalledRuntimes(string installationDirectory) { installationDirectory = Path.Combine(installationDirectory, "shared"); + if (!Directory.Exists(installationDirectory)) + return; + foreach (string directory in Directory.EnumerateDirectories(installationDirectory)) _installedRuntimes.Add(new DotNetInstallationInfo(directory)); + _installedRuntimes.Sort(); } diff --git a/src/AsmResolver.DotNet/DynamicMethodDefinition.cs b/src/AsmResolver.DotNet/DynamicMethodDefinition.cs index 90e5865bf..b95d8f003 100644 --- a/src/AsmResolver.DotNet/DynamicMethodDefinition.cs +++ b/src/AsmResolver.DotNet/DynamicMethodDefinition.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; @@ -18,6 +18,7 @@ public class DynamicMethodDefinition : MethodDefinition /// /// Target Module /// Dynamic Method / Delegate / DynamicResolver + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls ResolveDynamicResolver and FromDynamicMethod")] public DynamicMethodDefinition(ModuleDefinition module,object dynamicMethodObj) : base(new MetadataToken(TableIndex.Method, 0)) { diff --git a/src/AsmResolver.DotNet/DynamicMethodHelper.cs b/src/AsmResolver.DotNet/DynamicMethodHelper.cs index 7e856d135..f3cece5db 100644 --- a/src/AsmResolver.DotNet/DynamicMethodHelper.cs +++ b/src/AsmResolver.DotNet/DynamicMethodHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -81,6 +81,7 @@ private static void InterpretEHInfo(CilMethodBody methodBody, ReferenceImporter } } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls GetTypes")] public static object ResolveDynamicResolver(object dynamicMethodObj) { //Convert dynamicMethodObj to DynamicResolver diff --git a/src/AsmResolver.DotNet/EventDefinition.cs b/src/AsmResolver.DotNet/EventDefinition.cs index 649ce3734..f319e1270 100644 --- a/src/AsmResolver.DotNet/EventDefinition.cs +++ b/src/AsmResolver.DotNet/EventDefinition.cs @@ -136,19 +136,19 @@ public IList Semantics } /// - /// Gets the method definition representing the add accessor of this event definition. + /// Gets the method definition representing the first add accessor of this event definition. /// public MethodDefinition? AddMethod => Semantics.FirstOrDefault(s => s.Attributes == MethodSemanticsAttributes.AddOn)?.Method; /// - /// Gets the method definition representing the remove accessor of this event definition. + /// Gets the method definition representing the first remove accessor of this event definition. /// public MethodDefinition? RemoveMethod => Semantics.FirstOrDefault(s => s.Attributes == MethodSemanticsAttributes.RemoveOn)?.Method; /// - /// Gets the method definition representing the fire accessor of this event definition. + /// Gets the method definition representing the first fire accessor of this event definition. /// public MethodDefinition? FireMethod => Semantics.FirstOrDefault(s => s.Attributes == MethodSemanticsAttributes.Fire)?.Method; @@ -164,12 +164,36 @@ public IList CustomAttributes } } + /// + /// Clear and apply these methods to the event definition. + /// + /// The method definition representing the add accessor of this event definition. + /// The method definition representing the remove accessor of this event definition. + /// The method definition representing the fire accessor of this event definition. + public void SetSemanticMethods(MethodDefinition? addMethod, MethodDefinition? removeMethod, MethodDefinition? fireMethod) + { + Semantics.Clear(); + if (addMethod is not null) + Semantics.Add(new MethodSemantics(addMethod, MethodSemanticsAttributes.AddOn)); + if (removeMethod is not null) + Semantics.Add(new MethodSemantics(removeMethod, MethodSemanticsAttributes.RemoveOn)); + if (fireMethod is not null) + Semantics.Add(new MethodSemantics(fireMethod, MethodSemanticsAttributes.Fire)); + } + /// public bool IsAccessibleFromType(TypeDefinition type) => Semantics.Any(s => s.Method?.IsAccessibleFromType(type) ?? false); IMemberDefinition IMemberDescriptor.Resolve() => this; + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (EventType?.IsImportedInModule(module) ?? false); + } + /// /// Obtains the list of custom attributes assigned to the member. /// @@ -181,7 +205,7 @@ public IList CustomAttributes new OwnedCollection(this); /// - /// Obtains the name of the property definition. + /// Obtains the name of the event definition. /// /// The name. /// @@ -190,7 +214,7 @@ public IList CustomAttributes protected virtual Utf8String? GetName() => null; /// - /// Obtains the event type of the property definition. + /// Obtains the event type of the event definition. /// /// The event type. /// @@ -199,7 +223,7 @@ public IList CustomAttributes protected virtual ITypeDefOrRef? GetEventType() => null; /// - /// Obtains the declaring type of the property definition. + /// Obtains the declaring type of the event definition. /// /// The declaring type. /// @@ -208,7 +232,7 @@ public IList CustomAttributes protected virtual TypeDefinition? GetDeclaringType() => null; /// - /// Obtains the methods associated to this property definition. + /// Obtains the methods associated to this event definition. /// /// The method semantic objects. /// diff --git a/src/AsmResolver.DotNet/ExportedType.cs b/src/AsmResolver.DotNet/ExportedType.cs index 935a0a795..bf83bd24d 100644 --- a/src/AsmResolver.DotNet/ExportedType.cs +++ b/src/AsmResolver.DotNet/ExportedType.cs @@ -28,8 +28,8 @@ public class ExportedType : protected ExportedType(MetadataToken token) : base(token) { - _name = new LazyVariable(() => GetName()); - _namespace = new LazyVariable(() => GetNamespace()); + _name = new LazyVariable(GetName); + _namespace = new LazyVariable(GetNamespace); _implementation = new LazyVariable(GetImplementation); } @@ -145,6 +145,13 @@ public IList CustomAttributes /// public TypeDefinition? Resolve() => Module?.MetadataResolver.ResolveType(this); + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Implementation?.IsImportedInModule(module) ?? false); + } + IMemberDefinition? IMemberDescriptor.Resolve() => Resolve(); /// diff --git a/src/AsmResolver.DotNet/FieldDefinition.cs b/src/AsmResolver.DotNet/FieldDefinition.cs index 08dc3832d..585f5ef3d 100644 --- a/src/AsmResolver.DotNet/FieldDefinition.cs +++ b/src/AsmResolver.DotNet/FieldDefinition.cs @@ -386,6 +386,13 @@ public IList CustomAttributes FieldDefinition IFieldDescriptor.Resolve() => this; + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + IMemberDefinition IMemberDescriptor.Resolve() => this; /// diff --git a/src/AsmResolver.DotNet/FileReference.cs b/src/AsmResolver.DotNet/FileReference.cs index e8f78cdb0..ef1f3c225 100644 --- a/src/AsmResolver.DotNet/FileReference.cs +++ b/src/AsmResolver.DotNet/FileReference.cs @@ -120,6 +120,9 @@ public IList CustomAttributes } } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// /// Obtains the name of the referenced file. /// diff --git a/src/AsmResolver.DotNet/IImplementation.cs b/src/AsmResolver.DotNet/IImplementation.cs index 1674e3120..7d99282d7 100644 --- a/src/AsmResolver.DotNet/IImplementation.cs +++ b/src/AsmResolver.DotNet/IImplementation.cs @@ -4,7 +4,7 @@ namespace AsmResolver.DotNet /// Represents a member that is either a reference to an external file, assembly or type, and can be referenced by /// an Implementation coded index. /// - public interface IImplementation : IFullNameProvider, IModuleProvider, IHasCustomAttribute + public interface IImplementation : IFullNameProvider, IModuleProvider, IHasCustomAttribute, IImportable { } } diff --git a/src/AsmResolver.DotNet/IImportable.cs b/src/AsmResolver.DotNet/IImportable.cs new file mode 100644 index 000000000..9fdbc2c76 --- /dev/null +++ b/src/AsmResolver.DotNet/IImportable.cs @@ -0,0 +1,19 @@ +namespace AsmResolver.DotNet +{ + /// + /// Represents an entity in a .NET module that can be imported using the . + /// + public interface IImportable + { + /// + /// Determines whether the descriptor of the member is fully imported in the provided module. + /// + /// The module that is supposed to import the member. + /// true if the descriptor of the member is fully imported by the module, false otherwise. + /// + /// This method verifies all references in the descriptor of the member only. It does not verify any additional + /// data or contents (such as a method body) associated to the member. + /// + bool IsImportedInModule(ModuleDefinition module); + } +} diff --git a/src/AsmResolver.DotNet/IMemberDescriptor.cs b/src/AsmResolver.DotNet/IMemberDescriptor.cs index 75cf4f91d..e837e453b 100644 --- a/src/AsmResolver.DotNet/IMemberDescriptor.cs +++ b/src/AsmResolver.DotNet/IMemberDescriptor.cs @@ -3,7 +3,7 @@ namespace AsmResolver.DotNet /// /// Provides members for describing a (reference to a) member defined in a .NET assembly. /// - public interface IMemberDescriptor : IFullNameProvider, IModuleProvider + public interface IMemberDescriptor : IFullNameProvider, IModuleProvider, IImportable { /// /// When this member is defined in a type, gets the enclosing type. diff --git a/src/AsmResolver.DotNet/IResolutionScope.cs b/src/AsmResolver.DotNet/IResolutionScope.cs index 22c397ff0..444a63298 100644 --- a/src/AsmResolver.DotNet/IResolutionScope.cs +++ b/src/AsmResolver.DotNet/IResolutionScope.cs @@ -3,7 +3,7 @@ namespace AsmResolver.DotNet /// /// Represents a member that can be referenced by a ResolutionScope coded index. /// - public interface IResolutionScope : IMetadataMember, INameProvider, IModuleProvider + public interface IResolutionScope : IMetadataMember, INameProvider, IModuleProvider, IImportable { /// /// Gets the underlying assembly that this scope defines. diff --git a/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs b/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs index 7e63115fc..a4fd0d641 100644 --- a/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs +++ b/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs @@ -73,6 +73,9 @@ public static InvalidTypeDefOrRef Get(InvalidTypeSignatureError error) return instance; } + /// + public bool IsImportedInModule(ModuleDefinition module) => false; + IMemberDefinition? IMemberDescriptor.Resolve() => null; TypeDefinition? ITypeDescriptor.Resolve() => null; diff --git a/src/AsmResolver.DotNet/MemberReference.cs b/src/AsmResolver.DotNet/MemberReference.cs index 000940b0a..8254f8a3a 100644 --- a/src/AsmResolver.DotNet/MemberReference.cs +++ b/src/AsmResolver.DotNet/MemberReference.cs @@ -153,6 +153,13 @@ public IList CustomAttributes throw new ArgumentOutOfRangeException(); } + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + FieldDefinition? IFieldDescriptor.Resolve() { if (!IsField) diff --git a/src/AsmResolver.DotNet/MethodDefinition.cs b/src/AsmResolver.DotNet/MethodDefinition.cs index e79a423a4..59b6eb989 100644 --- a/src/AsmResolver.DotNet/MethodDefinition.cs +++ b/src/AsmResolver.DotNet/MethodDefinition.cs @@ -691,6 +691,13 @@ public IList GenericParameters MethodDefinition IMethodDescriptor.Resolve() => this; + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + IMemberDefinition IMemberDescriptor.Resolve() => this; /// diff --git a/src/AsmResolver.DotNet/MethodSpecification.cs b/src/AsmResolver.DotNet/MethodSpecification.cs index 02da0eb30..430c9952e 100644 --- a/src/AsmResolver.DotNet/MethodSpecification.cs +++ b/src/AsmResolver.DotNet/MethodSpecification.cs @@ -99,6 +99,13 @@ public IList CustomAttributes /// public MethodDefinition? Resolve() => Method?.Resolve(); + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return (Method?.IsImportedInModule(module) ?? false) + && (Signature?.IsImportedInModule(module) ?? false); + } + IMemberDefinition? IMemberDescriptor.Resolve() => Resolve(); /// diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index d6b06f18a..36b89e058 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -1104,6 +1104,9 @@ protected IAssemblyResolver CreateAssemblyResolver(IFileService fileService) /// public override string ToString() => Name ?? string.Empty; + /// + bool IImportable.IsImportedInModule(ModuleDefinition module) => this == module; + /// /// Rebuilds the .NET module to a portable executable file and writes it to the file system. /// diff --git a/src/AsmResolver.DotNet/ModuleReference.cs b/src/AsmResolver.DotNet/ModuleReference.cs index 4feacbf12..9191d9ad7 100644 --- a/src/AsmResolver.DotNet/ModuleReference.cs +++ b/src/AsmResolver.DotNet/ModuleReference.cs @@ -76,6 +76,9 @@ public IList CustomAttributes } } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// /// Obtains the name of the module. /// diff --git a/src/AsmResolver.DotNet/PropertyDefinition.cs b/src/AsmResolver.DotNet/PropertyDefinition.cs index 05c94c9b2..3df05c38c 100644 --- a/src/AsmResolver.DotNet/PropertyDefinition.cs +++ b/src/AsmResolver.DotNet/PropertyDefinition.cs @@ -170,23 +170,44 @@ public IList CustomAttributes } /// - /// Gets the method definition representing the get accessor of this property definition. + /// Gets the method definition representing the first get accessor of this property definition. /// public MethodDefinition? GetMethod => Semantics.FirstOrDefault(s => s.Attributes == MethodSemanticsAttributes.Getter)?.Method; /// - /// Gets the method definition representing the set accessor of this property definition. + /// Gets the method definition representing the first set accessor of this property definition. /// public MethodDefinition? SetMethod => Semantics.FirstOrDefault(s => s.Attributes == MethodSemanticsAttributes.Setter)?.Method; + /// + /// Clear and apply these methods to the property definition. + /// + /// The method definition representing the get accessor of this property definition. + /// The method definition representing the set accessor of this property definition. + public void SetSemanticMethods(MethodDefinition? getMethod, MethodDefinition? setMethod) + { + Semantics.Clear(); + if (getMethod is not null) + Semantics.Add(new MethodSemantics(getMethod, MethodSemanticsAttributes.Getter)); + if (setMethod is not null) + Semantics.Add(new MethodSemantics(setMethod, MethodSemanticsAttributes.Setter)); + } + /// public bool IsAccessibleFromType(TypeDefinition type) => Semantics.Any(s => s.Method?.IsAccessibleFromType(type) ?? false); IMemberDefinition IMemberDescriptor.Resolve() => this; + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + /// /// Obtains the name of the property definition. /// diff --git a/src/AsmResolver.DotNet/ReferenceImporter.cs b/src/AsmResolver.DotNet/ReferenceImporter.cs index b1c858e76..8af788d7d 100644 --- a/src/AsmResolver.DotNet/ReferenceImporter.cs +++ b/src/AsmResolver.DotNet/ReferenceImporter.cs @@ -47,7 +47,7 @@ public IResolutionScope ImportScope(IResolutionScope? scope) { if (scope is null) throw new ArgumentNullException(nameof(scope)); - if (scope.Module == TargetModule) + if (scope.IsImportedInModule(TargetModule)) return scope; return scope switch @@ -69,7 +69,7 @@ protected virtual AssemblyReference ImportAssembly(AssemblyDescriptor assembly) { if (assembly is null) throw new ArgumentNullException(nameof(assembly)); - if (assembly is AssemblyReference r && r.Module == TargetModule) + if (assembly is AssemblyReference r && assembly.IsImportedInModule(TargetModule)) return r; var reference = TargetModule.AssemblyReferences.FirstOrDefault(a => _comparer.Equals(a, assembly)); @@ -92,7 +92,7 @@ public virtual ModuleReference ImportModule(ModuleReference module) { if (module is null) throw new ArgumentNullException(nameof(module)); - if (module.Module == TargetModule) + if (module.IsImportedInModule(TargetModule)) return module; var reference = TargetModule.ModuleReferences.FirstOrDefault(a => _comparer.Equals(a, module)); @@ -139,10 +139,14 @@ protected virtual ITypeDefOrRef ImportType(TypeDefinition type) { AssertTypeIsValid(type); - if (type.Module == TargetModule) + if (type.IsImportedInModule(TargetModule)) return type; - return new TypeReference(TargetModule, ImportScope(type.Module!), type.Namespace, type.Name); + return new TypeReference( + TargetModule, + ImportScope(((ITypeDescriptor) type).Scope), + type.Namespace, + type.Name); } /// @@ -154,7 +158,7 @@ protected virtual ITypeDefOrRef ImportType(TypeReference type) { AssertTypeIsValid(type); - if (type.Module == TargetModule) + if (type.IsImportedInModule(TargetModule)) return type; return new TypeReference(TargetModule, ImportScope(type.Scope!), type.Namespace, type.Name); @@ -171,7 +175,7 @@ protected virtual ITypeDefOrRef ImportType(TypeSpecification type) if (type.Signature is null) throw new ArgumentNullException(nameof(type)); - if (type.Module == TargetModule) + if (type.IsImportedInModule(TargetModule)) return type; return new TypeSpecification(ImportTypeSignature(type.Signature)); @@ -186,7 +190,7 @@ public virtual TypeSignature ImportTypeSignature(TypeSignature type) { if (type is null) throw new ArgumentNullException(nameof(type)); - if (type.Module == TargetModule) + if (type.IsImportedInModule(TargetModule)) return type; return type.AcceptVisitor(this); @@ -212,11 +216,8 @@ public virtual ITypeDefOrRef ImportType(Type type) throw new ArgumentNullException(nameof(type)); var importedTypeSig = ImportTypeSignature(type); - if (importedTypeSig is TypeDefOrRefSignature - || importedTypeSig is CorLibTypeSignature) - { + if (importedTypeSig is TypeDefOrRefSignature or CorLibTypeSignature) return importedTypeSig.GetUnderlyingTypeDefOrRef()!; - } return new TypeSpecification(importedTypeSig); } @@ -266,6 +267,7 @@ private TypeSignature ImportArrayType(Type type) var result = new ArrayTypeSignature(baseType); for (int i = 0; i < rank; i++) result.Dimensions.Add(new ArrayDimension()); + return result; } @@ -318,7 +320,7 @@ public virtual IMethodDefOrRef ImportMethod(IMethodDefOrRef method) if (method.Signature is null) throw new ArgumentException("Cannot import a method that does not have a signature."); - if (method.Module == TargetModule) + if (method.IsImportedInModule(TargetModule)) return method; return new MemberReference( @@ -406,7 +408,7 @@ public virtual MethodSpecification ImportMethod(MethodSpecification method) if (method.DeclaringType is null) throw new ArgumentException("Cannot import a method that is not added to a type."); - if (method.Module == TargetModule) + if (method.IsImportedInModule(TargetModule)) return method; var memberRef = ImportMethod(method.Method); @@ -479,7 +481,7 @@ public virtual IFieldDescriptor ImportField(IFieldDescriptor field) if (field.Signature is null) throw new ArgumentException("Cannot import a field that does not have a signature."); - if (field.Module == TargetModule) + if (field.IsImportedInModule(TargetModule)) return field; return new MemberReference( diff --git a/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs b/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs index adf51b04e..d192bf2ba 100644 --- a/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs +++ b/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs @@ -32,6 +32,9 @@ public ReflectionAssemblyDescriptor(ModuleDefinition parentModule, AssemblyName /// protected override Utf8String? GetCulture() => _assemblyName.CultureName; + /// + public override bool IsImportedInModule(ModuleDefinition module) => false; + /// public override bool IsCorLib => Name is not null && KnownCorLibs.KnownCorLibNames.Contains(Name); diff --git a/src/AsmResolver.DotNet/RequiresUnreferencedCodeAttribute.cs b/src/AsmResolver.DotNet/RequiresUnreferencedCodeAttribute.cs new file mode 100644 index 000000000..c5fd0fea6 --- /dev/null +++ b/src/AsmResolver.DotNet/RequiresUnreferencedCodeAttribute.cs @@ -0,0 +1,34 @@ +#if !NET6_0_OR_GREATER +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Indicates that the specified method requires dynamic access to code that is not + /// referenced statically, for example, through System.Reflection. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)] + internal sealed class RequiresUnreferencedCodeAttribute : Attribute + { + /// + /// Gets a message that contains information about the usage of unreferenced code. + /// + public string Message { get; } + + /// + /// Gets or sets an optional URL that contains more information about the method, + /// why it requires unreferenced code, and what options a consumer has to deal with + /// it. + /// + public string? Url { get; set; } + + /// + /// Initializes a new instance of the System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute + /// class with the specified message. + /// + /// A message that contains information about the usage of unreferenced code. + public RequiresUnreferencedCodeAttribute(string message) + { + Message = message; + } + } +} +#endif diff --git a/src/AsmResolver.DotNet/Signatures/CallingConventionSignature.cs b/src/AsmResolver.DotNet/Signatures/CallingConventionSignature.cs index f7d33ae5c..4ed13994d 100644 --- a/src/AsmResolver.DotNet/Signatures/CallingConventionSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/CallingConventionSignature.cs @@ -7,7 +7,7 @@ namespace AsmResolver.DotNet.Signatures /// Provides a base for all signature that deal with a calling convention. This includes most member signatures, /// such as method and field signatures. /// - public abstract class CallingConventionSignature : ExtendableBlobSignature + public abstract class CallingConventionSignature : ExtendableBlobSignature, IImportable { private const CallingConventionAttributes SignatureTypeMask = (CallingConventionAttributes)0xF; @@ -148,5 +148,8 @@ public bool IsSentinel set => Attributes = (Attributes & ~CallingConventionAttributes.Sentinel) | (value ? CallingConventionAttributes.Sentinel : 0); } + + /// + public abstract bool IsImportedInModule(ModuleDefinition module); } } diff --git a/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs b/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs index c17695745..240bd396e 100644 --- a/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs @@ -91,6 +91,18 @@ protected override void WriteContents(BlobSerializationContext context) TypeArguments[i].Write(context); } + /// + public override bool IsImportedInModule(ModuleDefinition module) + { + for (int i = 0; i < TypeArguments.Count; i++) + { + if (!TypeArguments[i].IsImportedInModule(module)) + return false; + } + + return true; + } + /// public override string ToString() { diff --git a/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs b/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs index ccf81a2a5..e8c8de393 100644 --- a/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs @@ -68,6 +68,18 @@ public IList VariableTypes get; } + /// + public override bool IsImportedInModule(ModuleDefinition module) + { + for (int i = 0; i < VariableTypes.Count; i++) + { + if (!VariableTypes[i].IsImportedInModule(module)) + return false; + } + + return true; + } + /// protected override void WriteContents(BlobSerializationContext context) { diff --git a/src/AsmResolver.DotNet/Signatures/MemberSignature.cs b/src/AsmResolver.DotNet/Signatures/MemberSignature.cs index a34b3bdba..2d5e412d1 100644 --- a/src/AsmResolver.DotNet/Signatures/MemberSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/MemberSignature.cs @@ -27,6 +27,9 @@ protected TypeSignature MemberReturnType set; } + /// + public override bool IsImportedInModule(ModuleDefinition module) => MemberReturnType.IsImportedInModule(module); + /// public override string ToString() { diff --git a/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs b/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs index 50452b60f..4e58f029a 100644 --- a/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs +++ b/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs @@ -78,6 +78,29 @@ public IList SentinelParameterTypes get; } = new List(); + /// + public override bool IsImportedInModule(ModuleDefinition module) + { + if (!ReturnType.IsImportedInModule(module)) + return false; + + for (int i = 0; i < ParameterTypes.Count; i++) + { + var x = ParameterTypes[i]; + if (!x.IsImportedInModule(module)) + return false; + } + + for (int i = 0; i < SentinelParameterTypes.Count; i++) + { + var x = SentinelParameterTypes[i]; + if (!x.IsImportedInModule(module)) + return false; + } + + return true; + } + /// /// Initializes the and properties by reading /// the parameter count, return type and parameter fields of the signature from the provided input stream. diff --git a/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeSignature.cs b/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeSignature.cs index f7afe504a..e31005637 100644 --- a/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeSignature.cs @@ -20,6 +20,7 @@ public partial class SignatureComparer : IEqualityComparer, IEqualityComparer, IEqualityComparer, + IEqualityComparer, IEqualityComparer>, IEqualityComparer> { @@ -59,6 +60,7 @@ public bool Equals(TypeSignature? x, TypeSignature? y) case ElementType.Boxed: return Equals(x as BoxedTypeSignature, y as BoxedTypeSignature); case ElementType.FnPtr: + return Equals(x as FunctionPointerTypeSignature, y as FunctionPointerTypeSignature); case ElementType.Internal: case ElementType.Modifier: throw new NotSupportedException(); @@ -309,6 +311,22 @@ public int GetHashCode(ArrayTypeSignature obj) } } + /// + public bool Equals(FunctionPointerTypeSignature? x, FunctionPointerTypeSignature? y) + { + if (ReferenceEquals(x, y)) + return true; + if (x is null || y is null) + return false; + return Equals(x.Signature, y.Signature); + } + + /// + public int GetHashCode(FunctionPointerTypeSignature obj) + { + return obj.Signature.GetHashCode(); + } + /// public bool Equals(IList? x, IList? y) { diff --git a/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeSignature.cs index 69db2a00c..ae2a6974c 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/CorLibTypeSignature.cs @@ -78,6 +78,9 @@ ElementType switch return Type.Resolve(); } + /// + public override bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() { diff --git a/src/AsmResolver.DotNet/Signatures/Types/CustomModifierTypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/CustomModifierTypeSignature.cs index 34baa3006..64ec085e1 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/CustomModifierTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/CustomModifierTypeSignature.cs @@ -75,6 +75,10 @@ public override string Name TState state) => visitor.VisitCustomModifierType(this, state); + /// + public override bool IsImportedInModule(ModuleDefinition module) => + ModifierType.IsImportedInModule(module) && base.IsImportedInModule(module); + /// protected override void WriteContents(BlobSerializationContext context) { diff --git a/src/AsmResolver.DotNet/Signatures/Types/FunctionPointerTypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/FunctionPointerTypeSignature.cs index d5aeb810c..ff2acc2c8 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/FunctionPointerTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/FunctionPointerTypeSignature.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; namespace AsmResolver.DotNet.Signatures.Types @@ -48,6 +49,9 @@ public MethodSignature Signature public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() => Signature?.ReturnType?.Module?.CorLibTypeFactory.IntPtr.Type; + /// + public override bool IsImportedInModule(ModuleDefinition module) => Signature.IsImportedInModule(module); + /// protected override void WriteContents(BlobSerializationContext context) { diff --git a/src/AsmResolver.DotNet/Signatures/Types/GenericInstanceTypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/GenericInstanceTypeSignature.cs index 6872344f2..7ed057d8b 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/GenericInstanceTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/GenericInstanceTypeSignature.cs @@ -108,6 +108,21 @@ public override bool IsValueType /// public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() => GenericType; + /// + public override bool IsImportedInModule(ModuleDefinition module) + { + if (!GenericType.IsImportedInModule(module)) + return false; + + for (int i = 0; i < TypeArguments.Count; i++) + { + if (!TypeArguments[i].IsImportedInModule(module)) + return false; + } + + return true; + } + /// protected override void WriteContents(BlobSerializationContext context) { diff --git a/src/AsmResolver.DotNet/Signatures/Types/GenericParameterSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/GenericParameterSignature.cs index f3af9b9d8..24f487016 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/GenericParameterSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/GenericParameterSignature.cs @@ -9,8 +9,6 @@ namespace AsmResolver.DotNet.Signatures.Types /// public class GenericParameterSignature : TypeSignature { - private readonly IResolutionScope? _scope; - /// /// Creates a new reference to a generic parameter. /// @@ -30,7 +28,7 @@ public GenericParameterSignature(GenericParameterType parameterType, int index) /// The index of the referenced parameter. public GenericParameterSignature(ModuleDefinition module, GenericParameterType parameterType, int index) { - _scope = module; + Scope = module; ParameterType = parameterType; Index = index; } @@ -74,7 +72,10 @@ public int Index public override string? Namespace => null; /// - public override IResolutionScope? Scope => _scope; + public override IResolutionScope? Scope + { + get; + } /// public override bool IsValueType => false; @@ -82,6 +83,9 @@ public int Index /// public override TypeDefinition? Resolve() => null; + /// + public override bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() => null; diff --git a/src/AsmResolver.DotNet/Signatures/Types/SentinelTypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/SentinelTypeSignature.cs index 79e5f7709..49e7faa18 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/SentinelTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/SentinelTypeSignature.cs @@ -33,6 +33,9 @@ public class SentinelTypeSignature : TypeSignature /// public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() => null; + /// + public override bool IsImportedInModule(ModuleDefinition module) => true; + /// protected override void WriteContents(BlobSerializationContext context) => context.Writer.WriteByte((byte) ElementType); diff --git a/src/AsmResolver.DotNet/Signatures/Types/TypeDefOrRefSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/TypeDefOrRefSignature.cs index 0222d9b59..cab577dc4 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/TypeDefOrRefSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/TypeDefOrRefSignature.cs @@ -60,6 +60,9 @@ public override bool IsValueType /// public override TypeDefinition? Resolve() => Type.Resolve(); + /// + public override bool IsImportedInModule(ModuleDefinition module) => Type.IsImportedInModule(module); + /// public override ITypeDefOrRef ToTypeDefOrRef() => Type; diff --git a/src/AsmResolver.DotNet/Signatures/Types/TypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/TypeSignature.cs index 2373fc3bf..f5419ddfe 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/TypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/TypeSignature.cs @@ -376,6 +376,9 @@ internal static void WriteFieldOrPropType(BlobSerializationContext context, Type public TypeSignature InstantiateGenericTypes(GenericContext context) => AcceptVisitor(GenericTypeActivator.Instance, context); + /// + public abstract bool IsImportedInModule(ModuleDefinition module); + /// /// Visit the current type signature using the provided visitor. /// diff --git a/src/AsmResolver.DotNet/Signatures/Types/TypeSpecificationSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/TypeSpecificationSignature.cs index cd0bf2d18..412964357 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/TypeSpecificationSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/TypeSpecificationSignature.cs @@ -23,6 +23,9 @@ public TypeSignature BaseType set; } + /// + public override ModuleDefinition? Module => BaseType.Module; + /// public override string? Namespace => BaseType.Namespace; @@ -37,6 +40,9 @@ public TypeSignature BaseType public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() => BaseType.GetUnderlyingTypeDefOrRef(); + /// + public override bool IsImportedInModule(ModuleDefinition module) => BaseType.IsImportedInModule(module); + /// protected override void WriteContents(BlobSerializationContext context) { diff --git a/src/AsmResolver.DotNet/TypeDefinition.cs b/src/AsmResolver.DotNet/TypeDefinition.cs index 65852f98c..d1757a8f8 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -680,6 +680,9 @@ public TypeSignature ToTypeSignature() ?? new TypeDefOrRefSignature(this, IsValueType); } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// public bool IsAccessibleFromType(TypeDefinition type) { diff --git a/src/AsmResolver.DotNet/TypeReference.cs b/src/AsmResolver.DotNet/TypeReference.cs index 57a5fe8b6..113a8892d 100644 --- a/src/AsmResolver.DotNet/TypeReference.cs +++ b/src/AsmResolver.DotNet/TypeReference.cs @@ -136,6 +136,9 @@ public TypeSignature ToTypeSignature() ?? new TypeDefOrRefSignature(this, IsValueType); } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + /// public TypeDefinition? Resolve() => Module?.MetadataResolver.ResolveType(this); diff --git a/src/AsmResolver.DotNet/TypeSpecification.cs b/src/AsmResolver.DotNet/TypeSpecification.cs index 9d9a4713d..d268bf85a 100644 --- a/src/AsmResolver.DotNet/TypeSpecification.cs +++ b/src/AsmResolver.DotNet/TypeSpecification.cs @@ -95,6 +95,9 @@ public IList CustomAttributes public TypeSignature ToTypeSignature() => Signature ?? throw new ArgumentException("Signature embedded into the type specification is null."); + /// + public bool IsImportedInModule(ModuleDefinition module) => Signature?.IsImportedInModule(module) ?? false; + /// public TypeDefinition? Resolve() => Module?.MetadataResolver.ResolveType(this); diff --git a/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj b/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj index 6341887ff..5c0bfda0f 100644 --- a/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj +++ b/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj @@ -9,6 +9,7 @@ true enable net6.0;netcoreapp3.1;netstandard2.0 + true diff --git a/src/AsmResolver.PE.File/IPEFile.cs b/src/AsmResolver.PE.File/IPEFile.cs index 6f22ca61a..57e4e50a9 100644 --- a/src/AsmResolver.PE.File/IPEFile.cs +++ b/src/AsmResolver.PE.File/IPEFile.cs @@ -61,6 +61,24 @@ PEMappingMode MappingMode get; } + /// + /// Gets or sets the padding data in between the last section header and the first section. + /// + public ISegment? ExtraSectionData + { + get; + set; + } + + /// + /// Gets or sets the data appended to the end of the file (EoF), if available. + /// + public ISegment? EofData + { + get; + set; + } + /// /// Finds the section containing the provided virtual address. /// diff --git a/src/AsmResolver.PE.File/PEFile.cs b/src/AsmResolver.PE.File/PEFile.cs index e89eac9ff..d1dc0f2d7 100644 --- a/src/AsmResolver.PE.File/PEFile.cs +++ b/src/AsmResolver.PE.File/PEFile.cs @@ -21,6 +21,7 @@ public class PEFile : IPEFile public const uint ValidPESignature = 0x4550; // "PE\0\0" private readonly LazyVariable _extraSectionData; + private readonly LazyVariable _eofData; private IList? _sections; /// @@ -43,6 +44,7 @@ public PEFile(DosHeader dosHeader, FileHeader fileHeader, OptionalHeader optiona FileHeader = fileHeader ?? throw new ArgumentNullException(nameof(fileHeader)); OptionalHeader = optionalHeader ?? throw new ArgumentNullException(nameof(optionalHeader)); _extraSectionData = new LazyVariable(GetExtraSectionData); + _eofData = new LazyVariable(GetEofData); MappingMode = PEMappingMode.Unmapped; } @@ -92,15 +94,20 @@ public PEMappingMode MappingMode protected set; } - /// - /// Gets or sets the padding data in between the last section header and the first section. - /// + /// public ISegment? ExtraSectionData { get => _extraSectionData.Value; set => _extraSectionData.Value = value; } + /// + public ISegment? EofData + { + get => _eofData.Value; + set => _eofData.Value = value; + } + /// /// Reads an unmapped PE file from the disk. /// @@ -360,7 +367,7 @@ public bool TryCreateReaderAtRva(uint rva, uint size, out BinaryStreamReader rea /// public void UpdateHeaders() { - var oldSections = Sections.Select(_ => _.CreateHeader()).ToList(); + var oldSections = Sections.Select(x => x.CreateHeader()).ToList(); FileHeader.NumberOfSections = (ushort) Sections.Count; @@ -383,6 +390,8 @@ public void UpdateHeaders() var lastSection = Sections[Sections.Count - 1]; OptionalHeader.SizeOfImage = lastSection.Rva + lastSection.GetVirtualSize().Align(OptionalHeader.SectionAlignment); + + EofData?.UpdateOffsets(lastSection.Offset + lastSection.GetPhysicalSize(), OptionalHeader.SizeOfImage); } /// @@ -474,28 +483,30 @@ public void Write(IBinaryStreamWriter writer) // NT headers writer.Offset = DosHeader.NextHeaderOffset; - writer.WriteUInt32(ValidPESignature); FileHeader.Write(writer); OptionalHeader.Write(writer); // Section headers. writer.Offset = OptionalHeader.Offset + FileHeader.SizeOfOptionalHeader; - foreach (var section in Sections) - section.CreateHeader().Write(writer); + for (int i = 0; i < Sections.Count; i++) + Sections[i].CreateHeader().Write(writer); // Data between section headers and sections. ExtraSectionData?.Write(writer); // Sections. - writer.Offset = OptionalHeader.SizeOfHeaders; - foreach (var section in Sections) + for (int i = 0; i < Sections.Count; i++) { + var section = Sections[i]; writer.Offset = section.Offset; section.Contents?.Write(writer); writer.Align(OptionalHeader.FileAlignment); } + + // EOF Data. + EofData?.Write(writer); } /// @@ -515,5 +526,14 @@ public void Write(IBinaryStreamWriter writer) /// This method is called upon the initialization of the property. /// protected virtual ISegment? GetExtraSectionData() => null; + + /// + /// Obtains any data appended to the end of the file (EoF). + /// + /// The extra data. + /// + /// This method is called upon the initialization of the property. + /// + protected virtual ISegment? GetEofData() => null; } } diff --git a/src/AsmResolver.PE.File/SerializedPEFile.cs b/src/AsmResolver.PE.File/SerializedPEFile.cs index ded5c5b04..c9bff4dab 100644 --- a/src/AsmResolver.PE.File/SerializedPEFile.cs +++ b/src/AsmResolver.PE.File/SerializedPEFile.cs @@ -10,7 +10,7 @@ namespace AsmResolver.PE.File /// public class SerializedPEFile : PEFile { - private readonly IList _sectionHeaders; + private readonly List _sectionHeaders; private readonly BinaryStreamReader _reader; /// @@ -46,7 +46,7 @@ public SerializedPEFile(in BinaryStreamReader reader, PEMappingMode mode) // Data between section headers and sections. int extraSectionDataLength = (int) (DosHeader.Offset + OptionalHeader.SizeOfHeaders - _reader.Offset); if (extraSectionDataLength != 0) - ExtraSectionData = DataSegment.FromReader(ref _reader, extraSectionDataLength); + ExtraSectionData = _reader.ReadSegment((uint) extraSectionDataLength); } /// @@ -77,5 +77,19 @@ protected override IList GetSections() return result; } + /// + protected override ISegment? GetEofData() + { + if (MappingMode != PEMappingMode.Unmapped) + return null; + + var lastSection = _sectionHeaders[_sectionHeaders.Count - 1]; + ulong offset = lastSection.PointerToRawData + lastSection.SizeOfRawData; + + var reader = _reader.ForkAbsolute(offset); + return reader.Length > 0 + ? reader.ReadSegment(reader.Length) + : null; + } } } diff --git a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj index 0baf5f6c0..219f54c0d 100644 --- a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj +++ b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj @@ -7,6 +7,7 @@ 1701;1702;NU5105 enable net6.0;netcoreapp3.1;netstandard2.0 + true diff --git a/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs b/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs index d7db9fd79..9904b1a1a 100644 --- a/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs +++ b/src/AsmResolver.PE.Win32Resources/Icon/IconResource.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -12,7 +12,7 @@ public class IconResource : IWin32Resource /// /// Used to keep track of icon groups. /// - private readonly IDictionary _entries = new Dictionary(); + private readonly Dictionary _entries = new Dictionary(); /// /// Obtains the icon group resources from the provided root win32 resources directory. diff --git a/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs b/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs index 2089b4eec..3d88a0b1a 100644 --- a/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs +++ b/src/AsmResolver.PE.Win32Resources/Version/StringTable.cs @@ -122,7 +122,7 @@ public static StringTable FromReader(ref BinaryStreamReader reader) return new KeyValuePair(header.Key, value); } - private readonly IDictionary _entries = new Dictionary(); + private readonly Dictionary _entries = new Dictionary(); /// /// Creates a new string table. diff --git a/src/AsmResolver.PE.Win32Resources/Version/VersionInfoResource.cs b/src/AsmResolver.PE.Win32Resources/Version/VersionInfoResource.cs index 2b7334ef2..666591d7c 100644 --- a/src/AsmResolver.PE.Win32Resources/Version/VersionInfoResource.cs +++ b/src/AsmResolver.PE.Win32Resources/Version/VersionInfoResource.cs @@ -98,7 +98,7 @@ private static VersionTableEntry ReadNextEntry(ref BinaryStreamReader reader) } private FixedVersionInfo _fixedVersionInfo = new FixedVersionInfo(); - private readonly IDictionary _entries = new Dictionary(); + private readonly Dictionary _entries = new Dictionary(); /// public override string Key => VsVersionInfoKey; diff --git a/src/AsmResolver.PE/AsmResolver.PE.csproj b/src/AsmResolver.PE/AsmResolver.PE.csproj index 43c3a61d5..058547040 100644 --- a/src/AsmResolver.PE/AsmResolver.PE.csproj +++ b/src/AsmResolver.PE/AsmResolver.PE.csproj @@ -8,6 +8,7 @@ 1701;1702;NU5105 enable net6.0;netcoreapp3.1;netstandard2.0 + true diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs index 017bb9490..4d903de19 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs @@ -31,7 +31,7 @@ public class TablesStream : SegmentBase, IMetadataStream /// public const string UncompressedStreamName = "#Schema"; - private readonly IDictionary _indexEncoders; + private readonly Dictionary _indexEncoders; private readonly LazyVariable> _tables; private readonly LazyVariable> _layouts; diff --git a/src/AsmResolver/AsmResolver.csproj b/src/AsmResolver/AsmResolver.csproj index da0e0e6cb..07dbf2281 100644 --- a/src/AsmResolver/AsmResolver.csproj +++ b/src/AsmResolver/AsmResolver.csproj @@ -3,11 +3,12 @@ AsmResolver The base library for the AsmResolver executable file inspection toolsuite. - exe pe dotnet cil inspection manipulation assembly disassembly + exe pe dotnet cil inspection manipulation assembly disassembly true 1701;1702;NU5105 enable net6.0;netcoreapp3.1;netstandard2.0 + true diff --git a/test/AsmResolver.Benchmarks/DotNetBundleBenchmark.cs b/test/AsmResolver.Benchmarks/DotNetBundleBenchmark.cs new file mode 100644 index 000000000..00acd4fc9 --- /dev/null +++ b/test/AsmResolver.Benchmarks/DotNetBundleBenchmark.cs @@ -0,0 +1,19 @@ +using System.IO; +using AsmResolver.DotNet.Bundles; +using BenchmarkDotNet.Attributes; + +namespace AsmResolver.Benchmarks +{ + [MemoryDiagnoser] + public class DotNetBundleBenchmark + { + private static readonly byte[] HelloWorldSingleFileV6 = Properties.Resources.HelloWorld_SingleFile_V6; + private readonly MemoryStream _outputStream = new(); + + [Benchmark] + public void ReadBundleManifestV6() + { + _ = BundleManifest.FromBytes(HelloWorldSingleFileV6); + } + } +} diff --git a/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs b/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs index 86ed39706..ee7805eca 100644 --- a/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs +++ b/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs @@ -99,5 +99,15 @@ public class Resources { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] HelloWorld_SingleFile_V6 { + get { + object obj = ResourceManager.GetObject("HelloWorld_SingleFile_V6", resourceCulture); + return ((byte[])(obj)); + } + } } } diff --git a/test/AsmResolver.Benchmarks/Properties/Resources.resx b/test/AsmResolver.Benchmarks/Properties/Resources.resx index 83f78bc25..585e32796 100644 --- a/test/AsmResolver.Benchmarks/Properties/Resources.resx +++ b/test/AsmResolver.Benchmarks/Properties/Resources.resx @@ -130,4 +130,7 @@ ..\Resources\HelloWorld.ManyMethods.deflate;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\HelloWorld.SingleFile.v6.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + diff --git a/test/AsmResolver.Benchmarks/Resources/HelloWorld.SingleFile.v6.exe b/test/AsmResolver.Benchmarks/Resources/HelloWorld.SingleFile.v6.exe new file mode 100644 index 000000000..e980f978e Binary files /dev/null and b/test/AsmResolver.Benchmarks/Resources/HelloWorld.SingleFile.v6.exe differ diff --git a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj index 30de93753..a29c5c0d5 100644 --- a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj +++ b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleFileTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleFileTest.cs new file mode 100644 index 000000000..cf0bddfb0 --- /dev/null +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleFileTest.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Text; +using AsmResolver.DotNet.Bundles; +using AsmResolver.PE.DotNet.Metadata.Strings; +using AsmResolver.PE.DotNet.Metadata.Tables; +using AsmResolver.PE.DotNet.Metadata.Tables.Rows; +using Xunit; + +namespace AsmResolver.DotNet.Tests.Bundles +{ + public class BundleFileTest + { + [Fact] + public void ReadUncompressedStringContents() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + var file = manifest.Files.First(f => f.Type == BundleFileType.RuntimeConfigJson); + string contents = Encoding.UTF8.GetString(file.GetData()); + + Assert.Equal(@"{ + ""runtimeOptions"": { + ""tfm"": ""net6.0"", + ""framework"": { + ""name"": ""Microsoft.NETCore.App"", + ""version"": ""6.0.0"" + }, + ""configProperties"": { + ""System.Reflection.Metadata.MetadataUpdater.IsSupported"": false + } + } +}", contents); + } + + [Fact] + public void ReadUncompressedAssemblyContents() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + var bundleFile = manifest.Files.First(f => f.RelativePath == "HelloWorld.dll"); + + var embeddedImage = ModuleDefinition.FromBytes(bundleFile.GetData()); + Assert.Equal("HelloWorld.dll", embeddedImage.Name); + } + } +} diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs new file mode 100644 index 000000000..423509378 --- /dev/null +++ b/test/AsmResolver.DotNet.Tests/Bundles/BundleManifestTest.cs @@ -0,0 +1,288 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using AsmResolver.DotNet.Bundles; +using AsmResolver.IO; +using AsmResolver.PE; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; +using AsmResolver.PE.Win32Resources.Version; +using AsmResolver.Tests.Runners; +using Xunit; + +namespace AsmResolver.DotNet.Tests.Bundles +{ + public class BundleManifestTest : IClassFixture + { + private readonly TemporaryDirectoryFixture _fixture; + + public BundleManifestTest(TemporaryDirectoryFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void ReadBundleManifestHeaderV1() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V1); + Assert.Equal(1u, manifest.MajorVersion); + Assert.Equal("j7LK4is5ipe1CCtiafaTb8uhSOR7JhI=", manifest.BundleID); + Assert.Equal(new[] + { + "HelloWorld.dll", "HelloWorld.deps.json", "HelloWorld.runtimeconfig.json" + }, manifest.Files.Select(f => f.RelativePath)); + } + + [Fact] + public void ReadBundleManifestHeaderV2() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V2); + Assert.Equal(2u, manifest.MajorVersion); + Assert.Equal("poUQ+RBCefcEL4xrSAXdE2I5M+5D_Pk=", manifest.BundleID); + Assert.Equal(new[] + { + "HelloWorld.dll", "HelloWorld.deps.json", "HelloWorld.runtimeconfig.json" + }, manifest.Files.Select(f => f.RelativePath)); + } + + [Fact] + public void ReadBundleManifestHeaderV6() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + Assert.Equal(6u, manifest.MajorVersion); + Assert.Equal("lc43r48XAQNxN7Cx8QQvO9JgZI5lqPA=", manifest.BundleID); + Assert.Equal(new[] + { + "HelloWorld.dll", "HelloWorld.deps.json", "HelloWorld.runtimeconfig.json" + }, manifest.Files.Select(f => f.RelativePath)); + } + + [SkippableFact] + public void WriteBundleManifestV1Windows() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + AssertWriteManifestWindowsPreservesOutput( + BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V1), + "3.1", + "HelloWorld.dll", + $"Hello, World!{Environment.NewLine}"); + } + + [SkippableFact] + public void WriteBundleManifestV2Windows() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + AssertWriteManifestWindowsPreservesOutput( + BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V2), + "5.0", + "HelloWorld.dll", + $"Hello, World!{Environment.NewLine}"); + } + + [SkippableFact] + public void WriteBundleManifestV6Windows() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + AssertWriteManifestWindowsPreservesOutput( + BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6), + "6.0", + "HelloWorld.dll", + $"Hello, World!{Environment.NewLine}"); + } + + [SkippableFact] + public void MarkFilesAsCompressed() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + manifest.Files.First(f => f.RelativePath == "HelloWorld.dll").Compress(); + + using var stream = new MemoryStream(); + ulong address = manifest.WriteManifest(new BinaryStreamWriter(stream), false); + + var reader = ByteArrayDataSource.CreateReader(stream.ToArray()); + reader.Offset = address; + var newManifest = BundleManifest.FromReader(reader); + AssertBundlesAreEqual(manifest, newManifest); + } + + [SkippableTheory()] + [InlineData(SubSystem.WindowsCui)] + [InlineData(SubSystem.WindowsGui)] + public void WriteWithSubSystem(SubSystem subSystem) + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + string appHostTemplatePath = FindAppHostTemplate("6.0"); + + using var stream = new MemoryStream(); + manifest.WriteUsingTemplate(stream, new BundlerParameters(appHostTemplatePath, "HelloWorld.dll") + { + SubSystem = subSystem + }); + + var newFile = PEFile.FromBytes(stream.ToArray()); + Assert.Equal(subSystem, newFile.OptionalHeader.SubSystem); + } + + [SkippableFact] + public void WriteWithWin32Resources() + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6_WithResources); + string appHostTemplatePath = FindAppHostTemplate("6.0"); + + // Obtain expected version info. + var oldImage = PEImage.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6_WithResources); + var versionInfo = VersionInfoResource.FromDirectory(oldImage.Resources!)!; + + // Bundle with PE image as template for PE headers and resources. + using var stream = new MemoryStream(); + manifest.WriteUsingTemplate(stream, new BundlerParameters( + File.ReadAllBytes(appHostTemplatePath), + "HelloWorld.dll", + oldImage)); + + // Verify new file still runs as expected. + string output = _fixture + .GetRunner() + .RunAndCaptureOutput("HelloWorld.exe", stream.ToArray()); + + Assert.Equal($"Hello, World!{Environment.NewLine}", output); + + // Verify that resources were added properly. + var newImage = PEImage.FromBytes(stream.ToArray()); + Assert.NotNull(newImage.Resources); + var newVersionInfo = VersionInfoResource.FromDirectory(newImage.Resources); + Assert.NotNull(newVersionInfo); + Assert.Equal(versionInfo.FixedVersionInfo.FileVersion, newVersionInfo.FixedVersionInfo.FileVersion); + } + + [Fact] + public void NewManifestShouldGenerateBundleIdIfUnset() + { + var manifest = new BundleManifest(6); + + manifest.Files.Add(new BundleFile("HelloWorld.dll", BundleFileType.Assembly, + Properties.Resources.HelloWorld_NetCore)); + manifest.Files.Add(new BundleFile("HelloWorld.runtimeconfig.json", BundleFileType.RuntimeConfigJson, + Encoding.UTF8.GetBytes(@"{ + ""runtimeOptions"": { + ""tfm"": ""net6.0"", + ""includedFrameworks"": [ + { + ""name"": ""Microsoft.NETCore.App"", + ""version"": ""6.0.0"" + } + ] + } +}"))); + + Assert.Null(manifest.BundleID); + + using var stream = new MemoryStream(); + manifest.WriteUsingTemplate(stream, new BundlerParameters( + FindAppHostTemplate("6.0"), + "HelloWorld.dll")); + + Assert.NotNull(manifest.BundleID); + } + + [Fact] + public void SameManifestContentsShouldResultInSameBundleID() + { + var manifest = BundleManifest.FromBytes(Properties.Resources.HelloWorld_SingleFile_V6); + + var newManifest = new BundleManifest(manifest.MajorVersion); + foreach (var file in manifest.Files) + newManifest.Files.Add(new BundleFile(file.RelativePath, file.Type, file.GetData())); + + Assert.Equal(manifest.BundleID, newManifest.GenerateDeterministicBundleID()); + } + + private void AssertWriteManifestWindowsPreservesOutput( + BundleManifest manifest, + string sdkVersion, + string fileName, + string expectedOutput, + [CallerFilePath] string className = "File", + [CallerMemberName] string methodName = "Method") + { + string appHostTemplatePath = FindAppHostTemplate(sdkVersion); + + using var stream = new MemoryStream(); + manifest.WriteUsingTemplate(stream, new BundlerParameters(appHostTemplatePath, fileName)); + + var newManifest = BundleManifest.FromBytes(stream.ToArray()); + AssertBundlesAreEqual(manifest, newManifest); + + string output = _fixture + .GetRunner() + .RunAndCaptureOutput( + Path.ChangeExtension(fileName, ".exe"), + stream.ToArray(), + null, + 5000, + className, + methodName); + + Assert.Equal(expectedOutput, output); + } + + private static string FindAppHostTemplate(string sdkVersion) + { + string sdkPath = Path.Combine(DotNetCorePathProvider.DefaultInstallationPath!, "sdk"); + string? sdkVersionPath = null; + foreach (string dir in Directory.GetDirectories(sdkPath)) + { + if (Path.GetFileName(dir).StartsWith(sdkVersion)) + { + sdkVersionPath = Path.Combine(dir); + break; + } + } + + if (string.IsNullOrEmpty(sdkVersionPath)) + { + throw new InvalidOperationException( + $"Could not find the apphost template for .NET SDK version {sdkVersion}. This is an indication that the test environment does not have this SDK installed."); + } + + string fileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) + ? "apphost.exe" + : "apphost"; + + string finalPath = Path.Combine(sdkVersionPath, "AppHostTemplate", fileName); + if (!File.Exists(finalPath)) + { + throw new InvalidOperationException( + $"Could not find the apphost template for .NET SDK version {sdkVersion}. This is an indication that the test environment does not have this SDK installed."); + } + + return finalPath; + } + + private static void AssertBundlesAreEqual(BundleManifest manifest, BundleManifest newManifest) + { + Assert.Equal(manifest.MajorVersion, newManifest.MajorVersion); + Assert.Equal(manifest.MinorVersion, newManifest.MinorVersion); + Assert.Equal(manifest.BundleID, newManifest.BundleID); + + Assert.Equal(manifest.Files.Count, newManifest.Files.Count); + for (int i = 0; i < manifest.Files.Count; i++) + { + var file = manifest.Files[i]; + var newFile = newManifest.Files[i]; + Assert.Equal(file.Type, newFile.Type); + Assert.Equal(file.RelativePath, newFile.RelativePath); + Assert.Equal(file.IsCompressed, newFile.IsCompressed); + Assert.Equal(file.GetData(), newFile.GetData()); + } + } + } +} diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index 141a67ba8..c2e4010ea 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -136,6 +136,34 @@ public class Resources { } } + public static byte[] HelloWorld_SingleFile_V1 { + get { + object obj = ResourceManager.GetObject("HelloWorld_SingleFile_V1", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] HelloWorld_SingleFile_V2 { + get { + object obj = ResourceManager.GetObject("HelloWorld_SingleFile_V2", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] HelloWorld_SingleFile_V6 { + get { + object obj = ResourceManager.GetObject("HelloWorld_SingleFile_V6", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] HelloWorld_SingleFile_V6_WithResources { + get { + object obj = ResourceManager.GetObject("HelloWorld_SingleFile_V6_WithResources", resourceCulture); + return ((byte[])(obj)); + } + } + public static byte[] Assembly1_Forwarder { get { object obj = ResourceManager.GetObject("Assembly1_Forwarder", resourceCulture); diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index c6ab516aa..5e722a4a9 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -57,6 +57,18 @@ ..\Resources\HelloWorld.WithAttribute.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\HelloWorld.SingleFile.v1.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\HelloWorld.SingleFile.v2.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\HelloWorld.SingleFile.v6.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\HelloWorld.SingleFile.v6.WithResources.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\Assembly1_Forwarder.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs b/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs index be9f40fc4..83ec26082 100644 --- a/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs +++ b/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs @@ -11,9 +11,9 @@ namespace AsmResolver.DotNet.Tests { public class ReferenceImporterTest { - private static readonly SignatureComparer _comparer = new SignatureComparer(); + private static readonly SignatureComparer Comparer = new(); - private readonly AssemblyReference _dummyAssembly = new AssemblyReference("SomeAssembly", new Version(1, 2, 3, 4)); + private readonly AssemblyReference _dummyAssembly = new("SomeAssembly", new Version(1, 2, 3, 4)); private readonly ModuleDefinition _module; private readonly ReferenceImporter _importer; @@ -28,7 +28,7 @@ public void ImportNewAssemblyShouldAddToModule() { var result = _importer.ImportScope(_dummyAssembly); - Assert.Equal(_dummyAssembly, result, _comparer); + Assert.Equal(_dummyAssembly, result, Comparer); Assert.Contains(result, _module.AssemblyReferences); } @@ -52,7 +52,7 @@ public void ImportNewTypeShouldCreateNewReference() var type = new TypeReference(_dummyAssembly, "SomeNamespace", "SomeName"); var result = _importer.ImportType(type); - Assert.Equal(type, result, _comparer); + Assert.Equal(type, result, Comparer); Assert.Equal(_module, result.Module); } @@ -78,7 +78,7 @@ public void ImportTypeDefFromDifferentModuleShouldReturnTypeRef() var result = _importer.ImportType(definition); Assert.IsAssignableFrom(result); - Assert.Equal(definition, result, _comparer); + Assert.Equal(definition, result, Comparer); } [Fact] @@ -100,11 +100,42 @@ public void ImportNestedTypeShouldImportParentType() var result = _importer.ImportType(nested); - Assert.Equal(nested, result, _comparer); + Assert.Equal(nested, result, Comparer); Assert.Equal(_module, result.Module); Assert.Equal(_module, result.DeclaringType.Module); } + [Fact] + public void ImportNestedTypeDefinitionShouldImportParentType() + { + var otherAssembly = new AssemblyDefinition(_dummyAssembly.Name, _dummyAssembly.Version); + var otherModule = new ModuleDefinition("OtherModule"); + otherAssembly.Modules.Add(otherModule); + + var objectType = otherModule.CorLibTypeFactory.Object.ToTypeDefOrRef(); + + var declaringType = new TypeDefinition( + "SomeNamespace", + "SomeName", + TypeAttributes.Class | TypeAttributes.Public, + objectType); + var nestedType = new TypeDefinition( + null, + "NestedType", + TypeAttributes.Class | TypeAttributes.NestedPublic, + objectType); + + declaringType.NestedTypes.Add(nestedType); + otherModule.TopLevelTypes.Add(declaringType); + + var reference = _importer.ImportType(nestedType); + + Assert.NotNull(reference.DeclaringType); + Assert.Equal(declaringType, reference.DeclaringType, Comparer); + Assert.Equal(_module, reference.Module); + Assert.Equal(_module, reference.DeclaringType.Module); + } + [Fact] public void ImportSimpleTypeFromReflectionShouldResultInTypeRef() { @@ -170,7 +201,7 @@ public void ImportMethodFromExternalModuleShouldResultInMemberRef() var result = _importer.ImportMethod(method); - Assert.Equal(method, result, _comparer); + Assert.Equal(method, result, Comparer); Assert.Same(_module, result.Module); } @@ -217,7 +248,7 @@ public void ImportGenericMethodFromReflectionShouldResultInMethodSpec() Assert.Equal(new TypeSignature[] { _module.CorLibTypeFactory.String - }, specification.Signature.TypeArguments, _comparer); + }, specification.Signature.TypeArguments, Comparer); } [Fact] @@ -231,7 +262,7 @@ public void ImportFieldFromExternalModuleShouldResultInMemberRef() var result = _importer.ImportField(field); - Assert.Equal(field, result, _comparer); + Assert.Equal(field, result, Comparer); Assert.Same(_module, result.Module); } @@ -264,5 +295,174 @@ public void ImportFieldFromReflectionShouldResultInMemberRef() Assert.Equal(field.DeclaringType.FullName, result.DeclaringType.FullName); Assert.Equal(field.FieldType.FullName, ((FieldSignature) result.Signature).FieldType.FullName); } + + [Fact] + public void ImportNonImportedTypeDefOrRefShouldResultInNewInstance() + { + var signature = new TypeReference(_module.CorLibTypeFactory.CorLibScope, "System.IO", "Stream") + .ToTypeSignature(); + + var imported = _importer.ImportTypeSignature(signature); + + Assert.NotSame(signature, imported); + Assert.Equal(signature, imported, Comparer); + Assert.Equal(_module, imported.Module); + } + + [Fact] + public void ImportTypeSpecWithNonImportedBaseTypeShouldResultInNewInstance() + { + var signature = new TypeReference(_module.CorLibTypeFactory.CorLibScope, "System.IO", "Stream") + .ToTypeSignature() + .MakeSzArrayType(); + + var imported = _importer.ImportTypeSignature(signature); + var newInstance = Assert.IsAssignableFrom(imported); + Assert.NotSame(signature, newInstance); + Assert.Equal(signature, newInstance, Comparer); + Assert.Equal(_module, newInstance.BaseType.Module); + } + + [Fact] + public void ImportFullyImportedTypeDefOrRefShouldResultInSameInstance() + { + var signature = new TypeReference(_module, _module.CorLibTypeFactory.CorLibScope, "System.IO", "Stream") + .ToTypeSignature(); + + var imported = _importer.ImportTypeSignature(signature); + Assert.Same(signature, imported); + } + + [Fact] + public void ImportFullyImportedTypeSpecShouldResultInSameInstance() + { + var signature = new TypeReference(_module, _module.CorLibTypeFactory.CorLibScope, "System.IO", "Stream") + .ToTypeSignature() + .MakeSzArrayType(); + + var imported = _importer.ImportTypeSignature(signature); + Assert.Same(signature, imported); + } + + [Fact] + public void ImportGenericTypeSigWithNonImportedTypeArgumentShouldResultInNewInstance() + { + // https://github.com/Washi1337/AsmResolver/issues/268 + + var genericType = new TypeDefinition("SomeNamespace", "SomeName", TypeAttributes.Class); + genericType.GenericParameters.Add(new GenericParameter("T")); + _module.TopLevelTypes.Add(genericType); + + var instance = genericType.MakeGenericInstanceType( + new TypeDefOrRefSignature( + new TypeReference(_module.CorLibTypeFactory.CorLibScope, "System.IO", "Stream"), false) + ); + + var imported = _importer.ImportTypeSignature(instance); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.NotSame(instance, newInstance); + Assert.Equal(_module, newInstance.Module); + Assert.Equal(_module, newInstance.TypeArguments[0].Module); + } + + [Fact] + public void ImportFullyImportedGenericTypeSigShouldResultInSameInstance() + { + // https://github.com/Washi1337/AsmResolver/issues/268 + + var genericType = new TypeDefinition("SomeNamespace", "SomeName", TypeAttributes.Class); + genericType.GenericParameters.Add(new GenericParameter("T")); + _module.TopLevelTypes.Add(genericType); + + var instance = genericType.MakeGenericInstanceType( + new TypeDefOrRefSignature( + new TypeReference(_module, _module.CorLibTypeFactory.CorLibScope, "System.IO", "Stream"), false) + ); + + var imported = _importer.ImportTypeSignature(instance); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.Same(instance, newInstance); + } + + [Fact] + public void ImportCustomModifierTypeWithNonImportedModifierTypeShouldResultInNewInstance() + { + var signature = new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeType") + .ToTypeSignature() + .MakeModifierType(new TypeReference(_dummyAssembly, "SomeNamespace", "SomeModifierType"), true); + + var imported = _importer.ImportTypeSignature(signature); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.NotSame(signature, newInstance); + Assert.Equal(_module, newInstance.Module); + Assert.Equal(_module, newInstance.ModifierType.Module); + } + + [Fact] + public void ImportFullyImportedCustomModifierTypeShouldResultInSameInstance() + { + var signature = new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeType") + .ToTypeSignature() + .MakeModifierType(new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeModifierType"), true); + + var imported = _importer.ImportTypeSignature(signature); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.Same(signature, newInstance); + } + + [Fact] + public void ImportFunctionPointerTypeWithNonImportedParameterShouldResultInNewInstance() + { + var signature = MethodSignature + .CreateStatic( + _module.CorLibTypeFactory.Void, + new TypeReference(_dummyAssembly, "SomeNamespace", "SomeType").ToTypeSignature()) + .MakeFunctionPointerType(); + + var imported = _importer.ImportTypeSignature(signature); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.NotSame(signature, newInstance); + Assert.Equal(signature, newInstance, Comparer); + Assert.Equal(_module, newInstance.Module); + Assert.Equal(_module, newInstance.Signature.ParameterTypes[0].Module); + } + + [Fact] + public void ImportFunctionPointerTypeWithNonImportedReturnTypeShouldResultInNewInstance() + { + var signature = MethodSignature + .CreateStatic( + new TypeReference(_dummyAssembly, "SomeNamespace", "SomeType").ToTypeSignature(), + _module.CorLibTypeFactory.Int32) + .MakeFunctionPointerType(); + + var imported = _importer.ImportTypeSignature(signature); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.NotSame(signature, newInstance); + Assert.Equal(signature, newInstance, Comparer); + Assert.Equal(_module, newInstance.Module); + Assert.Equal(_module, newInstance.Signature.ReturnType.Module); + } + + [Fact] + public void ImportFullyImportedFunctionPointerTypeShouldResultInSameInstance() + { + var signature = MethodSignature + .CreateStatic( + _module.CorLibTypeFactory.Void, + new TypeReference(_module, _dummyAssembly, "SomeNamespace", "SomeType").ToTypeSignature()) + .MakeFunctionPointerType(); + + var imported = _importer.ImportTypeSignature(signature); + + var newInstance = Assert.IsAssignableFrom(imported); + Assert.Same(signature, newInstance); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v1.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v1.exe new file mode 100644 index 000000000..67e3b49a2 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v1.exe differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v2.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v2.exe new file mode 100644 index 000000000..99c1be95e Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v2.exe differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.WithResources.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.WithResources.exe new file mode 100644 index 000000000..9da59aba9 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.WithResources.exe differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.exe new file mode 100644 index 000000000..e980f978e Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.SingleFile.v6.exe differ diff --git a/test/AsmResolver.PE.File.Tests/PEFileTest.cs b/test/AsmResolver.PE.File.Tests/PEFileTest.cs index c7fe42964..7a1c47463 100644 --- a/test/AsmResolver.PE.File.Tests/PEFileTest.cs +++ b/test/AsmResolver.PE.File.Tests/PEFileTest.cs @@ -2,6 +2,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Text; using AsmResolver.IO; using AsmResolver.PE.File.Headers; using AsmResolver.Tests.Runners; @@ -168,5 +169,76 @@ public void SectionsInMappedBinaryShouldUseVirtualAddressesAsOffset() Assert.Equal(expected, actual); } } + + [Fact] + public void PEWithNoEofData() + { + var file = PEFile.FromBytes(Properties.Resources.HelloWorld); + Assert.Null(file.EofData); + } + + [Fact] + public void ReadEofData() + { + var file = PEFile.FromBytes(Properties.Resources.HelloWorld_EOF); + byte[] data = Assert.IsAssignableFrom(file.EofData).ToArray(); + Assert.Equal(Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz"), data); + } + + [Fact] + public void AddNewEofData() + { + byte[] expected = { 1, 2, 3, 4 }; + + var file = PEFile.FromBytes(Properties.Resources.HelloWorld); + Assert.Null(file.EofData); + file.EofData = new DataSegment(expected); + + using var stream = new MemoryStream(); + file.Write(stream); + byte[] newFileBytes = stream.ToArray(); + + Assert.Equal(expected, newFileBytes[^expected.Length..]); + + var newFile = PEFile.FromBytes(newFileBytes); + var readable = Assert.IsAssignableFrom(newFile.EofData); + Assert.Equal(expected, readable.ToArray()); + } + + [Fact] + public void ModifyExistingEofData() + { + var file = PEFile.FromBytes(Properties.Resources.HelloWorld_EOF); + byte[] data = Assert.IsAssignableFrom(file.EofData).ToArray(); + Array.Reverse(data); + file.EofData = new DataSegment(data); + + using var stream = new MemoryStream(); + file.Write(stream); + byte[] newFileBytes = stream.ToArray(); + + Assert.Equal(data, newFileBytes[^data.Length..]); + + var newFile = PEFile.FromBytes(newFileBytes); + byte[] newData = Assert.IsAssignableFrom(newFile.EofData).ToArray(); + Assert.Equal(data, newData); + } + + [Fact] + public void RemoveExistingEofData() + { + var file = PEFile.FromBytes(Properties.Resources.HelloWorld_EOF); + byte[] originalData = Assert.IsAssignableFrom(file.EofData).ToArray(); + file.EofData = null; + + using var stream = new MemoryStream(); + file.Write(stream); + byte[] newFileBytes = stream.ToArray(); + + Assert.NotEqual(originalData, newFileBytes[^originalData.Length..]); + + var newFile = PEFile.FromBytes(newFileBytes); + Assert.Null(newFile.EofData); + } } } diff --git a/test/AsmResolver.PE.File.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.PE.File.Tests/Properties/Resources.Designer.cs index 0e74fe94c..f2f817fd5 100644 --- a/test/AsmResolver.PE.File.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.PE.File.Tests/Properties/Resources.Designer.cs @@ -80,6 +80,16 @@ public class Resources { } } + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] HelloWorld_EOF { + get { + object obj = ResourceManager.GetObject("HelloWorld_EOF", resourceCulture); + return ((byte[])(obj)); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/test/AsmResolver.PE.File.Tests/Properties/Resources.resx b/test/AsmResolver.PE.File.Tests/Properties/Resources.resx index 293c5e462..c33ee0174 100644 --- a/test/AsmResolver.PE.File.Tests/Properties/Resources.resx +++ b/test/AsmResolver.PE.File.Tests/Properties/Resources.resx @@ -124,6 +124,9 @@ ..\Resources\HelloWorld.dump;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\HelloWorld.EOF.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\NativeMemoryDemos.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/test/AsmResolver.PE.File.Tests/Resources/HelloWorld.EOF.exe b/test/AsmResolver.PE.File.Tests/Resources/HelloWorld.EOF.exe new file mode 100644 index 000000000..90549fef6 Binary files /dev/null and b/test/AsmResolver.PE.File.Tests/Resources/HelloWorld.EOF.exe differ diff --git a/test/AsmResolver.PE.Tests/DotNet/Metadata/UserStringsStreamTest.cs b/test/AsmResolver.PE.Tests/DotNet/Metadata/UserStringsStreamTest.cs index 9b3c274ac..fb350b97a 100644 --- a/test/AsmResolver.PE.Tests/DotNet/Metadata/UserStringsStreamTest.cs +++ b/test/AsmResolver.PE.Tests/DotNet/Metadata/UserStringsStreamTest.cs @@ -22,7 +22,7 @@ private static void AssertHasString(byte[] streamData, string needle) [InlineData("")] [InlineData("ABC")] [InlineData("DEF")] - public void FindExistingString(string? value) => AssertHasString(new byte[] + public void FindExistingString(string value) => AssertHasString(new byte[] { 0x00, 0x07, 0x41, 0x00, 0x42, 0x00, 0x43, 0x00, 0x00, diff --git a/test/AsmResolver.Tests/Runners/PERunner.cs b/test/AsmResolver.Tests/Runners/PERunner.cs index 3f5a674fb..c2e4ff405 100644 --- a/test/AsmResolver.Tests/Runners/PERunner.cs +++ b/test/AsmResolver.Tests/Runners/PERunner.cs @@ -58,6 +58,17 @@ public string Rebuild(PEFile peFile, string fileName, string testClass, string t return fullPath; } + public string RunAndCaptureOutput(string fileName, byte[] contents, string[]? arguments = null, + int timeout = 5000, + [CallerFilePath] string testClass = "File", + [CallerMemberName] string testMethod = "Test") + { + testClass = Path.GetFileNameWithoutExtension(testClass); + string testExecutablePath = GetTestExecutablePath(testClass, testMethod, fileName); + File.WriteAllBytes(testExecutablePath, contents); + return RunAndCaptureOutput(testExecutablePath, arguments, timeout); + } + public string RunAndCaptureOutput(string filePath, string[]? arguments = null, int timeout = 5000) { var info = GetStartInfo(filePath, arguments);