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/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e95f10964..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Bug report -about: Create a report to help improve AsmResolver -title: '' -labels: bug -assignees: '' - ---- - -## Describe the bug -A clear and concise description of what the bug is. - -## To Reproduce -Steps to reproduce the behavior. Preferably with sample code and sample input/output files. - -## Expected behavior -A clear and concise description of what you expected to happen. - -## Screenshots -If applicable, add screenshots to help explain your problem. - -## Platform - - OS: [e.g. Windows 10, Linux] - - AsmResolver Version: [e.g. 4.5.1] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 000000000..6548e9f62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,61 @@ +name: Bug Report +description: File a bug or crash report +labels: ["bug"] +body: + - type: input + id: asmresolver_version + attributes: + label: AsmResolver Version + placeholder: 4.11.0 + validations: + required: true + - type: input + id: dotnet_version + attributes: + label: .NET Version + placeholder: .NET 6.0 + - type: dropdown + attributes: + label: Operating System + options: + - Windows + - Linux + - MacOS + - Other + validations: + required: true + - type: textarea + id: description + attributes: + label: Describe the Bug + description: A clear and concise description of what the bug is. + validations: + required: true + - type: textarea + id: how_to_reproduce + attributes: + label: How To Reproduce + description: The steps on how to reproduce the bug. Preferably with sample code and/or sample input files. + validations: + required: true + - type: textarea + id: expected_behavior + attributes: + label: Expected Behavior + description: Describe the result that you expect to get after performing the steps. + validations: + required: true + - type: textarea + id: actual_behavior + attributes: + label: Actual Behavior + description: Describe the actual behavior that you observed after performing the steps. + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional Context + description: Any other information that may help us fix the issue goes here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 3e0c05e63..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for AsmResolver -title: '' -labels: enhancement -assignees: '' - ---- - -## Problem Description -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -## Proposal -A clear and concise description of what you want to happen. - -## Alternatives -A clear and concise description of any alternative solutions or features you've considered. - -## Additional context -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 000000000..679f68408 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,36 @@ +name: Feature Request +description: Suggest a new feature +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: Problem Description + description: A clear and concise description of what the problem is. + placeholder: | + Example: "I'm always frustrated when [...]", or "I often need to [...]" + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: | + A clear and concise description of what you want to happen. You can be relatively open-ended, but including + syntax suggestions or pseudo code is appreciated as it may help conveying your idea. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional Context + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/other.yaml b/.github/ISSUE_TEMPLATE/other.yaml new file mode 100644 index 000000000..b0da1e372 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/other.yaml @@ -0,0 +1,8 @@ +name: Other +description: Something else? +body: + - type: textarea + id: description + attributes: + label: Description + description: Please describe your issue. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..0006a8e24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "nuget" # See documentation for possible values + target-branch: "development" + directory: "/" # Location of package manifests + schedule: + interval: "daily" + + - package-ecosystem: "github-actions" + target-branch: "development" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "daily" diff --git a/AsmResolver.sln b/AsmResolver.sln index 77d8e6856..21b8f5456 100644 --- a/AsmResolver.sln +++ b/AsmResolver.sln @@ -81,9 +81,24 @@ 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}" +Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "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}") = "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 + Directory.Build.props = Directory.Build.props + appveyor.yml = appveyor.yml + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsmResolver.Symbols.Pdb", "src\AsmResolver.Symbols.Pdb\AsmResolver.Symbols.Pdb.csproj", "{9E311832-D0F2-42CA-84DD-9A91B88F0287}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AsmResolver.Symbols.Pdb.Tests", "test\AsmResolver.Symbols.Pdb.Tests\AsmResolver.Symbols.Pdb.Tests.csproj", "{AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -483,6 +498,30 @@ Global {2D1DF5DA-7367-4490-B3F0-B996348E150B}.Release|x64.Build.0 = Release|Any CPU {2D1DF5DA-7367-4490-B3F0-B996348E150B}.Release|x86.ActiveCfg = Release|Any CPU {2D1DF5DA-7367-4490-B3F0-B996348E150B}.Release|x86.Build.0 = Release|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x64.Build.0 = Debug|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Debug|x86.Build.0 = Debug|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|Any CPU.Build.0 = Release|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x64.ActiveCfg = Release|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x64.Build.0 = Release|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x86.ActiveCfg = Release|Any CPU + {9E311832-D0F2-42CA-84DD-9A91B88F0287}.Release|x86.Build.0 = Release|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x64.Build.0 = Debug|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Debug|x86.Build.0 = Debug|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|Any CPU.Build.0 = Release|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x64.ActiveCfg = Release|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x64.Build.0 = Release|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x86.ActiveCfg = Release|Any CPU + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -525,6 +564,8 @@ Global {40483E28-C703-4933-BA5B-9512EF6E6A21} = {EA971BB0-94BA-44DB-B16C-212D2DB27E17} {CF6A7E02-37DC-4963-AC14-76D74ADCD87A} = {B3AF102B-ABE1-41B2-AE48-C40702F45AB0} {2D1DF5DA-7367-4490-B3F0-B996348E150B} = {B3AF102B-ABE1-41B2-AE48-C40702F45AB0} + {9E311832-D0F2-42CA-84DD-9A91B88F0287} = {34A95168-A162-4F6A-803B-B6F221FE9EA6} + {AAD604B6-ABE5-4DBC-A2D9-4EF8E815B2EE} = {786C1732-8C96-45DD-97BB-639C9AA7F45B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3302AC79-6D23-4E7D-8C5F-C0C7261044D0} diff --git a/Directory.Build.props b/Directory.Build.props index bc687b4c7..da8160f3a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,13 +1,13 @@ - Copyright © Washi 2016-2021 + Copyright © Washi 2016-2022 https://github.com/Washi1337/AsmResolver https://github.com/Washi1337/AsmResolver/LICENSE.md https://github.com/Washi1337/AsmResolver git - 9 - 4.8.0 + 10 + 5.0.0-beta.1 diff --git a/README.md b/README.md index db28f348a..9bf2ec12f 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,54 @@ -AsmResolver -=========== +# 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. AsmResolver is released under the MIT license. -Binaries --------- +## Features + +AsmResolver has a lot of features. Below a non-exhaustive list: + +- [x] Create, read and write PE files + - [x] Inspect and update PE headers. + - [x] Create, read and write sections. +- [x] Create, read and write various data directories + - [x] Debug Directory (CodeView) + - [x] .NET Directory + - [x] CIL assembler and disassemblers + - [x] Metadata Directory (tables, strings, user-strings, blobs, GUIDs) + - [x] Resources Directory + - [x] Strong Name Signing + - [x] VTable Fixup Directory + - [x] Exception Directory (AMD64) + - [x] Export Directory + - [x] Import Directory + - [x] Base Relocation Directory + - [x] TLS Directory + - [x] Win32 Resources Directory +- [x] Fully mutable object model for .NET modules that is similar to System.Reflection + - [x] Strong type-system with many useful factory methods for quickly constructing new metadata. + - [x] Full metadata importing and cloning (useful for injecting metadata into another assembly) + - [x] .NET Framework 2.0+, .NET Core and .NET 5+ binary file support. + - [x] Infer memory layout of types statically. + - [x] Create, read and write managed resource sets (`.resources` files) + - [x] Create new method bodies containing native code. + - [x] Highly configurable reader and writer options and custom error handling for both. + - [x] Rich support for AppHost and SingleFileHost bundled files. + + +## Documentation + +Check out the [wiki](https://asmresolver.readthedocs.org/) for guides and information on how to use the library. + + +## Binaries Stable builds: @@ -26,13 +65,7 @@ Nightly builds: | development | [![Development branch build status](https://img.shields.io/appveyor/ci/Washi1337/AsmResolver/development.svg)](https://ci.appveyor.com/project/Washi1337/asmresolver/branch/development) -Documentation -------------- -Check out the [wiki](https://asmresolver.readthedocs.org/) for guides and information on how to use the library. - - -Compiling ---------- +## Compiling The solution can be build using the .NET SDK or an IDE that works with the .NET SDK (such as Visual Studio and JetBrains Rider). The main packages target .NET Standard 2.0, and the xUnit test projects target .NET Core 3.1. @@ -47,25 +80,23 @@ To run all tests, simply run: $ dotnet test ``` +## Contributing -Contributing ------------- See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on general workflow and code style. -Acknowledgements ----------------- +## Found a bug or have questions? -AsmResolver started out as a hobby project, but has grown into a community project with various contributors. Without these people, AsmResolver would not have been where it is today! +Please use the [issue tracker](https://github.com/Washi1337/AsmResolver/issues). Try to be as descriptive as possible. -- Special thanks to all the people who contributed [directly with code commits](https://github.com/Washi1337/AsmResolver/graphs/contributors). +You can also join the [Discord](https://discord.gg/Y7DTBkbhJJ) to engage more directly with the community. -- 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. +## Acknowledgements -If you feel you have been under-represented in these acknowledgements, feel free to contact me. +AsmResolver started out as a hobby project, but has grown into a community project with various contributors. Without these people, AsmResolver would not have been where it is today! +- Special thanks to all the people who contributed [directly with code commits](https://github.com/Washi1337/AsmResolver/graphs/contributors). -Found a bug or have questions? ------------------------------- -Please use the [issue tracker](https://github.com/Washi1337/AsmResolver/issues). Try to be as descriptive as possible. +- 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. diff --git a/appveyor.yml b/appveyor.yml index 9b205fb48..6c4e999bc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -3,8 +3,8 @@ only: - master - image: Visual Studio 2019 - version: 4.8.0-master-build.{build} + image: Visual Studio 2022 + version: 4.11.2-master-build.{build} configuration: Release skip_commits: @@ -23,7 +23,7 @@ deploy: provider: NuGet api_key: - secure: DCeHUu0aAsOjRnoi2DpcuXpj0apD7dxHzglSamP7LGzcZjhIvTBi1ONnjIa7L2zm + secure: L3fXsS7umzD8zwAvTsdGxOg/E6tQ4IR4MfwBAcO8elE7ZwjZ8HO8UPwjiWbp4RMw skip_symbols: false artifact: /.*\.nupkg/ @@ -32,8 +32,8 @@ only: - development - image: Visual Studio 2019 - version: 4.8.0-dev-build.{build} + image: Visual Studio 2022 + version: 5.0.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/dotnet/importing.rst b/docs/dotnet/importing.rst index 24e4e21ad..7b1bb8dd2 100644 --- a/docs/dotnet/importing.rst +++ b/docs/dotnet/importing.rst @@ -5,16 +5,27 @@ Reference Importing .NET modules use entries in the TypeRef or MemberRef tables to reference types or members from external assemblies. Importing references into the current module therefore form a key role when creating new- or modifying existing .NET modules. When a member is not imported into the current module, a ``MemberNotImportedException`` will be thrown when you are trying to create a PE image or write the module to the disk. -AsmResolver provides the ``ReferenceImporter`` class that does most of the heavy lifting. +AsmResolver provides the ``ReferenceImporter`` class that does most of the heavy lifting. Obtaining an instance of ``ReferenceImporter`` can be done in two ways. -All samples in this document assume there is an instance of ``ReferenceImporter`` created using the following code: +Either instantiate one yourself: .. code-block:: csharp + ModuleDefinition module = ... var importer = new ReferenceImporter(module); +Or obtain the default instance that comes with every ``ModuleDefinition`` object. This avoids allocating new reference importers every time. -Importing metadata members +.. code-block:: csharp + + ModuleDefinition module = ... + var importer = module.DefaultImporter; + + +The example snippets that will follow in this articule assume that there is such a ``ReferenceImporter`` object instantiated using either of these two methods, and is stored in an ``importer`` variable. + + +Importing existing members -------------------------- Metadata members from external modules can be imported using the ``ReferenceImporter`` class using one of the following members: @@ -59,18 +70,28 @@ Below an example of how to import a type definition called ``SomeType``: ITypeDefOrRef importedType = importer.ImportType(typeToImport); -Importing type signatures -------------------------- +These types also implement the ``IImportable`` interface. This means you can also use the ``member.ImportWith`` method instead: + +.. code-block:: csharp + + ModuleDefinition externalModule = ModuleDefinition.FromFile(...); + TypeDefinition typeToImport = externalModule.TopLevelTypes.First(t => t.Name == "SomeType"); + + ITypeDefOrRef importedType = typeToImport.ImportWith(importer); + + +Importing existing type signatures +---------------------------------- Type signatures can also be imported using the ``ReferenceImporter`` class, but these should be imported using the ``ImportTypeSignature`` method instead. -.. note:: +.. note:: If a corlib type signature is imported, the appropriate type from the ``CorLibTypeFactory`` of the target module will be selected, regardless of whether CorLib versions are compatible with each other. -Importing using reflection --------------------------- +Importing using System.Reflection +--------------------------------- Types and members can also be imported by passing on an instance of various ``System.Reflection`` classes. @@ -90,22 +111,72 @@ Types and members can also be imported by passing on an instance of various ``Sy | ``FieldInfo`` | ``ImportScope`` | ``MemberReference`` | +---------------------------+------------------------+----------------------+ - -There is limited support for importing compound types. Types that can be imported through reflection include: +There is limited support for importing complex types. Types that can be imported through reflection include: - Pointer types. - By-reference types. -- Array types: - - If an array contains only one dimension, a ``SzArrayTypeSignature`` is returned. Otherwise a ``ArrayTypeSignature`` is created. +- Array types (If an array contains only one dimension, a ``SzArrayTypeSignature`` is returned. Otherwise a ``ArrayTypeSignature`` is created). - Generic parameters. - Generic type instantiations. -Instantiations of generic methods are supported. +Instantiations of generic methods are also supported. + + +Creating new references +----------------------- + +Member references can also be created and imported without having direct access to its member definition or ``System.Reflection`` instance. It is possible to create new instances of ``TypeReference`` and ``MemberReference`` using the constructors, but the preferred way is to use the factory methods that allow for a more fluent syntax. Below an example on how to create a fully imported reference to ``void System.Console.WriteLine(string)``: + +.. code-block:: csharp + + var factory = module.CorLibTypeFactory; + var importedMethod = factory.CorLibScope + .CreateTypeReference("System", "Console") + .CreateMemberReference("WriteLine", MethodSignature.CreateStatic( + factory.Void, factory.String)) + .ImportWith(importer); + + // importedMethod now references "void System.Console.WriteLine(string)" + +Generic type instantiations can also be created using ``MakeGenericInstanceType``: + +.. code-block:: csharp + + ModuleDefinition module = ... + + var factory = module.CorLibTypeFactory; + var importedMethod = factory.CorLibScope + .CreateTypeReference("System.Collections.Generic", "List`1") + .MakeGenericInstanceType(factory.Int32) + .ToTypeDefOrRef() + .CreateMemberReference("Add", MethodSignature.CreateInstance( + factory.Void, + new GenericParameterSignature(GenericParameterType.Type, 0))) + .ImportWith(importer); + + // importedMethod now references "System.Collections.Generic.List`1.Add(!0)" + + +Similarly, generic method instantiations can be constructed using ``MakeGenericInstanceMethod``: + +.. code-block:: csharp + + ModuleDefinition module = ... + + var factory = module.CorLibTypeFactory; + var importedMethod = factory.CorLibScope + .CreateTypeReference("System", "Array") + .CreateMemberReference("Empty", MethodSignature.CreateStatic( + new GenericParameterSignature(GenericParameterType.Method, 0).MakeSzArrayType(), 1)) + .MakeGenericInstanceMethod(factory.String) + .ImportWith(importer); + + // importedMethod now references "!0[] System.Array.Empty()" .. _dotnet-importer-common-caveats: -Common Caveats using the Importer +Common Caveats using the Importer --------------------------------- Caching and reuse of instances @@ -116,20 +187,17 @@ The default implementation of ``ReferenceImporter`` does not maintain a cache. E Importing cross-framework versions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``ReferenceImporter`` does not support importing across different versions of the target framework. Members are being imported as-is, and are not automatically adjusted to conform with other versions of a library. +The ``ReferenceImporter`` does not support importing across different versions of the target framework. Members are being imported as-is, and are not automatically adjusted to conform with other versions of a library. -As a result, trying to import from for example a library part of the .NET Framework into a module targeting .NET Core or vice versa has a high chance of producing an invalid .NET binary that cannot be executed by the runtime. For example, attempting to import a reference to ``[System.Runtime] System.DateTime`` into a module targeting .NET Framework will result in a new reference targeting a .NET Core library (``System.Runtime``) as opposed to the appropriate .NET Framework library (``mscorlib``). +As a result, trying to import from for example a library part of the .NET Framework into a module targeting .NET Core or vice versa has a high chance of producing an invalid .NET binary that cannot be executed by the runtime. For example, attempting to import a reference to ``[System.Runtime] System.DateTime`` into a module targeting .NET Framework will result in a new reference targeting a .NET Core library (``System.Runtime``) as opposed to the appropriate .NET Framework library (``mscorlib``). This is a common mistake when trying to import using metadata provided by ``System.Reflection``. For example, if the host application that uses AsmResolver targets .NET Core but the input file is targeting .NET Framework, then you will run in the exact issue described in the above. .. code-block:: csharp - var targetModule = ModuleDefinition.FromFile(...); - var importer = new ReferenceImporter(targetModule); - var reference = importer.ImportType(typeof(DateTime)); // `reference` will target `[mscorlib] System.DateTime` when running on .NET Framework, and `[System.Runtime] System.DateTime` when running on .NET Core. -Therefore, always make sure you are importing from a .NET module that is compatible with the target .NET module. \ No newline at end of file +Therefore, always make sure you are importing from a .NET module that is compatible with the target .NET module. diff --git a/docs/dotnet/managed-method-bodies.rst b/docs/dotnet/managed-method-bodies.rst index db72bba4b..281be6f31 100644 --- a/docs/dotnet/managed-method-bodies.rst +++ b/docs/dotnet/managed-method-bodies.rst @@ -90,14 +90,14 @@ By default, depending on the value of ``OpCode.OperandType``, ``Operand`` contai +----------------------------------------+----------------------------------------------+ .. warning:: - + Providing an incorrect operand type will result in the CIL assembler to fail assembling the method body upon writing the module to the disk. Creating a new instruction can be done using one of the constructors, together with the ``CilOpCodes`` static class: -.. code-block:: csharp +.. code-block:: csharp - body.Instructions.AddRange(new[] + body.Instructions.AddRange(new[] { new CilInstruction(CilOpCodes.Ldstr, "Hello, World!"), new CilInstruction(CilOpCodes.Ret), @@ -105,7 +105,7 @@ Creating a new instruction can be done using one of the constructors, together w However, the preferred way of adding instructions to add or insert new instructions is to use one of the ``Add`` or ``Insert`` overloads that directly take an opcode and operand. This is because it avoids an allocation of an array, and the overloads perform immediate validation on the created instruction. -.. code-block:: csharp +.. code-block:: csharp var instructions = body.Instructions; instructions.Add(CilOpCodes.Ldstr, "Hello, World!"); @@ -115,16 +115,16 @@ However, the preferred way of adding instructions to add or insert new instructi Pushing 32-bit integer constants onto the stack ----------------------------------------------- -In CIL, pushing integer constants onto the stack is done using one of the ``ldc.i4`` instruction variants. +In CIL, pushing integer constants onto the stack is done using one of the ``ldc.i4`` instruction variants. The recommended way to create such an instruction is not to use the constructor, but instead use the ``CilInstruction.CreateLdcI4(int)`` method instead. This automatically selects the smallest possible opcode possible and sets the operand accordingly: -.. code-block:: csharp +.. code-block:: csharp CilInstruction push1 = CilInstruction.CreateLdcI4(1); // Returns "ldc.i4.1" macro CilInstruction pushShort = CilInstruction.CreateLdcI4(123); // Returns "ldc.i4.s 123" macro CilInstruction pushLarge = CilInstruction.CreateLdcI4(12345678); // Returns "ldc.i4 12345678" - + If we want to get the pushed value, we can use the ``CilInstruction.GetLdcI4Constant()`` method. This method works on any of the ``ldc.i4`` variants, including all the macro opcodes that do not explicitly define an operand such as ``ldc.i4.1``. @@ -133,7 +133,7 @@ Branching Instructions Branch instructions are instructions that (might) transfer control to another part of the method body. To reference the instruction to jump to (the branch target), ``ICilLabel`` is used. The easiest way to create such a label is to use the ``CreateLabel()`` function on the instruction to reference: -.. code-block:: csharp +.. code-block:: csharp CilInstruction targetInstruction = ... ICilLabel label = targetInstruction.CreateLabel(); @@ -142,7 +142,7 @@ Branch instructions are instructions that (might) transfer control to another pa Alternatively, when using the ``Add`` or ``Insert`` overloads, it is possible to use the return value of these overloads. -.. code-block:: csharp +.. code-block:: csharp var instructions = body.Instructions; var label = new CilInstructionLabel(); @@ -150,8 +150,8 @@ Alternatively, when using the ``Add`` or ``Insert`` overloads, it is possible to instructions.Add(CilOpCodes.Br, label); /* ... */ label.Instruction = instruction.Add(CilOpCodes.Ret); - - + + The ``switch`` operation uses a ``IList`` instead. .. note:: @@ -159,10 +159,10 @@ The ``switch`` operation uses a ``IList`` instead. When a branching instruction contains a ``null`` label or a label that references an instruction that is not present in the method body, AsmResolver will by default report an exception upon serializing the code stream. This can be disabled by setting ``VerifyLabelsOnBuild`` to ``false``. -Finding instructions by offset +Finding instructions by offset ------------------------------ -Instructions stored in a method body are indexed not by offset, but by order of occurrence. If it is required to find an instruction by offset, it is possible to use the ``Instructions.GetByOffset(int)`` method, which performs a binary search (O(log(n))) and is faster than a linear search (O(n)) such as a for loop or using a construction like ``.First(i => i.Offset == offset)`` provided by ``System.Linq``. +Instructions stored in a method body are indexed not by offset, but by order of occurrence. If it is required to find an instruction by offset, it is possible to use the ``Instructions.GetByOffset(int)`` method, which performs a binary search (O(log(n))) and is faster than a linear search (O(n)) such as a for loop or using a construction like ``.First(i => i.Offset == offset)`` provided by ``System.Linq``. For ``GetByOffset`` to work, it is required that all offsets in the instruction collection are up to date. Recalculating all offsets within an instruction collection can be done through ``Instructions.CalculateOffsets()``. @@ -180,12 +180,12 @@ For ``GetByOffset`` to work, it is required that all offsets in the instruction instruction1 = body.Instructions[index]; -Referencing members +Referencing members ------------------- As specified by the table above, operations such as a ``call`` require a member as operand. -It is important that the member referenced in the operand of such an instruction is imported in the module. This can be done using the ``ReferenceImporter`` class. +It is important that the member referenced in the operand of such an instruction is imported in the module. This can be done using the ``ReferenceImporter`` class. Below an example on how to use the ``ReferenceImporter`` to emit a call to ``Console::WriteLine(string)`` using reflection: @@ -238,7 +238,44 @@ Instructions can be formatted using e.g. an instance of the ``CilInstructionForm Console.WriteLine(formatter.FormatInstruction(instruction)); -Exception handlers +Patching CIL instructions +------------------------- + +Instructions can be added or removed using the ``Add``, ``Insert``, ``Remove`` and ``RemoveAt`` methods: + +.. code-block:: csharp + + body.Instructions.Add(CilOpCodes.Ldstr, "Hello, world!"); + body.Instructions.Insert(i, CilOpCodes.Ldc_I4, 1234); + body.Instructions.RemoveAt(i); + +... or by using the indexer to replace existing instructions: + +.. code-block:: csharp + + body.Instructions[i] = new CilInstruction(CilOpCodes.Ret); + +Removing or replacing instructions may not always be favourable. The original ``CilInstruction`` object might be used as a reference for a branch target or exception handler boundary. Removing or replacing these ``CilInstruction`` objects would therefore break these kinds of references, rendering the body invalid. Rather than updating all references manually, it may therefore be wiser to reuse the ``CilInstruction`` object and simply modify the ``OpCode`` and ``Operand`` properties instead: + +.. code-block:: csharp + + body.Instructions[i].OpCode = CilOpCodes.Ldc_I4; + body.Instructions[i].Operand = 1234; + +AsmResolver provides a helper function ``ReplaceWith`` that shortens the code into a single line: + +.. code-block:: csharp + + body.Instructions[i].ReplaceWith(CilOpCodes.Ldc_I4, 1234); + +Since it is very common to replace instructions with a `nop`, AsmResolver also defines a special ``ReplaceWithNop`` helper function: + +.. code-block:: csharp + + body.Instructions[i].ReplaceWithNop(); + + +Exception handlers ------------------ Exception handlers are regions in the method body that are protected from exceptions. In AsmResolver, they are represented by the ``CilExceptionHandler`` class, and define the following properties: @@ -267,4 +304,4 @@ The max stack can be computed by using the ``ComputeMaxStack`` method. By defaul .. note:: - If a ``StackImbalanceException`` is thrown upon writing the module to the disk, or upon calling ``ComputeMaxStack``, it means that not all execution paths in the provided CIL code push or pop the expected amount of values. It is a good indication that the provided CIL code is invalid. \ No newline at end of file + If a ``StackImbalanceException`` is thrown upon writing the module to the disk, or upon calling ``ComputeMaxStack``, it means that not all execution paths in the provided CIL code push or pop the expected amount of values. It is a good indication that the provided CIL code is invalid. diff --git a/docs/dotnet/unmanaged-method-bodies.rst b/docs/dotnet/unmanaged-method-bodies.rst index dee5d9c5e..c5edc20d3 100644 --- a/docs/dotnet/unmanaged-method-bodies.rst +++ b/docs/dotnet/unmanaged-method-bodies.rst @@ -20,7 +20,7 @@ Allowing native code in modules To make the CLR treat the output file as a mixed mode application, the ``ILOnly`` flag needs to be unset: -.. code-block:: csharp +.. code-block:: csharp ModuleDefinition module = ... module.Attributes &= ~DotNetDirectoryFlags.ILOnly; @@ -80,7 +80,7 @@ In the following sections, we will briefly go over each of them. Writing native code ------------------- -The contents of a native method body can be set through the ``Code`` property. This is a ``byte[]`` that represents the raw code stream to be executed. Below an example of a simple method body written in x86 64-bit assembly code, that returns the constant ``0x1337``: +The contents of a native method body can be set through the ``Code`` property. This is a ``byte[]`` that represents the raw code stream to be executed. Below an example of a simple method body written in x86 64-bit assembly code, that returns the constant ``1337``: .. code-block:: csharp @@ -92,14 +92,20 @@ The contents of a native method body can be set through the ``Code`` property. T .. note:: - + Since native method bodies are platform dependent, AsmResolver does not provide a standard way to encode these instructions. To construct the byte array that you need for a particular implementation of a method body, consider using a third-party assembler or assembler library. -References to external symbols ------------------------------- +Symbols and Address Fixups +-------------------------- + +In a lot of cases, native method bodies that references symbols (such as imported functions) require direct addresses to be referenced within its instructions. Since the addresses of these symbols are not known yet upon creating a ``NativeMethodBody``, it is not possible to encode such an operand directly in the ``Code`` byte array. To support these kinds of references regardless, AsmResolver can be instructed to apply address fixups just before writing the body to the disk. These instructions are essentially small pieces of information that tell AsmResolver that at a particular offset the bytes should be replaced with a reference to a symbol in the final PE. This can be applied to any object that implements ``ISymbol``. In the following, two of the most commonly used symbols will be discussed. + + +Imported Symbols +~~~~~~~~~~~~~~~~ -In a lot of cases, methods require making calls to functions defined in external libraries and native method bodies are no exception. In the PE file format, these kinds of symbols are often put into the imports directory. This is essentially a table of names that the Windows PE loader will go through, look up the actual address of each name, and put it in the import address table. Typically, when a piece of code is meant to make a call to an external function, the code will make an indirect call to an entry stored in this table. In x86 64-bit, using nasm syntax, a call to the ``puts`` function might look like the following snippet: +In the PE file format, symbols from external modules are often imported by placing an entry into the imports directory. This is essentially a table of names that the Windows PE loader will go through, look up the actual address of each name, and put it in the import address table. Typically, when a piece of code is meant to make a call to an external function, the code will make an indirect call to an entry stored in this table. In x86 64-bit, using nasm syntax, a call to the ``puts`` function might look like the following snippet: .. code-block:: csharp @@ -108,10 +114,6 @@ In a lot of cases, methods require making calls to functions defined in external call qword [rel puts] ... -Since the import directory is not constructed yet when we are operating on the abstraction level of a ``ModuleDefinition``, the address of the import address entry is still unknown. Therefore, it is not possible to encode an operand like the one in the call instruction of the above example. - -To support these kinds of references in native method bodies regardless, it is possible to instruct AsmResolver to apply address fixups just before writing the body to the disk. These are essentially small pieces of information that tell AsmResolver that at a particular offset the bytes should be replaced with a reference to a symbol in the final PE. - Consider the following example x86 64-bit code, that is printing the text ``Hello from the unmanaged world!`` to the standard output stream using the ``puts`` function. .. code-block:: csharp @@ -135,16 +137,16 @@ Consider the following example x86 64-bit code, that is printing the text ``Hell 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ged wor" 0x6c, 0x64, 0x21, 0x00 // "ld!" }; - -Notice how the operand of the call instruction is left at zero (`0x00`) bytes. To let AsmResolver know that these 4 bytes are to be replaced by an address to an entry in the import address table, we first create a new instance of ``ImportedSymbol``, representing the ``puts`` symbol: + +Notice how the operand of the ``call`` instruction is left at zero (``0x00``) bytes. To let AsmResolver know that these 4 bytes are to be replaced by an address to an entry in the import address table, we first create a new instance of ``ImportedSymbol``, representing the ``puts`` symbol: .. code-block:: csharp var ucrtbased = new ImportedModule("ucrtbased.dll"); var puts = new ImportedSymbol(0x4fc, "puts"); ucrtbased.Symbols.Add(puts); - + We can then add it as a fixup to the method body: @@ -155,12 +157,49 @@ We can then add it as a fixup to the method body: )); -The type of fixup that is required will depend on the architecture and instruction that is used. Below an overview: +Local Symbols +~~~~~~~~~~~~~ + +If a native body is supposed to process or return some data that is defined within the body itself, the ``NativeLocalSymbol`` class can be used. + +Consider the following example x86 32-bit snippet, that returns the virtual address of a string. + +.. code-block:: csharp + + 0xB8, 0x00, 0x00, 0x00, 0x00 // mov eax, message + 0xc3, // ret + + // message (unicode): + 0x48, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f, 0x00, 0x2c, 0x00, 0x20, 0x00, // "Hello, " + 0x77, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x21, 0x00, 0x00, 0x00 // "world!." + + +Notice how the operand of the ``mov`` instruction is left at zero (``0x00``) bytes. To let AsmResolver know that these 4 bytes are to be replaced by the actual virtual address to ``message``, we can define a local symbol and register an address fixup in the following manner: + +.. code-block:: csharp + + var message = new NativeLocalSymbol(body, offset: 0x6); + body.AddressFixups.Add(new AddressFixup( + 0x1, AddressFixupType.Absolute32BitAddress, message + )); + + +.. warning:: + + The ``NativeLocalSymbol`` can only be used within the code of the native method body itself. This is due to the fact that these types of symbols are not processed further after serializing a ``NativeMethodBody`` to a ``CodeSegment`` by the default method body serializer. + + +Fixup Types +~~~~~~~~~~~ + +The type of fixup that is required will depend on the architecture and instruction that is used. Below an overview of all fixups that AsmResolver is able to apply: +--------------------------+-----------------------------------------------------------------------+---------------------------------+ | Fixup type | Description | Example instructions | +==========================+=======================================================================+=================================+ -| ``Absolute32BitAddress`` | The operand is an absolute virtual address | ``call dword [address]`` | +| ``Absolute32BitAddress`` | The operand is a 32-bit absolute virtual address | ``call dword [address]`` | ++--------------------------+-----------------------------------------------------------------------+---------------------------------+ +| ``Absolute64BitAddress`` | The operand is a 64-bit absolute virtual address | ``mov rax, address`` | +--------------------------+-----------------------------------------------------------------------+---------------------------------+ | ``Relative32BitAddress`` | The operand is an address relative to the current instruction pointer | ``call qword [rip+offset]`` | +--------------------------+-----------------------------------------------------------------------+---------------------------------+ diff --git a/docs/index.rst b/docs/index.rst index b3fee3b95..89f3b0aeb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -55,10 +55,12 @@ Table of Contents: dotnet/member-tree dotnet/type-signatures dotnet/importing + dotnet/methods dotnet/managed-method-bodies dotnet/unmanaged-method-bodies dotnet/managed-resources dotnet/cloning dotnet/token-allocation dotnet/type-memory-layout - dotnet/advanced-pe-image-building.rst \ No newline at end of file + dotnet/bundles + dotnet/advanced-pe-image-building.rst diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 000000000..bebbea9fa --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +!AsmResolver.Symbols.Pdb/ + diff --git a/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj b/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj index 46d0b6a32..456ce3119 100644 --- a/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj +++ b/src/AsmResolver.DotNet/AsmResolver.DotNet.csproj @@ -1,14 +1,13 @@ - + AsmResolver.DotNet High level .NET image models for the AsmResolver executable file inspection toolsuite. exe pe directories imports exports resources dotnet cil inspection manipulation assembly disassembly - netstandard2.0 true 1701;1702;NU5105 - 9 enable + net6.0;netcoreapp3.1;netstandard2.0 @@ -28,7 +27,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/AsmResolver.DotNet/AssemblyDefinition.cs b/src/AsmResolver.DotNet/AssemblyDefinition.cs index e99d44242..fabe500a6 100644 --- a/src/AsmResolver.DotNet/AssemblyDefinition.cs +++ b/src/AsmResolver.DotNet/AssemblyDefinition.cs @@ -222,6 +222,13 @@ protected virtual IList GetModules() return _publicKeyToken; } + /// + public override bool IsImportedInModule(ModuleDefinition module) => ManifestModule == module; + + /// + public override AssemblyReference ImportWith(ReferenceImporter importer) => + (AssemblyReference) importer.ImportScope(new AssemblyReference(this)); + /// public override AssemblyDefinition Resolve() => this; @@ -280,11 +287,14 @@ public void Write(string filePath, IPEImageBuilder imageBuilder, IPEFileBuilder if (directory is null || !Directory.Exists(directory)) throw new DirectoryNotFoundException(); - foreach (var module in Modules) + for (int i = 0; i < Modules.Count; i++) { - string modulePath = module == ManifestModule - ? filePath - : Path.Combine(directory, module.Name); + var module = Modules[i]; + string modulePath; + if (module == ManifestModule) + modulePath = filePath; + else + modulePath = Path.Combine(directory, module.Name ?? $"module{i}.bin"); module.Write(modulePath, imageBuilder, fileBuilder); } diff --git a/src/AsmResolver.DotNet/AssemblyDescriptor.cs b/src/AsmResolver.DotNet/AssemblyDescriptor.cs index 248812ba8..5d19e5b42 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; @@ -29,7 +31,7 @@ protected AssemblyDescriptor(MetadataToken token) : base(token) { _name = new LazyVariable(GetName); - _culture = new LazyVariable(() => GetCulture()); + _culture = new LazyVariable(GetCulture); Version = new Version(0, 0, 0, 0); } @@ -212,6 +214,19 @@ public IList CustomAttributes /// public override string ToString() => FullName; + /// + public abstract bool IsImportedInModule(ModuleDefinition module); + + /// + /// Imports the assembly descriptor using the provided reference importer. + /// + /// The importer object to use. + /// The imported assembly reference. + public abstract AssemblyReference ImportWith(ReferenceImporter importer); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// /// 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..d81b2b9c9 100644 --- a/src/AsmResolver.DotNet/AssemblyReference.cs +++ b/src/AsmResolver.DotNet/AssemblyReference.cs @@ -147,6 +147,13 @@ public AssemblyReference(AssemblyDescriptor descriptor) /// protected virtual byte[]? GetHashValue() => null; + /// + public override bool IsImportedInModule(ModuleDefinition module) => Module == module; + + /// + public override AssemblyReference ImportWith(ReferenceImporter importer) => + (AssemblyReference) importer.ImportScope(this); + /// public override AssemblyDefinition? Resolve() => Module?.MetadataResolver.AssemblyResolver.Resolve(this); diff --git a/src/AsmResolver.DotNet/Builder/Discovery/MemberDiscoverer.cs b/src/AsmResolver.DotNet/Builder/Discovery/MemberDiscoverer.cs index d6f233046..d6eeac3c6 100644 --- a/src/AsmResolver.DotNet/Builder/Discovery/MemberDiscoverer.cs +++ b/src/AsmResolver.DotNet/Builder/Discovery/MemberDiscoverer.cs @@ -343,7 +343,7 @@ private FieldDefinition AddPlaceHolderField(TypeDefinition placeHolderType, Meta var placeHolderField = new FieldDefinition( $"PlaceHolderField_{token.Rid.ToString()}", FieldPlaceHolderAttributes, - FieldSignature.CreateStatic(_module.CorLibTypeFactory.Object)); + _module.CorLibTypeFactory.Object); // Add the field to the type. placeHolderType.Fields.Add(placeHolderField); diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs index 37b5c136b..2aeee551a 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.CodedIndices.cs @@ -26,16 +26,16 @@ private void AddCustomAttribute(MetadataToken ownerToken, CustomAttribute attrib table.Add(attribute, row); } - private uint AddResolutionScope(IResolutionScope? scope) + private uint AddResolutionScope(IResolutionScope? scope, bool allowDuplicates, bool preserveRid) { if (!AssertIsImported(scope)) return 0; var token = scope.MetadataToken.Table switch { - TableIndex.AssemblyRef => GetAssemblyReferenceToken(scope as AssemblyReference), - TableIndex.TypeRef => GetTypeReferenceToken(scope as TypeReference), - TableIndex.ModuleRef => GetModuleReferenceToken(scope as ModuleReference), + TableIndex.AssemblyRef => AddAssemblyReference(scope as AssemblyReference, allowDuplicates, preserveRid), + TableIndex.TypeRef => AddTypeReference(scope as TypeReference, allowDuplicates, preserveRid), + TableIndex.ModuleRef => AddModuleReference(scope as ModuleReference, allowDuplicates, preserveRid), TableIndex.Module => 0, _ => throw new ArgumentOutOfRangeException(nameof(scope)) }; diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs index ff010957a..617253eef 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.MemberTree.cs @@ -132,6 +132,9 @@ public void DefineTypes(IEnumerable types) var typeDefTable = Metadata.TablesStream.GetTable(TableIndex.TypeDef); var nestedClassTable = Metadata.TablesStream.GetSortedTable(TableIndex.NestedClass); + if (types is ICollection collection) + typeDefTable.EnsureCapacity(typeDefTable.Count + collection.Count); + foreach (var type in types) { // At this point, we might not have added all type defs/refs/specs yet, so we cannot determine @@ -177,6 +180,8 @@ public void DefineTypes(IEnumerable types) public void DefineFields(IEnumerable fields) { var table = Metadata.TablesStream.GetTable(TableIndex.Field); + if (fields is ICollection collection) + table.EnsureCapacity(table.Count + collection.Count); foreach (var field in fields) { @@ -197,6 +202,8 @@ public void DefineFields(IEnumerable fields) public void DefineMethods(IEnumerable methods) { var table = Metadata.TablesStream.GetTable(TableIndex.Method); + if (methods is ICollection collection) + table.EnsureCapacity(table.Count + collection.Count); foreach (var method in methods) { @@ -227,6 +234,8 @@ public void DefineMethods(IEnumerable methods) public void DefineParameters(IEnumerable parameters) { var table = Metadata.TablesStream.GetTable(TableIndex.Param); + if (parameters is ICollection collection) + table.EnsureCapacity(table.Count + collection.Count); foreach (var parameter in parameters) { @@ -247,6 +256,8 @@ public void DefineParameters(IEnumerable parameters) public void DefineProperties(IEnumerable properties) { var table = Metadata.TablesStream.GetTable(TableIndex.Property); + if (properties is ICollection collection) + table.EnsureCapacity(table.Count + collection.Count); foreach (var property in properties) { @@ -267,6 +278,8 @@ public void DefineProperties(IEnumerable properties) public void DefineEvents(IEnumerable events) { var table = Metadata.TablesStream.GetTable(TableIndex.Event); + if (events is ICollection collection) + table.EnsureCapacity(table.Count + collection.Count); foreach (var @event in events) { @@ -299,10 +312,21 @@ public void FinalizeTypes() uint propertyList = 1; uint eventList = 1; + tablesStream.GetTable(TableIndex.FieldPtr) + .EnsureCapacity(tablesStream.GetTable(TableIndex.Field).Count); + tablesStream.GetTable(TableIndex.MethodPtr) + .EnsureCapacity(tablesStream.GetTable(TableIndex.Method).Count); + tablesStream.GetTable(TableIndex.ParamPtr) + .EnsureCapacity(tablesStream.GetTable(TableIndex.Param).Count); + tablesStream.GetTable(TableIndex.PropertyPtr) + .EnsureCapacity(tablesStream.GetTable(TableIndex.Property).Count); + tablesStream.GetTable(TableIndex.EventPtr) + .EnsureCapacity(tablesStream.GetTable(TableIndex.Event).Count); + for (uint rid = 1; rid <= typeDefTable.Count; rid++) { var typeToken = new MetadataToken(TableIndex.TypeDef, rid); - var type = _tokenMapping.GetTypeByToken(typeToken); + var type = _tokenMapping.GetTypeByToken(typeToken)!; // Update extends, field list and method list columns. ref var typeRow = ref typeDefTable.GetRowRef(rid); @@ -409,7 +433,7 @@ private void FinalizeMethods(ref bool paramPtrRequired) for (uint rid = 1; rid <= definitionTable.Count; rid++) { var newToken = new MetadataToken(TableIndex.Method, rid); - var method = _tokenMapping.GetMethodByToken(newToken); + var method = _tokenMapping.GetMethodByToken(newToken)!; // Serialize method body and update column. ref var row = ref definitionTable.GetRowRef(rid); diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs index 4615f1005..b3eca662b 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryBuffer.TokenProvider.cs @@ -11,17 +11,37 @@ public partial class DotNetDirectoryBuffer : IMetadataTokenProvider /// public MetadataToken GetTypeReferenceToken(TypeReference? type) + { + return AddTypeReference(type, false, false); + } + + /// + /// Adds a type reference to the buffer. + /// + /// The reference to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// + /// true if the metadata token of the type should be preserved, false otherwise. + /// + /// The newly assigned metadata token. + public MetadataToken AddTypeReference(TypeReference? type, bool allowDuplicates, bool preserveRid) { if (!AssertIsImported(type)) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.TypeRef); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.TypeRef); var row = new TypeReferenceRow( - AddResolutionScope(type.Scope), + AddResolutionScope(type.Scope, allowDuplicates, preserveRid), Metadata.StringsStream.GetStringIndex(type.Name), Metadata.StringsStream.GetStringIndex(type.Namespace)); - var token = table.Add(row); + var token = preserveRid + ? table.Insert(type.MetadataToken.Rid, row, allowDuplicates) + : table.Add(row, allowDuplicates); + _tokenMapping.Register(type, token); AddCustomAttributes(token, type); return token; @@ -90,17 +110,31 @@ public MetadataToken GetEventDefinitionToken(EventDefinition? @event) /// public MetadataToken GetMemberReferenceToken(MemberReference? member) + { + return AddMemberReference(member, false); + } + + /// + /// Adds a member reference to the buffer. + /// + /// The reference to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// The newly assigned metadata token. + public MetadataToken AddMemberReference(MemberReference? member, bool allowDuplicates) { if (!AssertIsImported(member)) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.MemberRef); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.MemberRef); var row = new MemberReferenceRow( AddMemberRefParent(member.Parent), Metadata.StringsStream.GetStringIndex(member.Name), Metadata.BlobStream.GetBlobIndex(this, member.Signature, ErrorListener)); - var token = table.Add(row); + var token = table.Add(row, allowDuplicates); _tokenMapping.Register(member, token); AddCustomAttributes(token, member); return token; @@ -108,15 +142,29 @@ public MetadataToken GetMemberReferenceToken(MemberReference? member) /// public MetadataToken GetStandAloneSignatureToken(StandAloneSignature? signature) + { + return AddStandAloneSignature(signature, false); + } + + /// + /// Adds a stand-alone signature to the buffer. + /// + /// The signature to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// The newly assigned metadata token. + public MetadataToken AddStandAloneSignature(StandAloneSignature? signature, bool allowDuplicates) { if (signature is null) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.StandAloneSig); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.StandAloneSig); var row = new StandAloneSignatureRow( Metadata.BlobStream.GetBlobIndex(this, signature.Signature, ErrorListener)); - var token = table.Add(row); + var token = table.Add(row, allowDuplicates); _tokenMapping.Register(signature, token); AddCustomAttributes(token, signature); return token; @@ -124,11 +172,28 @@ public MetadataToken GetStandAloneSignatureToken(StandAloneSignature? signature) /// public MetadataToken GetAssemblyReferenceToken(AssemblyReference? assembly) + { + return AddAssemblyReference(assembly, false, false); + } + + /// + /// Adds an assembly reference to the buffer. + /// + /// The reference to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// + /// true if the metadata token of the assembly should be preserved, false otherwise. + /// + /// The newly assigned metadata token. + public MetadataToken AddAssemblyReference(AssemblyReference? assembly, bool allowDuplicates, bool preserveRid) { if (assembly is null || !AssertIsImported(assembly)) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.AssemblyRef); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.AssemblyRef); var row = new AssemblyReferenceRow((ushort) assembly.Version.Major, (ushort) assembly.Version.Minor, @@ -140,7 +205,10 @@ public MetadataToken GetAssemblyReferenceToken(AssemblyReference? assembly) Metadata.StringsStream.GetStringIndex(assembly.Culture), Metadata.BlobStream.GetBlobIndex(assembly.HashValue)); - var token = table.Add(row); + var token = preserveRid + ? table.Insert(assembly.MetadataToken.Rid, row, allowDuplicates) + : table.Add(row, allowDuplicates); + AddCustomAttributes(token, assembly); return token; } @@ -151,28 +219,62 @@ public MetadataToken GetAssemblyReferenceToken(AssemblyReference? assembly) /// The reference to add. /// The new metadata token assigned to the module reference. public MetadataToken GetModuleReferenceToken(ModuleReference? reference) + { + return AddModuleReference(reference, false, false); + } + + /// + /// Adds a module reference to the buffer. + /// + /// The reference to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// + /// true if the metadata token of the module should be preserved, false otherwise. + /// + /// The newly assigned metadata token. + public MetadataToken AddModuleReference(ModuleReference? reference, bool allowDuplicates, bool preserveRid) { if (!AssertIsImported(reference)) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.ModuleRef); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.ModuleRef); var row = new ModuleReferenceRow(Metadata.StringsStream.GetStringIndex(reference.Name)); - var token = table.Add(row); + var token = preserveRid + ? table.Insert(reference.MetadataToken.Rid, row, allowDuplicates) + : table.Add(row, allowDuplicates); + AddCustomAttributes(token, reference); return token; } /// public MetadataToken GetTypeSpecificationToken(TypeSpecification? type) + { + return AddTypeSpecification(type, false); + } + + /// + /// Adds a type specification to the buffer. + /// + /// The specification to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// The newly assigned metadata token. + public MetadataToken AddTypeSpecification(TypeSpecification? type, bool allowDuplicates) { if (!AssertIsImported(type)) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.TypeSpec); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.TypeSpec); var row = new TypeSpecificationRow(Metadata.BlobStream.GetBlobIndex(this, type.Signature, ErrorListener)); - var token = table.Add(row); + var token = table.Add(row, allowDuplicates); _tokenMapping.Register(type, token); AddCustomAttributes(token, type); return token; @@ -180,16 +282,30 @@ public MetadataToken GetTypeSpecificationToken(TypeSpecification? type) /// public MetadataToken GetMethodSpecificationToken(MethodSpecification? method) + { + return AddMethodSpecification(method, false); + } + + /// + /// Adds a method specification to the buffer. + /// + /// The specification to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// + /// The newly assigned metadata token. + public MetadataToken AddMethodSpecification(MethodSpecification? method, bool allowDuplicates) { if (!AssertIsImported(method)) return MetadataToken.Zero; - var table = Metadata.TablesStream.GetTable(TableIndex.MethodSpec); + var table = Metadata.TablesStream.GetDistinctTable(TableIndex.MethodSpec); var row = new MethodSpecificationRow( AddMethodDefOrRef(method.Method), Metadata.BlobStream.GetBlobIndex(this, method.Signature, ErrorListener)); - var token = table.Add(row); + var token = table.Add(row, allowDuplicates); _tokenMapping.Register(method, token); AddCustomAttributes(token, method); return token; diff --git a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs index 8def5c196..7c0619f9f 100644 --- a/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs +++ b/src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs @@ -84,7 +84,7 @@ public IMethodBodySerializer MethodBodySerializer // When specified, import existing AssemblyRef, ModuleRef, TypeRef and MemberRef prior to adding any other // member reference or definition, to ensure that they are assigned their original RIDs. - ImportBasicTablesIntoTableBuffersIfSpecified(module, buffer); + ImportBasicTablesIfSpecified(module, buffer); // Define all types defined in the module. buffer.DefineTypes(discoveryResult.Types); @@ -102,7 +102,7 @@ public IMethodBodySerializer MethodBodySerializer // Import remaining preservable tables (Type specs, method specs, signatures etc). // We do this before finalizing any member to ensure that they are assigned their original RIDs. - ImportRemainingTablesIntoTableBuffersIfSpecified(module, buffer); + ImportRemainingTablesIfSpecified(module, buffer); // Finalize member definitions. buffer.FinalizeTypes(); @@ -197,7 +197,7 @@ private IMetadataBuffer CreateMetadataBuffer(ModuleDefinition module) return metadataBuffer; } - private void ImportBasicTablesIntoTableBuffersIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) + private void ImportBasicTablesIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { if (module.DotNetDirectory is null) return; @@ -210,37 +210,58 @@ private void ImportBasicTablesIntoTableBuffersIfSpecified(ModuleDefinition modul // and type reference tokens are still preserved, we need to prioritize these. if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveAssemblyReferenceIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.AssemblyRef, buffer.GetAssemblyReferenceToken); + { + ImportTables(module, TableIndex.AssemblyRef, + r => buffer.AddAssemblyReference(r, true, true)); + } if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveModuleReferenceIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.ModuleRef, buffer.GetModuleReferenceToken); + { + ImportTables(module, TableIndex.ModuleRef, + r => buffer.AddModuleReference(r, true, true)); + } if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveTypeReferenceIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.TypeRef, buffer.GetTypeReferenceToken); + { + ImportTables(module, TableIndex.TypeRef, + r => buffer.AddTypeReference(r, true, true)); + } } private void ImportTypeSpecsAndMemberRefsIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveTypeSpecificationIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.TypeSpec, buffer.GetTypeSpecificationToken); + { + ImportTables(module, TableIndex.TypeSpec, + s => buffer.AddTypeSpecification(s, true)); + } if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveMemberReferenceIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.MemberRef, buffer.GetMemberReferenceToken); + { + ImportTables(module, TableIndex.MemberRef, + r => buffer.AddMemberReference(r, true)); + } } - private void ImportRemainingTablesIntoTableBuffersIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) + private void ImportRemainingTablesIfSpecified(ModuleDefinition module, DotNetDirectoryBuffer buffer) { if (module.DotNetDirectory is null) return; if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveStandAloneSignatureIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.StandAloneSig, buffer.GetStandAloneSignatureToken); + { + ImportTables(module, TableIndex.StandAloneSig, + s => buffer.AddStandAloneSignature(s, true)); + } if ((MetadataBuilderFlags & MetadataBuilderFlags.PreserveMethodSpecificationIndices) != 0) - ImportTableIntoTableBuffers(module, TableIndex.MethodSpec, buffer.GetMethodSpecificationToken); + { + ImportTables(module, TableIndex.MethodSpec, + s => buffer.AddMethodSpecification(s, true)); + } } - private static void ImportTableIntoTableBuffers(ModuleDefinition module, TableIndex tableIndex, + private static void ImportTables(ModuleDefinition module, TableIndex tableIndex, Func importAction) { int count = module.DotNetDirectory!.Metadata diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs index b9283f323..4074289d4 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Blob/BlobStreamBuffer.cs @@ -14,9 +14,11 @@ 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); + private readonly MemoryStreamWriterPool _blobWriterPool = new(); + /// /// Creates a new blob stream buffer with the default blob stream name. /// @@ -116,11 +118,11 @@ public uint GetBlobIndex(ITypeCodedIndexProvider provider, BlobSignature? signat return 0u; // Serialize blob. - using var stream = new MemoryStream(); - var writer = new BinaryStreamWriter(stream); - signature.Write(new BlobSerializationContext(writer, provider, errorListener)); - return GetBlobIndex(stream.ToArray()); + using var rentedWriter = _blobWriterPool.Rent(); + signature.Write(new BlobSerializationContext(rentedWriter.Writer, provider, errorListener)); + + return GetBlobIndex(rentedWriter.GetData()); } /// 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/Strings/StringsStreamBlobSuffixComparer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Strings/StringsStreamBlobSuffixComparer.cs index 47da9a1a0..3faccd70c 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Strings/StringsStreamBlobSuffixComparer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Strings/StringsStreamBlobSuffixComparer.cs @@ -29,8 +29,15 @@ public int Compare(KeyValuePair x, KeyValuePair - public int Compare(byte[] x, byte[] y) + public int Compare(byte[]? x, byte[]? y) { + if (ReferenceEquals(x, y)) + return 0; + if (x is null) + return -1; + if (y is null) + return 1; + for (int i = x.Length - 1, j = y.Length - 1; i >= 0 && j >= 0; i--, j--) { int charComparison = x[i].CompareTo(y[j]); diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs index 1c1f7f796..ac966743d 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/DistinctMetadataTableBuffer.cs @@ -44,17 +44,68 @@ public DistinctMetadataTableBuffer(IMetadataTableBuffer underlyingBuffer) } } + /// + public void EnsureCapacity(int capacity) + { + _underlyingBuffer.EnsureCapacity(capacity); + +#if NETSTANDARD2_1_OR_GREATER + _entries.EnsureCapacity(capacity); +#endif + } + /// public ref TRow GetRowRef(uint rid) => ref _underlyingBuffer.GetRowRef(rid); /// - public MetadataToken Add(in TRow row) + public MetadataToken Add(in TRow row) => Add(row, false); + + /// + public MetadataToken Insert(uint rid, in TRow row) => Insert(rid, row, false); + + /// + /// Inserts a row into the metadata table at the provided row identifier. + /// + /// The row identifier. + /// The row to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// The metadata token that this row was assigned to. + public MetadataToken Insert(uint rid, in TRow row, bool allowDuplicates) + { + if (!_entries.TryGetValue(row, out var token)) + { + token = _underlyingBuffer.Insert(rid, in row); + _entries.Add(row, token); + } + else if (allowDuplicates) + { + token = _underlyingBuffer.Insert(rid, in row); + } + + return token; + } + + /// + /// Adds a row to the metadata table buffer. + /// + /// The row to add. + /// + /// true if the row is always to be added to the end of the buffer, false if a duplicated row + /// is supposed to be removed and the token of the original should be returned instead. + /// The metadata token that this row was assigned to. + public MetadataToken Add(in TRow row, bool allowDuplicates) { if (!_entries.TryGetValue(row, out var token)) { token = _underlyingBuffer.Add(in row); _entries.Add(row, token); } + else if (allowDuplicates) + { + token = _underlyingBuffer.Add(in row); + } return token; } diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs index bcf1b8bfd..b73b5061a 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/IMetadataTableBuffer.cs @@ -44,6 +44,12 @@ public interface IMetadataTableBuffer : IMetadataTableBuffer set; } + /// + /// Ensures the capacity of the table buffer is at least the provided amount of elements. + /// + /// The number of elements to store. + void EnsureCapacity(int capacity); + /// /// Gets or sets a reference to a row in the metadata table. /// @@ -56,5 +62,13 @@ public interface IMetadataTableBuffer : IMetadataTableBuffer /// The row to add. /// The metadata token that this row was assigned to. MetadataToken Add(in TRow row); + + /// + /// Inserts a row into the metadata table at the provided row identifier. + /// + /// The row identifier. + /// The row to add. + /// The metadata token that this row was assigned to. + MetadataToken Insert(uint rid, in TRow row); } } diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/SortedMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/SortedMetadataTableBuffer.cs index d8d9b9aa4..7f33b1694 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/SortedMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/SortedMetadataTableBuffer.cs @@ -12,6 +12,7 @@ namespace AsmResolver.DotNet.Builder.Metadata.Tables /// The type of members that are assigned new metadata rows. /// The type of rows to store. public class SortedMetadataTableBuffer : ISortedMetadataTableBuffer + where TKey : notnull where TRow : struct, IMetadataRow { private readonly List<(TKey Key, TRow Row)> _entries = new(); diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/TablesStreamBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/TablesStreamBuffer.cs index e379139e1..49651b052 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/TablesStreamBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/TablesStreamBuffer.cs @@ -120,6 +120,18 @@ public IMetadataTableBuffer GetTable(TableIndex table) return (IMetadataTableBuffer) _tableBuffers[(int) table]; } + /// + /// Gets a table buffer by its table index. + /// + /// The index of the table to get. + /// The type of rows the table stores. + /// The metadata table. + public DistinctMetadataTableBuffer GetDistinctTable(TableIndex table) + where TRow : struct, IMetadataRow + { + return (DistinctMetadataTableBuffer) _tableBuffers[(int) table]; + } + /// /// Gets a table buffer by its table index. /// @@ -171,6 +183,7 @@ private IMetadataTableBuffer Distinct(TableIndex table) } private ISortedMetadataTableBuffer Sorted(TableIndex table, int primaryColumn) + where TKey : notnull where TRow : struct, IMetadataRow { return new SortedMetadataTableBuffer( @@ -179,6 +192,7 @@ private IMetadataTableBuffer Distinct(TableIndex table) } private ISortedMetadataTableBuffer Sorted(TableIndex table, int primaryColumn, int secondaryColumn) + where TKey : notnull where TRow : struct, IMetadataRow { return new SortedMetadataTableBuffer( diff --git a/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs b/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs index e94a81d44..17717b185 100644 --- a/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs +++ b/src/AsmResolver.DotNet/Builder/Metadata/Tables/UnsortedMetadataTableBuffer.cs @@ -15,7 +15,9 @@ public class UnsortedMetadataTableBuffer : IMetadataTableBuffer where TRow : struct, IMetadataRow { private readonly RefList _entries = new(); + private readonly BitList _available = new(); private readonly MetadataTable _table; + private uint _currentRid; /// /// Creates a new unsorted metadata table buffer. @@ -33,7 +35,18 @@ public UnsortedMetadataTableBuffer(MetadataTable table) public virtual TRow this[uint rid] { get => _entries[(int) (rid - 1)]; - set => _entries[(int) (rid - 1)] = value; + set + { + _entries[(int) (rid - 1)] = value; + _available[(int) (rid - 1)] = false; + } + } + + /// + public void EnsureCapacity(int capacity) + { + if (_entries.Capacity < capacity) + _entries.Capacity = capacity; } /// @@ -42,13 +55,53 @@ public UnsortedMetadataTableBuffer(MetadataTable table) /// public virtual MetadataToken Add(in TRow row) { - _entries.Add(row); - return new MetadataToken(_table.TableIndex, (uint) _entries.Count); + // Move over unavailable slots. + while (_currentRid < _available.Count && !_available[(int) _currentRid]) + _currentRid++; + + // If we moved over all entries, we're adding to the end. + if (_currentRid == _entries.Count) + { + _currentRid++; + } + + return Insert(_currentRid++, row); + } + + /// + public MetadataToken Insert(uint rid, in TRow row) + { + EnsureRowsAllocated(rid); + + var token = new MetadataToken(_table.TableIndex, rid); + + if (!_available[(int) (rid - 1)]) + { + if (EqualityComparer.Default.Equals(row, _entries[(int) (rid - 1)])) + return token; + + throw new InvalidOperationException($"Token 0x{token.ToString()} is already in use."); + } + + this[rid] = row; + return token; + } + + private void EnsureRowsAllocated(uint rid) + { + while (_entries.Count < rid) + { + _entries.Add(default); + _available.Add(true); + } } /// public void FlushToTable() { + if (_table.Capacity < _entries.Count) + _table.Capacity = _entries.Count; + foreach (var row in _entries) _table.Add(row); } 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/PEImageBuildResult.cs b/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs index faec68be5..95aef83eb 100644 --- a/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs +++ b/src/AsmResolver.DotNet/Builder/PEImageBuildResult.cs @@ -34,7 +34,7 @@ public PEImageBuildResult(IPEImage? image, DiagnosticBag diagnosticBag, ITokenMa /// Gets a value indicating whether the image was constructed successfully or not. /// [MemberNotNullWhen(false, nameof(ConstructedImage))] - public bool HasFailed => ConstructedImage is null; + public bool HasFailed => DiagnosticBag.IsFatal; /// /// Gets the bag containing the diagnostics that were collected during the construction of the image. 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/Builder/TokenMapping.cs b/src/AsmResolver.DotNet/Builder/TokenMapping.cs index 9ae079ba4..678dc1753 100644 --- a/src/AsmResolver.DotNet/Builder/TokenMapping.cs +++ b/src/AsmResolver.DotNet/Builder/TokenMapping.cs @@ -102,13 +102,13 @@ public void Register(IMetadataMember member, MetadataToken newToken) /// /// The new token. /// The type, or null if no type is assigned to the provided token. - public TypeDefinition GetTypeByToken(MetadataToken newToken) => _typeDefTokens.GetKey(newToken); + public TypeDefinition? GetTypeByToken(MetadataToken newToken) => _typeDefTokens.GetKey(newToken); /// /// Gets the method assigned to the provided metadata token. /// /// The new token. /// The type, or null if no method is assigned to the provided token. - public MethodDefinition GetMethodByToken(MetadataToken newToken) => _methodTokens.GetKey(newToken); + public MethodDefinition? GetMethodByToken(MetadataToken newToken) => _methodTokens.GetKey(newToken); } } 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/CilInstructionCollection.cs b/src/AsmResolver.DotNet/Code/Cil/CilInstructionCollection.cs index 8287e1895..970bc7b26 100644 --- a/src/AsmResolver.DotNet/Code/Cil/CilInstructionCollection.cs +++ b/src/AsmResolver.DotNet/Code/Cil/CilInstructionCollection.cs @@ -260,7 +260,7 @@ private void ExpandMacro(CilInstruction instruction) break; case CilCode.Ldarga_S: - instruction.OpCode = CilOpCodes.Ldarga_S; + instruction.OpCode = CilOpCodes.Ldarga; break; case CilCode.Starg_S: @@ -314,7 +314,7 @@ private void ExpandMacro(CilInstruction instruction) instruction.OpCode = CilOpCodes.Brtrue; break; case CilCode.Bge_Un_S: - instruction.OpCode = CilOpCodes.Bge_Un_S; + instruction.OpCode = CilOpCodes.Bge_Un; break; case CilCode.Bgt_Un_S: instruction.OpCode = CilOpCodes.Bgt_Un; diff --git a/src/AsmResolver.DotNet/Code/Cil/CilLabelVerifier.cs b/src/AsmResolver.DotNet/Code/Cil/CilLabelVerifier.cs index 6e32ef202..9ad289287 100644 --- a/src/AsmResolver.DotNet/Code/Cil/CilLabelVerifier.cs +++ b/src/AsmResolver.DotNet/Code/Cil/CilLabelVerifier.cs @@ -130,7 +130,7 @@ private void AddDiagnostic(string message) { _diagnostics ??= new List(); _cachedName ??= _body.Owner.SafeToString(); - _diagnostics.Add(new InvalidCilInstructionException($"[{_cachedName}]: {message}")); + _diagnostics.Add(new InvalidCilInstructionException($"[In {_cachedName}]: {message}")); } } diff --git a/src/AsmResolver.DotNet/Code/Cil/CilMethodBody.cs b/src/AsmResolver.DotNet/Code/Cil/CilMethodBody.cs index de4b9718a..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)) @@ -147,7 +148,7 @@ public static CilMethodBody FromDynamicMethod(MethodDefinition method, object dy //Get Runtime Fields byte[] code = FieldReader.ReadField(dynamicMethodObj, "m_code")!; object scope = FieldReader.ReadField(dynamicMethodObj, "m_scope")!; - var tokenList = FieldReader.ReadField>(scope, "m_tokens")!; + var tokenList = FieldReader.ReadField>(scope, "m_tokens")!; byte[] localSig = FieldReader.ReadField(dynamicMethodObj, "m_localSignature")!; byte[] ehHeader = FieldReader.ReadField(dynamicMethodObj, "m_exceptionHeader")!; var ehInfos = FieldReader.ReadField>(dynamicMethodObj, "m_exceptions")!; diff --git a/src/AsmResolver.DotNet/Code/Cil/CilMethodBodySerializer.cs b/src/AsmResolver.DotNet/Code/Cil/CilMethodBodySerializer.cs index 5f920444a..48026a8a6 100644 --- a/src/AsmResolver.DotNet/Code/Cil/CilMethodBodySerializer.cs +++ b/src/AsmResolver.DotNet/Code/Cil/CilMethodBodySerializer.cs @@ -15,6 +15,8 @@ namespace AsmResolver.DotNet.Code.Cil /// public class CilMethodBodySerializer : IMethodBodySerializer { + private readonly MemoryStreamWriterPool _writerPool = new(); + /// /// Gets or sets the value of an override switch indicating whether the max stack should always be recalculated /// or should always be preserved. @@ -88,7 +90,7 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont } // Serialize CIL stream. - var code = BuildRawCodeStream(context, body); + byte[] code = BuildRawCodeStream(context, body); // Build method body. var rawBody = body.IsFat @@ -98,8 +100,7 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont return rawBody.ToReference(); } - private static CilRawMethodBody BuildTinyMethodBody(byte[] code) => - new CilRawTinyMethodBody(code); + private static CilRawMethodBody BuildTinyMethodBody(byte[] code) => new CilRawTinyMethodBody(code); private CilRawMethodBody BuildFatMethodBody(MethodBodySerializationContext context, CilMethodBody body, byte[] code) { @@ -137,35 +138,33 @@ private CilRawMethodBody BuildFatMethodBody(MethodBodySerializationContext conte return fatBody; } - private static byte[] BuildRawCodeStream(MethodBodySerializationContext context, CilMethodBody body) + private byte[] BuildRawCodeStream(MethodBodySerializationContext context, CilMethodBody body) { - using var codeStream = new MemoryStream(); var bag = context.ErrorListener; - var writer = new BinaryStreamWriter(codeStream); + using var rentedWriter = _writerPool.Rent(); var assembler = new CilAssembler( - writer, + rentedWriter.Writer, new CilOperandBuilder(context.TokenProvider, bag), - body.Owner.SafeToString(), + body.Owner.SafeToString, bag); assembler.WriteInstructions(body.Instructions); - return codeStream.ToArray(); + return rentedWriter.GetData(); } private byte[] SerializeExceptionHandlers(MethodBodySerializationContext context, IList exceptionHandlers, bool needsFatFormat) { - using var sectionStream = new MemoryStream(); - var writer = new BinaryStreamWriter(sectionStream); + using var rentedWriter = _writerPool.Rent(); for (int i = 0; i < exceptionHandlers.Count; i++) { var handler = exceptionHandlers[i]; - WriteExceptionHandler(context, writer, handler, needsFatFormat); + WriteExceptionHandler(context, rentedWriter.Writer, handler, needsFatFormat); } - return sectionStream.ToArray(); + return rentedWriter.GetData(); } private void WriteExceptionHandler(MethodBodySerializationContext context, IBinaryStreamWriter writer, CilExceptionHandler handler, bool useFatFormat) diff --git a/src/AsmResolver.DotNet/Code/Cil/DynamicCilOperandResolver.cs b/src/AsmResolver.DotNet/Code/Cil/DynamicCilOperandResolver.cs index 203e7f554..d3244f8e2 100644 --- a/src/AsmResolver.DotNet/Code/Cil/DynamicCilOperandResolver.cs +++ b/src/AsmResolver.DotNet/Code/Cil/DynamicCilOperandResolver.cs @@ -16,11 +16,11 @@ namespace AsmResolver.DotNet.Code.Cil public class DynamicCilOperandResolver : PhysicalCilOperandResolver { private readonly ModuleReaderContext _readerContext; - private readonly IList _tokens; + private readonly IList _tokens; private readonly ReferenceImporter _importer; /// - public DynamicCilOperandResolver(SerializedModuleDefinition contextModule, CilMethodBody methodBody, IList tokens) + public DynamicCilOperandResolver(SerializedModuleDefinition contextModule, CilMethodBody methodBody, IList tokens) : base(contextModule, methodBody) { _tokens = tokens ?? throw new ArgumentNullException(nameof(tokens)); @@ -34,13 +34,13 @@ public DynamicCilOperandResolver(SerializedModuleDefinition contextModule, CilMe switch (token.Table) { case TableIndex.TypeDef: - var type = _tokens[(int) token.Rid]; + object? type = _tokens[(int) token.Rid]; if (type is RuntimeTypeHandle runtimeTypeHandle) return _importer.ImportType(Type.GetTypeFromHandle(runtimeTypeHandle)); break; case TableIndex.Field: - var field = _tokens[(int) token.Rid]; + object? field = _tokens[(int) token.Rid]; if (field is null) return null; @@ -61,10 +61,18 @@ public DynamicCilOperandResolver(SerializedModuleDefinition contextModule, CilMe case TableIndex.Method: case TableIndex.MemberRef: - var obj = _tokens[(int) token.Rid]; + object? obj = _tokens[(int) token.Rid]; + + if (obj is null) + return null; if (obj is RuntimeMethodHandle methodHandle) - return _importer.ImportMethod(MethodBase.GetMethodFromHandle(methodHandle)); + { + var method = MethodBase.GetMethodFromHandle(methodHandle); + return method is not null + ? _importer.ImportMethod(method) + : null; + } if (obj.GetType().FullName == "System.Reflection.Emit.GenericMethodInfo") { @@ -75,7 +83,9 @@ public DynamicCilOperandResolver(SerializedModuleDefinition contextModule, CilMe hasHandle ? mMethod : mHandle, context); - return _importer.ImportMethod(method); + return method is not null + ? _importer.ImportMethod(method) + : null; } if (obj.GetType().FullName == "System.Reflection.Emit.VarArgMethod") @@ -84,7 +94,7 @@ public DynamicCilOperandResolver(SerializedModuleDefinition contextModule, CilMe break; case TableIndex.StandAloneSig: - var reader = ByteArrayDataSource.CreateReader((byte[]) _tokens[(int) token.Rid]); + var reader = ByteArrayDataSource.CreateReader((byte[]) _tokens[(int) token.Rid]!); return CallingConventionSignature.FromReader(new BlobReadContext(_readerContext), ref reader); } diff --git a/src/AsmResolver.DotNet/Code/Cil/StackImbalanceException.cs b/src/AsmResolver.DotNet/Code/Cil/StackImbalanceException.cs index 79b89bf64..ac6f97fbc 100644 --- a/src/AsmResolver.DotNet/Code/Cil/StackImbalanceException.cs +++ b/src/AsmResolver.DotNet/Code/Cil/StackImbalanceException.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace AsmResolver.DotNet.Code.Cil { @@ -13,8 +13,7 @@ public class StackImbalanceException : Exception /// The method body in which the inconsistency was detected. /// The offset at which the inconsistency was detected. public StackImbalanceException(CilMethodBody body, int offset) - : base(string.Format("Stack imbalance was detected at offset IL_{0:X4} in method body of {1}", - offset, body.Owner)) + : base($"Stack imbalance was detected at offset IL_{offset:X4} in method body of {body.Owner}") { Body = body; Offset = offset; @@ -36,4 +35,4 @@ public int Offset get; } } -} \ No newline at end of file +} diff --git a/src/AsmResolver.DotNet/Code/MethodBody.cs b/src/AsmResolver.DotNet/Code/MethodBody.cs index ad3b4828a..b9837850d 100644 --- a/src/AsmResolver.DotNet/Code/MethodBody.cs +++ b/src/AsmResolver.DotNet/Code/MethodBody.cs @@ -1,27 +1,37 @@ -using System; - -namespace AsmResolver.DotNet.Code -{ - /// - /// Represents a body of a method defined in a .NET assembly. - /// - public abstract class MethodBody - { - /// - /// Initializes a new empty method body. - /// - /// The owner of the method body. - protected MethodBody(MethodDefinition owner) - { - Owner = owner ?? throw new ArgumentNullException(nameof(owner)); - } - - /// - /// Gets the method that owns the method body. - /// - public MethodDefinition Owner - { - get; - } - } -} \ No newline at end of file +using System; + +namespace AsmResolver.DotNet.Code +{ + /// + /// Represents a body of a method defined in a .NET assembly. + /// + public abstract class MethodBody + { + /// + /// Initializes a new empty method body. + /// + /// The owner of the method body. + protected MethodBody(MethodDefinition owner) + { + Owner = owner ?? throw new ArgumentNullException(nameof(owner)); + } + + /// + /// Gets the method that owns the method body. + /// + public MethodDefinition Owner + { + get; + } + + /// + /// When this method is stored in a serialized module, gets or sets the reference to the beginning of the + /// raw contents of the body. + /// + public ISegmentReference? Address + { + get; + set; + } + } +} diff --git a/src/AsmResolver.DotNet/Code/Native/NativeLocalSymbol.cs b/src/AsmResolver.DotNet/Code/Native/NativeLocalSymbol.cs new file mode 100644 index 000000000..8d165fe41 --- /dev/null +++ b/src/AsmResolver.DotNet/Code/Native/NativeLocalSymbol.cs @@ -0,0 +1,43 @@ +namespace AsmResolver.DotNet.Code.Native +{ + /// + /// Represents a symbol within a native method body. + /// + public class NativeLocalSymbol : ISymbol + { + /// + /// Creates a new native local symbol. + /// + /// The body that defines this symbol. + /// The offset relative to the start of the method body. + public NativeLocalSymbol(NativeMethodBody body, uint offset) + { + Body = body; + Offset = offset; + } + + /// + /// Gets the body that this symbol is defined in. + /// + public NativeMethodBody Body + { + get; + } + + /// + /// Gets the offset of the symbol, relative to the start of the method body. + /// + public uint Offset + { + get; + } + + /// + public ISegmentReference? GetReference() => Body.Address is not null + ? new RelativeReference(Body.Address, (int) Offset) + : null; + + /// + public override string ToString() => $"{Body.Owner.SafeToString()}+{Offset:X}"; + } +} diff --git a/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs b/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs index 2c8c879b0..e4253b717 100644 --- a/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs +++ b/src/AsmResolver.DotNet/Code/Native/NativeMethodBodySerializer.cs @@ -12,7 +12,7 @@ public class NativeMethodBodySerializer : IMethodBodySerializer /// public ISegmentReference SerializeMethodBody(MethodBodySerializationContext context, MethodDefinition method) { - if (!(method.MethodBody is NativeMethodBody nativeMethodBody)) + if (method.MethodBody is not NativeMethodBody nativeMethodBody) return SegmentReference.Null; var provider = context.SymbolsProvider; @@ -25,23 +25,55 @@ public ISegmentReference SerializeMethodBody(MethodBodySerializationContext cont { // Import symbol. var fixup = nativeMethodBody.AddressFixups[i]; - var symbol = provider.ImportSymbol(fixup.Symbol); + var symbol = FinalizeSymbol(segment, provider, fixup.Symbol); // Create new fixup with imported symbol. segment.AddressFixups.Add(new AddressFixup(fixup.Offset, fixup.Type, symbol)); // Add base relocation when necessary. - // TODO: keep architecture into account.. - if (fixup.Type == AddressFixupType.Absolute32BitAddress) - { - var relocation = new BaseRelocation( - RelocationType.HighLow, - segment.ToReference((int) fixup.Offset)); - provider.RegisterBaseRelocation(relocation); - } + AddBaseRelocations(segment, provider, fixup); } return segment.ToReference(); } + + /// + /// Registers base relocations for the provided address fixup, if required. + /// + /// The code segment that is being constructed. + /// The object responsible for providing symbols referenced by the native method body. + /// The fixup to build base relocations for. + protected virtual void AddBaseRelocations(CodeSegment segment, INativeSymbolsProvider provider, AddressFixup fixup) + { + switch (fixup.Type) + { + case AddressFixupType.Absolute32BitAddress: + provider.RegisterBaseRelocation(new BaseRelocation( + RelocationType.HighLow, + segment.ToReference((int) fixup.Offset))); + break; + + case AddressFixupType.Absolute64BitAddress: + provider.RegisterBaseRelocation(new BaseRelocation( + RelocationType.Dir64, + segment.ToReference((int) fixup.Offset))); + break; + } + } + + /// + /// Ensures the right symbol is used within the method body. + /// + /// The code segment that is being constructed. + /// The object responsible for providing symbols referenced by the native method body. + /// The symbol to reference. + /// The symbol. + protected virtual ISymbol FinalizeSymbol(CodeSegment result, INativeSymbolsProvider provider, ISymbol symbol) + { + if (symbol is NativeLocalSymbol local) + return new Symbol(result.ToReference((int) local.Offset)); + + return provider.ImportSymbol(symbol); + } } } diff --git a/src/AsmResolver.DotNet/Code/UnresolvedMethodBody.cs b/src/AsmResolver.DotNet/Code/UnresolvedMethodBody.cs new file mode 100644 index 000000000..52123b1f3 --- /dev/null +++ b/src/AsmResolver.DotNet/Code/UnresolvedMethodBody.cs @@ -0,0 +1,20 @@ +namespace AsmResolver.DotNet.Code +{ + /// + /// Provides a wrapper around a , pointing to the beginning of a method body. + /// The interpretation of the data behind the pointer was left to the user. + /// + public class UnresolvedMethodBody : MethodBody + { + /// + /// Creates a new unresolved method body stub. + /// + /// The owner of the method body. + /// The reference to the start of the method body. + public UnresolvedMethodBody(MethodDefinition owner, ISegmentReference address) + : base(owner) + { + Address = address; + } + } +} diff --git a/src/AsmResolver.DotNet/Collections/LazyRidListRelation.cs b/src/AsmResolver.DotNet/Collections/LazyRidListRelation.cs index 8ee9a5c18..763648a44 100644 --- a/src/AsmResolver.DotNet/Collections/LazyRidListRelation.cs +++ b/src/AsmResolver.DotNet/Collections/LazyRidListRelation.cs @@ -15,67 +15,86 @@ internal class LazyRidListRelation private readonly IMetadata _metadata; private readonly TableIndex _associationTable; + private readonly TableIndex _memberTable; private readonly GetOwnerRidDelegate _getOwnerRid; - private readonly GetMemberListDelegate _getMemberList; + private readonly GetMemberListDelegate _getMemberRange; private readonly object _lock = new(); - private IDictionary? _memberLists; - private IDictionary? _memberOwners; + // We use a dictionary here instead of an array, as not all owners might have an association. + // e.g. not all TypeDefs have a PropertyMap assigned to them. + private Dictionary? _memberRanges; + + // We can use an array instead of a dictionary here since all members will have an owner. + // _memberOwnerRids[memberRid - 1] = ownerRid. + private uint[]? _memberOwnerRids; public LazyRidListRelation( IMetadata metadata, + TableIndex memberTable, TableIndex associationTable, GetOwnerRidDelegate getOwnerRid, GetMemberListDelegate getMemberList) { _metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); _getOwnerRid = getOwnerRid ?? throw new ArgumentNullException(nameof(getOwnerRid)); - _getMemberList = getMemberList ?? throw new ArgumentNullException(nameof(getMemberList)); + _getMemberRange = getMemberList ?? throw new ArgumentNullException(nameof(getMemberList)); _associationTable = associationTable; + _memberTable = memberTable; } - [MemberNotNull(nameof(_memberLists))] - [MemberNotNull(nameof(_memberOwners))] + [MemberNotNull(nameof(_memberRanges))] + [MemberNotNull(nameof(_memberOwnerRids))] private void EnsureIsInitialized() { - if (_memberLists is null || _memberOwners is null) + // Note: This is a hot path, thus we don't lock here until we're absolutely sure we need to initialize. + if (_memberRanges is null || _memberOwnerRids is null) Initialize(); } - [MemberNotNull(nameof(_memberLists))] - [MemberNotNull(nameof(_memberOwners))] + [MemberNotNull(nameof(_memberRanges))] + [MemberNotNull(nameof(_memberOwnerRids))] private void Initialize() { lock (_lock) { - if (_memberLists is not null && _memberOwners is not null) + // Check if some other thread was also initializing it in the mean time. + if (_memberRanges is not null && _memberOwnerRids is not null) return; var tablesStream = _metadata.GetStream(); var associationTable = tablesStream.GetTable(_associationTable); + var memberTable = tablesStream.GetTable(_memberTable); - _memberLists = new Dictionary(); - _memberOwners = new Dictionary(); + // Note: we don't assign the _memberRanges and _memberOwnerRids directly here. + // This is to prevent a very nasty but subtle race condition (and also a data race) from happening, where + // EnsureIsInitialized might return prematurely before the lists are fully initialized. + // See: https://github.com/Washi1337/AsmResolver/issues/299 + var memberRanges = new Dictionary(); + uint[] memberOwnerRids = new uint[memberTable.Count]; for (int i = 0; i < associationTable.Count; i++) { - uint rid = (uint) (i + 1); - InitializeMemberList(_getOwnerRid(rid, associationTable[i]), _getMemberList(rid)); + uint associationRid = (uint) (i + 1); + + uint ownerRid = _getOwnerRid(associationRid, associationTable[i]); + var memberRange = _getMemberRange(associationRid); + + memberRanges[ownerRid] = memberRange; + foreach (var memberToken in memberRange) + memberOwnerRids[memberToken.Rid - 1] = ownerRid; } - } - } - private void InitializeMemberList(uint ownerRid, MetadataRange memberRange) - { - _memberLists![ownerRid] = memberRange; - foreach (var token in memberRange) - _memberOwners![token.Rid] = ownerRid; + // Assign in reverse order. This is again to ensure a data race does not happen with the conditions + // happening in EnsureIsInitialized. + _memberOwnerRids = memberOwnerRids; + _memberRanges = memberRanges; + } } public MetadataRange GetMemberRange(uint ownerRid) { EnsureIsInitialized(); - return _memberLists.TryGetValue(ownerRid, out var range) + return _memberRanges.TryGetValue(ownerRid, out var range) ? range : MetadataRange.Empty; } @@ -83,8 +102,8 @@ public MetadataRange GetMemberRange(uint ownerRid) public uint GetMemberOwner(uint memberRid) { EnsureIsInitialized(); - return _memberOwners.TryGetValue(memberRid, out uint ownerRid) - ? ownerRid + return memberRid - 1 < _memberOwnerRids.Length + ? _memberOwnerRids[memberRid - 1] : 0; } } diff --git a/src/AsmResolver.DotNet/Collections/MethodSemanticsCollection.cs b/src/AsmResolver.DotNet/Collections/MethodSemanticsCollection.cs index 0310136af..35cfefda3 100644 --- a/src/AsmResolver.DotNet/Collections/MethodSemanticsCollection.cs +++ b/src/AsmResolver.DotNet/Collections/MethodSemanticsCollection.cs @@ -20,6 +20,16 @@ public MethodSemanticsCollection(IHasSemantics owner) { } + /// + /// Creates a new instance of the class. + /// + /// The owner of the collection. + /// The initial number of elements the collection can store. + public MethodSemanticsCollection(IHasSemantics owner, int capacity) + : base(owner, capacity) + { + } + internal bool ValidateMembership { get; 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 d8493aaab..227751398 100644 --- a/src/AsmResolver.DotNet/Config/Json/RuntimeConfiguration.cs +++ b/src/AsmResolver.DotNet/Config/Json/RuntimeConfiguration.cs @@ -8,13 +8,6 @@ namespace AsmResolver.DotNet.Config.Json /// public class RuntimeConfiguration { - private static readonly JsonSerializerOptions JsonSerializerOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - IgnoreNullValues = true - }; - /// /// Parses runtime configuration from a JSON file. /// @@ -32,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); } /// @@ -66,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..18e57b9e5 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,10 +12,10 @@ namespace AsmResolver.DotNet /// public class DefaultMetadataResolver : IMetadataResolver { - private readonly IDictionary _typeCache; + private readonly ConcurrentDictionary _typeCache; private readonly SignatureComparer _comparer = new() { - AcceptNewerAssemblyVersionNumbers = true + IgnoreAssemblyVersionNumbers = 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/DotNetCoreAssemblyResolver.cs b/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs index 87aef8062..2bf0ed76d 100644 --- a/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs +++ b/src/AsmResolver.DotNet/DotNetCoreAssemblyResolver.cs @@ -172,10 +172,14 @@ private sealed class RuntimeNameComparer : IComparer public static readonly RuntimeNameComparer Instance = new(); /// - public int Compare(string x, string y) + public int Compare(string? x, string? y) { if (x == y) return 0; + if (x is null) + return -1; + if (y is null) + return 1; if (x == KnownRuntimeNames.NetCoreApp) return 1; if (y == KnownRuntimeNames.NetCoreApp) 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 e6ad33f2b..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; @@ -60,7 +60,7 @@ private static void InterpretEHInfo(CilMethodBody methodBody, ReferenceImporter int tryEnd = FieldReader.ReadField(ehInfo, "m_endAddr"); int handlerStart = FieldReader.ReadField(ehInfo, "m_catchAddr")![i]; int handlerEnd = FieldReader.ReadField(ehInfo, "m_catchEndAddr")![i]; - var exceptionType = FieldReader.ReadField(ehInfo, "m_catchClass")![i]; + var exceptionType = FieldReader.ReadField(ehInfo, "m_catchClass")![i]; var handlerType = (CilExceptionHandlerType) FieldReader.ReadField(ehInfo, "m_type")![i]; var endTryLabel = instructions.GetByOffset(tryEnd)?.CreateLabel() ?? new CilOffsetLabel(tryEnd); @@ -74,13 +74,14 @@ private static void InterpretEHInfo(CilMethodBody methodBody, ReferenceImporter FilterStart = null, HandlerStart = instructions.GetLabel(handlerStart), HandlerEnd = instructions.GetLabel(handlerEnd), - ExceptionType = exceptionType != null ? importer.ImportType(exceptionType) : null + ExceptionType = exceptionType is not null ? importer.ImportType(exceptionType) : null }; methodBody.ExceptionHandlers.Add(handler); } } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Calls GetTypes")] public static object ResolveDynamicResolver(object dynamicMethodObj) { //Convert dynamicMethodObj to DynamicResolver @@ -113,7 +114,7 @@ public static object ResolveDynamicResolver(object dynamicMethodObj) dynamicMethodObj = Activator.CreateInstance(dynamicResolver, (BindingFlags) (-1), null, new[] { ilGenerator - }, null); + }, null)!; } return dynamicMethodObj; } diff --git a/src/AsmResolver.DotNet/EventDefinition.cs b/src/AsmResolver.DotNet/EventDefinition.cs index 649ce3734..d21d1036b 100644 --- a/src/AsmResolver.DotNet/EventDefinition.cs +++ b/src/AsmResolver.DotNet/EventDefinition.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -136,19 +137,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 +165,39 @@ 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); + } + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => throw new NotSupportedException(); + /// /// Obtains the list of custom attributes assigned to the member. /// @@ -181,7 +209,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 +218,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 +227,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 +236,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..323bc800e 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,23 @@ public IList CustomAttributes /// public TypeDefinition? Resolve() => Module?.MetadataResolver.ResolveType(this); + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Implementation?.IsImportedInModule(module) ?? false); + } + + /// + /// Imports the exported type using the provided importer object. + /// + /// The reference importer to use. + /// The imported type. + public ExportedType ImportWith(ReferenceImporter importer) => importer.ImportType(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + IMemberDefinition? IMemberDescriptor.Resolve() => Resolve(); /// diff --git a/src/AsmResolver.DotNet/FieldDefinition.cs b/src/AsmResolver.DotNet/FieldDefinition.cs index 1961a4db1..39b7e5c3e 100644 --- a/src/AsmResolver.DotNet/FieldDefinition.cs +++ b/src/AsmResolver.DotNet/FieldDefinition.cs @@ -4,6 +4,7 @@ using AsmResolver.Collections; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Marshal; +using AsmResolver.DotNet.Signatures.Types; using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; @@ -56,11 +57,6 @@ protected FieldDefinition(MetadataToken token) /// The name of the field. /// The attributes. /// The signature of the field. - /// - /// For a valid .NET image, if of the signature referenced by - /// is set, the bit should be unset in - /// and vice versa. - /// public FieldDefinition(string? name, FieldAttributes attributes, FieldSignature? signature) : this(new MetadataToken(TableIndex.Field, 0)) { @@ -69,6 +65,22 @@ public FieldDefinition(string? name, FieldAttributes attributes, FieldSignature? Signature = signature; } + /// + /// Creates a new field definition. + /// + /// The name of the field. + /// The attributes. + /// The type of values the field contains. + public FieldDefinition(string? name, FieldAttributes attributes, TypeSignature? fieldType) + : this(new MetadataToken(TableIndex.Field, 0)) + { + Name = name; + Attributes = attributes; + Signature = fieldType is not null + ? new FieldSignature(fieldType) + : null; + } + /// /// Gets or sets the name of the field. /// @@ -374,6 +386,23 @@ public IList CustomAttributes FieldDefinition IFieldDescriptor.Resolve() => this; + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + + /// + /// Imports the field using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported field. + public IFieldDescriptor ImportWith(ReferenceImporter importer) => importer.ImportField(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + IMemberDefinition IMemberDescriptor.Resolve() => this; /// diff --git a/src/AsmResolver.DotNet/FileReference.cs b/src/AsmResolver.DotNet/FileReference.cs index e8f78cdb0..8de6ba74a 100644 --- a/src/AsmResolver.DotNet/FileReference.cs +++ b/src/AsmResolver.DotNet/FileReference.cs @@ -120,6 +120,20 @@ public IList CustomAttributes } } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + + /// + /// Imports the file using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported file reference. + public FileReference ImportWith(ReferenceImporter importer) => + (FileReference) importer.ImportImplementation(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// /// Obtains the name of the referenced file. /// diff --git a/src/AsmResolver.DotNet/FullNameGenerator.cs b/src/AsmResolver.DotNet/FullNameGenerator.cs index e04c42aa2..fe2409c38 100644 --- a/src/AsmResolver.DotNet/FullNameGenerator.cs +++ b/src/AsmResolver.DotNet/FullNameGenerator.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; @@ -36,6 +37,12 @@ public static string GetFieldFullName(string? name, ITypeDescriptor? declaringTy /// The full name public static string GetMethodFullName(string? name, ITypeDescriptor? declaringType, MethodSignature? signature) { + if (signature?.GenericParameterCount > 0) + { + return GetMethodFullName(name, declaringType, signature, + Enumerable.Repeat("?", signature.GenericParameterCount)); + } + string returnTypeString = signature?.ReturnType.FullName ?? TypeSignature.NullTypeToString; string parameterTypesString = GetParameterTypesString(signature); @@ -53,16 +60,23 @@ public static string GetMethodFullName(string? name, ITypeDescriptor? declaringT /// The signature of the method. /// The type arguments. /// The full name - public static string GetMethodFullName(string? name, ITypeDescriptor? declaringType, MethodSignature? signature, - IEnumerable typeArguments) + public static string GetMethodFullName( + string? name, + ITypeDescriptor? declaringType, + MethodSignature? signature, + IEnumerable typeArguments) { string returnTypeString = signature?.ReturnType.FullName ?? TypeSignature.NullTypeToString; string parameterTypesString = GetParameterTypesString(signature); - string typeArgumentsString = string.Join(", ", typeArguments); + + string[] argumentNames = typeArguments.ToArray(); + string typeArgumentsString = argumentNames.Length>0 + ? $"<{string.Join(", ", argumentNames)}>" + : string.Empty; return declaringType is null - ? $"{returnTypeString} {name}<{typeArgumentsString}>({parameterTypesString})" - : $"{returnTypeString} {declaringType}::{name}<{typeArgumentsString}>({parameterTypesString})"; + ? $"{returnTypeString} {name}{typeArgumentsString}({parameterTypesString})" + : $"{returnTypeString} {declaringType}::{name}{typeArgumentsString}({parameterTypesString})"; } /// 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..872e578aa --- /dev/null +++ b/src/AsmResolver.DotNet/IImportable.cs @@ -0,0 +1,26 @@ +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); + + /// + /// Imports the member using the provided reference importer object. + /// + /// The reference importer to use for importing the object. + /// The imported member. + IImportable ImportWith(ReferenceImporter importer); + } +} 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/ITypeDefOrRef.cs b/src/AsmResolver.DotNet/ITypeDefOrRef.cs index bf80f8481..a81bc335b 100644 --- a/src/AsmResolver.DotNet/ITypeDefOrRef.cs +++ b/src/AsmResolver.DotNet/ITypeDefOrRef.cs @@ -28,5 +28,12 @@ public interface ITypeDefOrRef : ITypeDescriptor, IMemberRefParent, IHasCustomAt { get; } + + /// + /// Imports the type using the provided reference importer object. + /// + /// The reference importer to use for importing the type. + /// The imported type. + new ITypeDefOrRef ImportWith(ReferenceImporter importer); } } diff --git a/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs b/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs index 7e63115fc..aa0dc667d 100644 --- a/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs +++ b/src/AsmResolver.DotNet/InvalidTypeDefOrRef.cs @@ -73,6 +73,15 @@ public static InvalidTypeDefOrRef Get(InvalidTypeSignatureError error) return instance; } + /// + public bool IsImportedInModule(ModuleDefinition module) => false; + + /// + ITypeDefOrRef ITypeDefOrRef.ImportWith(ReferenceImporter importer) => throw new InvalidOperationException(); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => throw new InvalidOperationException(); + IMemberDefinition? IMemberDescriptor.Resolve() => null; TypeDefinition? ITypeDescriptor.Resolve() => null; @@ -84,6 +93,5 @@ public static InvalidTypeDefOrRef Get(InvalidTypeSignatureError error) /// public override string ToString() => ((IFullNameProvider) this).Name!; - } } diff --git a/src/AsmResolver.DotNet/KnownCorLibs.cs b/src/AsmResolver.DotNet/KnownCorLibs.cs index 106d21c03..fbc0d3893 100644 --- a/src/AsmResolver.DotNet/KnownCorLibs.cs +++ b/src/AsmResolver.DotNet/KnownCorLibs.cs @@ -70,6 +70,16 @@ public static class KnownCorLibs 0x7C, 0xEC, 0x85, 0xD7, 0xBE, 0xA7, 0x79, 0x8E }); + /// + /// References System.Private.CoreLib.dll, Version=7.0.0.0, PublicKeyToken=7CEC85D7BEA7798E. This is used by .NET + /// assemblies targeting .NET 7.0. + /// + public static readonly AssemblyReference SystemPrivateCoreLib_v7_0_0_0 = new AssemblyReference("System.Private.CoreLib", + new Version(7, 0, 0, 0), false, new byte[] + { + 0x7C, 0xEC, 0x85, 0xD7, 0xBE, 0xA7, 0x79, 0x8E + }); + /// /// References System.Runtime.dll, Version=4.0.20.0, PublicKeyToken=B03F5F7F11D50A3A. This is used by .NET /// assemblies targeting .NET standard 1.3 and 1.4. @@ -101,7 +111,7 @@ public static class KnownCorLibs }); /// - /// References System.Runtime.dll, Version=4.2.1.0, PublicKeyToken=B03F5F7F11D50A3A. This is used by .NET + /// References System.Runtime.dll, Version=4.2.2.0, PublicKeyToken=B03F5F7F11D50A3A. This is used by .NET /// assemblies targeting .NET Core 3.1. /// public static readonly AssemblyReference SystemRuntime_v4_2_2_0 = new AssemblyReference("System.Runtime", @@ -130,6 +140,16 @@ public static class KnownCorLibs 0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A }); + /// + /// References System.Runtime.dll, Version=7.0.0.0, PublicKeyToken=B03F5F7F11D50A3A. This is used by .NET + /// assemblies targeting .NET 7.0. + /// + public static readonly AssemblyReference SystemRuntime_v7_0_0_0 = new AssemblyReference("System.Runtime", + new Version(7, 0, 0, 0), false, new byte[] + { + 0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A + }); + /// /// References netstandard.dll, Version=2.0.0.0, PublicKeyToken=CC7B13FFCD2DDD51. This is used by .NET /// assemblies targeting .NET standard 2.0. @@ -164,9 +184,11 @@ static KnownCorLibs() SystemRuntime_v4_2_2_0, SystemRuntime_v5_0_0_0, SystemRuntime_v6_0_0_0, + SystemRuntime_v7_0_0_0, SystemPrivateCoreLib_v4_0_0_0, SystemPrivateCoreLib_v5_0_0_0, - SystemPrivateCoreLib_v6_0_0_0 + SystemPrivateCoreLib_v6_0_0_0, + SystemPrivateCoreLib_v7_0_0_0 }; KnownCorLibNames = new HashSet(KnownCorLibReferences.Select(r => r.Name!.Value)); diff --git a/src/AsmResolver.DotNet/MemberReference.cs b/src/AsmResolver.DotNet/MemberReference.cs index 000940b0a..69a274884 100644 --- a/src/AsmResolver.DotNet/MemberReference.cs +++ b/src/AsmResolver.DotNet/MemberReference.cs @@ -153,6 +153,25 @@ public IList CustomAttributes throw new ArgumentOutOfRangeException(); } + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + + /// + /// Imports the member using the provided reference importer object. + /// + /// The reference importer to use for importing the object. + /// The imported member. + public MemberReference ImportWith(ReferenceImporter importer) => IsMethod + ? (MemberReference) importer.ImportMethod(this) + : (MemberReference) importer.ImportField(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + FieldDefinition? IFieldDescriptor.Resolve() { if (!IsField) diff --git a/src/AsmResolver.DotNet/Memory/TypeMemoryLayoutDetector.cs b/src/AsmResolver.DotNet/Memory/TypeMemoryLayoutDetector.cs index d0cb8c501..2ae1e422c 100644 --- a/src/AsmResolver.DotNet/Memory/TypeMemoryLayoutDetector.cs +++ b/src/AsmResolver.DotNet/Memory/TypeMemoryLayoutDetector.cs @@ -251,10 +251,8 @@ private TypeMemoryLayout InferExplicitLayout(TypeDefinition type, uint alignment // All fields in an explicitly laid out structure need to have a field offset assigned. if (!field.FieldOffset.HasValue) { - throw new ArgumentException(string.Format( - "{0} ({1}) is defined in a type with explicit layout, but does not have a field offset assigned.", - field.FullName, - field.MetadataToken.ToString())); + throw new ArgumentException( + $"{field.FullName} ({field.MetadataToken}) is defined in a type with explicit layout, but does not have a field offset assigned."); } uint offset = (uint) field.FieldOffset.Value; diff --git a/src/AsmResolver.DotNet/MethodDefinition.cs b/src/AsmResolver.DotNet/MethodDefinition.cs index e79a423a4..4a32048be 100644 --- a/src/AsmResolver.DotNet/MethodDefinition.cs +++ b/src/AsmResolver.DotNet/MethodDefinition.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading; using AsmResolver.Collections; using AsmResolver.DotNet.Code; @@ -27,7 +28,7 @@ public class MethodDefinition : IHasSecurityDeclaration, IManagedEntrypoint { - private readonly LazyVariable _name; + private readonly LazyVariable _name; private readonly LazyVariable _declaringType; private readonly LazyVariable _signature; private readonly LazyVariable _methodBody; @@ -47,7 +48,7 @@ public class MethodDefinition : protected MethodDefinition(MetadataToken token) : base(token) { - _name =new LazyVariable(GetName); + _name =new LazyVariable(GetName); _declaringType = new LazyVariable(GetDeclaringType); _signature = new LazyVariable(GetSignature); _methodBody = new LazyVariable(GetBody); @@ -100,7 +101,13 @@ public MethodDefinition(string? name, MethodAttributes attributes, MethodSignatu } /// - public string FullName => FullNameGenerator.GetMethodFullName(Name, DeclaringType, Signature); + public string FullName => FullNameGenerator.GetMethodFullName( + Name, + DeclaringType, + Signature, + GenericParameters.Count > 0 + ? GenericParameters.Select(x => x.Name?.Value ?? NullName) + : Enumerable.Empty()); /// /// Gets or sets the attributes associated to the method. @@ -691,6 +698,23 @@ public IList GenericParameters MethodDefinition IMethodDescriptor.Resolve() => this; + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return Module == module + && (Signature?.IsImportedInModule(module) ?? false); + } + + /// + /// Imports the method using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported method. + public IMethodDefOrRef ImportWith(ReferenceImporter importer) => importer.ImportMethod(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + IMemberDefinition IMemberDescriptor.Resolve() => this; /// @@ -715,7 +739,7 @@ public bool IsAccessibleFromType(TypeDefinition type) /// /// This method is called upon initialization of the property. /// - protected virtual string? GetName() => null; + protected virtual Utf8String? GetName() => null; /// /// Obtains the declaring type of the method definition. diff --git a/src/AsmResolver.DotNet/MethodSpecification.cs b/src/AsmResolver.DotNet/MethodSpecification.cs index 02da0eb30..31ca1226e 100644 --- a/src/AsmResolver.DotNet/MethodSpecification.cs +++ b/src/AsmResolver.DotNet/MethodSpecification.cs @@ -73,7 +73,7 @@ public MethodSpecification(IMethodDefOrRef? method, GenericInstanceMethodSignatu Name, DeclaringType, Method?.Signature, - Signature?.TypeArguments ?? Enumerable.Empty()); + Signature?.TypeArguments.Select(x => x.FullName) ?? Enumerable.Empty()); /// public ModuleDefinition? Module => Method?.Module; @@ -99,6 +99,23 @@ public IList CustomAttributes /// public MethodDefinition? Resolve() => Method?.Resolve(); + /// + public bool IsImportedInModule(ModuleDefinition module) + { + return (Method?.IsImportedInModule(module) ?? false) + && (Signature?.IsImportedInModule(module) ?? false); + } + + /// + /// Imports the method specification using the provided reference importer. + /// + /// The reference importer to use. + /// The imported method specification. + public MethodSpecification ImportWith(ReferenceImporter importer) => importer.ImportMethod(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + IMemberDefinition? IMemberDescriptor.Resolve() => Resolve(); /// diff --git a/src/AsmResolver.DotNet/ModuleDefinition.cs b/src/AsmResolver.DotNet/ModuleDefinition.cs index 8891c859c..3e693c258 100644 --- a/src/AsmResolver.DotNet/ModuleDefinition.cs +++ b/src/AsmResolver.DotNet/ModuleDefinition.cs @@ -54,6 +54,7 @@ public class ModuleDefinition : private readonly LazyVariable _runtimeVersion; private readonly LazyVariable _nativeResources; private IList? _debugData; + private ReferenceImporter? _defaultImporter; /// /// Reads a .NET module from the provided input buffer. @@ -303,10 +304,9 @@ public ModuleDefinition(string? name, AssemblyReference corLib) Name = name; var importer = new ReferenceImporter(this); - corLib = (AssemblyReference)importer.ImportScope(corLib); + corLib = (AssemblyReference) importer.ImportScope(corLib); CorLibTypeFactory = new CorLibTypeFactory(corLib); - AssemblyReferences.Add(corLib); OriginalTargetRuntime = DetectTargetRuntime(); MetadataResolver = new DefaultMetadataResolver(CreateAssemblyResolver(UncachedFileService.Instance)); @@ -762,6 +762,19 @@ public IMetadataResolver MetadataResolver set => _managedEntrypoint.Value = value; } + /// + /// Gets the default importer instance for this module. + /// + public ReferenceImporter DefaultImporter + { + get + { + if (_defaultImporter is null) + Interlocked.CompareExchange(ref _defaultImporter, GetDefaultImporter(), null); + return _defaultImporter; + } + } + /// /// Looks up a member by its metadata token. /// @@ -1049,6 +1062,15 @@ public TypeDefinition GetOrCreateModuleType() /// protected virtual IList GetDebugData() => new List(); + /// + /// Obtains the default reference importer assigned to this module. + /// + /// The importer. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ReferenceImporter GetDefaultImporter() => new(this); + /// /// Detects the runtime that this module targets. /// @@ -1105,6 +1127,19 @@ protected IAssemblyResolver CreateAssemblyResolver(IFileService fileService) /// public override string ToString() => Name ?? string.Empty; + /// + bool IImportable.IsImportedInModule(ModuleDefinition module) => this == module; + + /// + /// Imports the module using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported module. + public ModuleReference ImportWith(ReferenceImporter importer) => importer.ImportModule(new ModuleReference(Name)); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// /// 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..1c4cef6d4 100644 --- a/src/AsmResolver.DotNet/ModuleReference.cs +++ b/src/AsmResolver.DotNet/ModuleReference.cs @@ -76,6 +76,19 @@ public IList CustomAttributes } } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + + /// + /// Imports the module reference using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported module. + public ModuleReference ImportWith(ReferenceImporter importer) => importer.ImportModule(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// /// Obtains the name of the module. /// diff --git a/src/AsmResolver.DotNet/PropertyDefinition.cs b/src/AsmResolver.DotNet/PropertyDefinition.cs index 05c94c9b2..325bacd11 100644 --- a/src/AsmResolver.DotNet/PropertyDefinition.cs +++ b/src/AsmResolver.DotNet/PropertyDefinition.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -170,23 +171,47 @@ 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); + } + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => throw new NotSupportedException(); + /// /// Obtains the name of the property definition. /// diff --git a/src/AsmResolver.DotNet/ReferenceImporter.cs b/src/AsmResolver.DotNet/ReferenceImporter.cs index 5f2429407..33d904554 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 @@ -60,6 +60,27 @@ public IResolutionScope ImportScope(IResolutionScope? scope) }; } + /// + /// Imports an implementation reference. + /// + /// The implementation reference to import. + /// The imported implementation reference. + public IImplementation ImportImplementation(IImplementation? implementation) + { + if (implementation is null) + throw new ArgumentNullException(nameof(implementation)); + if (implementation.IsImportedInModule(TargetModule)) + return implementation; + + return implementation switch + { + AssemblyReference assembly => ImportAssembly(assembly), + ExportedType type => ImportType(type), + FileReference file => ImportFile(file), + _ => throw new ArgumentOutOfRangeException(nameof(implementation)) + }; + } + /// /// Imports a reference to an assembly. /// @@ -69,7 +90,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)); @@ -83,6 +104,29 @@ protected virtual AssemblyReference ImportAssembly(AssemblyDescriptor assembly) return reference; } + /// + /// Imports a file reference. + /// + /// The file to import. + /// The imported file. + protected virtual FileReference ImportFile(FileReference file) + { + if (file is null) + throw new ArgumentNullException(nameof(file)); + if (file.IsImportedInModule(TargetModule)) + return file; + + var reference = TargetModule.FileReferences.FirstOrDefault(a => a.Name == file.Name); + + if (reference is null) + { + reference = new FileReference(file.Name, file.Attributes); + TargetModule.FileReferences.Add(reference); + } + + return reference; + } + /// /// Imports a reference to a module. /// @@ -92,7 +136,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 +183,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 +202,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,12 +219,35 @@ 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)); } + /// + /// Imports a forwarded type. + /// + /// The type to import. + /// The imported type. + public virtual ExportedType ImportType(ExportedType type) + { + if (type is null) + throw new ArgumentNullException(nameof(type)); + if (type.IsImportedInModule(TargetModule)) + return type; + + var result = TargetModule.ExportedTypes.FirstOrDefault(a => _comparer.Equals(a, type)); + + if (result is null) + { + result = new ExportedType(ImportImplementation(type.Implementation), type.Namespace, type.Name); + TargetModule.ExportedTypes.Add(result); + } + + return result; + } + /// /// Imports the given type signature into the target module. /// @@ -186,7 +257,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 +283,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 +334,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 +387,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 +475,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); @@ -435,8 +504,8 @@ public virtual IMethodDescriptor ImportMethod(MethodBase method) ? ImportTypeSignature(info.ReturnType) : TargetModule.CorLibTypeFactory.Void; - var parameters = (method.DeclaringType != null && method.DeclaringType.IsConstructedGenericType) - ? method.Module.ResolveMethod(method.MetadataToken).GetParameters() + var parameters = method.DeclaringType is { IsConstructedGenericType: true } + ? method.Module.ResolveMethod(method.MetadataToken)!.GetParameters() : method.GetParameters(); var parameterTypes = new TypeSignature[parameters.Length]; @@ -479,7 +548,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( @@ -512,16 +581,14 @@ public MemberReference ImportField(FieldInfo field) if (field is null) throw new ArgumentNullException(nameof(field)); - if (field.DeclaringType != null && field.DeclaringType.IsConstructedGenericType) - field = field.Module.ResolveField(field.MetadataToken); + if (field.DeclaringType is { IsConstructedGenericType: true }) + field = field.Module.ResolveField(field.MetadataToken)!; var scope = field.DeclaringType != null ? ImportType(field.DeclaringType) : TargetModule.GetModuleType(); - var signature = new FieldSignature(field.IsStatic ? 0 : CallingConventionAttributes.HasThis, - ImportTypeSignature(field.FieldType)); - + var signature = new FieldSignature(ImportTypeSignature(field.FieldType)); return new MemberReference(scope, field.Name, signature); } diff --git a/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs b/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs index 66cb50656..082c7f483 100644 --- a/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs +++ b/src/AsmResolver.DotNet/ReflectionAssemblyDescriptor.cs @@ -1,3 +1,4 @@ +using System; using System.Reflection; using AsmResolver.PE.DotNet.Metadata.Tables; @@ -22,7 +23,7 @@ public ReflectionAssemblyDescriptor(ModuleDefinition parentModule, AssemblyName { _parentModule = parentModule; _assemblyName = assemblyName; - Version = assemblyName.Version; + Version = assemblyName.Version ?? new Version(); } /// @@ -31,6 +32,13 @@ public ReflectionAssemblyDescriptor(ModuleDefinition parentModule, AssemblyName /// protected override Utf8String? GetCulture() => _assemblyName.CultureName; + /// + public override bool IsImportedInModule(ModuleDefinition module) => false; + + /// + public override AssemblyReference ImportWith(ReferenceImporter importer) => + (AssemblyReference) importer.ImportScope(new AssemblyReference(this)); + /// 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/SafeExtensions.cs b/src/AsmResolver.DotNet/SafeExtensions.cs index f567a20a7..47de4b13b 100644 --- a/src/AsmResolver.DotNet/SafeExtensions.cs +++ b/src/AsmResolver.DotNet/SafeExtensions.cs @@ -1,4 +1,3 @@ -using System; using AsmResolver.DotNet.Builder; namespace AsmResolver.DotNet @@ -12,14 +11,14 @@ public static string SafeToString(this IMetadataMember? self) try { - string value = self.ToString(); + string value = self.ToString()!; if (value.Length > 200) value = $"{value.Remove(197)}... (truncated)"; if (self.MetadataToken.Rid != 0) value = $"{value} (0x{self.MetadataToken.ToString()})"; return value; } - catch (Exception) + catch { return $"0x{self.MetadataToken.ToString()}"; } @@ -32,9 +31,9 @@ public static string SafeToString(this object? self) try { - return self.ToString(); + return self.ToString()!; } - catch (Exception) + catch { return self.GetType().ToString(); } diff --git a/src/AsmResolver.DotNet/Serialized/CachedSerializedMemberFactory.cs b/src/AsmResolver.DotNet/Serialized/CachedSerializedMemberFactory.cs index 016500ecd..c21fc9e91 100644 --- a/src/AsmResolver.DotNet/Serialized/CachedSerializedMemberFactory.cs +++ b/src/AsmResolver.DotNet/Serialized/CachedSerializedMemberFactory.cs @@ -9,6 +9,7 @@ namespace AsmResolver.DotNet.Serialized internal class CachedSerializedMemberFactory { private readonly ModuleReaderContext _context; + private readonly TablesStream _tablesStream; private TypeReference?[]? _typeReferences; private TypeDefinition?[]? _typeDefinitions; @@ -38,6 +39,7 @@ internal class CachedSerializedMemberFactory internal CachedSerializedMemberFactory(ModuleReaderContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); + _tablesStream = _context.Image.DotNetDirectory!.Metadata!.GetStream(); } internal bool TryLookupMember(MetadataToken token, [NotNullWhen(true)] out IMetadataMember? member) @@ -247,9 +249,7 @@ internal bool TryLookupMember(MetadataToken token, [NotNullWhen(true)] out IMeta where TMember : class, IMetadataMember { // Obtain table. - var table = (MetadataTable) _context.Image.DotNetDirectory!.Metadata - !.GetStream() - .GetTable(token.Table); + var table = (MetadataTable) _tablesStream.GetTable(token.Table); // Check if within bounds. if (token.Rid == 0 || token.Rid > table.Count) diff --git a/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs b/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs index 0ff094b92..07dbb19ea 100644 --- a/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs +++ b/src/AsmResolver.DotNet/Serialized/DefaultMethodBodyReader.cs @@ -2,7 +2,6 @@ using AsmResolver.DotNet.Code; using AsmResolver.DotNet.Code.Cil; using AsmResolver.PE.DotNet.Cil; -using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; namespace AsmResolver.DotNet.Serialized @@ -16,27 +15,26 @@ public class DefaultMethodBodyReader : IMethodBodyReader /// public virtual MethodBody? ReadMethodBody(ModuleReaderContext context, MethodDefinition owner, in MethodDefinitionRow row) { + var bodyReference = row.Body; + if (bodyReference == SegmentReference.Null) + return null; + + MethodBody? result = null; + try { - if (row.Body.CanRead) + if (bodyReference.CanRead && owner.IsIL) { - if (owner.IsIL) - { - var reader = row.Body.CreateReader(); - var rawBody = CilRawMethodBody.FromReader(context, ref reader); - return rawBody is not null - ? CilMethodBody.FromRawMethodBody(context, owner, rawBody) - : null; - } - else - { - context.NotSupported($"Body of method {owner.MetadataToken} is native and unbounded which is not supported."); - // TODO: handle native method bodies. - } + var reader = bodyReference.CreateReader(); + var rawBody = CilRawMethodBody.FromReader(context, ref reader); + if (rawBody is not null) + result = CilMethodBody.FromRawMethodBody(context, owner, rawBody); } - else if (row.Body.IsBounded && row.Body.GetSegment() is CilRawMethodBody rawMethodBody) + + if (result is null && bodyReference.IsBounded) { - return CilMethodBody.FromRawMethodBody(context, owner, rawMethodBody); + if (bodyReference.GetSegment() is CilRawMethodBody rawMethodBody) + result = CilMethodBody.FromRawMethodBody(context, owner, rawMethodBody); } } catch (Exception ex) @@ -44,7 +42,13 @@ public class DefaultMethodBodyReader : IMethodBodyReader context.RegisterException(new BadImageFormatException($"Failed to parse the method body of {owner.MetadataToken}.", ex)); } - return null; + if (result is not null) + { + result.Address = bodyReference; + return result; + } + + return new UnresolvedMethodBody(owner, bodyReference); } } } diff --git a/src/AsmResolver.DotNet/Serialized/SerializedAssemblyDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedAssemblyDefinition.cs index b4310c159..c5b4e557f 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedAssemblyDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedAssemblyDefinition.cs @@ -166,7 +166,7 @@ public override bool TryGetTargetFramework(out DotNetRuntimeInfo info) continue; object? element = attribute.Signature.FixedArguments[0].Element; - if (element is string or Utf8String && DotNetRuntimeInfo.TryParse(element.ToString(), out info)) + if (element is string or Utf8String && DotNetRuntimeInfo.TryParse(element.ToString()!, out info)) return true; } diff --git a/src/AsmResolver.DotNet/Serialized/SerializedGenericParameter.cs b/src/AsmResolver.DotNet/Serialized/SerializedGenericParameter.cs index 841c47adc..3d876c1d5 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedGenericParameter.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedGenericParameter.cs @@ -58,10 +58,11 @@ public SerializedGenericParameter(ModuleReaderContext context, MetadataToken tok /// protected override IList GetConstraints() { - var result = new OwnedCollection(this); - var module = _context.ParentModule; - foreach (uint rid in module.GetGenericParameterConstraints(MetadataToken)) + var rids = module.GetGenericParameterConstraints(MetadataToken); + var result = new OwnedCollection(this, rids.Count); + + foreach (uint rid in rids) { var constraintToken = new MetadataToken(TableIndex.GenericParamConstraint, rid); result.Add((GenericParameterConstraint) module.LookupMember(constraintToken)); diff --git a/src/AsmResolver.DotNet/Serialized/SerializedMethodDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedMethodDefinition.cs index d000183e1..c35f28b9e 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedMethodDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedMethodDefinition.cs @@ -40,7 +40,7 @@ public SerializedMethodDefinition(ModuleReaderContext context, MetadataToken tok } /// - protected override string? GetName() + protected override Utf8String? GetName() { return _context.Metadata.TryGetStream(out var stringsStream) ? stringsStream.GetStringByIndex(_row.Name) @@ -81,9 +81,10 @@ public SerializedMethodDefinition(ModuleReaderContext context, MetadataToken tok /// protected override IList GetParameterDefinitions() { - var result = new OwnedCollection(this); + var parameterRange = _context.ParentModule.GetParameterRange(MetadataToken.Rid); + var result = new OwnedCollection(this, parameterRange.Count); - foreach (var token in _context.ParentModule.GetParameterRange(MetadataToken.Rid)) + foreach (var token in parameterRange) { if (_context.ParentModule.TryLookupMember(token, out var member) && member is ParameterDefinition parameter) result.Add(parameter); @@ -108,9 +109,10 @@ protected override IList GetParameterDefinitions() /// protected override IList GetGenericParameters() { - var result = new OwnedCollection(this); + var rids = _context.ParentModule.GetGenericParameters(MetadataToken); + var result = new OwnedCollection(this, rids.Count); - foreach (uint rid in _context.ParentModule.GetGenericParameters(MetadataToken)) + foreach (uint rid in rids) { if (_context.ParentModule.TryLookupMember(new MetadataToken(TableIndex.GenericParam, rid), out var member) && member is GenericParameter genericParameter) diff --git a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.Metadata.cs b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.Metadata.cs index f7e13d09f..e81e548aa 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.Metadata.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.Metadata.cs @@ -59,7 +59,7 @@ private void EnsureTypeDefinitionTreeInitialized() return typeDefTree; } - internal IEnumerable GetNestedTypeRids(uint enclosingTypeRid) + internal ICollection GetNestedTypeRids(uint enclosingTypeRid) { EnsureTypeDefinitionTreeInitialized(); return _typeDefTree.GetValues(enclosingTypeRid); @@ -107,8 +107,8 @@ private void InitializeMethodSemantics() var semanticsTable = tablesStream.GetTable(TableIndex.MethodSemantics); var encoder = tablesStream.GetIndexEncoder(CodedIndex.HasSemantics); - var semantics = new OneToManyRelation(); - var semanticMethods = new Dictionary(); + var semantics = new OneToManyRelation(semanticsTable.Count); + var semanticMethods = new Dictionary(semanticsTable.Count); for (int i = 0; i < semanticsTable.Count; i++) { var methodSemanticsRow = semanticsTable[i]; @@ -123,7 +123,7 @@ private void InitializeMethodSemantics() Interlocked.CompareExchange(ref _semanticMethods, semanticMethods, null); } - internal IEnumerable GetMethodSemantics(MetadataToken owner) + internal ICollection GetMethodSemantics(MetadataToken owner) { EnsureMethodSemanticsInitialized(); return _semantics.GetValues(owner); @@ -155,7 +155,7 @@ private void EnsureConstantsInitialized() var constantTable = tablesStream.GetTable(TableIndex.Constant); var encoder = tablesStream.GetIndexEncoder(CodedIndex.HasConstant); - var constants = new OneToOneRelation(); + var constants = new OneToOneRelation(constantTable.Count); for (int i = 0; i < constantTable.Count; i++) { var ownerToken = encoder.DecodeIndex(constantTable[i].Parent); @@ -199,7 +199,7 @@ private void EnsureCustomAttributesInitialized() var attributeTable = tablesStream.GetTable(TableIndex.CustomAttribute); var encoder = tablesStream.GetIndexEncoder(CodedIndex.HasCustomAttribute); - var customAttributes = new OneToManyRelation(); + var customAttributes = new OneToManyRelation(attributeTable.Count); for (int i = 0; i < attributeTable.Count; i++) { var ownerToken = encoder.DecodeIndex(attributeTable[i].Parent); @@ -228,9 +228,10 @@ internal MetadataToken GetCustomAttributeOwner(uint attributeRid) internal IList GetCustomAttributeCollection(IHasCustomAttribute owner) { EnsureCustomAttributesInitialized(); - var result = new OwnedCollection(owner); + var rids = _customAttributes.GetValues(owner.MetadataToken); + var result = new OwnedCollection(owner, rids.Count); - foreach (uint rid in _customAttributes.GetValues(owner.MetadataToken)) + foreach (uint rid in rids) { var attribute = (CustomAttribute) LookupMember(new MetadataToken(TableIndex.CustomAttribute, rid)); result.Add(attribute); @@ -252,7 +253,7 @@ private void EnsureSecurityDeclarationsInitialized() var declarationTable = tablesStream.GetTable(TableIndex.DeclSecurity); var encoder = tablesStream.GetIndexEncoder(CodedIndex.HasDeclSecurity); - var securityDeclarations = new OneToManyRelation(); + var securityDeclarations = new OneToManyRelation(declarationTable.Count); for (int i = 0; i < declarationTable.Count; i++) { var ownerToken = encoder.DecodeIndex(declarationTable[i].Parent); @@ -272,9 +273,10 @@ internal MetadataToken GetSecurityDeclarationOwner(uint attributeRid) internal IList GetSecurityDeclarationCollection(IHasSecurityDeclaration owner) { EnsureSecurityDeclarationsInitialized(); - var result = new OwnedCollection(owner); + var rids = _securityDeclarations.GetValues(owner.MetadataToken); + var result = new OwnedCollection(owner, rids.Count); - foreach (uint rid in _securityDeclarations.GetValues(owner.MetadataToken)) + foreach (uint rid in rids) { var attribute = (SecurityDeclaration) LookupMember(new MetadataToken(TableIndex.DeclSecurity, rid)); result.Add(attribute); @@ -296,7 +298,7 @@ private void EnsureGenericParametersInitialized() var parameterTable = tablesStream.GetTable(TableIndex.GenericParam); var encoder = tablesStream.GetIndexEncoder(CodedIndex.TypeOrMethodDef); - var genericParameters = new OneToManyRelation(); + var genericParameters = new OneToManyRelation(parameterTable.Count); for (int i = 0; i < parameterTable.Count; i++) { var ownerToken = encoder.DecodeIndex(parameterTable[i].Owner); @@ -331,7 +333,7 @@ private void EnsureGenericParameterConstrainsInitialized() var tablesStream = DotNetDirectory.Metadata!.GetStream(); var constraintTable = tablesStream.GetTable(TableIndex.GenericParamConstraint); - var constraints = new OneToManyRelation(); + var constraints = new OneToManyRelation(constraintTable.Count); for (int i = 0; i < constraintTable.Count; i++) { var ownerToken = new MetadataToken(TableIndex.GenericParam, constraintTable[i].Owner); @@ -366,7 +368,7 @@ private void EnsureInterfacesInitialized() var tablesStream = DotNetDirectory.Metadata!.GetStream(); var interfaceImplTable = tablesStream.GetTable(TableIndex.InterfaceImpl); - var interfaces = new OneToManyRelation(); + var interfaces = new OneToManyRelation(interfaceImplTable.Count); for (int i = 0; i < interfaceImplTable.Count; i++) { var ownerToken = new MetadataToken(TableIndex.TypeDef, interfaceImplTable[i].Class); @@ -401,7 +403,7 @@ private void EnsureMethodImplementationsInitialized() var tablesStream = DotNetDirectory.Metadata!.GetStream(); var methodImplTable = tablesStream.GetTable(TableIndex.MethodImpl); - var methodImplementations = new OneToManyRelation(); + var methodImplementations = new OneToManyRelation(methodImplTable.Count); for (int i = 0; i < methodImplTable.Count; i++) { var ownerToken = new MetadataToken(TableIndex.TypeDef, methodImplTable[i].Class); @@ -430,7 +432,7 @@ private void EnsureClassLayoutsInitialized() var tablesStream = DotNetDirectory.Metadata!.GetStream(); var layoutTable = tablesStream.GetTable(TableIndex.ClassLayout); - var layouts = new OneToOneRelation(); + var layouts = new OneToOneRelation(layoutTable.Count); for (int i = 0; i < layoutTable.Count; i++) { var ownerToken = new MetadataToken(TableIndex.TypeDef, layoutTable[i].Parent); @@ -460,7 +462,7 @@ private void EnsureImplementationMapsInitialized() var mapTable = tablesStream.GetTable(TableIndex.ImplMap); var encoder = tablesStream.GetIndexEncoder(CodedIndex.TypeOrMethodDef); - var maps = new OneToOneRelation(); + var maps = new OneToOneRelation(mapTable.Count); for (int i = 0; i < mapTable.Count; i++) { var ownerToken = encoder.DecodeIndex(mapTable[i].MemberForwarded); @@ -495,7 +497,7 @@ private void EnsureFieldRvasInitialized() var tablesStream = DotNetDirectory.Metadata!.GetStream(); var rvaTable = tablesStream.GetTable(TableIndex.FieldRva); - var rvas = new OneToOneRelation(); + var rvas = new OneToOneRelation(rvaTable.Count); for (int i = 0; i < rvaTable.Count; i++) { var ownerToken = new MetadataToken(TableIndex.Field, rvaTable[i].Field); @@ -525,7 +527,7 @@ private void EnsureFieldMarshalsInitialized() var marshalTable = tablesStream.GetTable(TableIndex.FieldMarshal); var encoder = tablesStream.GetIndexEncoder(CodedIndex.HasFieldMarshal); - var marshals = new OneToOneRelation(); + var marshals = new OneToOneRelation(marshalTable.Count); for (int i = 0; i < marshalTable.Count; i++) { var ownerToken = encoder.DecodeIndex(marshalTable[i].Parent); @@ -571,7 +573,7 @@ private void EnsureFieldLayoutsInitialized() var tablesStream = DotNetDirectory.Metadata!.GetStream(); var layoutTable = tablesStream.GetTable(TableIndex.FieldLayout); - var fieldLayouts = new OneToOneRelation(); + var fieldLayouts = new OneToOneRelation(layoutTable.Count); for (int i = 0; i < layoutTable.Count; i++) { var ownerToken = new MetadataToken(TableIndex.Field, layoutTable[i].Field); diff --git a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs index ba9e52777..5ed4f0d4a 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedModuleDefinition.cs @@ -77,15 +77,15 @@ public SerializedModuleDefinition(IPEImage peImage, ModuleReaderParameters reade readerParameters.PEReaderParameters.FileService)); // Prepare lazy RID lists. - _fieldLists = new LazyRidListRelation(metadata, TableIndex.TypeDef, + _fieldLists = new LazyRidListRelation(metadata, TableIndex.Field, TableIndex.TypeDef, (rid, _) => rid, tablesStream.GetFieldRange); - _methodLists = new LazyRidListRelation(metadata, TableIndex.TypeDef, + _methodLists = new LazyRidListRelation(metadata, TableIndex.Method, TableIndex.TypeDef, (rid, _) => rid, tablesStream.GetMethodRange); - _paramLists = new LazyRidListRelation(metadata, TableIndex.Method, + _paramLists = new LazyRidListRelation(metadata, TableIndex.Param, TableIndex.Method, (rid, _) => rid, tablesStream.GetParameterRange); - _propertyLists = new LazyRidListRelation(metadata, TableIndex.PropertyMap, + _propertyLists = new LazyRidListRelation(metadata, TableIndex.Property, TableIndex.PropertyMap, (_, map) => map.Parent, tablesStream.GetPropertyRange); - _eventLists = new LazyRidListRelation(metadata, TableIndex.EventMap, + _eventLists = new LazyRidListRelation(metadata, TableIndex.Event, TableIndex.EventMap, (_, map) => map.Parent, tablesStream.GetEventRange); } @@ -204,12 +204,18 @@ protected override IList GetTopLevelTypes() { EnsureTypeDefinitionTreeInitialized(); - var types = new OwnedCollection(this); - var typeDefTable = ReaderContext.Metadata .GetStream() .GetTable(TableIndex.TypeDef); + int nestedTypeCount = ReaderContext.Metadata + .GetStream() + .GetTable(TableIndex.NestedClass) + .Count; + + var types = new OwnedCollection(this, + typeDefTable.Count - nestedTypeCount); + for (int i = 0; i < typeDefTable.Count; i++) { uint rid = (uint) i + 1; @@ -226,12 +232,12 @@ protected override IList GetTopLevelTypes() /// protected override IList GetAssemblyReferences() { - var result = new OwnedCollection(this); - var table = ReaderContext.Metadata .GetStream() .GetTable(TableIndex.AssemblyRef); + var result = new OwnedCollection(this, table.Count); + // Don't use the member factory here, this method may be called before the member factory is initialized. for (int i = 0; i < table.Count; i++) { @@ -245,12 +251,12 @@ protected override IList GetAssemblyReferences() /// protected override IList GetModuleReferences() { - var result = new OwnedCollection(this); - var table = ReaderContext.Metadata .GetStream() .GetTable(TableIndex.ModuleRef); + var result = new OwnedCollection(this, table.Count); + for (int i = 0; i < table.Count; i++) { var token = new MetadataToken(TableIndex.ModuleRef, (uint) i + 1); @@ -264,12 +270,12 @@ protected override IList GetModuleReferences() /// protected override IList GetFileReferences() { - var result = new OwnedCollection(this); - var table = ReaderContext.Metadata .GetStream() .GetTable(TableIndex.File); + var result = new OwnedCollection(this, table.Count); + for (int i = 0; i < table.Count; i++) { var token = new MetadataToken(TableIndex.File, (uint) i + 1); @@ -283,12 +289,12 @@ protected override IList GetFileReferences() /// protected override IList GetResources() { - var result = new OwnedCollection(this); - var table = ReaderContext.Metadata .GetStream() .GetTable(TableIndex.ManifestResource); + var result = new OwnedCollection(this, table.Count); + for (int i = 0; i < table.Count; i++) { var token = new MetadataToken(TableIndex.ManifestResource, (uint) i + 1); @@ -302,12 +308,12 @@ protected override IList GetResources() /// protected override IList GetExportedTypes() { - var result = new OwnedCollection(this); - var table = ReaderContext.Metadata .GetStream() .GetTable(TableIndex.ExportedType); + var result = new OwnedCollection(this, table.Count); + for (int i = 0; i < table.Count; i++) { var token = new MetadataToken(TableIndex.ExportedType, (uint) i + 1); @@ -350,12 +356,11 @@ protected override IList GetExportedTypes() if (assemblyTable.Count > 0) { - var assembly = new SerializedAssemblyDefinition( + return new SerializedAssemblyDefinition( ReaderContext, new MetadataToken(TableIndex.Assembly, 1), assemblyTable[0], this); - return assembly; } return null; diff --git a/src/AsmResolver.DotNet/Serialized/SerializedPropertyDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedPropertyDefinition.cs index 9739939fd..b1f1fa227 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedPropertyDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedPropertyDefinition.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Collections; using AsmResolver.PE.DotNet.Metadata; @@ -70,11 +71,13 @@ public SerializedPropertyDefinition(ModuleReaderContext context, MetadataToken t /// protected override IList GetSemantics() { - var result = new MethodSemanticsCollection(this); + var module = _context.ParentModule; + var rids = module.GetMethodSemantics(MetadataToken); + + var result = new MethodSemanticsCollection(this, rids.Count); result.ValidateMembership = false; - var module = _context.ParentModule; - foreach (uint rid in module.GetMethodSemantics(MetadataToken)) + foreach (uint rid in rids) { var semanticsToken = new MetadataToken(TableIndex.MethodSemantics, rid); result.Add((MethodSemantics) module.LookupMember(semanticsToken)); diff --git a/src/AsmResolver.DotNet/Serialized/SerializedTypeDefinition.cs b/src/AsmResolver.DotNet/Serialized/SerializedTypeDefinition.cs index 9b87e76e3..2ee5afe6b 100644 --- a/src/AsmResolver.DotNet/Serialized/SerializedTypeDefinition.cs +++ b/src/AsmResolver.DotNet/Serialized/SerializedTypeDefinition.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using AsmResolver.Collections; using AsmResolver.DotNet.Collections; using AsmResolver.PE.DotNet.Metadata; @@ -67,9 +68,9 @@ public SerializedTypeDefinition(ModuleReaderContext context, MetadataToken token /// protected override IList GetNestedTypes() { - var result = new OwnedCollection(this); - var rids = _context.ParentModule.GetNestedTypeRids(MetadataToken.Rid); + var result = new OwnedCollection(this, rids.Count); + foreach (uint rid in rids) { var nestedType = (TypeDefinition) _context.ParentModule.LookupMember(new MetadataToken(TableIndex.TypeDef, rid)); @@ -107,7 +108,7 @@ protected override IList GetNestedTypes() private IList CreateMemberCollection(MetadataRange range) where TMember : class, IMetadataMember, IOwnedCollectionElement { - var result = new OwnedCollection(this); + var result = new OwnedCollection(this, range.Count); foreach (var token in range) result.Add((TMember) _context.ParentModule.LookupMember(token)); @@ -126,9 +127,10 @@ private IList CreateMemberCollection(MetadataRange range) /// protected override IList GetGenericParameters() { - var result = new OwnedCollection(this); + var rids = _context.ParentModule.GetGenericParameters(MetadataToken); + var result = new OwnedCollection(this, rids.Count); - foreach (uint rid in _context.ParentModule.GetGenericParameters(MetadataToken)) + foreach (uint rid in rids) { if (_context.ParentModule.TryLookupMember(new MetadataToken(TableIndex.GenericParam, rid), out var member) && member is GenericParameter genericParameter) @@ -143,9 +145,9 @@ protected override IList GetGenericParameters() /// protected override IList GetInterfaces() { - var result = new OwnedCollection(this); - var rids = _context.ParentModule.GetInterfaceImplementationRids(MetadataToken); + var result = new OwnedCollection(this, rids.Count); + foreach (uint rid in rids) { if (_context.ParentModule.TryLookupMember(new MetadataToken(TableIndex.InterfaceImpl, rid), out var member) @@ -161,13 +163,13 @@ protected override IList GetInterfaces() /// protected override IList GetMethodImplementations() { - var result = new List(); - var tablesStream = _context.Metadata.GetStream(); var table = tablesStream.GetTable(TableIndex.MethodImpl); var encoder = tablesStream.GetIndexEncoder(CodedIndex.MethodDefOrRef); var rids = _context.ParentModule.GetMethodImplementationRids(MetadataToken); + var result = new List(rids.Count); + foreach (uint rid in rids) { var row = table.GetByRid(rid); diff --git a/src/AsmResolver.DotNet/Signatures/BoxedArgument.cs b/src/AsmResolver.DotNet/Signatures/BoxedArgument.cs index 3f8d05d5a..26a5dcf14 100644 --- a/src/AsmResolver.DotNet/Signatures/BoxedArgument.cs +++ b/src/AsmResolver.DotNet/Signatures/BoxedArgument.cs @@ -41,7 +41,7 @@ private bool Equals(BoxedArgument other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return ReferenceEquals(this, obj) || obj is BoxedArgument other && Equals(other); } diff --git a/src/AsmResolver.DotNet/Signatures/CallingConventionSignature.cs b/src/AsmResolver.DotNet/Signatures/CallingConventionSignature.cs index f7d33ae5c..12d04b0e2 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,18 @@ public bool IsSentinel set => Attributes = (Attributes & ~CallingConventionAttributes.Sentinel) | (value ? CallingConventionAttributes.Sentinel : 0); } + + /// + public abstract bool IsImportedInModule(ModuleDefinition module); + + /// + /// Imports the signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported signature. + protected abstract CallingConventionSignature ImportWithInternal(ReferenceImporter importer); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWithInternal(importer); } } diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs index 96c1fbb89..58823dd1b 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeArgument.cs @@ -118,12 +118,12 @@ public override string ToString() return ElementToString(obj); } - private string ElementToString(object? element) => element switch + private static string ElementToString(object? element) => element switch { null => "null", IList list => $"{{{string.Join(", ", list.Select(ElementToString))}}}", string x => x.CreateEscapedString(), - _ => element.ToString() + _ => element.ToString() ?? string.Empty }; /// diff --git a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs index 1676016d9..15d4834fb 100644 --- a/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/CustomAttributeSignature.cs @@ -12,6 +12,8 @@ namespace AsmResolver.DotNet.Signatures /// public class CustomAttributeSignature : ExtendableBlobSignature { + private readonly List _fixedArguments; + private readonly List _namedArguments; private const ushort CustomAttributeSignaturePrologue = 0x0001; /// @@ -35,18 +37,20 @@ public class CustomAttributeSignature : ExtendableBlobSignature // Read fixed arguments. var parameterTypes = ctor.Signature?.ParameterTypes ?? Array.Empty(); + result._fixedArguments.Capacity = parameterTypes.Count; for (int i = 0; i < parameterTypes.Count; i++) { var argument = CustomAttributeArgument.FromReader(context, parameterTypes[i], ref reader); - result.FixedArguments.Add(argument); + result._fixedArguments.Add(argument); } // Read named arguments. ushort namedArgumentCount = reader.ReadUInt16(); + result._namedArguments.Capacity = namedArgumentCount; for (int i = 0; i < namedArgumentCount; i++) { var argument = CustomAttributeNamedArgument.FromReader(context, ref reader); - result.NamedArguments.Add(argument); + result._namedArguments.Add(argument); } return result; @@ -73,25 +77,19 @@ public CustomAttributeSignature(IEnumerable fixedArgume /// public CustomAttributeSignature(IEnumerable fixedArguments, IEnumerable namedArguments) { - FixedArguments = new List(fixedArguments); - NamedArguments = new List(namedArguments); + _fixedArguments = new List(fixedArguments); + _namedArguments = new List(namedArguments); } /// /// Gets a collection of fixed arguments that are passed onto the constructor of the attribute. /// - public IList FixedArguments - { - get; - } + public IList FixedArguments => _fixedArguments; /// /// Gets a collection of values that are assigned to fields and/or members of the attribute class. /// - public IList NamedArguments - { - get; - } + public IList NamedArguments => _namedArguments; /// public override string ToString() diff --git a/src/AsmResolver.DotNet/Signatures/FieldSignature.cs b/src/AsmResolver.DotNet/Signatures/FieldSignature.cs index d1f14c7a7..02b5495f4 100644 --- a/src/AsmResolver.DotNet/Signatures/FieldSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/FieldSignature.cs @@ -1,6 +1,6 @@ +using System; using AsmResolver.DotNet.Signatures.Types; using AsmResolver.IO; -using AsmResolver.PE.DotNet.Metadata.Tables.Rows; namespace AsmResolver.DotNet.Signatures { @@ -14,6 +14,9 @@ public class FieldSignature : MemberSignature /// /// The value type of the field. /// The signature. + [Obsolete("The HasThis bit in field signatures is ignored by the CLR. Use the constructor instead," + + " or when this call is used in an argument of a FieldDefinition constructor, use the overload" + + " taking TypeSignature instead.")] public static FieldSignature CreateStatic(TypeSignature fieldType) => new(CallingConventionAttributes.Field, fieldType); @@ -22,6 +25,9 @@ public static FieldSignature CreateStatic(TypeSignature fieldType) /// /// The value type of the field. /// The signature. + [Obsolete("The HasThis bit in field signatures is ignored by the CLR. Use the constructor instead," + + " or when this call is used in an argument of a FieldDefinition constructor, use the overload" + + " taking TypeSignature instead.")] public static FieldSignature CreateInstance(TypeSignature fieldType) => new(CallingConventionAttributes.Field | CallingConventionAttributes.HasThis, fieldType); @@ -61,7 +67,7 @@ public FieldSignature(CallingConventionAttributes attributes, TypeSignature fiel } /// - /// Gets the type of the object that the field stores. + /// Gets the type of the value that the field contains. /// public TypeSignature FieldType { @@ -88,5 +94,18 @@ protected override void WriteContents(BlobSerializationContext context) context.Writer.WriteByte((byte) Attributes); FieldType.Write(context); } + + /// + /// Imports the field signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported field signature. + public FieldSignature ImportWith(ReferenceImporter importer) => importer.ImportFieldSignature(this); + + /// + protected override CallingConventionSignature ImportWithInternal(ReferenceImporter importer) => ImportWith(importer); + + /// + public override string ToString() => FieldType.FullName; } } diff --git a/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs b/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs index c17695745..291a07c25 100644 --- a/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/GenericInstanceMethodSignature.cs @@ -21,14 +21,14 @@ public class GenericInstanceMethodSignature : CallingConventionSignature, IGener } var attributes = (CallingConventionAttributes) reader.ReadByte(); - var result = new GenericInstanceMethodSignature(attributes); if (!reader.TryReadCompressedUInt32(out uint count)) { context.ReaderContext.BadImage("Invalid number of type arguments in generic method signature."); - return result; + return new GenericInstanceMethodSignature(attributes); } + var result = new GenericInstanceMethodSignature(attributes, (int) count); for (int i = 0; i < count; i++) result.TypeArguments.Add(TypeSignature.FromReader(context, ref reader)); @@ -44,6 +44,17 @@ public GenericInstanceMethodSignature(CallingConventionAttributes attributes) { } + /// + /// Creates a new instantiation signature for a generic method. + /// + /// The attributes. + /// The initial number of elements that the property can store. + public GenericInstanceMethodSignature(CallingConventionAttributes attributes, int capacity) + : base(attributes) + { + TypeArguments = new List(capacity); + } + /// /// Creates a new instantiation signature for a generic method with the provided type arguments. /// @@ -91,6 +102,30 @@ 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; + } + + /// + /// Imports the generic method instantiation signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported signature. + public GenericInstanceMethodSignature ImportWith(ReferenceImporter importer) => + importer.ImportGenericInstanceMethodSignature(this); + + /// + protected override CallingConventionSignature ImportWithInternal(ReferenceImporter importer) => + ImportWith(importer); + /// public override string ToString() { diff --git a/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs b/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs index ccf81a2a5..2376fd95c 100644 --- a/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/LocalVariablesSignature.cs @@ -68,6 +68,30 @@ 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; + } + + /// + /// Imports the local variables signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported signature. + public LocalVariablesSignature ImportWith(ReferenceImporter importer) => + importer.ImportLocalVariablesSignature(this); + + /// + protected override CallingConventionSignature ImportWithInternal(ReferenceImporter importer) => + ImportWith(importer); + /// protected override void WriteContents(BlobSerializationContext context) { diff --git a/src/AsmResolver.DotNet/Signatures/MemberSignature.cs b/src/AsmResolver.DotNet/Signatures/MemberSignature.cs index 6f49dc659..e785171d8 100644 --- a/src/AsmResolver.DotNet/Signatures/MemberSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/MemberSignature.cs @@ -28,11 +28,6 @@ protected TypeSignature MemberReturnType } /// - public override string ToString() - { - return string.Format("{0}{1}", - HasThis ? "instance " : string.Empty, - MemberReturnType?.FullName ?? TypeSignature.NullTypeToString); - } + public override bool IsImportedInModule(ModuleDefinition module) => MemberReturnType.IsImportedInModule(module); } } diff --git a/src/AsmResolver.DotNet/Signatures/MethodSignature.cs b/src/AsmResolver.DotNet/Signatures/MethodSignature.cs index e3092c22c..818452aa6 100644 --- a/src/AsmResolver.DotNet/Signatures/MethodSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/MethodSignature.cs @@ -20,8 +20,10 @@ public class MethodSignature : MethodSignatureBase /// The method signature. public static MethodSignature FromReader(in BlobReadContext context, ref BinaryStreamReader reader) { - var result = new MethodSignature((CallingConventionAttributes) reader.ReadByte(), - context.ReaderContext.ParentModule.CorLibTypeFactory.Void, Enumerable.Empty()); + var result = new MethodSignature( + (CallingConventionAttributes) reader.ReadByte(), + context.ReaderContext.ParentModule.CorLibTypeFactory.Void, + Enumerable.Empty()); // Generic parameter count. if (result.IsGeneric) @@ -65,7 +67,7 @@ public static MethodSignature CreateStatic(TypeSignature returnType, params Type /// The signature. public static MethodSignature CreateStatic(TypeSignature returnType, int genericParameterCount, params TypeSignature[] parameterTypes) { - return new MethodSignature(0, returnType, parameterTypes) + return new MethodSignature(genericParameterCount > 0 ? CallingConventionAttributes.Generic : 0, returnType, parameterTypes) { GenericParameterCount = genericParameterCount }; @@ -89,7 +91,7 @@ public static MethodSignature CreateStatic(TypeSignature returnType, IEnumerable /// The signature. public static MethodSignature CreateStatic(TypeSignature returnType, int genericParameterCount, IEnumerable parameterTypes) { - return new MethodSignature(0, returnType, parameterTypes) + return new MethodSignature(genericParameterCount > 0 ? CallingConventionAttributes.Generic : 0, returnType, parameterTypes) { GenericParameterCount = genericParameterCount }; @@ -121,7 +123,11 @@ public static MethodSignature CreateInstance(TypeSignature returnType, params Ty /// The signature. public static MethodSignature CreateInstance(TypeSignature returnType, int genericParameterCount, params TypeSignature[] parameterTypes) { - return new MethodSignature(CallingConventionAttributes.HasThis, returnType, parameterTypes) + var attributes = genericParameterCount > 0 + ? CallingConventionAttributes.HasThis | CallingConventionAttributes.Generic + : CallingConventionAttributes.HasThis; + + return new MethodSignature(attributes, returnType, parameterTypes) { GenericParameterCount = genericParameterCount }; @@ -145,7 +151,11 @@ public static MethodSignature CreateInstance(TypeSignature returnType, IEnumerab /// The signature. public static MethodSignature CreateInstance(TypeSignature returnType, int genericParameterCount, IEnumerable parameterTypes) { - return new MethodSignature(CallingConventionAttributes.HasThis, returnType, parameterTypes) + var attributes = genericParameterCount > 0 + ? CallingConventionAttributes.HasThis | CallingConventionAttributes.Generic + : CallingConventionAttributes.HasThis; + + return new MethodSignature(attributes, returnType, parameterTypes) { GenericParameterCount = genericParameterCount }; @@ -207,16 +217,36 @@ protected override void WriteContents(BlobSerializationContext context) public override string ToString() { string prefix = HasThis ? "instance " : string.Empty; + string fullName = ReturnType.FullName; + string genericsString = GenericParameterCount > 0 ? $"<{string.Join(", ", new string('?', GenericParameterCount))}>" : string.Empty; - string parameterTypesString = string.Join(", ", ParameterTypes) + (IsSentinel ? ", ..." : string.Empty); - return string.Format("{0}{1} *{2}({3})", - prefix, - ReturnType?.FullName ?? TypeSignature.NullTypeToString, - genericsString, - parameterTypesString); + string parameterTypesString = string.Join(", ", ParameterTypes); + + string sentinelSuffix; + if (IsSentinel) + { + sentinelSuffix = ParameterTypes.Count > 0 + ? ", ..." + : " ...";} + else + { + sentinelSuffix = string.Empty; + } + + return $"{prefix}{fullName} *{genericsString}({parameterTypesString}{sentinelSuffix})"; } + + /// + /// Imports the method signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported signature. + public MemberSignature ImportWith(ReferenceImporter importer) => importer.ImportMethodSignature(this); + + /// + protected override CallingConventionSignature ImportWithInternal(ReferenceImporter importer) => ImportWith(importer); } } diff --git a/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs b/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs index 50452b60f..191bc2710 100644 --- a/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs +++ b/src/AsmResolver.DotNet/Signatures/MethodSignatureBase.cs @@ -10,28 +10,27 @@ namespace AsmResolver.DotNet.Signatures /// public abstract class MethodSignatureBase : MemberSignature { + private readonly List _parameterTypes; + /// /// Initializes the base of a method signature. /// - /// - /// - /// + /// The attributes associated to the signature. + /// The return type of the member. + /// The types of all (non-sentinel) parameters. protected MethodSignatureBase( CallingConventionAttributes attributes, TypeSignature memberReturnType, IEnumerable parameterTypes) : base(attributes, memberReturnType) { - ParameterTypes = new List(parameterTypes); + _parameterTypes = new List(parameterTypes); } /// /// Gets an ordered list of types indicating the types of the parameters that this member defines. /// - public IList ParameterTypes - { - get; - } + public IList ParameterTypes => _parameterTypes; /// /// Gets or sets the type of the value that this member returns. @@ -78,6 +77,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. @@ -97,6 +119,7 @@ protected void ReadParametersAndReturnType(in BlobReadContext context, ref Binar ReturnType = TypeSignature.FromReader(context, ref reader); // Parameter types. + _parameterTypes.Capacity = (int) parameterCount; bool sentinel = false; for (int i = 0; i < parameterCount; i++) { diff --git a/src/AsmResolver.DotNet/Signatures/PropertySignature.cs b/src/AsmResolver.DotNet/Signatures/PropertySignature.cs index db2b09326..5883cad30 100644 --- a/src/AsmResolver.DotNet/Signatures/PropertySignature.cs +++ b/src/AsmResolver.DotNet/Signatures/PropertySignature.cs @@ -126,10 +126,17 @@ public override string ToString() ? $"[{string.Join(", ", ParameterTypes)}]" : string.Empty; - return string.Format("{0}{1} *{2}", - prefix, - ReturnType.FullName, - parameterTypesString); + return $"{prefix}{ReturnType.FullName} *{parameterTypesString}"; } + + /// + /// Imports the property signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported signature. + public PropertySignature ImportWith(ReferenceImporter importer) => importer.ImportPropertySignature(this); + + /// + protected override CallingConventionSignature ImportWithInternal(ReferenceImporter importer) => ImportWith(importer); } } diff --git a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs index 11e47178b..92ea1cef5 100644 --- a/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs +++ b/src/AsmResolver.DotNet/Signatures/Security/SecurityAttribute.cs @@ -107,7 +107,6 @@ public void Write(BlobSerializationContext context) /// - public override string ToString() => - string.Format("{0}({1})", AttributeType, string.Join(", ", NamedArguments)); + public override string ToString() => $"{AttributeType}({string.Join(", ", NamedArguments)})"; } } diff --git a/src/AsmResolver.DotNet/Signatures/SignatureComparer.ResolutionScope.cs b/src/AsmResolver.DotNet/Signatures/SignatureComparer.ResolutionScope.cs index 0f91206d9..5eedef741 100644 --- a/src/AsmResolver.DotNet/Signatures/SignatureComparer.ResolutionScope.cs +++ b/src/AsmResolver.DotNet/Signatures/SignatureComparer.ResolutionScope.cs @@ -85,7 +85,7 @@ public bool Equals(AssemblyDescriptor? x, AssemblyDescriptor? y) return versionMatch && x.Name == y.Name - && x.Culture == y.Culture + && (x.Culture == y.Culture || (Utf8String.IsNullOrEmpty(x.Culture) && Utf8String.IsNullOrEmpty(y.Culture))) && Equals(x.GetPublicKeyToken(), y.GetPublicKeyToken()); } diff --git a/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeDefOrRef.cs b/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeDefOrRef.cs index cf0f19cc9..83de4c287 100644 --- a/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeDefOrRef.cs +++ b/src/AsmResolver.DotNet/Signatures/SignatureComparer.TypeDefOrRef.cs @@ -51,10 +51,16 @@ private bool SimpleTypeEquals(ITypeDescriptor x, ITypeDescriptor y) if (Equals(x.Scope, y.Scope)) return true; - // If scope does not match, it can still be a reference to an exported type. - return x.Resolve() is { } definition1 - && y.Resolve() is { } definition2 - && Equals(definition1.Module!.Assembly, definition2.Module!.Assembly); + // It can still be an exported type, we need to resolve the type then and check if the definitions match. + if (!Equals(x.Module, y.Module)) + { + return x.Resolve() is { } definition1 + && y.Resolve() is { } definition2 + && Equals(definition1.Module!.Assembly, definition2.Module!.Assembly) + && Equals(definition1.DeclaringType, definition2.DeclaringType); + } + + return false; } /// 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/ArrayDimension.cs b/src/AsmResolver.DotNet/Signatures/Types/ArrayDimension.cs index e06fbf743..8e13ee6a4 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/ArrayDimension.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/ArrayDimension.cs @@ -3,7 +3,7 @@ namespace AsmResolver.DotNet.Signatures.Types { /// - /// Represents a single dimension in an array specification. + /// Represents a single dimension in an array specification. /// public readonly struct ArrayDimension : IEquatable { @@ -39,7 +39,7 @@ public ArrayDimension(int? size, int? lowerBound) } /// - /// Gets or sets the lower bound for each index in the dimension (if specified). + /// Gets or sets the lower bound for each index in the dimension (if specified). /// /// /// When this value is not specified (null), a lower bound of 0 is assumed by the CLR. @@ -52,12 +52,10 @@ public ArrayDimension(int? size, int? lowerBound) /// /// Determines whether two dimensions are considered equal. /// - public bool Equals(ArrayDimension other) => - Size == other.Size && LowerBound == other.LowerBound; + public bool Equals(ArrayDimension other) => Size == other.Size && LowerBound == other.LowerBound; /// - public override bool Equals(object obj) => - obj is ArrayDimension other && Equals(other); + public override bool Equals(object? obj) => obj is ArrayDimension other && Equals(other); /// public override int GetHashCode() @@ -67,6 +65,6 @@ public override int GetHashCode() return (Size.GetHashCode() * 397) ^ LowerBound.GetHashCode(); } } - + } -} \ No newline at end of file +} diff --git a/src/AsmResolver.DotNet/Signatures/Types/ArrayTypeSignature.cs b/src/AsmResolver.DotNet/Signatures/Types/ArrayTypeSignature.cs index 193142038..e81140d88 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/ArrayTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/ArrayTypeSignature.cs @@ -87,7 +87,7 @@ internal new static ArrayTypeSignature FromReader(in BlobReadContext context, re return signature; } - var sizes = new List(); + var sizes = new List((int) numSizes); for (int i = 0; i < numSizes; i++) { if (!reader.TryReadCompressedUInt32(out uint size)) @@ -106,7 +106,7 @@ internal new static ArrayTypeSignature FromReader(in BlobReadContext context, re return signature; } - var loBounds = new List(); + var loBounds = new List((int) numLoBounds); for (int i = 0; i < numLoBounds; i++) { if (!reader.TryReadCompressedUInt32(out uint bound)) 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..1fe273fa5 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 @@ -46,7 +47,10 @@ public MethodSignature Signature /// public override ITypeDefOrRef? GetUnderlyingTypeDefOrRef() => - Signature?.ReturnType?.Module?.CorLibTypeFactory.IntPtr.Type; + 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..e05310b12 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/GenericInstanceTypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/GenericInstanceTypeSignature.cs @@ -10,6 +10,8 @@ namespace AsmResolver.DotNet.Signatures.Types /// public class GenericInstanceTypeSignature : TypeSignature, IGenericArgumentsProvider { + private readonly List _typeArguments; + internal new static GenericInstanceTypeSignature FromReader(in BlobReadContext context, ref BinaryStreamReader reader) { var genericType = TypeSignature.FromReader(context, ref reader); @@ -21,8 +23,9 @@ internal new static GenericInstanceTypeSignature FromReader(in BlobReadContext c return signature; } + signature._typeArguments.Capacity = (int) count; for (int i = 0; i < count; i++) - signature.TypeArguments.Add(TypeSignature.FromReader(context, ref reader)); + signature._typeArguments.Add(TypeSignature.FromReader(context, ref reader)); return signature; } @@ -53,7 +56,7 @@ public GenericInstanceTypeSignature(ITypeDefOrRef genericType, bool isValueType) IEnumerable typeArguments) { GenericType = genericType; - TypeArguments = new List(typeArguments); + _typeArguments = new List(typeArguments); IsValueType = isValueType; } @@ -72,10 +75,7 @@ public ITypeDefOrRef GenericType /// /// Gets a collection of type arguments used to instantiate the generic type. /// - public IList TypeArguments - { - get; - } + public IList TypeArguments => _typeArguments; /// public override string? Name @@ -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/Parsing/TypeNameLexer.cs b/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameLexer.cs index 1f548bc42..c7a8d4bd7 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameLexer.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameLexer.cs @@ -5,9 +5,10 @@ namespace AsmResolver.DotNet.Signatures.Types.Parsing { - internal class TypeNameLexer + internal struct TypeNameLexer { internal static readonly ISet ReservedChars = new HashSet("*+.,&[]…"); + private static readonly char[] TrimCharacters = " ".ToCharArray(); private readonly TextReader _reader; private readonly StringBuilder _buffer = new(); @@ -16,6 +17,8 @@ internal class TypeNameLexer public TypeNameLexer(TextReader reader) { _reader = reader ?? throw new ArgumentNullException(nameof(reader)); + HasConsumedTypeName = false; + _bufferedToken = default; } public bool HasConsumedTypeName @@ -125,7 +128,7 @@ private TypeNameToken ReadNumberOrIdentifierToken() _buffer.Append(currentChar); } - return new TypeNameToken(terminal, _buffer.ToString().Trim(' ')); + return new TypeNameToken(terminal, _buffer.ToString().Trim(TrimCharacters)); } private TypeNameToken ReadIdentifierToken() @@ -159,7 +162,7 @@ private TypeNameToken ReadIdentifierToken() _buffer.Append(currentChar); } - return new TypeNameToken(TypeNameTerminal.Identifier, _buffer.ToString().Trim(' ')); + return new TypeNameToken(TypeNameTerminal.Identifier, _buffer.ToString().Trim(TrimCharacters)); } private TypeNameToken ReadSymbolToken(TypeNameTerminal terminal) diff --git a/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameParser.cs b/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameParser.cs index e36d97b6a..78b70c343 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameParser.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/Parsing/TypeNameParser.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; @@ -9,19 +8,19 @@ namespace AsmResolver.DotNet.Signatures.Types.Parsing /// /// Provides a mechanism for parsing a fully assembly qualified name of a type. /// - public sealed class TypeNameParser + public struct TypeNameParser { // src/coreclr/src/vm/typeparse.cpp // https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/specifying-fully-qualified-type-names private static readonly SignatureComparer Comparer = new(); private readonly ModuleDefinition _module; - private readonly TypeNameLexer _lexer; + private TypeNameLexer _lexer; private TypeNameParser(ModuleDefinition module, TypeNameLexer lexer) { _module = module ?? throw new ArgumentNullException(nameof(module)); - _lexer = lexer ?? throw new ArgumentNullException(nameof(lexer)); + _lexer = lexer; } /// @@ -57,7 +56,7 @@ private TypeSignature ParseTypeSpec() SetScope(reference, scope); // Ensure corlib type sigs are used. - if (Comparer.Equals(scope, _module.CorLibTypeFactory.CorLibScope)) + if (Comparer.Equals(typeSpec.Scope, _module.CorLibTypeFactory.CorLibScope)) { var corlibType = _module.CorLibTypeFactory.FromType(typeSpec); if (corlibType != null) @@ -320,7 +319,7 @@ private List ParseDottedExpression(TypeNameTerminal terminal) } if (result.Count == 0) - throw new FormatException($"Expected {string.Join(", ",terminal)}."); + throw new FormatException($"Expected {terminal}."); return result; } @@ -328,41 +327,48 @@ private List ParseDottedExpression(TypeNameTerminal terminal) private AssemblyReference ParseAssemblyNameSpec() { string assemblyName = string.Join(".", ParseDottedExpression(TypeNameTerminal.Identifier)); - var assemblyRef = new AssemblyReference(assemblyName, new Version()); + var newReference = new AssemblyReference(assemblyName, new Version()); while (TryExpect(TypeNameTerminal.Comma).HasValue) { - var propertyToken = Expect(TypeNameTerminal.Identifier); + string propertyName = Expect(TypeNameTerminal.Identifier).Text; Expect(TypeNameTerminal.Equals); - switch (propertyToken.Text.ToLowerInvariant()) + if (propertyName.Equals("version", StringComparison.OrdinalIgnoreCase)) { - case "version": - assemblyRef.Version = ParseVersion(); - break; - - case "publickey": - assemblyRef.PublicKeyOrToken = ParseHexBlob(); - assemblyRef.HasPublicKey = true; - break; - - case "publickeytoken": - assemblyRef.PublicKeyOrToken = ParseHexBlob(); - assemblyRef.HasPublicKey = false; - break; - - case "culture": - string culture = ParseCulture(); - assemblyRef.Culture = !culture.Equals("neutral", StringComparison.OrdinalIgnoreCase) - ? culture - : null; - break; - - default: - throw new FormatException($"Unsupported {propertyToken.Text} assembly property."); + newReference.Version = ParseVersion(); + } + else if (propertyName.Equals("publickey", StringComparison.OrdinalIgnoreCase)) + { + newReference.PublicKeyOrToken = ParseHexBlob(); + newReference.HasPublicKey = true; + } + else if (propertyName.Equals("publickeytoken", StringComparison.OrdinalIgnoreCase)) + { + newReference.PublicKeyOrToken = ParseHexBlob(); + newReference.HasPublicKey = false; } + else if (propertyName.Equals("culture", StringComparison.OrdinalIgnoreCase)) + { + string culture = ParseCulture(); + newReference.Culture = !culture.Equals("neutral", StringComparison.OrdinalIgnoreCase) + ? culture + : null; + } + else + { + throw new FormatException($"Unsupported {propertyName} assembly property."); + } + } + + // Reuse imported assembly reference instance if possible. + for (int i = 0; i < _module.AssemblyReferences.Count; i++) + { + var existingReference = _module.AssemblyReferences[i]; + if (Comparer.Equals((AssemblyDescriptor) existingReference, newReference)) + return existingReference; } - return assemblyRef; + return newReference; } private Version ParseVersion() @@ -373,18 +379,31 @@ private Version ParseVersion() private byte[]? ParseHexBlob() { - var hexString = Expect(TypeNameTerminal.Identifier, TypeNameTerminal.Number).Text; + string hexString = Expect(TypeNameTerminal.Identifier, TypeNameTerminal.Number).Text; if (hexString == "null") return null; if (hexString.Length % 2 != 0) throw new FormatException("Provided hex string does not have an even length."); byte[] result = new byte[hexString.Length / 2]; - for (int i = 0; i < hexString.Length; i+=2) - result[i / 2] = byte.Parse(hexString.Substring(i, 2), NumberStyles.HexNumber); + for (int i = 0; i < hexString.Length; i += 2) + result[i / 2] = ParseHexByte(hexString, i); return result; } + private static byte ParseHexByte(string hexString, int index) + { + return (byte) ((ParseHexNibble(hexString[index]) << 4) | ParseHexNibble(hexString[index + 1])); + } + + private static byte ParseHexNibble(char nibble) => nibble switch + { + >= '0' and <= '9' => (byte) (nibble - '0'), + >= 'A' and <= 'F' => (byte) (nibble - 'A' + 10), + >= 'a' and <= 'f' => (byte) (nibble - 'a' + 10), + _ => throw new FormatException() + }; + private string ParseCulture() { return Expect(TypeNameTerminal.Identifier).Text; 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 50b806c51..389411e30 100644 --- a/src/AsmResolver.DotNet/Signatures/Types/TypeSignature.cs +++ b/src/AsmResolver.DotNet/Signatures/Types/TypeSignature.cs @@ -23,7 +23,7 @@ static TypeSignature() (BindingFlags) (-1), null, new[] {typeof(IntPtr)}, - null); + null)!; } /// @@ -163,8 +163,11 @@ public static TypeSignature FromReader(in BlobReadContext context, ref BinaryStr }; // Let the runtime translate the address to a type and import it. - var clrType = (Type) GetTypeFromHandleUnsafeMethod.Invoke(null, new object[] {address}); - var asmResType = new ReferenceImporter(context.ReaderContext.ParentModule).ImportType(clrType); + var clrType = (Type?) GetTypeFromHandleUnsafeMethod.Invoke(null, new object[] { address }); + var asmResType = clrType is not null + ? new ReferenceImporter(context.ReaderContext.ParentModule).ImportType(clrType) + : InvalidTypeDefOrRef.Get(InvalidTypeSignatureError.IllegalTypeSpec); + return new TypeDefOrRefSignature(asmResType); default: @@ -373,6 +376,19 @@ internal static void WriteFieldOrPropType(BlobSerializationContext context, Type public TypeSignature InstantiateGenericTypes(GenericContext context) => AcceptVisitor(GenericTypeActivator.Instance, context); + /// + public abstract bool IsImportedInModule(ModuleDefinition module); + + /// + /// Imports the type signature using the provided reference importer object. + /// + /// The reference importer to us. + /// The imported type. + public TypeSignature ImportWith(ReferenceImporter importer) => importer.ImportTypeSignature(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// /// 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..ce14eb497 100644 --- a/src/AsmResolver.DotNet/TypeDefinition.cs +++ b/src/AsmResolver.DotNet/TypeDefinition.cs @@ -680,6 +680,19 @@ public TypeSignature ToTypeSignature() ?? new TypeDefOrRefSignature(this, IsValueType); } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + + /// + /// Imports the type definition using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported type. + public ITypeDefOrRef ImportWith(ReferenceImporter importer) => importer.ImportType(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// public bool IsAccessibleFromType(TypeDefinition type) { diff --git a/src/AsmResolver.DotNet/TypeDescriptorExtensions.cs b/src/AsmResolver.DotNet/TypeDescriptorExtensions.cs index 568e90425..a108f6376 100644 --- a/src/AsmResolver.DotNet/TypeDescriptorExtensions.cs +++ b/src/AsmResolver.DotNet/TypeDescriptorExtensions.cs @@ -1,3 +1,5 @@ +using System; +using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; namespace AsmResolver.DotNet @@ -100,5 +102,53 @@ public static class TypeDescriptorExtensions { return new GenericInstanceTypeSignature(type.ToTypeDefOrRef(), type.IsValueType, typeArguments); } + + /// + /// Constructs a reference to a type within the provided resolution scope. + /// + /// The scope the type is defined in. + /// The namespace of the type. + /// The name of the type. + /// The constructed reference. + public static TypeReference CreateTypeReference(this IResolutionScope scope, string? ns, string name) + { + return new TypeReference(scope, ns, name); + } + + /// + /// Constructs a reference to a nested type. + /// + /// The enclosing type. + /// The name of the nested type. + /// The constructed reference. + /// + /// Occurs when cannot be used as a declaring type of a type reference. + /// + public static TypeReference CreateTypeReference(this ITypeDefOrRef declaringType, string nestedTypeName) + { + var parent = declaringType switch + { + TypeReference reference => reference, + TypeDefinition definition => definition.ToTypeReference(), + _ => throw new ArgumentOutOfRangeException() + }; + + return new TypeReference(parent, null, nestedTypeName); + } + + /// + /// Constructs a reference to a member declared within the provided parent member. + /// + /// The declaring member. + /// The name of the member to reference. + /// The signature of the member to reference. + /// The constructed reference. + public static MemberReference CreateMemberReference( + this IMemberRefParent parent, + string memberName, + MemberSignature signature) + { + return new MemberReference(parent, memberName, signature); + } } } diff --git a/src/AsmResolver.DotNet/TypeReference.cs b/src/AsmResolver.DotNet/TypeReference.cs index 57a5fe8b6..0b1332fe9 100644 --- a/src/AsmResolver.DotNet/TypeReference.cs +++ b/src/AsmResolver.DotNet/TypeReference.cs @@ -136,6 +136,19 @@ public TypeSignature ToTypeSignature() ?? new TypeDefOrRefSignature(this, IsValueType); } + /// + public bool IsImportedInModule(ModuleDefinition module) => Module == module; + + /// + /// Imports the type reference using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported type. + public ITypeDefOrRef ImportWith(ReferenceImporter importer) => importer.ImportType(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// public TypeDefinition? Resolve() => Module?.MetadataResolver.ResolveType(this); diff --git a/src/AsmResolver.DotNet/TypeSpecification.cs b/src/AsmResolver.DotNet/TypeSpecification.cs index 9d9a4713d..e8d805b4d 100644 --- a/src/AsmResolver.DotNet/TypeSpecification.cs +++ b/src/AsmResolver.DotNet/TypeSpecification.cs @@ -95,6 +95,19 @@ 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; + + /// + /// Imports the type specification using the provided reference importer object. + /// + /// The reference importer to use. + /// The imported type. + public ITypeDefOrRef ImportWith(ReferenceImporter importer) => (TypeSpecification) importer.ImportType(this); + + /// + IImportable IImportable.ImportWith(ReferenceImporter importer) => ImportWith(importer); + /// 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 15999d2f1..5c0bfda0f 100644 --- a/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj +++ b/src/AsmResolver.PE.File/AsmResolver.PE.File.csproj @@ -1,14 +1,15 @@ - + AsmResolver.PE.File Raw PE file models for the AsmResolver executable file inspection toolsuite. exe pe headers sections inspection manipulation assembly disassembly - netstandard2.0 true 1701;1702;NU5105 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 d16e37894..219f54c0d 100644 --- a/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj +++ b/src/AsmResolver.PE.Win32Resources/AsmResolver.PE.Win32Resources.csproj @@ -3,10 +3,11 @@ AsmResolver.PE.Win32Resources exe pe directories imports exports resources dotnet cil inspection manipulation assembly disassembly - netstandard2.0 true 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 5c3dd46fe..058547040 100644 --- a/src/AsmResolver.PE/AsmResolver.PE.csproj +++ b/src/AsmResolver.PE/AsmResolver.PE.csproj @@ -1,14 +1,14 @@ - + AsmResolver.PE PE image models for the AsmResolver executable file inspection toolsuite. exe pe directories imports exports resources dotnet cil inspection manipulation assembly disassembly - netstandard2.0 true 1701;1702;NU5105 - 9 enable + net6.0;netcoreapp3.1;netstandard2.0 + true @@ -18,11 +18,11 @@ bin\Release\AsmResolver.PE.xml - + - + all diff --git a/src/AsmResolver.PE/DotNet/Cil/CilAssembler.cs b/src/AsmResolver.PE/DotNet/Cil/CilAssembler.cs index a22248110..8b82d9027 100644 --- a/src/AsmResolver.PE/DotNet/Cil/CilAssembler.cs +++ b/src/AsmResolver.PE/DotNet/Cil/CilAssembler.cs @@ -12,8 +12,9 @@ public class CilAssembler { private readonly IBinaryStreamWriter _writer; private readonly ICilOperandBuilder _operandBuilder; - private readonly string? _diagnosticPrefix; + private readonly Func? _getMethodBodyName; private readonly IErrorListener _errorListener; + private string? _diagnosticPrefix; /// /// Creates a new CIL instruction encoder. @@ -21,7 +22,7 @@ public class CilAssembler /// The output stream to write the encoded instructions to. /// The object to use for creating raw operands. public CilAssembler(IBinaryStreamWriter writer, ICilOperandBuilder operandBuilder) - : this(writer, operandBuilder, null, ThrowErrorListener.Instance) + : this(writer, operandBuilder, default(string), ThrowErrorListener.Instance) { } @@ -46,6 +47,40 @@ public CilAssembler(IBinaryStreamWriter writer, ICilOperandBuilder operandBuilde : null; } + /// + /// Creates a new CIL instruction encoder. + /// + /// The output stream to write the encoded instructions to. + /// The object to use for creating raw operands. + /// A delegate that is used for lazily obtaining the name of the method body. + /// The object used for recording error listener. + public CilAssembler( + IBinaryStreamWriter writer, + ICilOperandBuilder operandBuilder, + Func? getMethodBodyName, + IErrorListener errorListener) + { + _writer = writer ?? throw new ArgumentNullException(nameof(writer)); + _errorListener = errorListener ?? throw new ArgumentNullException(nameof(errorListener)); + _operandBuilder = operandBuilder ?? throw new ArgumentNullException(nameof(operandBuilder)); + _getMethodBodyName = getMethodBodyName; + } + + private string? DiagnosticPrefix + { + get + { + if (_diagnosticPrefix is null && _getMethodBodyName is not null) + { + string? name = _getMethodBodyName(); + if (!string.IsNullOrEmpty(name)) + _diagnosticPrefix = $"[In {name}]: "; + } + + return _diagnosticPrefix; + } + } + /// /// Writes a collection of CIL instructions to the output stream. /// @@ -175,10 +210,10 @@ private int OperandToBranchDelta(CilInstruction instruction) return ThrowInvalidOperandType(instruction, typeof(ICilLabel), typeof(sbyte)); } - if (isShort && (delta < sbyte.MinValue || delta > sbyte.MaxValue)) + if (isShort && delta is < sbyte.MinValue or > sbyte.MaxValue) { _errorListener.RegisterException(new OverflowException( - $"{_diagnosticPrefix}Branch target at IL_{instruction.Offset:X4} is too far away for a ShortInlineBr instruction.")); + $"{DiagnosticPrefix}Branch target at IL_{instruction.Offset:X4} is too far away for a ShortInlineBr instruction.")); } return delta; @@ -190,7 +225,7 @@ private ushort OperandToLocalIndex(CilInstruction instruction) if (instruction.OpCode.OperandType == CilOperandType.ShortInlineVar && variableIndex > byte.MaxValue) { _errorListener.RegisterException(new OverflowException( - $"{_diagnosticPrefix}Local index at IL_{instruction.Offset:X4} is too large for a ShortInlineVar instruction.")); + $"{DiagnosticPrefix}Local index at IL_{instruction.Offset:X4} is too large for a ShortInlineVar instruction.")); } return unchecked((ushort) variableIndex); @@ -202,7 +237,7 @@ private ushort OperandToArgumentIndex(CilInstruction instruction) if (instruction.OpCode.OperandType == CilOperandType.ShortInlineArgument && variableIndex > byte.MaxValue) { _errorListener.RegisterException(new OverflowException( - $"{_diagnosticPrefix}Argument index at IL_{instruction.Offset:X4} is too large for a ShortInlineArgument instruction.")); + $"{DiagnosticPrefix}Argument index at IL_{instruction.Offset:X4} is too large for a ShortInlineArgument instruction.")); } return unchecked((ushort) variableIndex); @@ -247,7 +282,7 @@ private double OperandToFloat64(CilInstruction instruction) { string found = instruction.Operand?.GetType().Name ?? "null"; _errorListener.RegisterException(new ArgumentOutOfRangeException( - $"{_diagnosticPrefix}Expected a {expectedOperand.Name} operand at IL_{instruction.Offset:X4}, but found {found}.")); + $"{DiagnosticPrefix}Expected a {expectedOperand.Name} operand at IL_{instruction.Offset:X4}, but found {found}.")); return default; } @@ -263,7 +298,7 @@ private double OperandToFloat64(CilInstruction instruction) string found = instruction.Operand?.GetType().Name ?? "null"; _errorListener.RegisterException(new ArgumentOutOfRangeException( - $"{_diagnosticPrefix}Expected a {operandTypesString} operand at IL_{instruction.Offset:X4}, but found {found}.")); + $"{DiagnosticPrefix}Expected a {operandTypesString} operand at IL_{instruction.Offset:X4}, but found {found}.")); return default; } } diff --git a/src/AsmResolver.PE/DotNet/Cil/CilInstruction.cs b/src/AsmResolver.PE/DotNet/Cil/CilInstruction.cs index 1f2dc0fb5..59a0cda47 100644 --- a/src/AsmResolver.PE/DotNet/Cil/CilInstruction.cs +++ b/src/AsmResolver.PE/DotNet/Cil/CilInstruction.cs @@ -373,5 +373,39 @@ public bool IsLdcI4() CilCode.Ldc_I4_M1 => -1, _ => throw new ArgumentOutOfRangeException() }; + + /// + /// Replaces the operation code used by the instruction with a new one, and clears the operand. + /// + /// The new operation code. + /// + /// This method may be useful when patching a method body, where reusing the instruction object is favourable. + /// This can prevent breaking any references to the instruction (e.g. branch or exception handler targets). + /// + public void ReplaceWith(CilOpCode opCode) => ReplaceWith(opCode, null); + + /// + /// Replaces the operation code and operand used by the instruction with new ones. + /// + /// The new operation code. + /// The new operand. + /// + /// This method may be useful when patching a method body, where reusing the instruction object is favourable. + /// This can prevent breaking any references to the instruction (e.g. branch or exception handler targets). + /// + public void ReplaceWith(CilOpCode opCode, object? operand) + { + OpCode = opCode; + Operand = operand; + } + + /// + /// Clears the operand and replaces the operation code with a (No-Operation). + /// + /// + /// This method may be useful when patching a method body, where reusing the instruction object is favourable. + /// This can prevent breaking any references to the instruction (e.g. branch or exception handler targets). + /// + public void ReplaceWithNop() => ReplaceWith(CilOpCodes.Nop); } } diff --git a/src/AsmResolver.PE/DotNet/Cil/CilInstructionFormatter.cs b/src/AsmResolver.PE/DotNet/Cil/CilInstructionFormatter.cs index f06b5b5e8..58bd6cdae 100644 --- a/src/AsmResolver.PE/DotNet/Cil/CilInstructionFormatter.cs +++ b/src/AsmResolver.PE/DotNet/Cil/CilInstructionFormatter.cs @@ -87,7 +87,7 @@ public string FormatInstruction(CilInstruction instruction) short longIndex => $"A_{longIndex.ToString()}", byte shortIndex => $"A_{shortIndex.ToString()}", null => InvalidOperandString, - _ => operand.ToString() + _ => operand.ToString() ?? string.Empty }; /// @@ -101,7 +101,7 @@ public string FormatInstruction(CilInstruction instruction) short longIndex => $"V_{longIndex.ToString()}", byte shortIndex => $"V_{shortIndex.ToString()}", null => InvalidOperandString, - _ => operand.ToString() + _ => operand.ToString() ?? string.Empty }; /// @@ -130,7 +130,7 @@ public string FormatInstruction(CilInstruction instruction) { MetadataToken token => FormatToken(token), null => InvalidOperandString, - _ => operand.ToString() + _ => operand.ToString() ?? string.Empty }; /// @@ -150,7 +150,7 @@ public string FormatInstruction(CilInstruction instruction) IEnumerable target => $"({string.Join(", ", target.Select(FormatBranchTarget))})", IEnumerable offsets => $"({string.Join(", ", offsets.Select(x => FormatBranchTarget(x)))})", null => InvalidOperandString, - _ => operand.ToString() + _ => operand.ToString() ?? string.Empty }; /// @@ -175,7 +175,7 @@ public string FormatInstruction(CilInstruction instruction) ICilLabel target => FormatLabel(target.Offset), int offset => FormatLabel(offset), null => InvalidOperandString, - _ => operand.ToString() + _ => operand.ToString() ?? string.Empty }; /// @@ -187,7 +187,7 @@ public string FormatInstruction(CilInstruction instruction) { MetadataToken token => FormatToken(token), null => InvalidOperandString, - _ => operand.ToString() + _ => operand.ToString() ?? string.Empty }; } diff --git a/src/AsmResolver.PE/DotNet/Cil/CilInstructionLabel.cs b/src/AsmResolver.PE/DotNet/Cil/CilInstructionLabel.cs index 9fb14dad5..4418b78a7 100644 --- a/src/AsmResolver.PE/DotNet/Cil/CilInstructionLabel.cs +++ b/src/AsmResolver.PE/DotNet/Cil/CilInstructionLabel.cs @@ -39,7 +39,7 @@ public CilInstructionLabel(CilInstruction instruction) public bool Equals(ICilLabel? other) => other is not null && Offset == other.Offset; /// - public override bool Equals(object obj) => Equals(obj as ICilLabel); + public override bool Equals(object? obj) => Equals(obj as ICilLabel); /// public override int GetHashCode() => Offset; diff --git a/src/AsmResolver.PE/DotNet/Cil/CilOffsetLabel.cs b/src/AsmResolver.PE/DotNet/Cil/CilOffsetLabel.cs index 7d0a80f0a..36fcc4709 100644 --- a/src/AsmResolver.PE/DotNet/Cil/CilOffsetLabel.cs +++ b/src/AsmResolver.PE/DotNet/Cil/CilOffsetLabel.cs @@ -27,7 +27,7 @@ public int Offset public bool Equals(ICilLabel? other) => other is not null && Offset == other.Offset; /// - public override bool Equals(object obj) => Equals(obj as ICilLabel); + public override bool Equals(object? obj) => Equals(obj as ICilLabel); /// public override int GetHashCode() => Offset; diff --git a/src/AsmResolver.PE/DotNet/Cil/CilOpCode.cs b/src/AsmResolver.PE/DotNet/Cil/CilOpCode.cs index 1a1418b6b..b10e2f377 100644 --- a/src/AsmResolver.PE/DotNet/Cil/CilOpCode.cs +++ b/src/AsmResolver.PE/DotNet/Cil/CilOpCode.cs @@ -1,143 +1,143 @@ -namespace AsmResolver.PE.DotNet.Cil -{ - /// - /// Describes the operation that a single CIL instruction performs. - /// - public readonly struct CilOpCode - { - internal const int ValueBitLength = 8; - internal const int ValueOffset = 0; - - internal const int TwoBytesBitLength = 1; - internal const int TwoBytesOffset = ValueOffset + ValueBitLength; - - internal const int StackBehaviourBitLength = 5; - internal const int StackBehaviourPushOffset = TwoBytesOffset + TwoBytesBitLength; - internal const int StackBehaviourPopOffset = StackBehaviourPushOffset + StackBehaviourBitLength; - - internal const int OpCodeTypeBitLength = 3; - internal const int OpCodeTypeOffset = StackBehaviourPopOffset + StackBehaviourBitLength; - - internal const int OperandTypeBitLength = 5; - internal const int OperandTypeOffset = OpCodeTypeOffset + OpCodeTypeBitLength; - - internal const int FlowControlBitLength = 4; - internal const int FlowControlOffset = OperandTypeOffset + OperandTypeBitLength; - - /// - /// Determines whether two operation codes encode the same operation. - /// - /// The first operation code. - /// The second operation code. - /// true if the same operation code is encoded, false otherwise. - public static bool operator ==(CilOpCode a, CilOpCode b) => a.Equals(b); - - /// - /// Determines whether two operation codes do not encode the same operation. - /// - /// The first operation code. - /// The second operation code. - /// true if a different operation code is encoded, false otherwise. - public static bool operator !=(CilOpCode a, CilOpCode b) => !(a == b); - - - // To reduce the memory footprint of a single CIL operation code, we put every property into a single 32 bit - // number, using the following layout: - // - // |31.. ..27|26.. ..22|21.19|18.. ..14|13.. ..9|8|7.. ..0| - // +---------+---------+-----+---------+---------+-+---------------+ - // | flwctrl | operand | opc | stckpop | stckpsh |L| opcode byte | - // +---------+---------+-----+---------+---------+-+---------------+ - // - - private readonly uint _value; - - internal CilOpCode(uint value) - { - _value = value; - - if (IsLarge) - CilOpCodes.MultiByteOpCodes[Byte2] = this; - else - CilOpCodes.SingleByteOpCodes[Byte1] = this; - } - - /// - /// Gets the mnemonic of the operation code. - /// - public string Mnemonic => CilOpCodeNames.Names[IsLarge ? Byte2 + 256 : Byte1]; - - /// - /// Gets the value of the operation code. - /// - public CilCode Code => (CilCode) ((ushort) ((_value >> ValueOffset) & 0xFF) | (IsLarge ? 0xFE00 : 0)); - - /// - /// Gets a value indicating whether the operation code is large or not. If this value is true, the code - /// needs two bytes to be encoded, otherwise it only needs one. - /// - public bool IsLarge => ((_value >> TwoBytesOffset) & 1) == 1; - - /// - /// Gets the size in bytes of the operation code. - /// - /// - /// This does not include the operand of the instruction. - /// - public int Size => IsLarge ? 2 : 1; - - /// - /// Gets the first byte that appears in the instruction stream encoding this operation. - /// - public byte Byte1 => (byte) (IsLarge ? 0xFE : (_value >> ValueOffset) & 0xFF); - - /// - /// Gets the second byte that appears in the instruction stream encoding this operation. - /// - /// - /// This property only has meaning if is true. - /// - public byte Byte2 => (byte) (IsLarge ? (_value >> ValueOffset) & 0xFF : 0); - - /// - /// Gets a value indicating the stack push behaviour of the instruction. - /// - public CilStackBehaviour StackBehaviourPush => (CilStackBehaviour) ((_value >> StackBehaviourPushOffset) & 0b11111); - - /// - /// Gets a value indicating the stack pop behaviour of the instruction. - /// - public CilStackBehaviour StackBehaviourPop => (CilStackBehaviour) ((_value >> StackBehaviourPopOffset) & 0b11111); - - /// - /// Gets a value indicating the category of the operation code. - /// - public CilOpCodeType OpCodeType => (CilOpCodeType) ((_value >> OpCodeTypeOffset) & 0b111); - - /// - /// Gets a value indicating the category of the operand. - /// - public CilOperandType OperandType => (CilOperandType) ((_value >> OperandTypeOffset) & 0b11111); - - /// - /// Gets a value indicating the flow control behaviour of the operation. - /// - public CilFlowControl FlowControl => (CilFlowControl) ((_value >> FlowControlOffset) & 0b1111); - - /// - public override string ToString() => Mnemonic; - - /// - /// Determines whether the provided operation code is encoding the same operation. - /// - /// The other operation code. - /// true if the same operation code is encoded, false otherwise. - public bool Equals(CilOpCode other) => Code == other.Code; - - /// - public override bool Equals(object obj) => obj is CilOpCode other && Equals(other); - - /// - public override int GetHashCode() => (int) Code; - } -} \ No newline at end of file +namespace AsmResolver.PE.DotNet.Cil +{ + /// + /// Describes the operation that a single CIL instruction performs. + /// + public readonly struct CilOpCode + { + internal const int ValueBitLength = 8; + internal const int ValueOffset = 0; + + internal const int TwoBytesBitLength = 1; + internal const int TwoBytesOffset = ValueOffset + ValueBitLength; + + internal const int StackBehaviourBitLength = 5; + internal const int StackBehaviourPushOffset = TwoBytesOffset + TwoBytesBitLength; + internal const int StackBehaviourPopOffset = StackBehaviourPushOffset + StackBehaviourBitLength; + + internal const int OpCodeTypeBitLength = 3; + internal const int OpCodeTypeOffset = StackBehaviourPopOffset + StackBehaviourBitLength; + + internal const int OperandTypeBitLength = 5; + internal const int OperandTypeOffset = OpCodeTypeOffset + OpCodeTypeBitLength; + + internal const int FlowControlBitLength = 4; + internal const int FlowControlOffset = OperandTypeOffset + OperandTypeBitLength; + + /// + /// Determines whether two operation codes encode the same operation. + /// + /// The first operation code. + /// The second operation code. + /// true if the same operation code is encoded, false otherwise. + public static bool operator ==(CilOpCode a, CilOpCode b) => a.Equals(b); + + /// + /// Determines whether two operation codes do not encode the same operation. + /// + /// The first operation code. + /// The second operation code. + /// true if a different operation code is encoded, false otherwise. + public static bool operator !=(CilOpCode a, CilOpCode b) => !(a == b); + + + // To reduce the memory footprint of a single CIL operation code, we put every property into a single 32 bit + // number, using the following layout: + // + // |31.. ..27|26.. ..22|21.19|18.. ..14|13.. ..9|8|7.. ..0| + // +---------+---------+-----+---------+---------+-+---------------+ + // | flwctrl | operand | opc | stckpop | stckpsh |L| opcode byte | + // +---------+---------+-----+---------+---------+-+---------------+ + // + + private readonly uint _value; + + internal CilOpCode(uint value) + { + _value = value; + + if (IsLarge) + CilOpCodes.MultiByteOpCodes[Byte2] = this; + else + CilOpCodes.SingleByteOpCodes[Byte1] = this; + } + + /// + /// Gets the mnemonic of the operation code. + /// + public string Mnemonic => CilOpCodeNames.Names[IsLarge ? Byte2 + 256 : Byte1]; + + /// + /// Gets the value of the operation code. + /// + public CilCode Code => (CilCode) ((ushort) ((_value >> ValueOffset) & 0xFF) | (IsLarge ? 0xFE00 : 0)); + + /// + /// Gets a value indicating whether the operation code is large or not. If this value is true, the code + /// needs two bytes to be encoded, otherwise it only needs one. + /// + public bool IsLarge => ((_value >> TwoBytesOffset) & 1) == 1; + + /// + /// Gets the size in bytes of the operation code. + /// + /// + /// This does not include the operand of the instruction. + /// + public int Size => IsLarge ? 2 : 1; + + /// + /// Gets the first byte that appears in the instruction stream encoding this operation. + /// + public byte Byte1 => (byte) (IsLarge ? 0xFE : (_value >> ValueOffset) & 0xFF); + + /// + /// Gets the second byte that appears in the instruction stream encoding this operation. + /// + /// + /// This property only has meaning if is true. + /// + public byte Byte2 => (byte) (IsLarge ? (_value >> ValueOffset) & 0xFF : 0); + + /// + /// Gets a value indicating the stack push behaviour of the instruction. + /// + public CilStackBehaviour StackBehaviourPush => (CilStackBehaviour) ((_value >> StackBehaviourPushOffset) & 0b11111); + + /// + /// Gets a value indicating the stack pop behaviour of the instruction. + /// + public CilStackBehaviour StackBehaviourPop => (CilStackBehaviour) ((_value >> StackBehaviourPopOffset) & 0b11111); + + /// + /// Gets a value indicating the category of the operation code. + /// + public CilOpCodeType OpCodeType => (CilOpCodeType) ((_value >> OpCodeTypeOffset) & 0b111); + + /// + /// Gets a value indicating the category of the operand. + /// + public CilOperandType OperandType => (CilOperandType) ((_value >> OperandTypeOffset) & 0b11111); + + /// + /// Gets a value indicating the flow control behaviour of the operation. + /// + public CilFlowControl FlowControl => (CilFlowControl) ((_value >> FlowControlOffset) & 0b1111); + + /// + public override string ToString() => Mnemonic; + + /// + /// Determines whether the provided operation code is encoding the same operation. + /// + /// The other operation code. + /// true if the same operation code is encoded, false otherwise. + public bool Equals(CilOpCode other) => Code == other.Code; + + /// + public override bool Equals(object? obj) => obj is CilOpCode other && Equals(other); + + /// + public override int GetHashCode() => (int) Code; + } +} diff --git a/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs b/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs index 55d2f9299..293562f36 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Strings/SerializedStringsStream.cs @@ -65,22 +65,7 @@ public SerializedStringsStream(string name, in BinaryStreamReader reader) if (!_cachedStrings.TryGetValue(index, out var value) && index < _reader.Length) { var stringsReader = _reader.ForkRelative(index); - byte[] rawData = stringsReader.ReadBytesUntil(0); - - if (rawData.Length == 0) - { - value = Utf8String.Empty; - } - else - { - // Trim off null terminator byte if its present. - int actualLength = rawData.Length; - if (rawData[actualLength - 1] == 0) - actualLength--; - - value = new Utf8String(rawData, 0, actualLength); - } - + value = stringsReader.ReadUtf8String(); _cachedStrings[index] = value; } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataTable.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataTable.cs index 2b8f01739..94983d7d9 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataTable.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataTable.cs @@ -88,6 +88,15 @@ protected RefList Rows /// public virtual int Count => Rows.Count; + /// + /// Gets or sets the total number of rows that the underlying array can store. + /// + public int Capacity + { + get => Rows.Capacity; + set => Rows.Capacity = value; + } + /// public bool IsReadOnly => false; // TODO: it might be necessary later to make this configurable. diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataToken.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataToken.cs index f936a9530..56d79aa17 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataToken.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataToken.cs @@ -3,7 +3,7 @@ namespace AsmResolver.PE.DotNet.Metadata.Tables { /// - /// Represents a metadata token, referencing a member using a table and a row index. + /// Represents a metadata token, referencing a member using a table and a row index. /// public readonly struct MetadataToken : IComparable { @@ -11,7 +11,7 @@ namespace AsmResolver.PE.DotNet.Metadata.Tables /// Represents the zero metadata token, or the absence of a metadata token. /// public static readonly MetadataToken Zero = new MetadataToken(0); - + /// /// Converts a 32-bit integer to a metadata token. /// @@ -43,7 +43,7 @@ namespace AsmResolver.PE.DotNet.Metadata.Tables { return a.Equals(b); } - + /// /// Determines whether two metadata tokens are not considered equal. That is, either the table index or the row /// identifier (or both) does not match the other. @@ -119,7 +119,7 @@ public bool Equals(MetadataToken other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MetadataToken other && Equals(other); } @@ -135,6 +135,6 @@ public int CompareTo(MetadataToken other) { return _value.CompareTo(other._value); } - + } -} \ No newline at end of file +} diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyDefinitionRow.cs index 92f7f8212..e1cdaaa55 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyDefinitionRow.cs @@ -200,7 +200,7 @@ public bool Equals(AssemblyDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is AssemblyDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyOSRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyOSRow.cs index 7a009dc9f..820abe0be 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyOSRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyOSRow.cs @@ -100,7 +100,7 @@ public bool Equals(AssemblyOSRow other) } /// - public override bool Equals(object obj) => obj is AssemblyOSRow other && Equals(other); + public override bool Equals(object? obj) => obj is AssemblyOSRow other && Equals(other); /// public override int GetHashCode() diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyProcessorRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyProcessorRow.cs index 1a02fe0ac..fd2ca7474 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyProcessorRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyProcessorRow.cs @@ -69,7 +69,7 @@ public bool Equals(AssemblyProcessorRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is AssemblyProcessorRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefOSRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefOSRow.cs index ea061ddbd..4d535d7fc 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefOSRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefOSRow.cs @@ -117,7 +117,7 @@ public bool Equals(AssemblyRefOSRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is AssemblyRefOSRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefProcessorRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefProcessorRow.cs index 337a6da27..e7e30f0cb 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefProcessorRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyRefProcessorRow.cs @@ -85,7 +85,7 @@ public bool Equals(AssemblyRefProcessorRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is AssemblyRefProcessorRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyReferenceRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyReferenceRow.cs index dec588c64..bdde149bc 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyReferenceRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/AssemblyReferenceRow.cs @@ -199,7 +199,7 @@ public bool Equals(AssemblyReferenceRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is AssemblyReferenceRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ClassLayoutRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ClassLayoutRow.cs index ac354d7c1..5506ddc35 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ClassLayoutRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ClassLayoutRow.cs @@ -101,7 +101,7 @@ public bool Equals(ClassLayoutRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ClassLayoutRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ConstantRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ConstantRow.cs index b653527d8..62da25716 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ConstantRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ConstantRow.cs @@ -133,7 +133,7 @@ public void Write(IBinaryStreamWriter writer, TableLayout layout) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ConstantRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/CustomAttributeRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/CustomAttributeRow.cs index d2bd968a6..aab495f23 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/CustomAttributeRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/CustomAttributeRow.cs @@ -105,7 +105,7 @@ public void Write(IBinaryStreamWriter writer, TableLayout layout) } /// - public override bool Equals(object obj) => obj is CustomAttributeRow other && Equals(other); + public override bool Equals(object? obj) => obj is CustomAttributeRow other && Equals(other); /// public override int GetHashCode() diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncLogRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncLogRow.cs index e55e6dc18..f625eb81c 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncLogRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncLogRow.cs @@ -84,7 +84,7 @@ public void Write(IBinaryStreamWriter writer, TableLayout layout) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is EncLogRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncMapRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncMapRow.cs index dcdf99df5..46c5115f3 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncMapRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EncMapRow.cs @@ -69,7 +69,7 @@ public void Write(IBinaryStreamWriter writer, TableLayout layout) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is EncMapRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventDefinitionRow.cs index 35ab1196c..cfa67f011 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventDefinitionRow.cs @@ -103,7 +103,7 @@ public bool Equals(EventDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is EventDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventMapRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventMapRow.cs index 74578f614..76cf6d5e8 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventMapRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventMapRow.cs @@ -84,7 +84,7 @@ public bool Equals(EventMapRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is EventMapRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventPointerRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventPointerRow.cs index 1c51f5593..5aa731856 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventPointerRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/EventPointerRow.cs @@ -69,7 +69,7 @@ public bool Equals(EventPointerRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is EventPointerRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ExportedTypeRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ExportedTypeRow.cs index 87d971d5f..cf846c6e7 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ExportedTypeRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ExportedTypeRow.cs @@ -143,7 +143,7 @@ public bool Equals(ExportedTypeRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ExportedTypeRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldDefinitionRow.cs index 2ab35b41e..0891d851d 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldDefinitionRow.cs @@ -103,7 +103,7 @@ public bool Equals(FieldDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is FieldDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldLayoutRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldLayoutRow.cs index 7cd0b6bf4..e4070b33e 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldLayoutRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldLayoutRow.cs @@ -84,7 +84,7 @@ public bool Equals(FieldLayoutRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is FieldLayoutRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldMarshalRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldMarshalRow.cs index a0d5456f1..b6300f871 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldMarshalRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldMarshalRow.cs @@ -86,7 +86,7 @@ public bool Equals(FieldMarshalRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is FieldMarshalRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldPointerRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldPointerRow.cs index 31adc510c..59973970b 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldPointerRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldPointerRow.cs @@ -70,7 +70,7 @@ public bool Equals(FieldPointerRow other) /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is FieldPointerRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldRvaRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldRvaRow.cs index 8cc8a9358..97d7b6b92 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldRvaRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FieldRvaRow.cs @@ -93,7 +93,7 @@ public bool Equals(FieldRvaRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is FieldRvaRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FileReferenceRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FileReferenceRow.cs index e8308cd70..07543d961 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FileReferenceRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/FileReferenceRow.cs @@ -98,7 +98,7 @@ public bool Equals(FileReferenceRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is FileReferenceRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterConstraintRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterConstraintRow.cs index 9f2ab657f..6e5fe6f24 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterConstraintRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterConstraintRow.cs @@ -86,7 +86,7 @@ public bool Equals(GenericParameterConstraintRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is GenericParameterConstraintRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterRow.cs index d052fb8c0..51f0be43c 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/GenericParameterRow.cs @@ -116,7 +116,7 @@ public bool Equals(GenericParameterRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is GenericParameterRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ImplementationMapRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ImplementationMapRow.cs index 12e014998..bb702eadf 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ImplementationMapRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ImplementationMapRow.cs @@ -116,7 +116,7 @@ public bool Equals(ImplementationMapRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ImplementationMapRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/InterfaceImplementationRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/InterfaceImplementationRow.cs index 66982eb6d..3d70f3f8d 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/InterfaceImplementationRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/InterfaceImplementationRow.cs @@ -86,7 +86,7 @@ public bool Equals(InterfaceImplementationRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is InterfaceImplementationRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ManifestResourceRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ManifestResourceRow.cs index ae8cd6d4f..f36d4300f 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ManifestResourceRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ManifestResourceRow.cs @@ -120,7 +120,7 @@ public bool Equals(ManifestResourceRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ManifestResourceRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MemberReferenceRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MemberReferenceRow.cs index e953e5bc1..f9a6e0d07 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MemberReferenceRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MemberReferenceRow.cs @@ -107,7 +107,7 @@ public bool Equals(MemberReferenceRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MemberReferenceRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodDefinitionRow.cs index 944503004..2b6df546f 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodDefinitionRow.cs @@ -168,7 +168,7 @@ public bool Equals(MethodDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MethodDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodImplementationRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodImplementationRow.cs index 8ffcdc0e9..a50f9b648 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodImplementationRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodImplementationRow.cs @@ -117,7 +117,7 @@ public bool Equals(MethodImplementationRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MethodImplementationRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodPointerRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodPointerRow.cs index f660fd143..631824d61 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodPointerRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodPointerRow.cs @@ -69,7 +69,7 @@ public bool Equals(MethodPointerRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MethodPointerRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSemanticsRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSemanticsRow.cs index 958624c72..bd56cb3c8 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSemanticsRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSemanticsRow.cs @@ -115,7 +115,7 @@ public bool Equals(MethodSemanticsRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MethodSemanticsRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSpecificationRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSpecificationRow.cs index 629601266..e4ddb7850 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSpecificationRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/MethodSpecificationRow.cs @@ -84,7 +84,7 @@ public bool Equals(MethodSpecificationRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is MethodSpecificationRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleDefinitionRow.cs index d5013b847..e29f2edcf 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleDefinitionRow.cs @@ -142,7 +142,7 @@ public bool Equals(ModuleDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ModuleDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleReferenceRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleReferenceRow.cs index 93dce3f17..930d0526d 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleReferenceRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ModuleReferenceRow.cs @@ -70,7 +70,7 @@ public bool Equals(ModuleReferenceRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ModuleReferenceRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/NestedClassRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/NestedClassRow.cs index 9d937dab7..9e1d690d4 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/NestedClassRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/NestedClassRow.cs @@ -84,7 +84,7 @@ public bool Equals(NestedClassRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is NestedClassRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterDefinitionRow.cs index d57d48953..385c1a2eb 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterDefinitionRow.cs @@ -101,7 +101,7 @@ public bool Equals(ParameterDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ParameterDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterPointerRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterPointerRow.cs index d318057ed..5033cacd2 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterPointerRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/ParameterPointerRow.cs @@ -69,7 +69,7 @@ public bool Equals(ParameterPointerRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is ParameterPointerRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyDefinitionRow.cs index 4a526e590..fe2e01440 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyDefinitionRow.cs @@ -116,7 +116,7 @@ public bool Equals(PropertyDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is PropertyDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyMapRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyMapRow.cs index 947e1b0e4..350d6bc6f 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyMapRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyMapRow.cs @@ -99,7 +99,7 @@ public bool Equals(PropertyMapRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is PropertyMapRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyPointerRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyPointerRow.cs index 97be25f86..2026040a2 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyPointerRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/PropertyPointerRow.cs @@ -69,7 +69,7 @@ public bool Equals(PropertyPointerRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is PropertyPointerRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/SecurityDeclarationRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/SecurityDeclarationRow.cs index 4f0fd5a66..18e220604 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/SecurityDeclarationRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/SecurityDeclarationRow.cs @@ -99,7 +99,7 @@ public bool Equals(SecurityDeclarationRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is SecurityDeclarationRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/StandAloneSignatureRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/StandAloneSignatureRow.cs index 52881d8e9..0e4a1b00f 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/StandAloneSignatureRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/StandAloneSignatureRow.cs @@ -69,7 +69,7 @@ public bool Equals(StandAloneSignatureRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is StandAloneSignatureRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeDefinitionRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeDefinitionRow.cs index 880db0866..0d5f5c65d 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeDefinitionRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeDefinitionRow.cs @@ -160,7 +160,7 @@ public bool Equals(TypeDefinitionRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is TypeDefinitionRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeReferenceRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeReferenceRow.cs index 34850ceae..526ea07df 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeReferenceRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeReferenceRow.cs @@ -114,7 +114,7 @@ public bool Equals(TypeReferenceRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is TypeReferenceRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeSpecificationRow.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeSpecificationRow.cs index 2875db9a7..a22336b20 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeSpecificationRow.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/Tables/Rows/TypeSpecificationRow.cs @@ -70,7 +70,7 @@ public bool Equals(TypeSpecificationRow other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is TypeSpecificationRow other && Equals(other); } diff --git a/src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs b/src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs index 017bb9490..d8f673e46 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; @@ -755,7 +755,7 @@ protected TableLayout[] GetTableLayouts() /// The row identifier of the type definition to obtain the fields from. /// The range of metadata tokens. public MetadataRange GetFieldRange(uint typeDefRid) => - GetMemberRange(TableIndex.TypeDef, typeDefRid, 4, + GetMemberRange(TableIndex.TypeDef, typeDefRid, 4, TableIndex.Field, TableIndex.FieldPtr); /// @@ -764,7 +764,7 @@ protected TableLayout[] GetTableLayouts() /// The row identifier of the type definition to obtain the methods from. /// The range of metadata tokens. public MetadataRange GetMethodRange(uint typeDefRid) => - GetMemberRange(TableIndex.TypeDef, typeDefRid, 5, + GetMemberRange(TableIndex.TypeDef, typeDefRid, 5, TableIndex.Method, TableIndex.MethodPtr); /// @@ -773,7 +773,7 @@ protected TableLayout[] GetTableLayouts() /// The row identifier of the method definition to obtain the parameters from. /// The range of metadata tokens. public MetadataRange GetParameterRange(uint methodDefRid) => - GetMemberRange(TableIndex.Method, methodDefRid, 5, + GetMemberRange(TableIndex.Method, methodDefRid, 5, TableIndex.Param, TableIndex.ParamPtr); /// @@ -782,7 +782,7 @@ protected TableLayout[] GetTableLayouts() /// The row identifier of the property map to obtain the properties from. /// The range of metadata tokens. public MetadataRange GetPropertyRange(uint propertyMapRid) => - GetMemberRange(TableIndex.PropertyMap, propertyMapRid, 1, + GetMemberRange(TableIndex.PropertyMap, propertyMapRid, 1, TableIndex.Property, TableIndex.PropertyPtr); /// @@ -791,16 +791,21 @@ protected TableLayout[] GetTableLayouts() /// The row identifier of the event map to obtain the events from. /// The range of metadata tokens. public MetadataRange GetEventRange(uint eventMapRid) => - GetMemberRange(TableIndex.EventMap, eventMapRid, 1, + GetMemberRange(TableIndex.EventMap, eventMapRid, 1, TableIndex.Event, TableIndex.EventPtr); - private MetadataRange GetMemberRange(TableIndex ownerTableIndex, uint ownerRid, int ownerColumnIndex, - TableIndex memberTableIndex, TableIndex redirectTableIndex) + private MetadataRange GetMemberRange( + TableIndex ownerTableIndex, + uint ownerRid, + int ownerColumnIndex, + TableIndex memberTableIndex, + TableIndex redirectTableIndex) + where TOwnerRow : struct, IMetadataRow { int index = (int) (ownerRid - 1); // Check if valid owner RID. - var ownerTable = GetTable(ownerTableIndex); + var ownerTable = GetTable(ownerTableIndex); if (index < 0 || index >= ownerTable.Count) return MetadataRange.Empty; diff --git a/src/AsmResolver.PE/DotNet/Metadata/UserStrings/SerializedUserStringsStream.cs b/src/AsmResolver.PE/DotNet/Metadata/UserStrings/SerializedUserStringsStream.cs index ed4bdcc7e..b50077898 100644 --- a/src/AsmResolver.PE/DotNet/Metadata/UserStrings/SerializedUserStringsStream.cs +++ b/src/AsmResolver.PE/DotNet/Metadata/UserStrings/SerializedUserStringsStream.cs @@ -9,7 +9,7 @@ namespace AsmResolver.PE.DotNet.Metadata.UserStrings /// public class SerializedUserStringsStream : UserStringsStream { - private readonly Dictionary _cachedStrings = new(); + private readonly Dictionary _cachedStrings = new(); private readonly BinaryStreamReader _reader; /// @@ -61,7 +61,7 @@ public SerializedUserStringsStream(string name, BinaryStreamReader reader) { index &= 0x00FFFFFF; - if (!_cachedStrings.TryGetValue(index, out string value) && index < _reader.Length) + if (!_cachedStrings.TryGetValue(index, out string? value) && index < _reader.Length) { var stringsReader = _reader.ForkRelative(index); diff --git a/src/AsmResolver.PE/DotNet/StrongName/StrongNameDataHashBuilder.cs b/src/AsmResolver.PE/DotNet/StrongName/StrongNameDataHashBuilder.cs index a4902cbb6..6fed154ad 100644 --- a/src/AsmResolver.PE/DotNet/StrongName/StrongNameDataHashBuilder.cs +++ b/src/AsmResolver.PE/DotNet/StrongName/StrongNameDataHashBuilder.cs @@ -65,7 +65,7 @@ public byte[] ComputeHash() _ => throw new NotSupportedException($"Invalid or unsupported hashing algorithm {_hashAlgorithm}.") }; - var buffer = new byte[0x1000]; + byte[] buffer = new byte[0x1000]; foreach (var range in _includedRanges) { @@ -85,7 +85,7 @@ public byte[] ComputeHash() } algorithm.TransformFinalBlock(Array.Empty(), 0, 0); - return algorithm.Hash; + return algorithm.Hash!; } private void ZeroRangesIfApplicable(byte[] buffer, OffsetRange currentRange) diff --git a/src/AsmResolver.PE/DotNet/StrongName/StrongNamePrivateKey.cs b/src/AsmResolver.PE/DotNet/StrongName/StrongNamePrivateKey.cs index 8a98cd0fe..dc517f811 100644 --- a/src/AsmResolver.PE/DotNet/StrongName/StrongNamePrivateKey.cs +++ b/src/AsmResolver.PE/DotNet/StrongName/StrongNamePrivateKey.cs @@ -92,16 +92,19 @@ public StrongNamePrivateKey(uint bitLength) /// /// The RSA parameters to import. public StrongNamePrivateKey(in RSAParameters parameters) - : base(parameters.Modulus, ByteSwap(parameters)) + : base(parameters.Modulus ?? throw new ArgumentException("The provided RSA parameters do not define a modulus."), + ByteSwap(parameters)) { Modulus = parameters.Modulus; - P = parameters.P; - Q = parameters.Q; - DP = parameters.DP; - DQ = parameters.DQ; - - InverseQ = parameters.InverseQ; - PrivateExponent = parameters.D; + P = parameters.P ?? throw new ArgumentException("The provided RSA parameters do not define prime P."); + Q = parameters.Q ?? throw new ArgumentException("The provided RSA parameters do not define prime Q.");; + DP = parameters.DP ?? throw new ArgumentException("The provided RSA parameters do not define DP.");; + DQ = parameters.DQ ?? throw new ArgumentException("The provided RSA parameters do not define DQ.");; + + InverseQ = parameters.InverseQ + ?? throw new ArgumentException("The provided RSA parameters do not define InverseQ."); + PrivateExponent = + parameters.D ?? throw new ArgumentException("The provided RSA parameters do not define D."); } /// @@ -223,6 +226,9 @@ public override void Write(IBinaryStreamWriter writer) private static uint ByteSwap(RSAParameters parameters) { + if (parameters.Exponent is null) + throw new ArgumentException("The provided RSA parameters do not define an exponent."); + uint exponent = 0; for (int i = 0; i < Math.Min(sizeof(uint), parameters.Exponent.Length); i++) exponent |= (uint) (parameters.Exponent[i] << (8 * i)); diff --git a/src/AsmResolver.PE/DotNet/StrongName/StrongNamePublicKey.cs b/src/AsmResolver.PE/DotNet/StrongName/StrongNamePublicKey.cs index 4068ff691..ae410cbb8 100644 --- a/src/AsmResolver.PE/DotNet/StrongName/StrongNamePublicKey.cs +++ b/src/AsmResolver.PE/DotNet/StrongName/StrongNamePublicKey.cs @@ -76,6 +76,11 @@ public StrongNamePublicKey(byte[] modulus, uint publicExponent) /// The RSA parameters to import. public StrongNamePublicKey(in RSAParameters parameters) { + if (parameters.Modulus is null) + throw new ArgumentException("RSA parameters does not define a modulus."); + if (parameters.Exponent is null) + throw new ArgumentException("RSA parameters does not define an exponent."); + Modulus = CopyReversed(parameters.Modulus); uint exponent = 0; for (int i = 0; i < Math.Min(sizeof(uint), parameters.Exponent.Length); i++) diff --git a/src/AsmResolver.PE/Imports/DefaultSymbolResolver.cs b/src/AsmResolver.PE/Imports/DefaultSymbolResolver.cs index 158b05a05..325ba9e19 100644 --- a/src/AsmResolver.PE/Imports/DefaultSymbolResolver.cs +++ b/src/AsmResolver.PE/Imports/DefaultSymbolResolver.cs @@ -676,7 +676,7 @@ private DefaultSymbolResolver() if (!_staticMappings.TryGetValue(moduleName, out var staticMapping)) return null; - return staticMapping.TryGetValue(symbol.Ordinal, out string exportName) + return staticMapping.TryGetValue(symbol.Ordinal, out string? exportName) ? new ExportedSymbol(SegmentReference.Null, exportName) : null; } diff --git a/src/AsmResolver.PE/Relocations/BaseRelocation.cs b/src/AsmResolver.PE/Relocations/BaseRelocation.cs index 8e71b3231..d9a5f5713 100644 --- a/src/AsmResolver.PE/Relocations/BaseRelocation.cs +++ b/src/AsmResolver.PE/Relocations/BaseRelocation.cs @@ -56,7 +56,7 @@ public bool Equals(BaseRelocation other) } /// - public override bool Equals(object obj) + public override bool Equals(object? obj) { return obj is BaseRelocation other && Equals(other); } diff --git a/src/AsmResolver.PE/Win32Resources/Builder/ResourceTableBuffer.cs b/src/AsmResolver.PE/Win32Resources/Builder/ResourceTableBuffer.cs index 0da671e29..cd8485fdd 100644 --- a/src/AsmResolver.PE/Win32Resources/Builder/ResourceTableBuffer.cs +++ b/src/AsmResolver.PE/Win32Resources/Builder/ResourceTableBuffer.cs @@ -8,8 +8,9 @@ namespace AsmResolver.PE.Win32Resources.Builder /// /// The type of entries to store in the table. public abstract class ResourceTableBuffer : SegmentBase + where TEntry : notnull { - private readonly IDictionary _entryOffsets = new Dictionary(); + private readonly Dictionary _entryOffsets = new(); private readonly ISegment _parentBuffer; private uint _length; diff --git a/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj b/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj new file mode 100644 index 000000000..168f407cc --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/AsmResolver.Symbols.Pdb.csproj @@ -0,0 +1,28 @@ + + + + AsmResolver + Windows PDB models for the AsmResolver executable file inspection toolsuite. + windows pdb symbols + enable + net6.0;netcoreapp3.1;netstandard2.0 + true + true + + + + true + bin\Debug\netstandard2.0\AsmResolver.Symbols.WindowsPdb.xml + + + + true + bin\Release\netstandard2.0\AsmResolver.xml + + + + + + + + diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiAttributes.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiAttributes.cs new file mode 100644 index 000000000..3fb37697a --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiAttributes.cs @@ -0,0 +1,30 @@ +using System; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members defining all attributes that can be assigned to a single DBI stream. +/// +[Flags] +public enum DbiAttributes : ushort +{ + /// + /// Indicates no attributes were assigned. + /// + None = 0, + + /// + /// Indicates the program was linked in an incremental manner. + /// + IncrementallyLinked = 1, + + /// + /// Indicates private symbols were stripped from the PDB file. + /// + PrivateSymbolsStripped = 2, + + /// + /// Indicates the program was linked using link.exe with the undocumented /DEBUG:CTYPES flag. + /// + HasConflictingTypes = 4, +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStream.cs new file mode 100644 index 000000000..247bd21ac --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStream.cs @@ -0,0 +1,595 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents the DBI Stream (also known as the Debug Information stream). +/// +public class DbiStream : SegmentBase +{ + /// + /// Gets the default fixed MSF stream index for the DBI stream. + /// + public const int StreamIndex = 3; + + private IList? _modules; + private IList? _sectionContributions; + private IList? _sectionMaps; + private readonly LazyVariable _typeServerMapStream; + private readonly LazyVariable _ecStream; + private IList? _sourceFiles; + private IList? _extraStreamIndices; + + /// + /// Creates a new empty DBI stream. + /// + public DbiStream() + { + _typeServerMapStream = new LazyVariable(GetTypeServerMapStream); + _ecStream = new LazyVariable(GetECStream); + IsNewVersionFormat = true; + } + + /// + /// Gets or sets the version signature assigned to the DBI stream. + /// + /// + /// This value should always be -1 for valid PDB files. + /// + public int VersionSignature + { + get; + set; + } = -1; + + /// + /// Gets or sets the version number of the DBI header. + /// + /// + /// Modern tooling only recognize the VC7.0 file format. + /// + public DbiStreamVersion VersionHeader + { + get; + set; + } = DbiStreamVersion.V70; + + /// + /// Gets or sets the number of times the DBI stream has been written. + /// + public uint Age + { + get; + set; + } = 1; + + /// + /// Gets or sets the MSF stream index of the Global Symbol Stream. + /// + public ushort GlobalStreamIndex + { + get; + set; + } + + /// + /// Gets or sets a bitfield containing the major and minor version of the toolchain that was used to build the program. + /// + public ushort BuildNumber + { + get; + set; + } + + /// + /// Gets or sets a value indicating that the DBI stream is using the new file format (NewDBI). + /// + public bool IsNewVersionFormat + { + get => (BuildNumber & 0x8000) != 0; + set => BuildNumber = (ushort) ((BuildNumber & ~0x8000) | (value ? 0x8000 : 0)); + } + + /// + /// Gets or sets the major version of the toolchain that was used to build the program. + /// + public byte BuildMajorVersion + { + get => (byte) ((BuildNumber >> 8) & 0x7F); + set => BuildNumber = (ushort) ((BuildNumber & ~0x7F00) | (value << 8)); + } + + /// + /// Gets or sets the minor version of the toolchain that was used to build the program. + /// + public byte BuildMinorVersion + { + get => (byte) (BuildNumber & 0xFF); + set => BuildNumber = (ushort) ((BuildNumber & ~0x00FF) | value); + } + + /// + /// Gets or sets the MSF stream index of the Public Symbol Stream. + /// + public ushort PublicStreamIndex + { + get; + set; + } + + /// + /// Gets or sets the version number of mspdbXXXX.dll that was used to produce this PDB file. + /// + public ushort PdbDllVersion + { + get; + set; + } + + /// + /// Gets or sets the MSF stream index of the Symbol Record Stream. + /// + public ushort SymbolRecordStreamIndex + { + get; + set; + } + + /// + /// Unknown. + /// + public ushort PdbDllRbld + { + get; + set; + } + + /// + /// Gets or sets the MSF stream index of the MFC type server. + /// + public uint MfcTypeServerIndex + { + get; + set; + } + + /// + /// Gets or sets attributes associated to the DBI stream. + /// + public DbiAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the machine type the program was compiled for. + /// + public MachineType Machine + { + get; + set; + } + + /// + /// Gets a collection of modules (object files) that were linked together into the program. + /// + public IList Modules + { + get + { + if (_modules is null) + Interlocked.CompareExchange(ref _modules, GetModules(), null); + return _modules; + } + } + + /// + /// Gets a collection of section contributions describing the layout of the sections of the final executable file. + /// + public IList SectionContributions + { + get + { + if (_sectionContributions is null) + Interlocked.CompareExchange(ref _sectionContributions, GetSectionContributions(), null); + return _sectionContributions; + } + } + + /// + /// Gets a collection of section mappings stored in the section mapping sub stream. + /// + /// + /// The exact purpose of this is unknown, but it seems to be always containing a copy of the sections in the final + /// executable file. + /// + public IList SectionMaps + { + get + { + if (_sectionMaps is null) + Interlocked.CompareExchange(ref _sectionMaps, GetSectionMaps(), null); + return _sectionMaps; + } + } + + /// + /// Gets or sets the contents of the type server map sub stream. + /// + /// + /// The exact purpose and layout of this sub stream is unknown, hence this property exposes the stream as + /// a raw segment. + /// + public ISegment? TypeServerMapStream + { + get => _typeServerMapStream.Value; + set => _typeServerMapStream.Value = value; + } + + /// + /// Gets or sets the contents of the Edit-and-Continue sub stream. + /// + /// + /// The exact purpose and layout of this sub stream is unknown, hence this property exposes the stream as + /// a raw segment. + /// + public ISegment? ECStream + { + get => _ecStream.Value; + set => _ecStream.Value = value; + } + + /// + /// Gets a collection of source files assigned to each module in . + /// + /// + /// Every collection of source files within this list corresponds to exactly the module within + /// at the same index. For example, the first source file list in this collection is the source file list of the + /// first module. + /// + public IList SourceFiles + { + get + { + if (_sourceFiles is null) + Interlocked.CompareExchange(ref _sourceFiles, GetSourceFiles(), null); + return _sourceFiles; + } + } + + /// + /// Gets a collection of indices referring to additional debug streams in the MSF file. + /// + public IList ExtraStreamIndices + { + get + { + if (_extraStreamIndices is null) + Interlocked.CompareExchange(ref _extraStreamIndices, GetExtraStreamIndices(), null); + return _extraStreamIndices; + } + } + + /// + /// Reads a single DBI stream from the provided input stream. + /// + /// The input stream. + /// The parsed DBI stream. + public static DbiStream FromReader(BinaryStreamReader reader) => new SerializedDbiStream(reader); + + /// + /// Obtains the list of module descriptors. + /// + /// The module descriptors + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetModules() => new List(); + + /// + /// Obtains the list of section contributions. + /// + /// The section contributions. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSectionContributions() => new List(); + + /// + /// Obtains the list of section maps. + /// + /// The section maps. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSectionMaps() => new List(); + + /// + /// Obtains the contents of the type server map sub stream. + /// + /// The contents of the sub stream. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ISegment? GetTypeServerMapStream() => null; + + /// + /// Obtains the contents of the EC sub stream. + /// + /// The contents of the sub stream. + /// + /// This method is called upon initialization of the property. + /// + protected virtual ISegment? GetECStream() => null; + + /// + /// Obtains a table that assigns a list of source files to every module referenced in the PDB file. + /// + /// The table. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetSourceFiles() => new List(); + + /// + /// Obtains the list of indices referring to additional debug streams in the MSF file. + /// + /// The list of indices. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetExtraStreamIndices() => new List(); + + /// + public override uint GetPhysicalSize() + { + return GetHeaderSize() + + GetModuleStreamSize() + + GetSectionContributionStreamSize() + + GetSectionMapStreamSize() + + GetSourceInfoStreamSize() + + GetTypeServerMapStreamSize() + + GetECStreamSize() + + GetOptionalDebugStreamSize() + ; + } + + private static uint GetHeaderSize() + { + return sizeof(int) // VersionSignature + + sizeof(DbiStreamVersion) // VersionHeader + + sizeof(uint) // Age + + sizeof(ushort) // GlobalStreamIndex + + sizeof(ushort) // BuildNumber + + sizeof(ushort) // PublicStreamIndex + + sizeof(ushort) // PdbDllVersion + + sizeof(ushort) // SymbolRecordStreamIndex + + sizeof(ushort) // PdbDllRbld + + sizeof(uint) // ModuleInfoSize + + sizeof(uint) // SectionContributionSize + + sizeof(uint) // SectionMapSize + + sizeof(uint) // SourceInfoSize + + sizeof(uint) // TypeServerMapSize + + sizeof(uint) // MfcTypeServerIndex + + sizeof(uint) // OptionalDebugStreamSize + + sizeof(uint) // ECStreamSize + + sizeof(DbiAttributes) // Attributes + + sizeof(MachineType) // MachineType + + sizeof(uint) // Padding + ; + } + + private uint GetModuleStreamSize() + { + return ((uint) Modules.Sum(m => m.GetPhysicalSize())).Align(sizeof(uint)); + } + + private uint GetSectionContributionStreamSize() + { + return sizeof(uint) // version + + SectionContribution.EntrySize * (uint) SectionContributions.Count; + } + + private uint GetSectionMapStreamSize() + { + return sizeof(ushort) // Count + + sizeof(ushort) // LogCount + + SectionMap.EntrySize * (uint) SectionMaps.Count; + } + + private uint GetSourceInfoStreamSize() + { + uint totalFileCount = 0; + uint nameBufferSize = 0; + var stringOffsets = new Dictionary(); + + // Simulate the construction of name buffer + for (int i = 0; i < SourceFiles.Count; i++) + { + var collection = SourceFiles[i]; + totalFileCount += (uint) collection.Count; + + for (int j = 0; j < collection.Count; j++) + { + // If name is not added yet, "append" to the name buffer. + var name = collection[j]; + if (!stringOffsets.ContainsKey(name)) + { + stringOffsets[name] = nameBufferSize; + nameBufferSize += (uint) name.ByteCount + 1u; + } + } + } + + return (sizeof(ushort) // ModuleCount + + sizeof(ushort) // SourceFileCount + + sizeof(ushort) * (uint) SourceFiles.Count // ModuleIndices + + sizeof(ushort) * (uint) SourceFiles.Count // SourceFileCounts + + sizeof(uint) * totalFileCount // NameOffsets + + nameBufferSize // NameBuffer + ).Align(4); + } + + private uint GetTypeServerMapStreamSize() + { + return TypeServerMapStream?.GetPhysicalSize().Align(sizeof(uint)) ?? 0u; + } + + private uint GetOptionalDebugStreamSize() + { + return (uint) (ExtraStreamIndices.Count * sizeof(ushort)); + } + + private uint GetECStreamSize() + { + return ECStream?.GetPhysicalSize() ?? 0u; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + WriteHeader(writer); + WriteModuleStream(writer); + WriteSectionContributionStream(writer); + WriteSectionMapStream(writer); + WriteSourceInfoStream(writer); + WriteTypeServerMapStream(writer); + WriteECStream(writer); + WriteOptionalDebugStream(writer); + } + + private void WriteHeader(IBinaryStreamWriter writer) + { + writer.WriteInt32(VersionSignature); + writer.WriteUInt32((uint) VersionHeader); + writer.WriteUInt32(Age); + writer.WriteUInt16(GlobalStreamIndex); + writer.WriteUInt16(BuildNumber); + writer.WriteUInt16(PublicStreamIndex); + writer.WriteUInt16(PdbDllVersion); + writer.WriteUInt16(SymbolRecordStreamIndex); + writer.WriteUInt16(PdbDllRbld); + + writer.WriteUInt32(GetModuleStreamSize()); + writer.WriteUInt32(GetSectionContributionStreamSize()); + writer.WriteUInt32(GetSectionMapStreamSize()); + writer.WriteUInt32(GetSourceInfoStreamSize()); + writer.WriteUInt32(GetTypeServerMapStreamSize()); + + writer.WriteUInt32(MfcTypeServerIndex); + + writer.WriteUInt32(GetOptionalDebugStreamSize()); + writer.WriteUInt32(GetECStreamSize()); + + writer.WriteUInt16((ushort) Attributes); + writer.WriteUInt16((ushort) Machine); + + writer.WriteUInt32(0); + } + + private void WriteModuleStream(IBinaryStreamWriter writer) + { + var modules = Modules; + for (int i = 0; i < modules.Count; i++) + modules[i].Write(writer); + + writer.Align(sizeof(uint)); + } + + private void WriteSectionContributionStream(IBinaryStreamWriter writer) + { + // TODO: make customizable + writer.WriteUInt32((uint) SectionContributionStreamVersion.Ver60); + + var contributions = SectionContributions; + for (int i = 0; i < contributions.Count; i++) + contributions[i].Write(writer); + + writer.Align(sizeof(uint)); + } + + private void WriteSectionMapStream(IBinaryStreamWriter writer) + { + var maps = SectionMaps; + + // Count and LogCount. + writer.WriteUInt16((ushort) maps.Count); + writer.WriteUInt16((ushort) maps.Count); + + // Entries. + for (int i = 0; i < maps.Count; i++) + maps[i].Write(writer); + + writer.Align(sizeof(uint)); + } + + private void WriteSourceInfoStream(IBinaryStreamWriter writer) + { + var sourceFiles = SourceFiles; + int totalFileCount = sourceFiles.Sum(x => x.Count); + + // Module and total file count (truncated to 16 bits) + writer.WriteUInt16((ushort) (sourceFiles.Count & 0xFFFF)); + writer.WriteUInt16((ushort) (totalFileCount & 0xFFFF)); + + // Module indices. Unsure if this is correct, but this array does not seem to be really used by the ref impl. + for (ushort i = 0; i < sourceFiles.Count; i++) + writer.WriteUInt16(i); + + // Source file counts. + for (int i = 0; i < sourceFiles.Count; i++) + writer.WriteUInt16((ushort) sourceFiles[i].Count); + + // Build up string buffer and name offset table. + using var stringBuffer = new MemoryStream(); + var stringWriter = new BinaryStreamWriter(stringBuffer); + var stringOffsets = new Dictionary(); + + for (int i = 0; i < sourceFiles.Count; i++) + { + var collection = sourceFiles[i]; + for (int j = 0; j < collection.Count; j++) + { + // If not present already, append to string buffer. + var name = collection[j]; + if (!stringOffsets.TryGetValue(name, out uint offset)) + { + offset = (uint) stringWriter.Offset; + stringOffsets[name] = offset; + stringWriter.WriteBytes(name.GetBytesUnsafe()); + stringWriter.WriteByte(0); + } + + // Write name offset + writer.WriteUInt32(offset); + } + } + + // Write string buffer. + writer.WriteBytes(stringBuffer.ToArray()); + + writer.Align(sizeof(uint)); + } + + private void WriteTypeServerMapStream(IBinaryStreamWriter writer) + { + TypeServerMapStream?.Write(writer); + writer.Align(sizeof(uint)); + } + + private void WriteOptionalDebugStream(IBinaryStreamWriter writer) + { + var extraIndices = ExtraStreamIndices; + + for (int i = 0; i < extraIndices.Count; i++) + writer.WriteUInt16(extraIndices[i]); + } + + private void WriteECStream(IBinaryStreamWriter writer) => ECStream?.Write(writer); +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStreamVersion.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStreamVersion.cs new file mode 100644 index 000000000..fe76de860 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/DbiStreamVersion.cs @@ -0,0 +1,15 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members defining all possible DBI stream format version numbers. +/// +public enum DbiStreamVersion +{ +#pragma warning disable CS1591 + VC41 = 930803, + V50 = 19960307, + V60 = 19970606, + V70 = 19990903, + V110 = 20091201 +#pragma warning restore CS1591 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptor.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptor.cs new file mode 100644 index 000000000..174ec2dab --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptor.cs @@ -0,0 +1,207 @@ +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents a reference to a single module (object file) that was linked into a program. +/// +public class ModuleDescriptor : IWritable +{ + /// + /// Gets or sets a description of the section within the final binary which contains code + /// and/or data from this module. + /// + public SectionContribution SectionContribution + { + get; + set; + } = new(); + + /// + /// Gets or sets the attributes assigned to this module descriptor. + /// + public ModuleDescriptorAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the index of the type server for this module. + /// + public ushort TypeServerIndex + { + get => (ushort) ((ushort) Attributes >> 8); + set => Attributes = (Attributes & ~ModuleDescriptorAttributes.TsmMask) | (ModuleDescriptorAttributes) (value << 8); + } + + /// + /// Gets or sets the MSF stream index of the stream that the symbols of this module. + /// + public ushort SymbolStreamIndex + { + get; + set; + } + + /// + /// Gets or sets the size of the CodeView data within the module's symbol stream. + /// + public uint SymbolDataSize + { + get; + set; + } + + /// + /// Gets or sets the size of the C11-style CodeView data within the module's symbol stream. + /// + public uint SymbolC11DataSize + { + get; + set; + } + + /// + /// Gets or sets the size of the C13-style CodeView data within the module's symbol stream. + /// + public uint SymbolC13DataSize + { + get; + set; + } + + /// + /// Gets or sets the number of source files that contributed to this module during the compilation. + /// + public ushort SourceFileCount + { + get; + set; + } + + /// + /// Gets or sets the offset in the names buffer of the primary translation unit. + /// + /// + /// For most compilers this value is set to zero. + /// + public uint SourceFileNameIndex + { + get; + set; + } + + /// + /// Gets or sets the offset in the names buffer of the PDB file. + /// + /// + /// For most modules (except the special * LINKER * module) this value is set to zero. + /// + public uint PdbFilePathNameIndex + { + get; + set; + } + + /// + /// Gets or sets the name of the module. + /// + /// + /// This is often a full path to the object file that was passed into link.exe directly, or a string in the + /// form of Import:dll_name + /// + public Utf8String? ModuleName + { + get; + set; + } + + /// + /// Gets or sets the name of the object file name. + /// + /// + /// In the case this module is linked directly passed to link.exe, this is the same as . + /// If the module comes from an archive, this is the full path to that archive. + /// + public Utf8String? ObjectFileName + { + get; + set; + } + + /// + /// Parses a single module descriptor from the provided input stream. + /// + /// The input stream. + /// THe parsed module descriptor. + public static ModuleDescriptor FromReader(ref BinaryStreamReader reader) + { + var result = new ModuleDescriptor(); + + reader.ReadUInt32(); + result.SectionContribution = SectionContribution.FromReader(ref reader); + result.Attributes = (ModuleDescriptorAttributes) reader.ReadUInt16(); + result.SymbolStreamIndex = reader.ReadUInt16(); + result.SymbolDataSize = reader.ReadUInt32(); + result.SymbolC11DataSize = reader.ReadUInt32(); + result.SymbolC13DataSize = reader.ReadUInt32(); + result.SourceFileCount = reader.ReadUInt16(); + reader.ReadUInt16(); + reader.ReadUInt32(); + result.SourceFileNameIndex = reader.ReadUInt32(); + result.PdbFilePathNameIndex = reader.ReadUInt32(); + result.ModuleName = reader.ReadUtf8String(); + result.ObjectFileName = reader.ReadUtf8String(); + reader.Align(4); + + return result; + } + + /// + public uint GetPhysicalSize() + { + return (sizeof(uint) // Unused1 + + SectionContribution.GetPhysicalSize() // SectionContribution + + sizeof(ModuleDescriptorAttributes) // Attributes + + sizeof(ushort) // SymbolStreamIndex + + sizeof(uint) // SymbolDataSize + + sizeof(uint) // SymbolC11DataSize + + sizeof(uint) // SymbolC13DataSize + + sizeof(ushort) // SourceFileCount + + sizeof(ushort) // Padding + + sizeof(uint) // Unused2 + + sizeof(uint) // SourceFileNameIndex + + sizeof(uint) // PdbFilePathNameIndex + + (uint) (ModuleName?.ByteCount ?? 0) + 1 // ModuleName + + (uint) (ObjectFileName?.ByteCount ?? 0) + 1 // ObjectFileName + ).Align(4); + } + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt32(0); + SectionContribution.Write(writer); + writer.WriteUInt16((ushort) Attributes); + writer.WriteUInt16(SymbolStreamIndex); + writer.WriteUInt32(SymbolDataSize); + writer.WriteUInt32(SymbolC11DataSize); + writer.WriteUInt32(SymbolC13DataSize); + writer.WriteUInt16(SourceFileCount); + writer.WriteUInt16(0); + writer.WriteUInt32(0); + writer.WriteUInt32(SourceFileNameIndex); + writer.WriteUInt32(PdbFilePathNameIndex); + if (ModuleName is not null) + writer.WriteBytes(ModuleName.GetBytesUnsafe()); + writer.WriteByte(0); + if (ObjectFileName is not null) + writer.WriteBytes(ObjectFileName.GetBytesUnsafe()); + writer.WriteByte(0); + writer.Align(4); + } + + /// + public override string ToString() => ModuleName ?? ObjectFileName ?? "?"; +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptorAttributes.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptorAttributes.cs new file mode 100644 index 000000000..5b6a018c0 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/ModuleDescriptorAttributes.cs @@ -0,0 +1,25 @@ +using System; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Defines all possible flags that can be assigned to a module descriptor. +/// +[Flags] +public enum ModuleDescriptorAttributes : ushort +{ + /// + /// Indicates the module has been written to since reading the PDB. + /// + Dirty = 1, + + /// + /// Indicates the module contains Edit & Continue information. + /// + EC = 2, + + /// + /// Provides a mask for the type server index that is stored within the flags. + /// + TsmMask = 0xFF00, +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContribution.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContribution.cs new file mode 100644 index 000000000..83daaae3e --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContribution.cs @@ -0,0 +1,128 @@ +using System.Text; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Describes the section in the final executable file that a particular object or module is stored at. +/// +public class SectionContribution : IWritable +{ + /// + /// The total size in bytes of a single on the disk. + /// + public const int EntrySize = + sizeof(ushort) // Section + + sizeof(ushort) // Padding1 + + sizeof(uint) // Offset + + sizeof(uint) // Size + + sizeof(uint) // Characteristics + + sizeof(ushort) // ModuleIndex + + sizeof(ushort) // Padding2 + + sizeof(uint) // DataCrc + + sizeof(uint) // RelocCrc + ; + + /// + /// Gets or sets the index of the section. + /// + public ushort Section + { + get; + set; + } + + /// + /// Gets or sets the offset within the section that this contribution starts at. + /// + public uint Offset + { + get; + set; + } + + /// + /// Gets or sets the size of the section contribution. + /// + public uint Size + { + get; + set; + } + + /// + /// Gets or sets the section flags and permissions associated to this section contribution. + /// + public SectionFlags Characteristics + { + get; + set; + } + + /// + /// Gets or sets the index of the module. + /// + public ushort ModuleIndex + { + get; + set; + } + + /// + /// Gets or sets a cyclic redundancy code that can be used to verify the data section of this contribution. + /// + public uint DataCrc + { + get; + set; + } + + /// + /// Gets or sets a cyclic redundancy code that can be used to verify the relocation section of this contribution. + /// + public uint RelocCrc + { + get; + set; + } + + /// + /// Parses a single section contribution from the provided input stream. + /// + /// The input stream. + /// The parsed section contribution. + public static SectionContribution FromReader(ref BinaryStreamReader reader) + { + var result = new SectionContribution(); + + result.Section = reader.ReadUInt16(); + reader.ReadUInt16(); + result.Offset = reader.ReadUInt32(); + result.Size = reader.ReadUInt32(); + result.Characteristics = (SectionFlags) reader.ReadUInt32(); + result.ModuleIndex = reader.ReadUInt16(); + reader.ReadUInt16(); + result.DataCrc = reader.ReadUInt32(); + result.RelocCrc = reader.ReadUInt32(); + + return result; + } + + /// + public uint GetPhysicalSize() => EntrySize; + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt16(Section); + writer.WriteUInt16(0); + writer.WriteUInt32(Offset); + writer.WriteUInt32(Size); + writer.WriteUInt32((uint) Characteristics); + writer.WriteUInt16(ModuleIndex); + writer.WriteUInt16(0); + writer.WriteUInt32(DataCrc); + writer.WriteUInt32(RelocCrc); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContributionStreamVersion.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContributionStreamVersion.cs new file mode 100644 index 000000000..48daaf3a4 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionContributionStreamVersion.cs @@ -0,0 +1,17 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members defining all valid versions of the Section Contribution sub stream's file format. +/// +public enum SectionContributionStreamVersion : uint +{ + /// + /// Indicates version 6.0 is used. + /// + Ver60 = 0xeffe0000 + 19970605, + + /// + /// Indicates version 2.0 is used. + /// + V2 = 0xeffe0000 + 20140516 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMap.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMap.cs new file mode 100644 index 000000000..bf43a7e56 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMap.cs @@ -0,0 +1,131 @@ +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents a single entry in the Section Map sub stream of the DBI stream. +/// +public class SectionMap : IWritable +{ + /// + /// The total size in bytes of a single on the disk. + /// + public const int EntrySize = + sizeof(ushort) // Attributes + + sizeof(ushort) // Ovl + + sizeof(ushort) // Group + + sizeof(ushort) // Frame + + sizeof(ushort) // SectionName + + sizeof(ushort) // ClassName + + sizeof(uint) // Offset + + sizeof(uint) // SectionLength + ; + + /// + /// Gets or sets the attributes assigned to this section map. + /// + public SectionMapAttributes Attributes + { + get; + set; + } + + /// + /// Gets or sets the logical overlay number of this section map. + /// + public ushort LogicalOverlayNumber + { + get; + set; + } + + /// + /// Gets or sets the group index into the descriptor array. + /// + public ushort Group + { + get; + set; + } + + /// + /// Gets or sets the frame index. + /// + public ushort Frame + { + get; + set; + } + + /// + /// Gets or sets the byte offset of the segment or group name in string table, or 0xFFFF if no name was assigned. + /// + public ushort SectionName + { + get; + set; + } + + /// + /// Gets or sets the byte offset of the class in the string table, or 0xFFFF if no name was assigned.. + /// + public ushort ClassName + { + get; + set; + } + + /// + /// Gets or sets the byte offset of the logical segment within physical segment. If group is set in flags, this is the offset of the group. + /// + public uint Offset + { + get; + set; + } + + /// + /// Gets or sets the number of bytes that the segment or group consists of. + /// + public uint SectionLength + { + get; + set; + } + + /// + /// Parses a single section map from the provided input stream. + /// + /// The input stream. + /// The parsed section map. + public static SectionMap FromReader(ref BinaryStreamReader reader) + { + return new SectionMap + { + Attributes = (SectionMapAttributes) reader.ReadUInt16(), + LogicalOverlayNumber = reader.ReadUInt16(), + Group = reader.ReadUInt16(), + Frame = reader.ReadUInt16(), + SectionName = reader.ReadUInt16(), + ClassName = reader.ReadUInt16(), + Offset = reader.ReadUInt32(), + SectionLength = reader.ReadUInt32() + }; + } + + /// + public uint GetPhysicalSize() => EntrySize; + + /// + public void Write(IBinaryStreamWriter writer) + { + writer.WriteUInt16((ushort) Attributes); + writer.WriteUInt16(LogicalOverlayNumber); + writer.WriteUInt16(Group); + writer.WriteUInt16(Frame); + writer.WriteUInt16(SectionName); + writer.WriteUInt16(ClassName); + writer.WriteUInt32(Offset); + writer.WriteUInt32(SectionLength); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMapAttributes.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMapAttributes.cs new file mode 100644 index 000000000..7f6449772 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SectionMapAttributes.cs @@ -0,0 +1,42 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Provides members describing all possible attributes that can be assigned to a single section map entry. +/// +public enum SectionMapAttributes : ushort +{ + /// + /// Indicates the segment is readable. + /// + Read = 1 << 0, + + /// + /// Indicates the segment is writable. + /// + Write = 1 << 1, + + /// + /// Indicates the segment is executable. + /// + Execute = 1 << 2, + + /// + /// Indicates the descriptor describes a 32-bit linear address. + /// + AddressIs32Bit = 1 << 3, + + /// + /// Indicates the frame represents a selector. + /// + IsSelector = 1 << 8, + + /// + /// Indicates the frame represents an absolute address. + /// + IsAbsoluteAddress = 1 << 9, + + /// + /// Indicates the descriptor represents a group. + /// + IsGroup = 1 << 10 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SerializedDbiStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SerializedDbiStream.cs new file mode 100644 index 000000000..962f02fc7 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SerializedDbiStream.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Implements a DBI stream that pulls its data from an input stream. +/// +public class SerializedDbiStream : DbiStream +{ + private readonly BinaryStreamReader _moduleInfoReader; + private readonly BinaryStreamReader _sectionContributionReader; + private readonly BinaryStreamReader _sectionMapReader; + private readonly BinaryStreamReader _sourceInfoReader; + private readonly BinaryStreamReader _typeServerMapReader; + private readonly BinaryStreamReader _optionalDebugHeaderReader; + private readonly BinaryStreamReader _ecReader; + + /// + /// Parses a DBI stream from an input stream reader. + /// + /// The input stream. + public SerializedDbiStream(BinaryStreamReader reader) + { + VersionSignature = reader.ReadInt32(); + VersionHeader = (DbiStreamVersion) reader.ReadUInt32(); + Age = reader.ReadUInt32(); + GlobalStreamIndex = reader.ReadUInt16(); + BuildNumber = reader.ReadUInt16(); + if (!IsNewVersionFormat) + throw new NotSupportedException("The DBI stream uses the legacy file format, which is not supported."); + + PublicStreamIndex = reader.ReadUInt16(); + PdbDllVersion = reader.ReadUInt16(); + SymbolRecordStreamIndex = reader.ReadUInt16(); + PdbDllRbld = reader.ReadUInt16(); + + uint moduleInfoSize = reader.ReadUInt32(); + uint sectionContributionSize = reader.ReadUInt32(); + uint sectionMapSize = reader.ReadUInt32(); + uint sourceInfoSize = reader.ReadUInt32(); + uint typeServerMapSize = reader.ReadUInt32(); + + MfcTypeServerIndex = reader.ReadUInt32(); + + uint optionalDebugHeaderSize = reader.ReadUInt32(); + uint ecSize = reader.ReadUInt32(); + + Attributes = (DbiAttributes) reader.ReadUInt16(); + Machine = (MachineType) reader.ReadUInt16(); + + _ = reader.ReadUInt32(); + + _moduleInfoReader = reader.ForkRelative(reader.RelativeOffset, moduleInfoSize); + reader.Offset += moduleInfoSize; + _sectionContributionReader = reader.ForkRelative(reader.RelativeOffset, sectionContributionSize); + reader.Offset += sectionContributionSize; + _sectionMapReader = reader.ForkRelative(reader.RelativeOffset, sectionMapSize); + reader.Offset += sectionMapSize; + _sourceInfoReader = reader.ForkRelative(reader.RelativeOffset, sourceInfoSize); + reader.Offset += sourceInfoSize; + _typeServerMapReader = reader.ForkRelative(reader.RelativeOffset, typeServerMapSize); + reader.Offset += typeServerMapSize; + _ecReader = reader.ForkRelative(reader.RelativeOffset, ecSize); + reader.Offset += ecSize; + _optionalDebugHeaderReader = reader.ForkRelative(reader.RelativeOffset, optionalDebugHeaderSize); + reader.Offset += optionalDebugHeaderSize; + } + + /// + protected override IList GetModules() + { + var result = new List(); + + var reader = _moduleInfoReader.Fork(); + while (reader.CanRead(1)) + result.Add(ModuleDescriptor.FromReader(ref reader)); + + return result; + } + + /// + protected override IList GetSectionContributions() + { + var result = new List(); + + var reader = _sectionContributionReader.Fork(); + var version = (SectionContributionStreamVersion) reader.ReadUInt32(); + + while (reader.CanRead(1)) + { + result.Add(SectionContribution.FromReader(ref reader)); + if (version == SectionContributionStreamVersion.V2) + reader.ReadUInt32(); + } + + return result; + } + + /// + protected override IList GetSectionMaps() + { + var reader = _sectionMapReader.Fork(); + + ushort count = reader.ReadUInt16(); + ushort logCount = reader.ReadUInt16(); + + var result = new List(count); + for (int i = 0; i < count; i++) + result.Add(SectionMap.FromReader(ref reader)); + + return result; + } + + /// + protected override ISegment? GetTypeServerMapStream() + { + var reader = _typeServerMapReader.Fork(); + return reader.Length != 0 + ? reader.ReadSegment(reader.Length) + : null; + } + + /// + protected override ISegment? GetECStream() + { + var reader = _ecReader.Fork(); + return reader.Length != 0 + ? reader.ReadSegment(reader.Length) + : null; + } + + /// + protected override IList GetSourceFiles() + { + var reader = _sourceInfoReader.Fork(); + + ushort moduleCount = reader.ReadUInt16(); + ushort sourceFileCount = reader.ReadUInt16(); + + // Read module indices. + ushort[] moduleIndices = new ushort[moduleCount]; + for (int i = 0; i < moduleCount; i++) + moduleIndices[i] = reader.ReadUInt16(); + + // Read module source file counts. + int actualFileCount = 0; + ushort[] moduleFileCounts = new ushort[moduleCount]; + for (int i = 0; i < moduleCount; i++) + { + ushort count = reader.ReadUInt16(); + moduleFileCounts[i] = count; + actualFileCount += count; + } + + // Scope on the name buffer. + var stringReaderBuffer = reader.ForkRelative((uint) (reader.RelativeOffset + actualFileCount * sizeof(uint))); + + // Construct source file lists. + var result = new List(moduleCount); + for (int i = 0; i < moduleCount; i++) + { + var files = new SourceFileCollection(moduleIndices[i]); + ushort fileCount = moduleFileCounts[i]; + + // Read all file paths for this module. + for (int j = 0; j < fileCount; j++) + { + uint nameOffset = reader.ReadUInt32(); + var nameReader = stringReaderBuffer.ForkRelative(nameOffset); + files.Add(nameReader.ReadUtf8String()); + } + + result.Add(files); + } + + return result; + } + + /// + protected override IList GetExtraStreamIndices() + { + var reader = _optionalDebugHeaderReader.Fork(); + + var result = new List((int) (reader.Length / sizeof(ushort))); + + while (reader.CanRead(sizeof(ushort))) + result.Add(reader.ReadUInt16()); + + return result; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SourceFileCollection.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SourceFileCollection.cs new file mode 100644 index 000000000..b7ef673e6 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Dbi/SourceFileCollection.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace AsmResolver.Symbols.Pdb.Metadata.Dbi; + +/// +/// Represents a collection of file paths used as input source code for a single module. +/// +public class SourceFileCollection : List +{ + /// + /// Creates a new empty source file collection. + /// + public SourceFileCollection() + { + } + + /// + /// Creates a new empty source file collection. + /// + /// The original module index for which this collection was compiled. + public SourceFileCollection(uint originalModuleIndex) + { + OriginalModuleIndex = originalModuleIndex; + } + + /// + /// Gets the original module index for which this collection was compiled (if available). + /// + /// + /// The exact purpose of this number is unclear, as this number cannot be reliably used as an index within the + /// DBI stream's module list. Use the index of this list within instead. + /// + public uint OriginalModuleIndex + { + get; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStream.cs new file mode 100644 index 000000000..eae02174c --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStream.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Represents the PDB Info Stream (also known as the PDB stream) +/// +public class InfoStream : SegmentBase +{ + /// + /// Gets the default fixed MSF stream index for the PDB Info stream. + /// + public const int StreamIndex = 1; + + private const int HeaderSize = + sizeof(InfoStreamVersion) // Version + + sizeof(uint) // Signature + + sizeof(uint) // Aage + + 16 //UniqueId + + sizeof(uint) // NameBufferSize + ; + + private IDictionary? _streamIndices; + private IList? _features; + + /// + /// Gets or sets the version of the file format of the PDB info stream. + /// + /// + /// Modern tooling only recognize the VC7.0 file format. + /// + public InfoStreamVersion Version + { + get; + set; + } = InfoStreamVersion.VC70; + + /// + /// Gets or sets the 32-bit UNIX time-stamp of the PDB file. + /// + public uint Signature + { + get; + set; + } + + /// + /// Gets or sets the number of times the PDB file has been written. + /// + public uint Age + { + get; + set; + } = 1; + + /// + /// Gets or sets the unique identifier assigned to the PDB file. + /// + public Guid UniqueId + { + get; + set; + } + + /// + /// Gets a mapping from stream names to their respective stream index within the underlying MSF file. + /// + public IDictionary StreamIndices + { + get + { + if (_streamIndices is null) + Interlocked.CompareExchange(ref _streamIndices, GetStreamIndices(), null); + return _streamIndices; + } + } + + /// + /// Gets a list of characteristics that this PDB has. + /// + public IList Features + { + get + { + if (_features is null) + Interlocked.CompareExchange(ref _features, GetFeatures(), null); + return _features; + } + } + + /// + /// Reads a single PDB info stream from the provided input stream. + /// + /// The input stream. + /// The parsed info stream. + public static InfoStream FromReader(BinaryStreamReader reader) => new SerializedInfoStream(reader); + + /// + /// Obtains the stream name to index mapping of the PDB file. + /// + /// The mapping. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IDictionary GetStreamIndices() => new Dictionary(); + + /// + /// Obtains the features of the PDB file. + /// + /// The features. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetFeatures() => new List + { + PdbFeature.VC140 + }; + + /// + public override uint GetPhysicalSize() + { + uint totalSize = HeaderSize; + + // Name buffer + foreach (var entry in StreamIndices) + totalSize += (uint) entry.Key.ByteCount + 1u; + + // Stream indices hash table. + totalSize += StreamIndices.GetPdbHashTableSize(ComputeStringHash); + + // Last NI + totalSize += sizeof(uint); + + // Feature codes. + totalSize += (uint) Features.Count * sizeof(PdbFeature); + + return totalSize; + } + + /// + public override void Write(IBinaryStreamWriter writer) + { + // Write basic info stream header. + writer.WriteUInt32((uint) Version); + writer.WriteUInt32(Signature); + writer.WriteUInt32(Age); + writer.WriteBytes(UniqueId.ToByteArray()); + + // Construct name buffer, keeping track of the offsets of every name. + using var nameBuffer = new MemoryStream(); + var nameWriter = new BinaryStreamWriter(nameBuffer); + + var stringOffsets = new Dictionary(); + foreach (var entry in StreamIndices) + { + uint offset = (uint) nameWriter.Offset; + nameWriter.WriteBytes(entry.Key.GetBytesUnsafe()); + nameWriter.WriteByte(0); + stringOffsets.Add(entry.Key, offset); + } + + writer.WriteUInt32((uint) nameBuffer.Length); + writer.WriteBytes(nameBuffer.ToArray()); + + // Write the hash table. + StreamIndices.WriteAsPdbHashTable(writer, + ComputeStringHash, + (key, value) => (stringOffsets[key], (uint) value)); + + // last NI, safe to put always zero. + writer.WriteUInt32(0); + + // Write feature codes. + var features = Features; + for (int i = 0; i < features.Count; i++) + writer.WriteUInt32((uint) features[i]); + } + + private static uint ComputeStringHash(Utf8String str) + { + // Note: The hash of a single entry is **deliberately** truncated to a 16 bit number. This is because + // the reference implementation of the name table returns a number of type HASH, which is a typedef + // for "unsigned short". If we don't do this, this will result in wrong buckets being filled in the + // hash table, and thus the serialization would fail. See NMTNI::hash() in Microsoft/microsoft-pdb. + + return (ushort) PdbHash.ComputeV1(str); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStreamVersion.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStreamVersion.cs new file mode 100644 index 000000000..d377f5ba1 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/InfoStreamVersion.cs @@ -0,0 +1,20 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Provides members defining all possible stream file format versions that PDB defines. +/// +public enum InfoStreamVersion +{ +#pragma warning disable CS1591 + VC2 = 19941610, + VC4 = 19950623, + VC41 = 19950814, + VC50 = 19960307, + VC98 = 19970604, + VC70Dep = 19990604, + VC70 = 20000404, + VC80 = 20030901, + VC110 = 20091201, + VC140 = 20140508, +#pragma warning restore CS1591 +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/PdbFeature.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/PdbFeature.cs new file mode 100644 index 000000000..16e5d3a66 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/PdbFeature.cs @@ -0,0 +1,28 @@ +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Provides members defining all possible features that a PDB can have. +/// +public enum PdbFeature : uint +{ + /// + /// Indicates no other feature flags are present, and that an IPI stream is present. + /// + VC110 = 20091201, + + /// + /// Indicates that other feature flags may be present, and that an IPI stream is present. + /// + VC140 = 20140508, + + /// + /// Indicates types can be duplicated in the TPI stream. + /// + NoTypeMerge = 0x4D544F4E, + + /// + /// Indicates the program was linked with /DEBUG:FASTLINK, and all type information is contained in the original + /// object files instead of TPI and IPI streams. + /// + MinimalDebugInfo = 0x494E494D, +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/Info/SerializedInfoStream.cs b/src/AsmResolver.Symbols.Pdb/Metadata/Info/SerializedInfoStream.cs new file mode 100644 index 000000000..f448ce67f --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/Info/SerializedInfoStream.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata.Info; + +/// +/// Implements an PDB info stream that pulls its data from an input stream. +/// +public class SerializedInfoStream : InfoStream +{ + private readonly BinaryStreamReader _reader; + private ulong _featureOffset; + + /// + /// Parses a PDB info stream from an input stream reader. + /// + /// The input stream. + public SerializedInfoStream(BinaryStreamReader reader) + { + Version = (InfoStreamVersion) reader.ReadUInt32(); + Signature = reader.ReadUInt32(); + Age = reader.ReadUInt32(); + + byte[] guidBytes = new byte[16]; + reader.ReadBytes(guidBytes, 0, guidBytes.Length); + + UniqueId = new Guid(guidBytes); + + _reader = reader; + } + + /// + protected override IDictionary GetStreamIndices() + { + var reader = _reader.Fork(); + uint length = reader.ReadUInt32(); + + var stringsReader = reader.ForkRelative(reader.RelativeOffset, length); + var hashTableReader = reader.ForkRelative(reader.RelativeOffset + length); + + var result = PdbHashTable.FromReader(ref hashTableReader, (key, value) => + { + var stringReader = stringsReader.ForkRelative(key); + var keyString = stringReader.ReadUtf8String(); + return (keyString, (int) value); + }); + + hashTableReader.ReadUInt32(); // lastNi (unused). + + _featureOffset = hashTableReader.Offset; + return result; + } + + /// + protected override IList GetFeatures() + { + // We need to read the stream name->index mapping to be able to read the features list of the PDB. + _ = StreamIndices; + + var result = new List(); + + var reader = _reader.ForkAbsolute(_featureOffset); + while (reader.CanRead(sizeof(uint))) + result.Add((PdbFeature) reader.ReadUInt32()); + + return result; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/PdbHash.cs b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHash.cs new file mode 100644 index 000000000..ed5a6a03b --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHash.cs @@ -0,0 +1,49 @@ +namespace AsmResolver.Symbols.Pdb.Metadata; + +/// +/// Provides methods for computing hash codes for a PDB hash table. +/// +public static class PdbHash +{ + /// + /// Computes the V1 hash code for a UTF-8 string. + /// + /// The string to compute the hash for. + /// The hash code. + /// + /// See PDB/include/misc.h for reference implementation. + /// + public static unsafe uint ComputeV1(Utf8String value) + { + uint result = 0; + + uint count = (uint) value.ByteCount; + + fixed (byte* ptr = value.GetBytesUnsafe()) + { + byte* p = ptr; + + while (count >= 4) + { + result ^= *(uint*) p; + count -= 4; + p += 4; + } + + if (count >= 2) + { + result ^= *(ushort*) p; + count -= 2; + p += 2; + } + + if (count == 1) + result ^= *p; + } + + result |= 0x20202020; + result ^= result >> 11; + + return result ^ (result >> 16); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Metadata/PdbHashTable.cs b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHashTable.cs new file mode 100644 index 000000000..fb58f25be --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Metadata/PdbHashTable.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Metadata; + +/// +/// Provides methods for serializing and deserializing dictionaries as PDB hash tables. +/// +public static class PdbHashTable +{ + // Reference implementation from PDB/include/map.h + // Specifically, Map::load, Map::find and Map::save. + + /// + /// Reads a single PDB hash table from the input stream and converts it into a dictionary. + /// + /// The input stream to read from. + /// A function that maps the raw key-value pairs into high level constructs. + /// The type of keys in the final dictionary. + /// The type of values in the final dictionary. + /// The reconstructed dictionary. + public static Dictionary FromReader( + ref BinaryStreamReader reader, + Func mapper) + where TKey : notnull + { + uint count = reader.ReadUInt32(); + reader.ReadUInt32(); // Capacity + + uint presentWordCount = reader.ReadUInt32(); + reader.RelativeOffset += presentWordCount * sizeof(uint); + + uint deletedWordCount = reader.ReadUInt32(); + reader.RelativeOffset += deletedWordCount * sizeof(uint); + + var result = new Dictionary((int) count); + for (int i = 0; i < count; i++) + { + (uint rawKey, uint rawValue) = (reader.ReadUInt32(), reader.ReadUInt32()); + var (key, value) = mapper(rawKey, rawValue); + result.Add(key, value); + } + + return result; + } + + /// + /// Computes the number of bytes required to store the provided dictionary as a PDB hash table. + /// + /// The dictionary to serialize. + /// A function that computes the hash code for a single key within the dictionary. + /// The type of keys in the input dictionary. + /// The type of values in the input dictionary. + /// The number of bytes required. + public static uint GetPdbHashTableSize( + this IDictionary dictionary, + Func hasher) + where TKey : notnull + { + var info = dictionary.ToPdbHashTable(hasher, null); + + return sizeof(uint) // Count + + sizeof(uint) // Capacity + + sizeof(uint) // Present bitvector word count + + info.PresentWordCount * sizeof(uint) // Present bitvector words + + sizeof(uint) // Deleted bitvector word count (== 0) + + (sizeof(uint) + sizeof(uint)) * (uint) dictionary.Count + ; + } + + /// + /// Serializes a dictionary to a PDB hash table to an output stream. + /// + /// The dictionary to serialize. + /// The output stream to write to. + /// A function that computes the hash code for a single key within the dictionary. + /// A function that maps every key-value pair to raw key-value uint32 pairs. + /// The type of keys in the input dictionary. + /// The type of values in the input dictionary. + public static void WriteAsPdbHashTable( + this IDictionary dictionary, + IBinaryStreamWriter writer, + Func hasher, + Func mapper) + where TKey : notnull + { + var hashTable = dictionary.ToPdbHashTable(hasher, mapper); + + // Write count and capacity. + writer.WriteInt32(dictionary.Count); + writer.WriteUInt32(hashTable.Capacity); + + // Determine which words in the present bitvector to write. + uint wordCount = (hashTable.Capacity + sizeof(uint) - 1) / sizeof(uint); + uint[] words = new uint[wordCount]; + hashTable.Present.CopyTo(words, 0); + while (wordCount > 0 && words[wordCount - 1] == 0) + wordCount--; + + // Write the present bitvector. + writer.WriteUInt32(wordCount); + for (int i = 0; i < wordCount; i++) + writer.WriteUInt32(words[i]); + + // Write deleted bitvector. We just always do 0 (i.e. no deleted buckets). + writer.WriteUInt32(0); + + // Write all buckets. + for (int i = 0; i < hashTable.Keys!.Length; i++) + { + if (hashTable.Present.Get(i)) + { + writer.WriteUInt32(hashTable.Keys![i]); + writer.WriteUInt32(hashTable.Values![i]); + } + } + } + + private static HashTableInfo ToPdbHashTable( + this IDictionary dictionary, + Func hasher, + Func? mapper) + where TKey : notnull + { + uint capacity = ComputeRequiredCapacity(dictionary.Count); + + // Avoid allocating buckets if we actually don't need to (e.g. if we're simply measuring the total size). + uint[]? keys; + uint[]? values; + + if (mapper is null) + { + keys = null; + values = null; + } + else + { + keys = new uint[capacity]; + values = new uint[capacity]; + } + + var present = new BitArray((int) capacity, false); + + // Fill in buckets. + foreach (var item in dictionary) + { + // Find empty bucket to place key-value pair in. + uint hash = hasher(item.Key); + uint index = hash % capacity; + while (present.Get((int) index)) + index = (index + 1) % capacity; + + // Mark bucket as used. + present.Set((int) index, true); + + // Store key-value pair. + if (mapper is not null) + { + (uint key, uint value) = mapper(item.Key, item.Value); + keys![index] = key; + values![index] = value; + } + } + + // Determine final word count in present bit vector. + uint wordCount = (capacity + sizeof(uint) - 1) / sizeof(uint); + uint[] words = new uint[wordCount]; + present.CopyTo(words, 0); + while (wordCount > 0 && words[wordCount - 1] == 0) + wordCount--; + + return new HashTableInfo(capacity, keys, values, present, wordCount); + } + + private static uint ComputeRequiredCapacity(int totalItemCount) + { + // "Simulate" adding all items to the hash table, effectively calculating the capacity of the map. + // TODO: This can probably be calculated with a single formula instead. + + uint capacity = 1; + for (int i = 0; i <= totalItemCount; i++) + { + // Reference implementation allows only 67% of the capacity to be used. + uint maxLoad = capacity * 2 / 3 + 1; + if (i >= maxLoad) + capacity = 2 * maxLoad; + } + + return capacity; + } + + private readonly struct HashTableInfo + { + public readonly uint Capacity; + public readonly uint[]? Keys; + public readonly uint[]? Values; + public readonly BitArray Present; + public readonly uint PresentWordCount; + + public HashTableInfo(uint capacity, uint[]? keys, uint[]? values, BitArray present, uint presentWordCount) + { + Capacity = capacity; + Keys = keys; + Values = values; + Present = present; + PresentWordCount = presentWordCount; + } + } + +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/Builder/FreeBlockMap.cs b/src/AsmResolver.Symbols.Pdb/Msf/Builder/FreeBlockMap.cs new file mode 100644 index 000000000..dea1712e0 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/Builder/FreeBlockMap.cs @@ -0,0 +1,38 @@ +using System.Collections; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Msf.Builder; + +/// +/// Represents a block within a MSF file that contains information on which blocks in the MSF file are free to use. +/// +public class FreeBlockMap : SegmentBase +{ + /// + /// Creates a new empty free block map. + /// + /// The size of a single block in the MSF file. + public FreeBlockMap(uint blockSize) + { + BitField = new BitArray((int) blockSize * 8, true); + } + + /// + /// Gets the bit field indicating which blocks in the MSF file are free to use. + /// + public BitArray BitField + { + get; + } + + /// + public override uint GetPhysicalSize() => (uint) (BitField.Count / 8); + + /// + public override void Write(IBinaryStreamWriter writer) + { + byte[] data = new byte[BitField.Count / 8]; + BitField.CopyTo(data, 0); + writer.WriteBytes(data); + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/Builder/IMsfFileBuilder.cs b/src/AsmResolver.Symbols.Pdb/Msf/Builder/IMsfFileBuilder.cs new file mode 100644 index 000000000..b51dcfe0b --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/Builder/IMsfFileBuilder.cs @@ -0,0 +1,14 @@ +namespace AsmResolver.Symbols.Pdb.Msf.Builder; + +/// +/// Provides members for constructing new MSF files. +/// +public interface IMsfFileBuilder +{ + /// + /// Reconstructs a new writable MSF file buffer from an instance of . + /// + /// The file to reconstruct. + /// The reconstructed buffer. + MsfFileBuffer CreateFile(MsfFile file); +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/Builder/MsfFileBuffer.cs b/src/AsmResolver.Symbols.Pdb/Msf/Builder/MsfFileBuffer.cs new file mode 100644 index 000000000..865715037 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/Builder/MsfFileBuffer.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Msf.Builder; + +/// +/// Represents a mutable buffer for building up a new MSF file. +/// +public class MsfFileBuffer : SegmentBase +{ + private readonly Dictionary _blockIndices = new(); + private readonly List _freeBlockMaps = new(2); + private readonly List _blocks; + + /// + /// Creates a new empty MSF file buffer. + /// + /// The block size to use. + public MsfFileBuffer(uint blockSize) + { + SuperBlock = new MsfSuperBlock + { + Signature = MsfSuperBlock.BigMsfSignature, + BlockSize = blockSize, + FreeBlockMapIndex = 1, + BlockCount = 3, + }; + + _blocks = new List((int) blockSize); + + InsertBlock(0, SuperBlock); + var fpm = GetOrCreateFreeBlockMap(1, out _); + InsertBlock(2, null); + + fpm.BitField[0] = false; + fpm.BitField[1] = false; + fpm.BitField[2] = false; + } + + /// + /// Gets the super block of the MSF file that is being constructed. + /// + public MsfSuperBlock SuperBlock + { + get; + } + + /// + /// Determines whether a block in the MSF file buffer is available or not. + /// + /// The index of the block. + /// true if the block is available, false otherwise. + public bool BlockIsAvailable(int blockIndex) + { + var freeBlockMap = GetOrCreateFreeBlockMap(blockIndex, out int offset); + if (offset < 3 && (blockIndex == 0 || offset > 0)) + return false; + return freeBlockMap.BitField[offset]; + } + + /// + /// Inserts a block of the provided MSF stream into the buffer. + /// + /// The MSF file index to insert the block into. + /// The stream to pull a chunk from. + /// The index of the chunk to store at the provided block index. + /// + /// Occurs when the index provided by is already in use. + /// + public void InsertBlock(int blockIndex, MsfStream stream, int chunkIndex) + { + var fpm = GetOrCreateFreeBlockMap(blockIndex, out int offset); + if (!fpm.BitField[offset]) + throw new ArgumentException($"Block {blockIndex} is already in use."); + + uint blockSize = SuperBlock.BlockSize; + var segment = new DataSourceSegment( + stream.Contents, + stream.Contents.BaseAddress + (ulong) (chunkIndex * blockSize), + (uint) (chunkIndex * blockSize), + (uint) Math.Min(stream.Contents.Length - (ulong) (chunkIndex * blockSize), blockSize)); + + InsertBlock(blockIndex, segment); + + int[] indices = GetMutableBlockIndicesForStream(stream); + indices[chunkIndex] = blockIndex; + + fpm.BitField[offset] = false; + } + + private void InsertBlock(int blockIndex, ISegment? segment) + { + // Ensure enough blocks are present in the backing-buffer. + while (_blocks.Count <= blockIndex) + _blocks.Add(null); + + // Insert block and update super block. + _blocks[blockIndex] = segment; + SuperBlock.BlockCount = (uint) _blocks.Count; + } + + private FreeBlockMap GetOrCreateFreeBlockMap(int blockIndex, out int offset) + { + int index = Math.DivRem(blockIndex, (int) SuperBlock.BlockSize, out offset); + while (_freeBlockMaps.Count <= index) + { + var freeBlockMap = new FreeBlockMap(SuperBlock.BlockSize); + _freeBlockMaps.Add(freeBlockMap); + InsertBlock(index + (int) SuperBlock.FreeBlockMapIndex, freeBlockMap); + } + + return _freeBlockMaps[index]; + } + + private int[] GetMutableBlockIndicesForStream(MsfStream stream) + { + if (!_blockIndices.TryGetValue(stream, out int[]? indices)) + { + indices = new int[stream.GetRequiredBlockCount(SuperBlock.BlockSize)]; + _blockIndices.Add(stream, indices); + } + + return indices; + } + + /// + /// Gets the allocated indices for the provided MSF stream. + /// + /// The stream. + /// The block indices. + public int[] GetBlockIndicesForStream(MsfStream stream) => (int[]) GetMutableBlockIndicesForStream(stream).Clone(); + + /// + /// Constructs a new MSF stream containing the stream directory. + /// + /// The files that the directory should list. + /// The constructed stream. + /// + /// This method does not add the stream to the buffer, nor does it update the super block. + /// + public MsfStream CreateStreamDirectory(IList streams) + { + using var contents = new MemoryStream(); + var writer = new BinaryStreamWriter(contents); + + // Stream count. + writer.WriteInt32(streams.Count); + + // Stream sizes. + for (int i = 0; i < streams.Count; i++) + writer.WriteUInt32((uint) streams[i].Contents.Length); + + // Stream indices. + for (int i = 0; i < streams.Count; i++) + { + int[] indices = GetMutableBlockIndicesForStream(streams[i]); + foreach (int index in indices) + writer.WriteInt32(index); + } + + return new MsfStream(contents.ToArray()); + } + + /// + /// Creates a new MSF stream containing the block indices of the stream directory. + /// + /// The stream directory to store the indices for. + /// The constructed stream. + /// + /// This method does not add the stream to the buffer, nor does it update the super block. + /// + public MsfStream CreateStreamDirectoryMap(MsfStream streamDirectory) + { + using var contents = new MemoryStream(); + var writer = new BinaryStreamWriter(contents); + + int[] indices = GetMutableBlockIndicesForStream(streamDirectory); + foreach (int index in indices) + writer.WriteInt32(index); + + return new MsfStream(contents.ToArray()); + } + + /// + public override uint GetPhysicalSize() => SuperBlock.BlockCount * SuperBlock.BlockSize; + + /// + public override void Write(IBinaryStreamWriter writer) + { + foreach (var block in _blocks) + { + if (block is null) + { + writer.WriteZeroes((int) SuperBlock.BlockSize); + } + else + { + block.Write(writer); + writer.Align(SuperBlock.BlockSize); + } + } + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/Builder/SequentialMsfFileBuilder.cs b/src/AsmResolver.Symbols.Pdb/Msf/Builder/SequentialMsfFileBuilder.cs new file mode 100644 index 000000000..ca02da2a9 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/Builder/SequentialMsfFileBuilder.cs @@ -0,0 +1,62 @@ +namespace AsmResolver.Symbols.Pdb.Msf.Builder; + +/// +/// Provides an implementation of the that places all blocks of every stream in sequence, +/// and effectively defragments the file system. +/// +public class SequentialMsfFileBuilder : IMsfFileBuilder +{ + /// + /// Gets the default instance of the class. + /// + public static SequentialMsfFileBuilder Instance + { + get; + } = new(); + + /// + public MsfFileBuffer CreateFile(MsfFile file) + { + var result = new MsfFileBuffer(file.BlockSize); + + // Block 0, 1, and 2 are reserved for the super block, FPM1 and FPM2. + int currentIndex = 3; + + // Add streams in sequence. + for (int i = 0; i < file.Streams.Count; i++) + AddStream(result, file.Streams[i], ref currentIndex); + + // Construct and add stream directory. + var directory = result.CreateStreamDirectory(file.Streams); + result.SuperBlock.DirectoryByteCount = (uint) directory.Contents.Length; + AddStream(result, directory, ref currentIndex); + + // Construct and add stream directory map. + var directoryMap = result.CreateStreamDirectoryMap(directory); + result.SuperBlock.DirectoryMapIndex = (uint) currentIndex; + AddStream(result, directoryMap, ref currentIndex); + + return result; + } + + private static void AddStream(MsfFileBuffer buffer, MsfStream stream, ref int currentIndex) + { + int blockCount = stream.GetRequiredBlockCount(buffer.SuperBlock.BlockSize); + + for (int j = 0; j < blockCount; j++, currentIndex++) + { + // Skip over any of the FPM indices. + switch (currentIndex % 4096) + { + case 1: + currentIndex += 2; + break; + case 2: + currentIndex++; + break; + } + + buffer.InsertBlock(currentIndex, stream, j); + } + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/MsfFile.cs b/src/AsmResolver.Symbols.Pdb/Msf/MsfFile.cs new file mode 100644 index 000000000..16aff99f9 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/MsfFile.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using AsmResolver.Collections; +using AsmResolver.IO; +using AsmResolver.Symbols.Pdb.Msf.Builder; + +namespace AsmResolver.Symbols.Pdb.Msf; + +/// +/// Models a file that is in the Microsoft Multi-Stream Format (MSF). +/// +public class MsfFile +{ + private uint _blockSize; + private IList? _streams; + + /// + /// Gets or sets the size of each block in the MSF file. + /// + /// + /// Occurs when the provided value is neither 512, 1024, 2048 or 4096. + /// + public uint BlockSize + { + get => _blockSize; + set + { + if (value is not (512 or 1024 or 2048 or 4096)) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "Block size must be either 512, 1024, 2048 or 4096 bytes."); + } + + _blockSize = value; + } + } + + /// + /// Gets a collection of streams that are present in the MSF file. + /// + public IList Streams + { + get + { + if (_streams is null) + Interlocked.CompareExchange(ref _streams, GetStreams(), null); + return _streams; + } + } + + /// + /// Creates a new empty MSF file with a default block size of 4096. + /// + public MsfFile() + : this(4096) + { + } + + /// + /// Creates a new empty MSF file with the provided block size. + /// + /// The block size to use. This must be a value of 512, 1024, 2048 or 4096. + /// Occurs when an invalid block size was provided. + public MsfFile(uint blockSize) + { + BlockSize = blockSize; + } + + /// + /// Reads an MSF file from a file on the disk. + /// + /// The path to the file to read. + /// The read MSF file. + public static MsfFile FromFile(string path) => FromFile(UncachedFileService.Instance.OpenFile(path)); + + /// + /// Reads an MSF file from an input file. + /// + /// The file to read. + /// The read MSF file. + public static MsfFile FromFile(IInputFile file) => FromReader(file.CreateReader()); + + /// + /// Interprets a byte array as an MSF file. + /// + /// The data to interpret. + /// The read MSF file. + public static MsfFile FromBytes(byte[] data) => FromReader(ByteArrayDataSource.CreateReader(data)); + + /// + /// Reads an MSF file from the provided input stream reader. + /// + /// The reader. + /// The read MSF file. + public static MsfFile FromReader(BinaryStreamReader reader) => new SerializedMsfFile(reader); + + /// + /// Obtains the list of streams stored in the MSF file. + /// + /// The streams. + /// + /// This method is called upon initialization of the property. + /// + protected virtual IList GetStreams() => new OwnedCollection(this); + + /// + /// Reconstructs and writes the MSF file to the disk. + /// + /// The path of the file to write to. + public void Write(string path) + { + using var fs = File.Create(path); + Write(fs); + } + + /// + /// Reconstructs and writes the MSF file to an output stream. + /// + /// The output stream. + public void Write(Stream stream) => Write(new BinaryStreamWriter(stream)); + + /// + /// Reconstructs and writes the MSF file to an output stream. + /// + /// The output stream. + public void Write(IBinaryStreamWriter writer) => Write(writer, SequentialMsfFileBuilder.Instance); + + /// + /// Reconstructs and writes the MSF file to an output stream. + /// + /// The output stream. + /// The builder to use for reconstructing the MSF file. + public void Write(IBinaryStreamWriter writer, IMsfFileBuilder builder) => builder.CreateFile(this).Write(writer); +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/MsfStream.cs b/src/AsmResolver.Symbols.Pdb/Msf/MsfStream.cs new file mode 100644 index 000000000..f5e871498 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/MsfStream.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AsmResolver.Collections; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Msf; + +/// +/// Represents a single stream in an Multi-Stream Format (MSF) file. +/// +public class MsfStream : IOwnedCollectionElement +{ + /// + /// Creates a new MSF stream with the provided contents. + /// + /// The raw data of the stream. + public MsfStream(byte[] data) + : this(new ByteArrayDataSource(data)) + { + } + + /// + /// Creates a new MSF stream with the provided data source as contents. + /// + /// The data source containing the raw data of the stream. + public MsfStream(IDataSource contents) + { + Contents = contents; + OriginalBlockIndices = Array.Empty(); + } + + /// + /// Initializes an MSF stream with a data source and a list of original block indices that the stream was based on. + /// + /// The data source containing the raw data of the stream. + /// The original block indices that this MSF stream was based on. + public MsfStream(IDataSource contents, IEnumerable originalBlockIndices) + { + Contents = contents; + OriginalBlockIndices = originalBlockIndices.ToArray(); + } + + /// + /// Gets the parent MSF file that this stream is embedded in. + /// + public MsfFile? Parent + { + get; + private set; + } + + MsfFile? IOwnedCollectionElement.Owner + { + get => Parent; + set => Parent = value; + } + + /// + /// Gets or sets the contents of the stream. + /// + public IDataSource Contents + { + get; + set; + } + + /// + /// Gets a collection of block indices that this stream was based of (if available). + /// + public IReadOnlyList OriginalBlockIndices + { + get; + } + + /// + /// Gets the amount of blocks that is required to store this MSF stream. + /// + /// The number of blocks. + /// Occurs when the stream is not added to a file. + public int GetRequiredBlockCount() + { + if (Parent is null) + { + throw new InvalidOperationException( + "Determining the required block count of a stream requires the stream to be added to an MSF file."); + } + + return GetRequiredBlockCount(Parent.BlockSize); + } + + /// + /// Gets the amount of blocks that is required to store this MSF stream, given the provided block size. + /// + /// The block size. + /// The number of blocks. + public int GetRequiredBlockCount(uint blockSize) + { + return (int) ((Contents.Length + blockSize - 1) / blockSize); + } + + /// + /// Creates a new binary reader that reads the raw contents of the stream. + /// + /// The constructed binary reader. + public BinaryStreamReader CreateReader() => new(Contents, Contents.BaseAddress, 0, (uint) Contents.Length); +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/MsfStreamDataSource.cs b/src/AsmResolver.Symbols.Pdb/Msf/MsfStreamDataSource.cs new file mode 100644 index 000000000..fa0e96cef --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/MsfStreamDataSource.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Msf; + +/// +/// Implements a data source for a single MSF stream that pulls data from multiple (fragmented) blocks. +/// +public class MsfStreamDataSource : IDataSource +{ + private readonly IDataSource[] _blocks; + private readonly long _blockSize; + + /// + /// Creates a new MSF stream data source. + /// + /// The length of the stream. + /// The size of an individual block. + /// The blocks + /// + /// Occurs when the total size of the provided blocks is smaller than + /// * . + /// + public MsfStreamDataSource(ulong length, uint blockSize, IEnumerable blocks) + : this(length, blockSize, blocks.Select(x => new ByteArrayDataSource(x))) + { + } + + /// + /// Creates a new MSF stream data source. + /// + /// The length of the stream. + /// The size of an individual block. + /// The blocks + /// + /// Occurs when the total size of the provided blocks is smaller than + /// * . + /// + public MsfStreamDataSource(ulong length, uint blockSize, IEnumerable blocks) + { + Length = length; + _blocks = blocks.ToArray(); + _blockSize = blockSize; + + if (length > (ulong) (_blocks.Length * blockSize)) + throw new ArgumentException("Provided length is larger than the provided blocks combined."); + } + + /// + public ulong BaseAddress => 0; + + /// + public ulong Length + { + get; + } + + /// + public byte this[ulong address] + { + get + { + if (!IsValidAddress(address)) + throw new IndexOutOfRangeException(); + + var block = GetBlockAndOffset(address, out ulong offset); + return block[block.BaseAddress + offset]; + } + } + + /// + public bool IsValidAddress(ulong address) => address < Length; + + /// + public int ReadBytes(ulong address, byte[] buffer, int index, int count) + { + int totalReadCount = 0; + int remainingBytes = Math.Min(count, (int) (Length - (address - BaseAddress))); + + while (remainingBytes > 0) + { + // Obtain current block and offset within block. + var block = GetBlockAndOffset(address, out ulong offset); + + // Read available bytes. + int readCount = Math.Min(remainingBytes, (int) _blockSize); + int actualReadCount = block.ReadBytes(block.BaseAddress + offset, buffer, index, readCount); + + // Move to the next block. + totalReadCount += actualReadCount; + address += (ulong) actualReadCount; + index += actualReadCount; + remainingBytes -= actualReadCount; + } + + return totalReadCount; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private IDataSource GetBlockAndOffset(ulong address, out ulong offset) + { + var block = _blocks[Math.DivRem((long) address, _blockSize, out long x)]; + offset = (ulong) x; + return block; + } +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/MsfSuperBlock.cs b/src/AsmResolver.Symbols.Pdb/Msf/MsfSuperBlock.cs new file mode 100644 index 000000000..cba31d1c8 --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/MsfSuperBlock.cs @@ -0,0 +1,132 @@ +using System; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Msf; + +/// +/// Represents the first block in a Multi-Stream Format (MSF) file. +/// +public sealed class MsfSuperBlock : SegmentBase +{ + // Used in MSF v2.0 + internal static readonly byte[] SmallMsfSignature = + { + 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20, 0x70, 0x72, + 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x20, 0x32, 0x2e, 0x30, + 0x30, 0x0d, 0x0a, 0x1a, 0x4a, 0x47 + }; + + // Used in MSF v7.0 + internal static readonly byte[] BigMsfSignature = + { + 0x4d, 0x69, 0x63, 0x72, 0x6f, 0x73, 0x6f, 0x66, 0x74, 0x20, 0x43, 0x2f, 0x43, 0x2b, 0x2b, 0x20, + 0x4d, 0x53, 0x46, 0x20, 0x37, 0x2e, 0x30, 0x30, 0x0d, 0x0a, 0x1a, 0x44, 0x53, 0x00, 0x00, 0x00 + }; + + /// + /// Gets or sets the magic file signature in the super block, identifying the format version of the MSF file. + /// + public byte[] Signature + { + get; + set; + } = (byte[]) BigMsfSignature.Clone(); + + /// + /// Gets or sets the size of an individual block in bytes. + /// + public uint BlockSize + { + get; + set; + } + + /// + /// Gets or sets the index of the block containing a bitfield indicating which blocks in the entire MSF file are + /// in use or not. + /// + public uint FreeBlockMapIndex + { + get; + set; + } + + /// + /// Gets or sets the total number of blocks in the MSF file. + /// + public uint BlockCount + { + get; + set; + } + + /// + /// Gets or sets the number of bytes of the stream directory in the MSF file. + /// + public uint DirectoryByteCount + { + get; + set; + } + + /// + /// Gets or sets the index of the block containing all block indices that make up the stream directory of the MSF + /// file. + /// + public uint DirectoryMapIndex + { + get; + set; + } + + /// + /// Reads a single MSF super block from the provided input stream. + /// + /// The input stream. + /// The parsed MSF super block. + /// Occurs when the super block is malformed. + public static MsfSuperBlock FromReader(ref BinaryStreamReader reader) + { + var result = new MsfSuperBlock(); + + // Check MSF header. + result.Signature = new byte[BigMsfSignature.Length]; + int count = reader.ReadBytes(result.Signature, 0, result.Signature.Length); + if (count != BigMsfSignature.Length || !ByteArrayEqualityComparer.Instance.Equals(result.Signature, BigMsfSignature)) + throw new BadImageFormatException("File does not start with a valid or supported MSF file signature."); + + result.BlockSize = reader.ReadUInt32(); + if (result.BlockSize is not (512 or 1024 or 2048 or 4096)) + throw new BadImageFormatException("Block size must be either 512, 1024, 2048 or 4096 bytes."); + + // We don't really use the free block map as we are not fully implementing the NTFS-esque file system, but we + // validate its contents regardless as a sanity check. + result.FreeBlockMapIndex = reader.ReadUInt32(); + if (result.FreeBlockMapIndex is not (1 or 2)) + throw new BadImageFormatException($"Free block map index must be 1 or 2, but was {result.FreeBlockMapIndex}."); + + result.BlockCount = reader.ReadUInt32(); + + result.DirectoryByteCount = reader.ReadUInt32(); + reader.Offset += sizeof(uint); + result.DirectoryMapIndex = reader.ReadUInt32(); + + return result; + } + + /// + public override uint GetPhysicalSize() => (uint) BigMsfSignature.Length + sizeof(uint) * 6; + + /// + public override void Write(IBinaryStreamWriter writer) + { + writer.WriteBytes(Signature); + writer.WriteUInt32(BlockSize); + writer.WriteUInt32(FreeBlockMapIndex); + writer.WriteUInt32(BlockCount); + writer.WriteUInt32(DirectoryByteCount); + writer.WriteUInt32(0); + writer.WriteUInt32(DirectoryMapIndex); + } + +} diff --git a/src/AsmResolver.Symbols.Pdb/Msf/SerializedMsfFile.cs b/src/AsmResolver.Symbols.Pdb/Msf/SerializedMsfFile.cs new file mode 100644 index 000000000..5570f188d --- /dev/null +++ b/src/AsmResolver.Symbols.Pdb/Msf/SerializedMsfFile.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using AsmResolver.Collections; +using AsmResolver.IO; + +namespace AsmResolver.Symbols.Pdb.Msf; + +/// +/// Provides an implementation for an MSF file that is read from an input file. +/// +/// +/// Currently, this model only supports version 7.0 of the file format. +/// +public class SerializedMsfFile : MsfFile +{ + private readonly BinaryStreamReader _reader; + private readonly MsfSuperBlock _originalSuperBlock; + private readonly IDataSource?[] _blocks; + + /// + /// Interprets an input stream as an MSF file version 7.0. + /// + /// The input stream. + /// Occurs when the MSF file is malformed. + public SerializedMsfFile(BinaryStreamReader reader) + { + _originalSuperBlock = MsfSuperBlock.FromReader(ref reader); + + BlockSize = _originalSuperBlock.BlockSize; + _blocks = new IDataSource?[_originalSuperBlock.BlockCount]; + _reader = reader; + } + + private IDataSource GetBlock(int index) + { + if (_blocks[index] is null) + { + // We lazily initialize all blocks by slicing the original data source of the reader. + var block = new DataSourceSlice( + _reader.DataSource, + _reader.DataSource.BaseAddress + (ulong) (index * _originalSuperBlock.BlockSize), + _originalSuperBlock.BlockSize); + + Interlocked.CompareExchange(ref _blocks[index], block, null); + } + + return _blocks[index]!; + } + + /// + protected override IList GetStreams() + { + // Get the block indices of the Stream Directory stream. + var indicesBlock = GetBlock((int) _originalSuperBlock.DirectoryMapIndex); + var indicesReader = new BinaryStreamReader(indicesBlock, indicesBlock.BaseAddress, 0, + GetBlockCount(_originalSuperBlock.DirectoryByteCount) * sizeof(uint)); + + // Access the Stream Directory stream. + var directoryStream = CreateStreamFromIndicesReader(ref indicesReader, _originalSuperBlock.DirectoryByteCount); + var directoryReader = directoryStream.CreateReader(); + + // Stream Directory format is as follows: + // - stream count: uint32 + // - stream sizes: uint32[stream count] + // - stream indices: uint32[stream count][] + + int streamCount = directoryReader.ReadInt32(); + + // Read sizes. + uint[] streamSizes = new uint[streamCount]; + for (int i = 0; i < streamCount; i++) + streamSizes[i] = directoryReader.ReadUInt32(); + + // Construct streams. + var result = new OwnedCollection(this, streamCount); + for (int i = 0; i < streamCount; i++) + { + // A size of 0xFFFFFFFF indicates the stream does not exist. + if (streamSizes[i] == uint.MaxValue) + continue; + + result.Add(CreateStreamFromIndicesReader(ref directoryReader, streamSizes[i])); + } + + return result; + } + + private MsfStream CreateStreamFromIndicesReader(ref BinaryStreamReader indicesReader, uint streamSize) + { + // Read all indices. + int[] indices = new int[GetBlockCount(streamSize)]; + for (int i = 0; i < indices.Length; i++) + indices[i] = indicesReader.ReadInt32(); + + // Transform indices to blocks. + var blocks = new IDataSource[indices.Length]; + for (int i = 0; i < blocks.Length; i++) + blocks[i] = GetBlock(indices[i]); + + // Construct stream. + var dataSource = new MsfStreamDataSource(streamSize, _originalSuperBlock.BlockSize, blocks); + return new MsfStream(dataSource, indices); + } + + private uint GetBlockCount(uint streamSize) + { + return (streamSize + _originalSuperBlock.BlockSize - 1) / _originalSuperBlock.BlockSize; + } +} diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/AnalyzerUtilities.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/AnalyzerUtilities.cs index 7bc93f796..1e565da56 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/AnalyzerUtilities.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/AnalyzerUtilities.cs @@ -1,61 +1,13 @@ -using System.Collections.Generic; using System.Linq; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; -using AsmResolver.DotNet.Signatures.Types; -namespace AsmResolver.Workspaces.DotNet.Analyzers +namespace AsmResolver.Workspaces.DotNet.Analyzers.Implementation { internal static class AnalyzerUtilities { private static readonly SignatureComparer _comparer = new (); - internal static IEnumerable FindBaseMethods(this MethodDefinition subject, - WorkspaceIndex index) - { - if (subject.DeclaringType is not { } declaringType) - yield break; - - var baseTypes = index - .GetOrCreateNode(declaringType) // Get indexed declaring type. - .ForwardRelations.GetObjects(DotNetRelations.BaseType) // Get types that this declaring type is implementing. - .ToArray(); - - foreach (var baseType in baseTypes) - { - if (baseType.Resolve() is not {} type) - continue; - - var candidates = type.Methods - .Where(m => m.Name == subject.Name) - .ToArray(); - - if (!candidates.Any()) - continue; - - GenericContext? context = null; - if (baseType is TypeSpecification {Signature: GenericInstanceTypeSignature genericSignature}) - context = new GenericContext(genericSignature, null); - - foreach (var candidate in candidates) - { - if (!candidate.IsVirtual || candidate.DeclaringType is null) - continue; - - bool isImplementation = candidate.DeclaringType.IsInterface && candidate.IsNewSlot; - bool isOverride = !candidate.DeclaringType.IsInterface && subject.IsReuseSlot; - if (!isImplementation && !isOverride) - continue; - var signature = candidate.Signature; - if (signature is not null && context.HasValue) - signature = signature.InstantiateGenericTypes(context.Value); - - if (_comparer.Equals(signature, subject.Signature)) - yield return candidate; - } - } - } - public static AssemblyDescriptor? GetAssembly(IMetadataMember member) => member switch { AssemblyDescriptor assembly => assembly, @@ -68,9 +20,8 @@ internal static class AnalyzerUtilities public static bool ContainsSubjectAssembly(this Workspace workspace, IMetadataMember member) => workspace is DotNetWorkspace dotNetWorkspace - && GetAssembly(member) is { } assembly - && dotNetWorkspace.Assemblies - .Any(a => _comparer.Equals(a, assembly)); - + && GetAssembly(member) is { } assembly + && dotNetWorkspace.Assemblies + .Any(a => _comparer.Equals(a, assembly)); } } diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeArgumentAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeArgumentAnalyzer.cs index 4cf2912b4..3a91abd7a 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeArgumentAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeArgumentAnalyzer.cs @@ -13,6 +13,7 @@ protected override void Analyze(AnalysisContext context, CustomAttributeArgument { if (context.HasAnalyzers(typeof(TypeSignature))) { + context.ScheduleForAnalysis(subject.ArgumentType); for (int i = 0; i < subject.Elements.Count; i++) { var element = subject.Elements[i]; diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeNamedArgumentAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeNamedArgumentAnalyzer.cs index 08e67e221..15c73e38d 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeNamedArgumentAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/CustomAttributeNamedArgumentAnalyzer.cs @@ -11,6 +11,11 @@ public class CustomAttributeNamedArgumentAnalyzer : ObjectAnalyzer protected override void Analyze(AnalysisContext context, CustomAttributeNamedArgument subject) { + if (context.HasAnalyzers(typeof(TypeSignature))) + { + context.ScheduleForAnalysis(subject.ArgumentType); + } + if (context.HasAnalyzers(typeof(CustomAttributeArgument))) { context.ScheduleForAnalysis(subject.Argument); diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/HasGenericParameterAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/HasGenericParameterAnalyzer.cs index de6f0e03b..f6f276c6b 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/HasGenericParameterAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/HasGenericParameterAnalyzer.cs @@ -12,8 +12,8 @@ public class HasGenericParameterAnalyzer : ObjectAnalyzer protected override void Analyze(AnalysisContext context, IHasGenericParameters subject) { bool hasGenericParameterAnalyzer = context.HasAnalyzers(typeof(GenericParameter)); - bool hasGenericParameterConstraintAnalyzer = context.HasAnalyzers(typeof(GenericParameter)); - + bool hasGenericParameterConstraintAnalyzer = context.HasAnalyzers(typeof(GenericParameterConstraint)); + for (int i = 0; i < subject.GenericParameters.Count; i++) { var genericParameter = subject.GenericParameters[i]; diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/MethodAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/MethodAnalyzer.cs index 8e27f6de6..e3a687317 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/MethodAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/MethodAnalyzer.cs @@ -7,30 +7,12 @@ namespace AsmResolver.Workspaces.DotNet.Analyzers.Definition { /// - /// Analyzes a for implicit base definitions, such as abstract methods in - /// base classes or methods in implemented interfaces. + /// Provides a default implementation for an analyzer. /// public class MethodAnalyzer : ObjectAnalyzer { /// protected override void Analyze(AnalysisContext context, MethodDefinition subject) - { - ScheduleMembersForAnalysis(context, subject); - - if (!subject.IsVirtual) - return; - - var index = context.Workspace.Index; - var node = index.GetOrCreateNode(subject); - - foreach (var baseMethod in subject.FindBaseMethods(context.Workspace.Index)) - { - var candidateNode = index.GetOrCreateNode(baseMethod); - node.ForwardRelations.Add(DotNetRelations.ImplementationMethod, candidateNode); - } - } - - private static void ScheduleMembersForAnalysis(AnalysisContext context, MethodDefinition subject) { // Schedule parameters for analysis. if (context.HasAnalyzers(typeof(ParameterDefinition))) @@ -50,7 +32,6 @@ private static void ScheduleMembersForAnalysis(AnalysisContext context, MethodDe { context.ScheduleForAnalysis(subject.CilMethodBody); } - } } } diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationMethodAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationMethodAnalyzer.cs new file mode 100644 index 000000000..5e1cec1bf --- /dev/null +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationMethodAnalyzer.cs @@ -0,0 +1,31 @@ +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Collections; +using AsmResolver.DotNet.Signatures; + +namespace AsmResolver.Workspaces.DotNet.Analyzers.Implementation +{ + /// + /// Analyzes a for implicit base definitions, such as abstract methods in + /// base classes or methods in implemented interfaces. + /// + public class ImplementationMethodAnalyzer : ObjectAnalyzer + { + /// + protected override void Analyze(AnalysisContext context, MethodDefinition subject) + { + if (!subject.IsVirtual) + return; + + var index = context.Workspace.Index; + var node = index.GetOrCreateNode(subject); + + foreach (var baseMethod in subject.FindBaseMethods(context.Workspace.Index)) + { + var candidateNode = index.GetOrCreateNode(baseMethod); + node.ForwardRelations.Add(DotNetRelations.ImplementationMethod, candidateNode); + } + } + } +} diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/SemanticsImplementationAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationSemanticsAnalyzer.cs similarity index 94% rename from src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/SemanticsImplementationAnalyzer.cs rename to src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationSemanticsAnalyzer.cs index 544e6138a..1f64b23a3 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Definition/SemanticsImplementationAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationSemanticsAnalyzer.cs @@ -1,13 +1,13 @@ using AsmResolver.DotNet; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; -namespace AsmResolver.Workspaces.DotNet.Analyzers.Definition +namespace AsmResolver.Workspaces.DotNet.Analyzers.Implementation { /// /// Analyzes a for implicit base definitions, such as abstract events or properties in /// base classes or members in implemented interfaces. /// - public class SemanticsImplementationAnalyzer : ObjectAnalyzer + public class ImplementationSemanticsAnalyzer : ObjectAnalyzer { /// protected override void Analyze(AnalysisContext context, IHasSemantics subject) diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationUtilities.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationUtilities.cs new file mode 100644 index 000000000..aba3c4d1c --- /dev/null +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Implementation/ImplementationUtilities.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.DotNet.Signatures.Types; + +namespace AsmResolver.Workspaces.DotNet.Analyzers.Implementation +{ + internal static class ImplementationUtilities + { + private static readonly SignatureComparer _comparer = new (); + + internal static IEnumerable FindBaseMethods(this MethodDefinition subject, + WorkspaceIndex index) + { + if (subject.DeclaringType is not { } declaringType) + yield break; + + var declaringTypeNode = index.GetOrCreateNode(declaringType); + var baseTypes = GetBaseTypes(declaringTypeNode); + + foreach (var baseType in baseTypes) + { + if (baseType.Resolve() is not {} type) + continue; + + var candidates = type.Methods + .Where(m => m.Name == subject.Name) + .ToArray(); + + if (!candidates.Any()) + continue; + + GenericContext? context = null; + if (baseType is TypeSpecification {Signature: GenericInstanceTypeSignature genericSignature}) + context = new GenericContext(genericSignature, null); + + foreach (var candidate in candidates) + { + if (!candidate.IsVirtual || candidate.DeclaringType is null) + continue; + + bool isImplementation = candidate.DeclaringType.IsInterface && candidate.IsNewSlot; + bool isOverride = !candidate.DeclaringType.IsInterface && subject.IsReuseSlot; + if (!isImplementation && !isOverride) + continue; + var signature = candidate.Signature; + if (signature is not null && context.HasValue) + signature = signature.InstantiateGenericTypes(context.Value); + + if (_comparer.Equals(signature, subject.Signature)) + yield return candidate; + } + } + } + + internal static IEnumerable GetBaseTypes(WorkspaceIndexNode baseNode) + { + var visited = new HashSet(); + var agenda = new Queue(); + agenda.Enqueue(baseNode); + while (agenda.Count != 0) + { + var node = agenda.Dequeue(); + if (!visited.Add(node)) + continue; + var baseTypeNodes = node.ForwardRelations.GetNodes(DotNetRelations.BaseType); + foreach (var baseTypeNode in baseTypeNodes) + { + var baseType = (ITypeDefOrRef)baseTypeNode.Subject; + agenda.Enqueue(baseTypeNode); + var baseTypeDefinitions = baseTypeNode.BackwardRelations.GetNodes(DotNetRelations.ReferenceType); + foreach (var baseTypeDefinition in baseTypeDefinitions) + agenda.Enqueue(baseTypeDefinition); + yield return baseType; + } + } + } + } +} diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/AssemblyReferenceAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/AssemblyReferenceAnalyzer.cs index efc5c46a9..7325413ad 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/AssemblyReferenceAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/AssemblyReferenceAnalyzer.cs @@ -1,4 +1,5 @@ using AsmResolver.DotNet; +using AsmResolver.Workspaces.DotNet.Analyzers.Implementation; namespace AsmResolver.Workspaces.DotNet.Analyzers.Reference { diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/ExportedTypeAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/ExportedTypeAnalyzer.cs index 71cec661a..776e0a92c 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/ExportedTypeAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/ExportedTypeAnalyzer.cs @@ -1,6 +1,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using System.Linq; +using AsmResolver.Workspaces.DotNet.Analyzers.Implementation; namespace AsmResolver.Workspaces.DotNet.Analyzers.Reference { diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/MemberReferenceAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/MemberReferenceAnalyzer.cs index 7a011b54d..ff246766f 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/MemberReferenceAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/MemberReferenceAnalyzer.cs @@ -1,6 +1,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using System.Linq; +using AsmResolver.Workspaces.DotNet.Analyzers.Implementation; namespace AsmResolver.Workspaces.DotNet.Analyzers.Reference { diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeReferenceAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeReferenceAnalyzer.cs index 99bba5505..177ff5bb9 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeReferenceAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeReferenceAnalyzer.cs @@ -1,6 +1,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using System.Linq; +using AsmResolver.Workspaces.DotNet.Analyzers.Implementation; namespace AsmResolver.Workspaces.DotNet.Analyzers.Reference { diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeSpecificationAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeSpecificationAnalyzer.cs index fdd8a06a4..ce7bbf030 100644 --- a/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeSpecificationAnalyzer.cs +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Reference/TypeSpecificationAnalyzer.cs @@ -2,6 +2,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.Workspaces.DotNet.Analyzers.Implementation; namespace AsmResolver.Workspaces.DotNet.Analyzers.Reference { diff --git a/src/AsmResolver.Workspaces.DotNet/Analyzers/Signature/GenericInstanceMethodSignatureAnalyzer.cs b/src/AsmResolver.Workspaces.DotNet/Analyzers/Signature/GenericInstanceMethodSignatureAnalyzer.cs new file mode 100644 index 000000000..4cde98d24 --- /dev/null +++ b/src/AsmResolver.Workspaces.DotNet/Analyzers/Signature/GenericInstanceMethodSignatureAnalyzer.cs @@ -0,0 +1,23 @@ +using AsmResolver.DotNet.Signatures; +using AsmResolver.DotNet.Signatures.Types; + +namespace AsmResolver.Workspaces.DotNet.Analyzers.Signature +{ + /// + /// Provides a default implementation for an analyzer. + /// + public class GenericInstanceMethodSignatureAnalyzer : ObjectAnalyzer + { + /// + protected override void Analyze(AnalysisContext context, GenericInstanceMethodSignature subject) + { + if (context.HasAnalyzers(typeof(TypeSignature))) + { + for (var i = 0; i < subject.TypeArguments.Count; i++) + { + context.ScheduleForAnalysis(subject.TypeArguments[i]); + } + } + } + } +} diff --git a/src/AsmResolver.Workspaces.DotNet/DotNetWorkspace.cs b/src/AsmResolver.Workspaces.DotNet/DotNetWorkspace.cs index 6706b0113..8098894ce 100644 --- a/src/AsmResolver.Workspaces.DotNet/DotNetWorkspace.cs +++ b/src/AsmResolver.Workspaces.DotNet/DotNetWorkspace.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using AsmResolver.DotNet; using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Collections; @@ -8,6 +8,7 @@ using AsmResolver.Workspaces.DotNet.Analyzers.Definition; using AsmResolver.Workspaces.DotNet.Analyzers.Reference; using AsmResolver.Workspaces.DotNet.Analyzers.Signature; +using AsmResolver.Workspaces.DotNet.Profiles; namespace AsmResolver.Workspaces.DotNet { @@ -21,38 +22,8 @@ public class DotNetWorkspace : Workspace /// public DotNetWorkspace() { - Analyzers.Register(typeof(AssemblyDefinition), new AssemblyAnalyzer()); - Analyzers.Register(typeof(ModuleDefinition), new ModuleAnalyzer()); - Analyzers.Register(typeof(TypeDefinition), new TypeAnalyzer()); - Analyzers.Register(typeof(MethodDefinition), new MethodAnalyzer()); - Analyzers.Register(typeof(MethodImplementation), new MethodImplementationAnalyzer()); - Analyzers.Register(typeof(IHasSemantics), new SemanticsImplementationAnalyzer()); - Analyzers.Register(typeof(TypeReference), new TypeReferenceAnalyzer()); - Analyzers.Register(typeof(MemberReference), new MemberReferenceAnalyzer()); - Analyzers.Register(typeof(IHasCustomAttribute), new HasCustomAttributeAnalyzer()); - Analyzers.Register(typeof(CustomAttribute), new CustomAttributeAnalyzer()); - Analyzers.Register(typeof(TypeSignature), new TypeSignatureAnalyzer()); - Analyzers.Register(typeof(MethodSignatureBase), new MethodSignatureBaseAnalyzer()); - Analyzers.Register(typeof(FieldSignature), new FieldSignatureAnalyzer()); - Analyzers.Register(typeof(FieldDefinition), new FieldAnalyzer()); - Analyzers.Register(typeof(PropertyDefinition), new PropertyAnalyzer()); - Analyzers.Register(typeof(EventDefinition), new EventAnalyzer()); - Analyzers.Register(typeof(IHasGenericParameters), new HasGenericParameterAnalyzer()); - Analyzers.Register(typeof(LocalVariablesSignature), new LocalVariablesSignatureAnalyzer()); - Analyzers.Register(typeof(IGenericArgumentsProvider), new GenericArgumentAnalyzer()); - Analyzers.Register(typeof(CilMethodBody), new CilMethodBodyAnalyzer()); - Analyzers.Register(typeof(CustomAttributeArgument), new CustomAttributeArgumentAnalyzer()); - Analyzers.Register(typeof(CustomAttributeNamedArgument), new CustomAttributeNamedArgumentAnalyzer()); - Analyzers.Register(typeof(AssemblyReference), new AssemblyReferenceAnalyzer()); - Analyzers.Register(typeof(TypeSpecification), new TypeSpecificationAnalyzer()); - Analyzers.Register(typeof(ExportedType), new ExportedTypeAnalyzer()); - Analyzers.Register(typeof(IHasSecurityDeclaration), new HasSecurityDeclarationAnalyzer()); - Analyzers.Register(typeof(SecurityDeclaration), new SecurityDeclarationAnalyzer()); - Analyzers.Register(typeof(CilExceptionHandler), new ExceptionHandlerAnalyzer()); - Analyzers.Register(typeof(CilLocalVariable), new CilLocalVariableAnalyzer()); - Analyzers.Register(typeof(StandAloneSignature), new StandaloneSignatureAnalyzer()); - Analyzers.Register(typeof(InterfaceImplementation), new InterfaceImplementationAnalyzer()); - Analyzers.Register(typeof(MethodSpecification), new MethodSpecificationAnalyzer()); + Profiles.Add(new DotNetTraversalProfile()); + Profiles.Add(new DotNetImplementationProfile()); } /// diff --git a/src/AsmResolver.Workspaces.DotNet/Profiles/DotNetImplementationProfile.cs b/src/AsmResolver.Workspaces.DotNet/Profiles/DotNetImplementationProfile.cs new file mode 100644 index 000000000..709e466bd --- /dev/null +++ b/src/AsmResolver.Workspaces.DotNet/Profiles/DotNetImplementationProfile.cs @@ -0,0 +1,21 @@ +using AsmResolver.DotNet; +using AsmResolver.Workspaces.DotNet.Analyzers.Implementation; + +namespace AsmResolver.Workspaces.DotNet.Profiles +{ + /// + /// Provides a default implementation of profile to connect all abstract, virtual and interface members. + /// + public class DotNetImplementationProfile : WorksapceProfile + { + /// + /// Creates a new instance of the class. + /// + public DotNetImplementationProfile() + { + Analyzers.Register(typeof(MethodDefinition), new ImplementationMethodAnalyzer()); + Analyzers.Register(typeof(IHasSemantics), new ImplementationSemanticsAnalyzer()); + + } + } +} diff --git a/src/AsmResolver.Workspaces.DotNet/Profiles/DotNetTraversalProfile.cs b/src/AsmResolver.Workspaces.DotNet/Profiles/DotNetTraversalProfile.cs new file mode 100644 index 000000000..6b034cdcf --- /dev/null +++ b/src/AsmResolver.Workspaces.DotNet/Profiles/DotNetTraversalProfile.cs @@ -0,0 +1,55 @@ +using AsmResolver.DotNet; +using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Signatures; +using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.Workspaces.DotNet.Analyzers.Definition; +using AsmResolver.Workspaces.DotNet.Analyzers.Reference; +using AsmResolver.Workspaces.DotNet.Analyzers.Signature; + +namespace AsmResolver.Workspaces.DotNet.Profiles +{ + /// + /// Provides a default implementation of profile to traverse all .net members. + /// + public class DotNetTraversalProfile : WorksapceProfile + { + /// + /// Creates a new instance of the class. + /// + public DotNetTraversalProfile() + { + Analyzers.Register(typeof(AssemblyDefinition), new AssemblyAnalyzer()); + Analyzers.Register(typeof(ModuleDefinition), new ModuleAnalyzer()); + Analyzers.Register(typeof(TypeDefinition), new TypeAnalyzer()); + Analyzers.Register(typeof(MethodDefinition), new MethodAnalyzer()); + Analyzers.Register(typeof(MethodImplementation), new MethodImplementationAnalyzer()); + Analyzers.Register(typeof(TypeReference), new TypeReferenceAnalyzer()); + Analyzers.Register(typeof(MemberReference), new MemberReferenceAnalyzer()); + Analyzers.Register(typeof(IHasCustomAttribute), new HasCustomAttributeAnalyzer()); + Analyzers.Register(typeof(CustomAttribute), new CustomAttributeAnalyzer()); + Analyzers.Register(typeof(TypeSignature), new TypeSignatureAnalyzer()); + Analyzers.Register(typeof(MethodSignatureBase), new MethodSignatureBaseAnalyzer()); + Analyzers.Register(typeof(FieldSignature), new FieldSignatureAnalyzer()); + Analyzers.Register(typeof(FieldDefinition), new FieldAnalyzer()); + Analyzers.Register(typeof(PropertyDefinition), new PropertyAnalyzer()); + Analyzers.Register(typeof(EventDefinition), new EventAnalyzer()); + Analyzers.Register(typeof(IHasGenericParameters), new HasGenericParameterAnalyzer()); + Analyzers.Register(typeof(LocalVariablesSignature), new LocalVariablesSignatureAnalyzer()); + Analyzers.Register(typeof(IGenericArgumentsProvider), new GenericArgumentAnalyzer()); + Analyzers.Register(typeof(CilMethodBody), new CilMethodBodyAnalyzer()); + Analyzers.Register(typeof(CustomAttributeArgument), new CustomAttributeArgumentAnalyzer()); + Analyzers.Register(typeof(CustomAttributeNamedArgument), new CustomAttributeNamedArgumentAnalyzer()); + Analyzers.Register(typeof(AssemblyReference), new AssemblyReferenceAnalyzer()); + Analyzers.Register(typeof(TypeSpecification), new TypeSpecificationAnalyzer()); + Analyzers.Register(typeof(ExportedType), new ExportedTypeAnalyzer()); + Analyzers.Register(typeof(IHasSecurityDeclaration), new HasSecurityDeclarationAnalyzer()); + Analyzers.Register(typeof(SecurityDeclaration), new SecurityDeclarationAnalyzer()); + Analyzers.Register(typeof(CilExceptionHandler), new ExceptionHandlerAnalyzer()); + Analyzers.Register(typeof(CilLocalVariable), new CilLocalVariableAnalyzer()); + Analyzers.Register(typeof(StandAloneSignature), new StandaloneSignatureAnalyzer()); + Analyzers.Register(typeof(InterfaceImplementation), new InterfaceImplementationAnalyzer()); + Analyzers.Register(typeof(MethodSpecification), new MethodSpecificationAnalyzer()); + Analyzers.Register(typeof(GenericInstanceMethodSignature), new GenericInstanceMethodSignatureAnalyzer()); + } + } +} diff --git a/src/AsmResolver.Workspaces/AnalysisContext.cs b/src/AsmResolver.Workspaces/AnalysisContext.cs index 754e034cd..d14e83bcf 100644 --- a/src/AsmResolver.Workspaces/AnalysisContext.cs +++ b/src/AsmResolver.Workspaces/AnalysisContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace AsmResolver.Workspaces { @@ -49,7 +50,7 @@ public ISet TraversedObjects /// true if there exists at least one analyzer that can analyze objects of the provided type, /// false otherwise. /// - public bool HasAnalyzers(Type type) => Workspace.Analyzers.HasAnalyzers(type); + public bool HasAnalyzers(Type type) => Workspace.Profiles.Any(profile => profile.Analyzers.HasAnalyzers(type)); /// /// Schedules the provided object if it was not scheduled before. diff --git a/src/AsmResolver.Workspaces/Workspace.cs b/src/AsmResolver.Workspaces/Workspace.cs index 723ccbe98..934fbbf75 100644 --- a/src/AsmResolver.Workspaces/Workspace.cs +++ b/src/AsmResolver.Workspaces/Workspace.cs @@ -1,4 +1,6 @@ -namespace AsmResolver.Workspaces +using System.Collections.Generic; + +namespace AsmResolver.Workspaces { /// /// Provides a base mechanism for indexing assemblies and their components. @@ -6,9 +8,9 @@ public abstract class Workspace { /// - /// Gets a collection of object analyzers that are used in this workspace. + /// Gets a ordered list of profiles for workspace analyzing. /// - public AnalyzerRepository Analyzers + public List Profiles { get; } = new(); @@ -27,16 +29,32 @@ public WorkspaceIndex Index /// The analysis context. protected void Analyze(AnalysisContext context) { - while (context.Agenda.Count > 0) + var traversedObjects = new HashSet(context.Agenda); + foreach (var profile in Profiles) { - var nextSubject = context.Agenda.Dequeue(); - var analyzers = Analyzers.GetAnalyzers(nextSubject.GetType()); - foreach (var analyzer in analyzers) + context.Agenda.Clear(); + foreach (object agenda in traversedObjects) + context.Agenda.Enqueue(agenda); + context.TraversedObjects.Clear(); + + while (context.Agenda.Count > 0) { - if (analyzer.CanAnalyze(context, nextSubject)) - analyzer.Analyze(context, nextSubject); + object nextSubject = context.Agenda.Dequeue(); + var analyzers = profile.Analyzers.GetAnalyzers(nextSubject.GetType()); + foreach (var analyzer in analyzers) + { + if (analyzer.CanAnalyze(context, nextSubject)) + analyzer.Analyze(context, nextSubject); + } } + + foreach (object traversedObject in context.TraversedObjects) + traversedObjects.Add(traversedObject); } + + context.TraversedObjects.Clear(); + foreach (object traversedObject in traversedObjects) + context.TraversedObjects.Add(traversedObject); } } } diff --git a/src/AsmResolver.Workspaces/WorkspaceProfile.cs b/src/AsmResolver.Workspaces/WorkspaceProfile.cs new file mode 100644 index 000000000..ede46922b --- /dev/null +++ b/src/AsmResolver.Workspaces/WorkspaceProfile.cs @@ -0,0 +1,17 @@ +namespace AsmResolver.Workspaces +{ + /// + /// Provides a base mechanism for storing and scheduling analyzers. + /// + public class WorksapceProfile + { + /// + /// Gets a collection of object analyzers. + /// + public AnalyzerRepository Analyzers + { + get; + } = new(); + + } +} diff --git a/src/AsmResolver/AsmResolver.csproj b/src/AsmResolver/AsmResolver.csproj index 3e731b8aa..07dbf2281 100644 --- a/src/AsmResolver/AsmResolver.csproj +++ b/src/AsmResolver/AsmResolver.csproj @@ -1,13 +1,14 @@ - + AsmResolver The base library for the AsmResolver executable file inspection toolsuite. - exe pe dotnet cil inspection manipulation assembly disassembly - netstandard2.0 + exe pe dotnet cil inspection manipulation assembly disassembly true 1701;1702;NU5105 enable + net6.0;netcoreapp3.1;netstandard2.0 + true diff --git a/src/AsmResolver/ByteArrayEqualityComparer.cs b/src/AsmResolver/ByteArrayEqualityComparer.cs index 82f1bba10..277226f65 100644 --- a/src/AsmResolver/ByteArrayEqualityComparer.cs +++ b/src/AsmResolver/ByteArrayEqualityComparer.cs @@ -84,8 +84,15 @@ public int GetHashCode(byte[] obj) } /// - public int Compare(byte[] x, byte[] y) + public int Compare(byte[]? x, byte[]? y) { + if (ReferenceEquals(x, y)) + return 0; + if (x is null) + return -1; + if (y is null) + return 1; + int length = Math.Min(x.Length, y.Length); for (int i = 0; i < length; i++) { diff --git a/src/AsmResolver/Collections/BitList.cs b/src/AsmResolver/Collections/BitList.cs new file mode 100644 index 000000000..c4224cd04 --- /dev/null +++ b/src/AsmResolver/Collections/BitList.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace AsmResolver.Collections +{ + /// + /// Represents a bit vector that can be resized dynamically. + /// + public class BitList : IList + { + private const int WordSize = sizeof(int) * 8; + private uint[] _words; + private int _version; + + /// + /// Creates a new bit list. + /// + public BitList() + { + _words = new uint[1]; + } + + /// + /// Creates a new bit list. + /// + /// The initial number of bits that the buffer should at least be able to store. + public BitList(int capacity) + { + _words = new uint[((uint) capacity).Align(WordSize)]; + } + + /// + public int Count + { + get; + private set; + } + + /// + public bool IsReadOnly => false; + + /// + public bool this[int index] + { + get + { + if (index >= Count) + throw new IndexOutOfRangeException(); + + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + return (_words[wordIndex] >> bitIndex & 1) != 0; + } + set + { + if (index >= Count) + throw new IndexOutOfRangeException(); + + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + _words[wordIndex] = (_words[wordIndex] & ~(1u << bitIndex)) | (value ? 1u << bitIndex : 0u); + _version++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int wordIndex, int bitIndex) SplitWordBitIndex(int index) + { + int wordIndex = Math.DivRem(index, WordSize, out int offset); + return (wordIndex, offset); + } + + /// + public void Add(bool item) + { + EnsureCapacity(Count + 1); + Count++; + this[Count - 1] = item; + _version++; + } + + /// + public void Clear() => Count = 0; + + /// + public bool Contains(bool item) => IndexOf(item) != -1; + + /// + public void CopyTo(bool[] array, int arrayIndex) + { + for (int i = 0; i < Count; i++) + array[arrayIndex + i] = this[i]; + } + + /// + public bool Remove(bool item) + { + int index = IndexOf(item); + if (index == -1) + return false; + + RemoveAt(index); + return true; + } + + /// + public int IndexOf(bool item) + { + for (int i = 0; i < Count; i++) + { + (int wordIndex, int bitIndex) = SplitWordBitIndex(i); + if ((_words[wordIndex] >> bitIndex & 1) != 0 == item) + return i; + } + + return -1; + } + + /// + public void Insert(int index, bool item) + { + if (index > Count) + throw new IndexOutOfRangeException(); + + EnsureCapacity(Count++); + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + + uint carry = _words[wordIndex] & (1u << (WordSize - 1)); + + // Insert bit into current word. + uint lowerMask = (1u << bitIndex) - 1; + uint upperMask = ~lowerMask; + _words[wordIndex] = (_words[wordIndex] & upperMask) << 1 // Shift left-side of the bit index by one + | (item ? 1u << bitIndex : 0u) // Insert bit. + | (_words[wordIndex] & lowerMask); // Keep right-side of the bit. + + for (int i = wordIndex + 1; i < _words.Length; i++) + { + uint nextCarry = _words[i] & (1u << (WordSize - 1)); + _words[i] = (_words[i] << 1) | (carry >> (WordSize - 1)); + carry = nextCarry; + } + + _version++; + } + + /// + public void RemoveAt(int index) + { + Count--; + (int wordIndex, int bitIndex) = SplitWordBitIndex(index); + + // Note we check both word count and actual bit count. Words in the buffer might contain garbage data for + // every bit index i >= Count. Also, there might be exactly enough words allocated for Count bits, i.e. + // there might not be a "next" word. + uint borrow = wordIndex + 1 < _words.Length && ((uint) index).Align(WordSize) < Count + ? _words[wordIndex + 1] & 1 + : 0; + + uint lowerMask = (1u << bitIndex) - 1; + uint upperMask = ~((1u << (bitIndex + 1)) - 1); + _words[wordIndex] = (_words[wordIndex] & upperMask) >> 1 // Shift left-side of the bit index by one + | (_words[wordIndex] & lowerMask) // Keep right-side of the bit. + | borrow << (WordSize - 1); // Copy first bit of next word into last bit of current. + + for (int i = wordIndex + 1; i < _words.Length; i++) + { + uint nextBorrow = i + 1 < _words.Length && ((uint) index).Align(WordSize) < Count + ? _words[i + 1] & 1 + : 0; + + _words[i] = (_words[i] >> 1) | (borrow << (WordSize - 1)); + borrow = nextBorrow; + } + + _version++; + } + + /// + /// Ensures the provided number of bits can be stored in the bit list. + /// + /// The number of bits to store in the list. + public void EnsureCapacity(int capacity) + { + if (capacity < WordSize * _words.Length) + return; + + int newWordCount = (int) (((uint) capacity).Align(WordSize) / 8); + Array.Resize(ref _words, newWordCount); + } + + /// + /// Returns an enumerator for all bits in the bit vector. + /// + /// The enumerator. + public Enumerator GetEnumerator() => new(this); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Represents an enumerator that iterates over all bits in a bit list. + /// + public struct Enumerator : IEnumerator + { + private readonly BitList _list; + private readonly int _version; + private int _index = -1; + + /// + /// Creates a new bit enumerator. + /// + /// The list to enumerate. + public Enumerator(BitList list) + { + _version = list._version; + _list = list; + } + + /// + public bool MoveNext() + { + if (_version != _list._version) + throw new InvalidOperationException("Collection was modified."); + + if (_index >= _list.Count) + return false; + + _index++; + return true; + } + + /// + public void Reset() => _index = -1; + + /// + public bool Current => _list[_index]; + + /// + object IEnumerator.Current => Current; + + /// + public void Dispose() + { + } + } + } +} diff --git a/src/AsmResolver/Collections/LazyList.cs b/src/AsmResolver/Collections/LazyList.cs index 90b584a75..0dd0cc0ce 100644 --- a/src/AsmResolver/Collections/LazyList.cs +++ b/src/AsmResolver/Collections/LazyList.cs @@ -12,7 +12,24 @@ namespace AsmResolver.Collections [DebuggerDisplay("Count = {" + nameof(Count) + "}")] public abstract class LazyList : IList { - private readonly List _items = new(); + private readonly List _items; + + /// + /// Creates a new, empty, uninitialized list. + /// + public LazyList() + { + _items = new List(); + } + + /// + /// Creates a new, empty, uninitialized list. + /// + /// The initial number of elements the list can store. + public LazyList(int capacity) + { + _items = new List(capacity); + } /// public TItem this[int index] @@ -39,6 +56,15 @@ public virtual int Count } } + /// + /// Gets or sets the total number of elements the list can contain before it has to resize its internal buffer. + /// + public int Capacity + { + get => _items.Capacity; + set => _items.Capacity = value; + } + /// public bool IsReadOnly => false; diff --git a/src/AsmResolver/Collections/OneToManyRelation.cs b/src/AsmResolver/Collections/OneToManyRelation.cs index 5a513868b..30df87146 100644 --- a/src/AsmResolver/Collections/OneToManyRelation.cs +++ b/src/AsmResolver/Collections/OneToManyRelation.cs @@ -8,9 +8,30 @@ namespace AsmResolver.Collections /// The type of objects to map. /// The type of objects to map to. public sealed class OneToManyRelation + where TKey : notnull + where TValue : notnull { - private readonly Dictionary> _memberLists = new(); - private readonly Dictionary _memberOwners = new(); + private readonly Dictionary> _memberLists; + private readonly Dictionary _memberOwners; + + /// + /// Creates a new, empty one-to-many relation mapping. + /// + public OneToManyRelation() + { + _memberLists = new Dictionary>(); + _memberOwners = new Dictionary(); + } + + /// + /// Creates a new, empty one-to-many relation mapping. + /// + /// The initial number of elements the relation can store. + public OneToManyRelation(int capacity) + { + _memberLists = new Dictionary>(capacity); + _memberOwners = new Dictionary(capacity); + } /// /// Registers a relation between two objects. diff --git a/src/AsmResolver/Collections/OneToOneRelation.cs b/src/AsmResolver/Collections/OneToOneRelation.cs index 46385866a..dbefc7bcc 100644 --- a/src/AsmResolver/Collections/OneToOneRelation.cs +++ b/src/AsmResolver/Collections/OneToOneRelation.cs @@ -8,9 +8,30 @@ namespace AsmResolver.Collections /// The first object type. /// The second object type. public sealed class OneToOneRelation + where TKey : notnull + where TValue : notnull { - private readonly Dictionary _keyToValue = new(); - private readonly Dictionary _valueToKey = new(); + private readonly Dictionary _keyToValue; + private readonly Dictionary _valueToKey; + + /// + /// Creates a new, empty one-to-one mapping. + /// + public OneToOneRelation() + { + _keyToValue = new Dictionary(); + _valueToKey = new Dictionary(); + } + + /// + /// Creates a new, empty one-to-one mapping. + /// + /// The initial number of elements the relation can store. + public OneToOneRelation(int capacity) + { + _keyToValue = new Dictionary(capacity); + _valueToKey = new Dictionary(capacity); + } /// /// Registers a one-to-one relation between two objects. @@ -36,7 +57,7 @@ public bool Add(TKey key, TValue value) /// /// The key. /// The value. - public TValue GetValue(TKey key) + public TValue? GetValue(TKey key) { _keyToValue.TryGetValue(key, out var value); return value; @@ -47,7 +68,7 @@ public TValue GetValue(TKey key) /// /// The value. /// The key. - public TKey GetKey(TValue value) + public TKey? GetKey(TValue value) { _valueToKey.TryGetValue(value, out var key); return key; diff --git a/src/AsmResolver/Collections/OwnedCollection.cs b/src/AsmResolver/Collections/OwnedCollection.cs index 82a7d6b33..6facee7e8 100644 --- a/src/AsmResolver/Collections/OwnedCollection.cs +++ b/src/AsmResolver/Collections/OwnedCollection.cs @@ -26,6 +26,17 @@ public OwnedCollection(TOwner owner) Owner = owner ?? throw new ArgumentNullException(nameof(owner)); } + /// + /// Creates a new empty collection that is owned by an object. + /// + /// The owner of the collection. + /// The initial number of elements the collection can store. + public OwnedCollection(TOwner owner, int capacity) + : base(capacity) + { + Owner = owner ?? throw new ArgumentNullException(nameof(owner)); + } + /// /// Gets the owner of the collection. /// diff --git a/src/AsmResolver/Collections/RefList.cs b/src/AsmResolver/Collections/RefList.cs index f454fe6ff..4fa091140 100644 --- a/src/AsmResolver/Collections/RefList.cs +++ b/src/AsmResolver/Collections/RefList.cs @@ -46,9 +46,23 @@ public RefList(int capacity) public int Count => _count; /// - /// Gets the capacity of the underlying array. + /// Gets or sets the total number of elements that the underlying array can store. /// - public int Capacity => _items.Length; + public int Capacity + { + get => _items.Length; + set + { + if (value == _items.Length) + return; + + if (value < _count) + throw new ArgumentException("Capacity must be equal or larger than the current number of elements in the list."); + + EnsureCapacity(value); + IncrementVersion(); + } + } /// /// Gets a number indicating the current version of the list. @@ -127,7 +141,7 @@ public RefList(int capacity) /// The element. public void Add(in T item) { - EnsureEnoughCapacity(_count + 1); + EnsureCapacity(_count + 1); _items[_count] = item; _count++; IncrementVersion(); @@ -194,7 +208,7 @@ public bool Remove(in T item) /// The element to insert. public void Insert(int index, in T item) { - EnsureEnoughCapacity(_count + 1); + EnsureCapacity(_count + 1); if (index < _count) Array.Copy(_items, index, _items, index + 1, _count - index); @@ -246,7 +260,7 @@ private void AssertIsValidIndex(int index) throw new IndexOutOfRangeException(); } - private void EnsureEnoughCapacity(int requiredCount) + private void EnsureCapacity(int requiredCount) { if (_items.Length >= requiredCount) return; diff --git a/src/AsmResolver/IO/BinaryStreamReader.cs b/src/AsmResolver/IO/BinaryStreamReader.cs index 74088ce4b..d339ceab3 100644 --- a/src/AsmResolver/IO/BinaryStreamReader.cs +++ b/src/AsmResolver/IO/BinaryStreamReader.cs @@ -328,18 +328,45 @@ public byte[] ReadToEnd() /// /// The delimeter byte to stop at. /// The read bytes, including the delimeter if it was found. - public byte[] ReadBytesUntil(byte delimeter) - { + public byte[] ReadBytesUntil(byte delimeter) => ReadBytesUntil(delimeter, true); + + /// + /// Reads bytes from the input stream until the provided delimeter byte is reached. + /// + /// The delimeter byte to stop at. + /// + /// true if the final delimeter should be included in the return value, false otherwise. + /// + /// The read bytes. + /// + /// This function always consumes the delimeter from the input stream if it is present, regardless of the value + /// of . + /// + public byte[] ReadBytesUntil(byte delimeter, bool includeDelimeterInReturn) + { + bool shouldReadExtra = false; + var lookahead = Fork(); while (lookahead.RelativeOffset < lookahead.Length) { byte b = lookahead.ReadByte(); if (b == delimeter) + { + if (!includeDelimeterInReturn) + { + lookahead.RelativeOffset--; + shouldReadExtra = true; + } break; + } } byte[] buffer = new byte[lookahead.RelativeOffset - RelativeOffset]; ReadBytes(buffer, 0, buffer.Length); + + if (shouldReadExtra) + ReadByte(); + return buffer; } @@ -347,17 +374,7 @@ public byte[] ReadBytesUntil(byte delimeter) /// Reads a null-terminated ASCII string from the input stream. /// /// The read ASCII string, excluding the null terminator. - public string ReadAsciiString() - { - byte[] data = ReadBytesUntil(0); - int length = data.Length; - - // Exclude trailing 0 byte. - if (data[data.Length - 1] == 0) - length--; - - return Encoding.ASCII.GetString(data, 0, length); - } + public string ReadAsciiString() => Encoding.ASCII.GetString(ReadBytesUntil(0, false)); /// /// Reads a zero-terminated Unicode string from the stream. @@ -378,6 +395,18 @@ public string ReadUnicodeString() return builder.ToString(); } + /// + /// Reads a null-terminated UTF-8 string from the input stream. + /// + /// The read UTF-8 string, excluding the null terminator. + public Utf8String ReadUtf8String() + { + byte[] data = ReadBytesUntil(0, false); + return data.Length != 0 + ? new Utf8String(data) + : Utf8String.Empty; + } + /// /// Reads either a 32-bit or a 64-bit number from the input stream. /// diff --git a/src/AsmResolver/IO/BinaryStreamWriter.cs b/src/AsmResolver/IO/BinaryStreamWriter.cs index 039d043c9..e55c7ed25 100644 --- a/src/AsmResolver/IO/BinaryStreamWriter.cs +++ b/src/AsmResolver/IO/BinaryStreamWriter.cs @@ -8,107 +8,113 @@ namespace AsmResolver.IO /// public class BinaryStreamWriter : IBinaryStreamWriter { - private readonly Stream _stream; - /// /// Creates a new binary stream writer using the provided output stream. /// /// The stream to write to. public BinaryStreamWriter(Stream stream) { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + BaseStream = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + /// + /// Gets the stream this writer writes to. + /// + public Stream BaseStream + { + get; } /// public ulong Offset { - get => (uint) _stream.Position; + get => (uint) BaseStream.Position; set { // Check if position actually changed before actually setting. If we don't do this, this can cause // performance issues on some systems. See https://github.com/Washi1337/AsmResolver/issues/232 - if (_stream.Position != (long) value) - _stream.Position = (long) value; + if (BaseStream.Position != (long) value) + BaseStream.Position = (long) value; } } /// - public uint Length => (uint) _stream.Length; + public uint Length => (uint) BaseStream.Length; /// public void WriteBytes(byte[] buffer, int startIndex, int count) { - _stream.Write(buffer, startIndex, count); + BaseStream.Write(buffer, startIndex, count); } /// public void WriteByte(byte value) { - _stream.WriteByte(value); + BaseStream.WriteByte(value); } /// public void WriteUInt16(ushort value) { - _stream.WriteByte((byte) (value & 0xFF)); - _stream.WriteByte((byte) ((value >> 8) & 0xFF)); + BaseStream.WriteByte((byte) (value & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 8) & 0xFF)); } /// public void WriteUInt32(uint value) { - _stream.WriteByte((byte) (value & 0xFF)); - _stream.WriteByte((byte) ((value >> 8) & 0xFF)); - _stream.WriteByte((byte) ((value >> 16) & 0xFF)); - _stream.WriteByte((byte) ((value >> 24) & 0xFF)); + BaseStream.WriteByte((byte) (value & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 8) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 16) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 24) & 0xFF)); } /// public void WriteUInt64(ulong value) { - _stream.WriteByte((byte) (value & 0xFF)); - _stream.WriteByte((byte) ((value >> 8) & 0xFF)); - _stream.WriteByte((byte) ((value >> 16) & 0xFF)); - _stream.WriteByte((byte) ((value >> 24) & 0xFF)); - _stream.WriteByte((byte) ((value >> 32) & 0xFF)); - _stream.WriteByte((byte) ((value >> 40) & 0xFF)); - _stream.WriteByte((byte) ((value >> 48) & 0xFF)); - _stream.WriteByte((byte) ((value >> 56) & 0xFF)); + BaseStream.WriteByte((byte) (value & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 8) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 16) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 24) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 32) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 40) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 48) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 56) & 0xFF)); } /// public void WriteSByte(sbyte value) { - _stream.WriteByte(unchecked((byte) value)); + BaseStream.WriteByte(unchecked((byte) value)); } /// public void WriteInt16(short value) { - _stream.WriteByte((byte) (value & 0xFF)); - _stream.WriteByte((byte) ((value >> 8) & 0xFF)); + BaseStream.WriteByte((byte) (value & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 8) & 0xFF)); } /// public void WriteInt32(int value) { - _stream.WriteByte((byte) (value & 0xFF)); - _stream.WriteByte((byte) ((value >> 8) & 0xFF)); - _stream.WriteByte((byte) ((value >> 16) & 0xFF)); - _stream.WriteByte((byte) ((value >> 24) & 0xFF)); + BaseStream.WriteByte((byte) (value & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 8) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 16) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 24) & 0xFF)); } /// public void WriteInt64(long value) { - _stream.WriteByte((byte) (value & 0xFF)); - _stream.WriteByte((byte) ((value >> 8) & 0xFF)); - _stream.WriteByte((byte) ((value >> 16) & 0xFF)); - _stream.WriteByte((byte) ((value >> 24) & 0xFF)); - _stream.WriteByte((byte) ((value >> 32) & 0xFF)); - _stream.WriteByte((byte) ((value >> 40) & 0xFF)); - _stream.WriteByte((byte) ((value >> 48) & 0xFF)); - _stream.WriteByte((byte) ((value >> 56) & 0xFF)); + BaseStream.WriteByte((byte) (value & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 8) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 16) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 24) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 32) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 40) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 48) & 0xFF)); + BaseStream.WriteByte((byte) ((value >> 56) & 0xFF)); } /// diff --git a/src/AsmResolver/IO/DataSourceSlice.cs b/src/AsmResolver/IO/DataSourceSlice.cs new file mode 100644 index 000000000..f107316a0 --- /dev/null +++ b/src/AsmResolver/IO/DataSourceSlice.cs @@ -0,0 +1,68 @@ +using System; + +namespace AsmResolver.IO +{ + /// + /// Represents a data source that only exposes a part (slice) of another data source. + /// + public class DataSourceSlice : IDataSource + { + private readonly IDataSource _source; + + /// + /// Creates a new data source slice. + /// + /// The original data source to slice. + /// The starting address. + /// The number of bytes. + /// + /// Occurs when and/or result in addresses that are invalid + /// in the original data source. + /// + public DataSourceSlice(IDataSource source, ulong start, ulong length) + { + _source = source; + + if (!source.IsValidAddress(start)) + throw new ArgumentOutOfRangeException(nameof(start)); + if (length > 0 && !source.IsValidAddress(start + length - 1)) + throw new ArgumentOutOfRangeException(nameof(length)); + + BaseAddress = start; + Length = length; + } + + /// + public ulong BaseAddress + { + get; + } + + /// + public ulong Length + { + get; + } + + /// + public byte this[ulong address] + { + get + { + if (!IsValidAddress(address)) + throw new IndexOutOfRangeException(); + return _source[address]; + } + } + + /// + public bool IsValidAddress(ulong address) => address >= BaseAddress && address - BaseAddress < Length; + + /// + public int ReadBytes(ulong address, byte[] buffer, int index, int count) + { + int maxCount = Math.Max(0, (int) (Length - (address - BaseAddress))); + return _source.ReadBytes(address, buffer, index, Math.Min(maxCount, count)); + } + } +} diff --git a/src/AsmResolver/IO/MemoryStreamWriterPool.cs b/src/AsmResolver/IO/MemoryStreamWriterPool.cs new file mode 100644 index 000000000..14a3ef691 --- /dev/null +++ b/src/AsmResolver/IO/MemoryStreamWriterPool.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Concurrent; +using System.IO; + +namespace AsmResolver.IO +{ + /// + /// Provides a pool of reusable instances of that are meant to be used for + /// constructing byte arrays. + /// + /// + /// This class is thread-safe. All threads are allowed to rent and return writers from this pool simultaneously. + /// + public class MemoryStreamWriterPool + { + private readonly ConcurrentQueue _writers = new(); + + /// + /// Rents a single binary stream writer. + /// + /// The writer. + public RentedWriter Rent() + { + if (!_writers.TryDequeue(out var writer)) + writer = new BinaryStreamWriter(new MemoryStream()); + + writer.BaseStream.SetLength(0); + return new RentedWriter(this, writer); + } + + private void Return(BinaryStreamWriter writer) => _writers.Enqueue(writer); + + /// + /// Represents a single instance of a that is rented by a writer pool. + /// + public ref struct RentedWriter + { + private bool _isDisposed = false; + private readonly BinaryStreamWriter _writer; + + internal RentedWriter(MemoryStreamWriterPool pool, BinaryStreamWriter writer) + { + Pool = pool; + _writer = writer; + } + + /// + /// Gets the pool the writer was rented from. + /// + public MemoryStreamWriterPool Pool + { + get; + } + + /// + /// Gets the writer instance. + /// + public BinaryStreamWriter Writer + { + get + { + if (_isDisposed) + throw new ObjectDisposedException(nameof(Writer)); + return _writer; + } + } + + /// + /// Gets the data that was written to the temporary stream. + /// + /// + public byte[] GetData() => ((MemoryStream) Writer.BaseStream).ToArray(); + + /// + /// Returns the stream writer to the pool. + /// + public void Dispose() + { + if (_isDisposed) + return; + + Pool.Return(Writer); + _isDisposed = true; + } + } + } +} diff --git a/src/AsmResolver/Utf8String.cs b/src/AsmResolver/Utf8String.cs index e2b87ff33..f3c43f540 100644 --- a/src/AsmResolver/Utf8String.cs +++ b/src/AsmResolver/Utf8String.cs @@ -268,22 +268,25 @@ public Utf8String Concat(byte[]? other) /// /// This operation performs a byte-level comparison of the two strings. /// - public bool Equals(Utf8String other) => ByteArrayEqualityComparer.Instance.Equals(_data, other._data); + public bool Equals(Utf8String? other) => + other is not null && ByteArrayEqualityComparer.Instance.Equals(_data, other._data); /// /// /// This operation performs a byte-level comparison of the two strings. /// - public bool Equals(byte[] other) => ByteArrayEqualityComparer.Instance.Equals(_data, other); + public bool Equals(byte[]? other) => other is not null && ByteArrayEqualityComparer.Instance.Equals(_data, other); /// /// /// This operation performs a byte-level comparison of the two strings. /// - public bool Equals(string other) => Value.Equals(other); + public bool Equals(string? other) =>other is not null && Value.Equals(other); /// - public int CompareTo(Utf8String other) => ByteArrayEqualityComparer.Instance.Compare(_data, other._data); + public int CompareTo(Utf8String? other) => other is not null + ? ByteArrayEqualityComparer.Instance.Compare(_data, other._data) + : 1; /// public IEnumerator GetEnumerator() => Value.GetEnumerator(); diff --git a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj index 645f5f42d..5931a82d2 100644 --- a/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj +++ b/test/AsmResolver.Benchmarks/AsmResolver.Benchmarks.csproj @@ -2,11 +2,11 @@ Exe - netcoreapp3.1 + net6.0;netcoreapp3.1 - + 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/LargeMethodBodiesBenchmark.cs b/test/AsmResolver.Benchmarks/LargeMethodBodiesBenchmark.cs index aabd4bb70..e8f389ae1 100644 --- a/test/AsmResolver.Benchmarks/LargeMethodBodiesBenchmark.cs +++ b/test/AsmResolver.Benchmarks/LargeMethodBodiesBenchmark.cs @@ -1,5 +1,4 @@ using System.IO; -using System.IO.Compression; using AsmResolver.DotNet; using BenchmarkDotNet.Attributes; @@ -8,26 +7,9 @@ namespace AsmResolver.Benchmarks [MemoryDiagnoser] public class LargeMethodBodiesBenchmark { - private static readonly byte[] HelloWorldPumped; + private static readonly byte[] HelloWorldPumped = Utilities.DecompressDeflate(Properties.Resources.HelloWorld_Pumped); private readonly MemoryStream _outputStream = new(); - static LargeMethodBodiesBenchmark() - { - HelloWorldPumped = DecompressDeflate(Properties.Resources.HelloWorld_Pumped); - } - - private static byte[] DecompressDeflate(byte[] compressedData) - { - using var decompressed = new MemoryStream(); - using (var compressed = new MemoryStream(compressedData)) - { - using var deflate = new DeflateStream(compressed, CompressionMode.Decompress); - deflate.CopyTo(decompressed); - } - - return decompressed.ToArray(); - } - [Benchmark] public void HelloWorldPumped_ModuleDefinition_Read() { diff --git a/test/AsmResolver.Benchmarks/ModuleReadWriteBenchmark.cs b/test/AsmResolver.Benchmarks/ModuleReadWriteBenchmark.cs new file mode 100644 index 000000000..206b2cb80 --- /dev/null +++ b/test/AsmResolver.Benchmarks/ModuleReadWriteBenchmark.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using AsmResolver.DotNet; +using BenchmarkDotNet.Attributes; +using static AsmResolver.Benchmarks.Properties.Resources; + +namespace AsmResolver.Benchmarks +{ + [MemoryDiagnoser] + public class ModuleReadWriteBenchmark + { + private static readonly byte[] HelloWorldApp = HelloWorld; + private static readonly byte[] CrackMeApp = Test; + private static readonly byte[] ManyMethods = Utilities.DecompressDeflate(HelloWorld_ManyMethods); + private static readonly byte[] CoreLib; + + private readonly MemoryStream _outputStream = new(); + + static ModuleReadWriteBenchmark() + { + var resolver = new DotNetCoreAssemblyResolver(new Version(3, 1, 0)); + string path = resolver.Resolve(KnownCorLibs.SystemPrivateCoreLib_v4_0_0_0)!.ManifestModule!.FilePath; + CoreLib = File.ReadAllBytes(path); + } + + [Benchmark] + public void HelloWorld_Read() + { + var file = ModuleDefinition.FromBytes(HelloWorldApp); + } + + [Benchmark] + public void HelloWorld_ReadWrite() + { + var file = ModuleDefinition.FromBytes(HelloWorldApp); + file.Write(_outputStream); + } + + [Benchmark] + public void CrackMe_Read() + { + var file = ModuleDefinition.FromBytes(CrackMeApp); + } + + [Benchmark] + public void CrackMe_ReadWrite() + { + var file = ModuleDefinition.FromBytes(CrackMeApp); + file.Write(_outputStream); + } + + [Benchmark] + public void ManyMethods_Read() + { + var file = ModuleDefinition.FromBytes(ManyMethods); + } + + [Benchmark] + public void ManyMethods_ReadWrite() + { + var file = ModuleDefinition.FromBytes(ManyMethods); + file.Write(_outputStream); + } + + [Benchmark] + public void CoreLib_ReadWrite() + { + var module = ModuleDefinition.FromBytes(CoreLib); + module.Write(_outputStream); + } + } +} diff --git a/test/AsmResolver.Benchmarks/PEFileReadWriteBenchmark.cs b/test/AsmResolver.Benchmarks/PEFileReadWriteBenchmark.cs new file mode 100644 index 000000000..2ebbeb913 --- /dev/null +++ b/test/AsmResolver.Benchmarks/PEFileReadWriteBenchmark.cs @@ -0,0 +1,54 @@ +using System.IO; +using BenchmarkDotNet.Attributes; +using static AsmResolver.Benchmarks.Properties.Resources; + +namespace AsmResolver.Benchmarks +{ + public class PEFileReadWriteBenchmark + { + private static readonly byte[] HelloWorldApp = HelloWorld; + private static readonly byte[] CrackMeApp = Test; + private static readonly byte[] ManyMethods = Utilities.DecompressDeflate(HelloWorld_ManyMethods); + + private readonly MemoryStream _outputStream = new(); + + [Benchmark] + public void HelloWorld_Read() + { + var file = PE.File.PEFile.FromBytes(HelloWorldApp); + } + + [Benchmark] + public void HelloWorld_ReadWrite() + { + var file = PE.File.PEFile.FromBytes(HelloWorldApp); + file.Write(_outputStream); + } + + [Benchmark] + public void CrackMe_Read() + { + var file = PE.File.PEFile.FromBytes(CrackMeApp); + } + + [Benchmark] + public void CrackMe_ReadWrite() + { + var file = PE.File.PEFile.FromBytes(CrackMeApp); + file.Write(_outputStream); + } + + [Benchmark] + public void ManyMethods_Read() + { + var file = PE.File.PEFile.FromBytes(ManyMethods); + } + + [Benchmark] + public void ManyMethods_ReadWrite() + { + var file = PE.File.PEFile.FromBytes(ManyMethods); + file.Write(_outputStream); + } + } +} diff --git a/test/AsmResolver.Benchmarks/PEImageReadWriteBenchmark.cs b/test/AsmResolver.Benchmarks/PEImageReadWriteBenchmark.cs new file mode 100644 index 000000000..be20c7030 --- /dev/null +++ b/test/AsmResolver.Benchmarks/PEImageReadWriteBenchmark.cs @@ -0,0 +1,57 @@ +using System.IO; +using AsmResolver.PE; +using AsmResolver.PE.DotNet.Builder; +using BenchmarkDotNet.Attributes; +using static AsmResolver.Benchmarks.Properties.Resources; + +namespace AsmResolver.Benchmarks +{ + [MemoryDiagnoser] + public class PEImageReadWriteBenchmark + { + private static readonly byte[] HelloWorldApp = HelloWorld; + private static readonly byte[] CrackMeApp = Test; + private static readonly byte[] ManyMethods = Utilities.DecompressDeflate(HelloWorld_ManyMethods); + + private readonly MemoryStream _outputStream = new(); + + [Benchmark] + public void HelloWorld_Read() + { + var file = PEImage.FromBytes(HelloWorldApp); + } + + [Benchmark] + public void HelloWorld_ReadWrite() + { + var image = PEImage.FromBytes(HelloWorldApp); + new ManagedPEFileBuilder().CreateFile(image).Write(_outputStream); + } + + [Benchmark] + public void CrackMe_Read() + { + var file = PEImage.FromBytes(HelloWorldApp); + } + + [Benchmark] + public void CrackMe_ReadWrite() + { + var image = PEImage.FromBytes(CrackMeApp); + new ManagedPEFileBuilder().CreateFile(image).Write(_outputStream); + } + + [Benchmark] + public void ManyMethods_Read() + { + var file = PEImage.FromBytes(ManyMethods); + } + + [Benchmark] + public void ManyMethods_ReadWrite() + { + var image = PEImage.FromBytes(ManyMethods); + new ManagedPEFileBuilder().CreateFile(image).Write(_outputStream); + } + } +} diff --git a/test/AsmResolver.Benchmarks/PEReadWriteBenchmark.cs b/test/AsmResolver.Benchmarks/PEReadWriteBenchmark.cs deleted file mode 100644 index 4674b494f..000000000 --- a/test/AsmResolver.Benchmarks/PEReadWriteBenchmark.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.IO; -using AsmResolver.DotNet; -using AsmResolver.PE; -using AsmResolver.PE.DotNet.Builder; -using BenchmarkDotNet.Attributes; - -namespace AsmResolver.Benchmarks -{ - [MemoryDiagnoser] - public class PEReadWriteBenchmark - { - private static readonly byte[] HelloWorldApp = Properties.Resources.HelloWorld; - private static readonly byte[] CrackMeApp = Properties.Resources.Test; - private readonly MemoryStream _outputStream = new(); - - [Benchmark] - public void HelloWorld_PEFile_Read() - { - var file = PE.File.PEFile.FromBytes(HelloWorldApp); - } - - [Benchmark] - public void HelloWorld_PEFile_ReadWrite() - { - var file = PE.File.PEFile.FromBytes(HelloWorldApp); - file.Write(_outputStream); - } - - [Benchmark] - public void HelloWorld_PEImage_Read() - { - var file = PEImage.FromBytes(HelloWorldApp); - } - - [Benchmark] - public void HelloWorld_PEImage_ReadWrite() - { - var image = PEImage.FromBytes(HelloWorldApp); - new ManagedPEFileBuilder().CreateFile(image).Write(_outputStream); - } - - [Benchmark] - public void HelloWorld_ModuleDefinition_Read() - { - var file = ModuleDefinition.FromBytes(HelloWorldApp); - } - - [Benchmark] - public void HelloWorld_ModuleDefinition_ReadWrite() - { - var file = ModuleDefinition.FromBytes(HelloWorldApp); - file.Write(_outputStream); - } - - [Benchmark] - public void CrackMe_PEFile_Read() - { - var file = PE.File.PEFile.FromBytes(CrackMeApp); - } - - [Benchmark] - public void CrackMe_PEFile_ReadWrite() - { - var file = PE.File.PEFile.FromBytes(CrackMeApp); - file.Write(_outputStream); - } - - [Benchmark] - public void CrackMe_PEImage_Read() - { - var file = PEImage.FromBytes(HelloWorldApp); - } - - [Benchmark] - public void CrackMe_PEImage_ReadWrite() - { - var image = PEImage.FromBytes(CrackMeApp); - new ManagedPEFileBuilder().CreateFile(image).Write(_outputStream); - } - - [Benchmark] - public void CrackMe_ModuleDefinition_Read() - { - var file = ModuleDefinition.FromBytes(CrackMeApp); - } - - [Benchmark] - public void CrackMe_ModuleDefinition_ReadWrite() - { - var file = ModuleDefinition.FromBytes(CrackMeApp); - file.Write(_outputStream); - } - } -} diff --git a/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs b/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs index 6d44032e3..ee7805eca 100644 --- a/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs +++ b/test/AsmResolver.Benchmarks/Properties/Resources.Designer.cs @@ -89,5 +89,25 @@ public class Resources { return ((byte[])(obj)); } } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] HelloWorld_ManyMethods { + get { + object obj = ResourceManager.GetObject("HelloWorld_ManyMethods", resourceCulture); + 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 a2a96ebb5..585e32796 100644 --- a/test/AsmResolver.Benchmarks/Properties/Resources.resx +++ b/test/AsmResolver.Benchmarks/Properties/Resources.resx @@ -127,4 +127,10 @@ ..\Resources\HelloWorld.pumped.deflate;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\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.ManyMethods.deflate b/test/AsmResolver.Benchmarks/Resources/HelloWorld.ManyMethods.deflate new file mode 100644 index 000000000..f62ac4be1 Binary files /dev/null and b/test/AsmResolver.Benchmarks/Resources/HelloWorld.ManyMethods.deflate differ 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.Benchmarks/Utilities.cs b/test/AsmResolver.Benchmarks/Utilities.cs new file mode 100644 index 000000000..1075aabcf --- /dev/null +++ b/test/AsmResolver.Benchmarks/Utilities.cs @@ -0,0 +1,20 @@ +using System.IO; +using System.IO.Compression; + +namespace AsmResolver.Benchmarks +{ + internal static class Utilities + { + public static byte[] DecompressDeflate(byte[] compressedData) + { + using var decompressed = new MemoryStream(); + using (var compressed = new MemoryStream(compressedData)) + { + using var deflate = new DeflateStream(compressed, CompressionMode.Decompress); + deflate.CopyTo(decompressed); + } + + return decompressed.ToArray(); + } + } +} diff --git a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj index 108836e48..5a428afc5 100644 --- a/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj +++ b/test/AsmResolver.DotNet.Tests/AsmResolver.DotNet.Tests.csproj @@ -1,4 +1,4 @@ - + net5.0 @@ -9,9 +9,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -20,6 +20,7 @@ + diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenMappingTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenMappingTest.cs index c62e752ca..af428c6a4 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenMappingTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenMappingTest.cs @@ -44,7 +44,7 @@ public void NewFieldDefinition() var field = new FieldDefinition( "MyField", FieldAttributes.Public | FieldAttributes.Static, - FieldSignature.CreateStatic(module.CorLibTypeFactory.Object)); + module.CorLibTypeFactory.Object); module.GetOrCreateModuleType().Fields.Add(field); // Rebuild. @@ -107,7 +107,7 @@ public void NewTypeReference() module.GetOrCreateModuleType().Fields.Add(new FieldDefinition( "MyField", FieldAttributes.Public | FieldAttributes.Static, - FieldSignature.CreateStatic(reference.ToTypeSignature()))); + reference.ToTypeSignature())); // Rebuild. var builder = new ManagedPEImageBuilder(); diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/AssemblyRefTokenPreservationTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/AssemblyRefTokenPreservationTest.cs index f9cda7e70..5f9c79830 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/AssemblyRefTokenPreservationTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/AssemblyRefTokenPreservationTest.cs @@ -1,8 +1,11 @@ using System.IO; using System.Linq; using AsmResolver.DotNet.Builder; +using AsmResolver.PE; using AsmResolver.PE.DotNet.Cil; +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.Builder.TokenPreservation @@ -14,10 +17,10 @@ public void PreserveAssemblyRefsNoChangeShouldAtLeastHaveOriginalAssemblyRefs() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalAssemblyRefs = GetMembers(module, TableIndex.AssemblyRef); - + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveAssemblyReferenceIndices); var newAssemblyRefs = GetMembers(newModule, TableIndex.AssemblyRef); - + Assert.Equal(originalAssemblyRefs, newAssemblyRefs.Take(originalAssemblyRefs.Count), Comparer); } @@ -26,14 +29,14 @@ public void PreserveAssemblyRefsWithTypeRefRemovedShouldAtLeastHaveOriginalAssem { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalAssemblyRefs = GetMembers(module, TableIndex.AssemblyRef); - - var instructions = module.ManagedEntrypointMethod.CilMethodBody.Instructions; + + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; instructions.Clear(); instructions.Add(CilOpCodes.Ret); - + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveAssemblyReferenceIndices); var newAssemblyRefs = GetMembers(newModule, TableIndex.AssemblyRef); - + Assert.Equal(originalAssemblyRefs, newAssemblyRefs.Take(originalAssemblyRefs.Count), Comparer); } @@ -42,22 +45,58 @@ public void PreserveAssemblyRefsWithExtraImportShouldAtLeastHaveOriginalAssembly { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalAssemblyRefs = GetMembers(module, TableIndex.AssemblyRef); - + var importer = new ReferenceImporter(module); - var exists = importer.ImportMethod(typeof(File).GetMethod("Exists", new[] {typeof(string)})); + var exists = importer.ImportMethod(typeof(File).GetMethod("Exists", new[] {typeof(string)})!); - var instructions = module.ManagedEntrypointMethod.CilMethodBody.Instructions; + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; instructions.RemoveAt(instructions.Count - 1); instructions.Add(CilOpCodes.Ldstr, "file.txt"); instructions.Add(CilOpCodes.Call, exists); instructions.Add(CilOpCodes.Pop); instructions.Add(CilOpCodes.Ret); - + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveAssemblyReferenceIndices); var newAssemblyRefs = GetMembers(newModule, TableIndex.AssemblyRef); - + Assert.Equal(originalAssemblyRefs, newAssemblyRefs.Take(originalAssemblyRefs.Count), Comparer); } + [Fact] + public void PreserveDuplicatedAssemblyRefs() + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var metadata = image.DotNetDirectory!.Metadata!; + var strings = metadata.GetStream(); + var table = metadata + .GetStream() + .GetTable(); + + // Duplicate mscorlib row. + var corlibRow = table.First(a => strings.GetStringByIndex(a.Name) == "mscorlib"); + table.Add(corlibRow); + + // Open module from modified image. + var module = ModuleDefinition.FromImage(image); + + // Obtain references to mscorlib. + var references = module.AssemblyReferences + .Where(t => t.Name == "mscorlib") + .ToArray(); + + Assert.Equal(2, references.Length); + + // Rebuild with preservation. + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveAssemblyReferenceIndices); + + var newReferences = newModule + .AssemblyReferences + .Where(t => t.Name == "mscorlib") + .ToArray(); + + Assert.Equal( + references.Select(r => r.MetadataToken).ToHashSet(), + newReferences.Select(r => r.MetadataToken).ToHashSet()); + } } -} \ No newline at end of file +} diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/FieldTokenPreservationTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/FieldTokenPreservationTest.cs index b80344617..845cc7eae 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/FieldTokenPreservationTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/FieldTokenPreservationTest.cs @@ -14,12 +14,12 @@ public class FieldTokenPreservationTest : TokenPreservationTestBase private static ModuleDefinition CreateSampleFieldDefsModule(int typeCount, int fieldsPerType) { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); - + for (int i = 0; i < typeCount; i++) { var dummyType = new TypeDefinition("Namespace", $"Type{i.ToString()}", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed); - + module.TopLevelTypes.Add(dummyType); for (int j = 0; j < fieldsPerType; j++) dummyType.Fields.Add(CreateDummyField(module, $"Field{j}")); @@ -28,18 +28,16 @@ private static ModuleDefinition CreateSampleFieldDefsModule(int typeCount, int f return RebuildAndReloadModule(module, MetadataBuilderFlags.None); } - private static FieldDefinition CreateDummyField(ModuleDefinition module, string name) - { - return new FieldDefinition(name, - FieldAttributes.Public | FieldAttributes.Static, - FieldSignature.CreateStatic(module.CorLibTypeFactory.Int32)); - } - + private static FieldDefinition CreateDummyField(ModuleDefinition module, string name) => new( + name, + FieldAttributes.Public | FieldAttributes.Static, + module.CorLibTypeFactory.Int32); + [Fact] public void PreserveFieldDefsNoChange() { var module = CreateSampleFieldDefsModule(10, 10); - + var newModule = RebuildAndReloadModule(module,MetadataBuilderFlags.PreserveFieldDefinitionIndices); AssertSameTokens(module, newModule, t => t.Fields); @@ -49,12 +47,12 @@ public void PreserveFieldDefsNoChange() public void PreserveFieldDefsChangeOrderOfTypes() { var module = CreateSampleFieldDefsModule(10, 10); - + const int swapIndex = 3; var type = module.TopLevelTypes[swapIndex]; module.TopLevelTypes.RemoveAt(swapIndex); module.TopLevelTypes.Insert(swapIndex + 1, type); - + var newModule = RebuildAndReloadModule(module,MetadataBuilderFlags.PreserveFieldDefinitionIndices); AssertSameTokens(module, newModule, t => t.Fields); @@ -70,7 +68,7 @@ public void PreserveFieldDefsChangeOrderOfFieldsInType() var field = type.Fields[swapIndex]; type.Fields.RemoveAt(swapIndex); type.Fields.Insert(swapIndex + 1, field); - + var newModule = RebuildAndReloadModule(module,MetadataBuilderFlags.PreserveFieldDefinitionIndices); AssertSameTokens(module, newModule, t => t.Fields); @@ -84,7 +82,7 @@ public void PreserveFieldDefsAddExtraField() var type = module.TopLevelTypes[2]; var field = CreateDummyField(module, "ExtraField"); type.Fields.Insert(3, field); - + var newModule = RebuildAndReloadModule(module,MetadataBuilderFlags.PreserveFieldDefinitionIndices); AssertSameTokens(module, newModule, t => t.Fields); @@ -99,10 +97,10 @@ public void PreserveFieldDefsRemoveField() const int indexToRemove = 3; var field = type.Fields[indexToRemove]; type.Fields.RemoveAt(indexToRemove); - + var newModule = RebuildAndReloadModule(module,MetadataBuilderFlags.PreserveFieldDefinitionIndices); AssertSameTokens(module, newModule, t => t.Fields, field.MetadataToken); } } -} \ No newline at end of file +} diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/MemberRefTokenPreservationTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/MemberRefTokenPreservationTest.cs index b801ea2f2..7d7665510 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/MemberRefTokenPreservationTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/MemberRefTokenPreservationTest.cs @@ -1,8 +1,11 @@ using System; using System.Linq; using AsmResolver.DotNet.Builder; +using AsmResolver.PE; using AsmResolver.PE.DotNet.Cil; +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.Builder.TokenPreservation @@ -27,7 +30,7 @@ public void PreserveMemberRefsWithTypeRefRemovedShouldAtLeastHaveOriginalMemberR var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalMemberRefs = GetMembers(module, TableIndex.MemberRef); - var instructions = module.ManagedEntrypointMethod.CilMethodBody.Instructions; + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; instructions.Clear(); instructions.Add(CilOpCodes.Ret); @@ -44,9 +47,9 @@ public void PreserveMemberRefsWithExtraImportShouldAtLeastHaveOriginalMemberRefs var originalMemberRefs = GetMembers(module, TableIndex.MemberRef); var importer = new ReferenceImporter(module); - var readKey = importer.ImportMethod(typeof(Console).GetMethod("ReadKey", Type.EmptyTypes)); + var readKey = importer.ImportMethod(typeof(Console).GetMethod("ReadKey", Type.EmptyTypes)!); - var instructions = module.ManagedEntrypointMethod.CilMethodBody.Instructions; + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; instructions.RemoveAt(instructions.Count - 1); instructions.Add(CilOpCodes.Call, readKey); instructions.Add(CilOpCodes.Pop); @@ -58,5 +61,43 @@ public void PreserveMemberRefsWithExtraImportShouldAtLeastHaveOriginalMemberRefs Assert.Equal(originalMemberRefs, newMemberRefs.Take(originalMemberRefs.Count), Comparer); } + [Fact] + public void PreserveDuplicatedTypeRefs() + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var metadata = image.DotNetDirectory!.Metadata!; + var strings = metadata.GetStream(); + var table = metadata + .GetStream() + .GetTable(); + + // Duplicate WriteLine row. + var writeLineRow = table.First(m => strings.GetStringByIndex(m.Name) == "WriteLine"); + table.Add(writeLineRow); + + // Open module from modified image. + var module = ModuleDefinition.FromImage(image); + + // Obtain references to Object. + var references = module + .GetImportedMemberReferences() + .Where(t => t.Name == "WriteLine") + .ToArray(); + + Assert.Equal(2, references.Length); + + // Rebuild with preservation. + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveMemberReferenceIndices); + + var newReferences = newModule + .GetImportedMemberReferences() + .Where(m => m.Name == "WriteLine") + .ToArray(); + + Assert.Equal( + references.Select(r => r.MetadataToken).ToHashSet(), + newReferences.Select(r => r.MetadataToken).ToHashSet()); + } + } } diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TokenPreservationTestBase.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TokenPreservationTestBase.cs index 452e4483c..3adf1bd7e 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TokenPreservationTestBase.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TokenPreservationTestBase.cs @@ -19,7 +19,7 @@ protected SignatureComparer Comparer protected static List GetMembers(ModuleDefinition module, TableIndex tableIndex) { - int count = module.DotNetDirectory.Metadata + int count = module.DotNetDirectory!.Metadata! .GetStream() .GetTable(tableIndex) .Count; @@ -38,10 +38,11 @@ protected static ModuleDefinition RebuildAndReloadModule(ModuleDefinition module }; var result = builder.CreateImage(module); - if (result.DiagnosticBag.HasErrors) + if (result.HasFailed) throw new AggregateException(result.DiagnosticBag.Exceptions); var newImage = result.ConstructedImage; + return ModuleDefinition.FromImage(newImage); } diff --git a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs index b2fbbc1b6..fa233b1e8 100644 --- a/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs +++ b/test/AsmResolver.DotNet.Tests/Builder/TokenPreservation/TypeRefTokenPreservationTest.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; using System.Linq; using AsmResolver.DotNet.Builder; +using AsmResolver.PE; using AsmResolver.PE.DotNet.Cil; +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.Builder.TokenPreservation @@ -14,10 +18,10 @@ public void PreserveTypeRefsNoChangeShouldAtLeastHaveOriginalTypeRefs() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalTypeRefs = GetMembers(module, TableIndex.TypeRef); - + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveTypeReferenceIndices); var newTypeRefs = GetMembers(newModule, TableIndex.TypeRef); - + Assert.Equal(originalTypeRefs, newTypeRefs.Take(originalTypeRefs.Count), Comparer); } @@ -26,14 +30,14 @@ public void PreserveTypeRefsWithTypeRefRemovedShouldAtLeastHaveOriginalTypeRefs( { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalTypeRefs = GetMembers(module, TableIndex.TypeRef); - - var instructions = module.ManagedEntrypointMethod.CilMethodBody.Instructions; + + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; instructions.Clear(); instructions.Add(CilOpCodes.Ret); - + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveTypeReferenceIndices); var newTypeRefs = GetMembers(newModule, TableIndex.TypeRef); - + Assert.Equal(originalTypeRefs, newTypeRefs.Take(originalTypeRefs.Count), Comparer); } @@ -42,21 +46,72 @@ public void PreserveTypeRefsWithExtraImportShouldAtLeastHaveOriginalTypeRefs() { var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_NetCore); var originalTypeRefs = GetMembers(module, TableIndex.TypeRef); - + var importer = new ReferenceImporter(module); - var readKey = importer.ImportMethod(typeof(Console).GetMethod("ReadKey", Type.EmptyTypes)); + var readKey = importer.ImportMethod(typeof(Console).GetMethod("ReadKey", Type.EmptyTypes)!); - var instructions = module.ManagedEntrypointMethod.CilMethodBody.Instructions; + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; instructions.RemoveAt(instructions.Count - 1); instructions.Add(CilOpCodes.Call, readKey); instructions.Add(CilOpCodes.Pop); instructions.Add(CilOpCodes.Ret); - + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveTypeReferenceIndices); var newTypeRefs = GetMembers(newModule, TableIndex.TypeRef); - + Assert.Equal(originalTypeRefs, newTypeRefs.Take(originalTypeRefs.Count), Comparer); } + [Fact] + public void PreserveDuplicatedTypeRefs() + { + var image = PEImage.FromBytes(Properties.Resources.HelloWorld); + var metadata = image.DotNetDirectory!.Metadata!; + var strings = metadata.GetStream(); + var table = metadata + .GetStream() + .GetTable(); + + // Duplicate Object row. + var objectRow = table.First(t => strings.GetStringByIndex(t.Name) == "Object"); + table.Add(objectRow); + + // Open module from modified image. + var module = ModuleDefinition.FromImage(image); + + // Obtain references to Object. + var references = module + .GetImportedTypeReferences() + .Where(t => t.Name == "Object") + .ToArray(); + + Assert.Equal(2, references.Length); + + // Rebuild with preservation. + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveTypeReferenceIndices); + + var newObjectReferences = newModule + .GetImportedTypeReferences() + .Where(t => t.Name == "Object") + .ToArray(); + + Assert.Equal( + references.Select(r => r.MetadataToken).ToHashSet(), + newObjectReferences.Select(r => r.MetadataToken).ToHashSet()); + } + + [Fact] + public void PreserveNestedTypeRefOrdering() + { + // https://github.com/Washi1337/AsmResolver/issues/329 + + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_UnusualNestedTypeRefOrder); + var originalTypeRefs = GetMembers(module, TableIndex.TypeRef); + + var newModule = RebuildAndReloadModule(module, MetadataBuilderFlags.PreserveTypeReferenceIndices); + var newTypeRefs = GetMembers(newModule, TableIndex.TypeRef); + + Assert.Equal(originalTypeRefs, newTypeRefs.Take(originalTypeRefs.Count), Comparer); + } } -} \ No newline at end of file +} diff --git a/test/AsmResolver.DotNet.Tests/Bundles/BundleFileTest.cs b/test/AsmResolver.DotNet.Tests/Bundles/BundleFileTest.cs new file mode 100644 index 000000000..edb113ca1 --- /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()).Replace("\r", ""); + + Assert.Equal(@"{ + ""runtimeOptions"": { + ""tfm"": ""net6.0"", + ""framework"": { + ""name"": ""Microsoft.NETCore.App"", + ""version"": ""6.0.0"" + }, + ""configProperties"": { + ""System.Reflection.Metadata.MetadataUpdater.IsSupported"": false + } + } +}".Replace("\r", ""), 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/Code/Native/NativeMethodBodyTest.cs b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs index 3d6515007..a41eae6e8 100644 --- a/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs +++ b/test/AsmResolver.DotNet.Tests/Code/Native/NativeMethodBodyTest.cs @@ -1,25 +1,39 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; using AsmResolver.DotNet.Code.Native; using AsmResolver.DotNet.Signatures; using AsmResolver.PE; using AsmResolver.PE.Code; using AsmResolver.PE.DotNet; +using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; using AsmResolver.PE.File.Headers; using AsmResolver.PE.Imports; +using AsmResolver.Tests.Runners; using Xunit; namespace AsmResolver.DotNet.Tests.Code.Native { - public class NativeMethodBodyTest + public class NativeMethodBodyTest : IClassFixture { + private const string NonWindowsPlatform = "Test produces a mixed mode assembly which is not supported on non-Windows platforms."; + + private TemporaryDirectoryFixture _fixture; + + public NativeMethodBodyTest(TemporaryDirectoryFixture fixture) + { + _fixture = fixture; + } + private static NativeMethodBody CreateDummyBody(bool isVoid, bool is32Bit) { var module = ModuleDefinition.FromBytes(Properties.Resources.TheAnswer_NetFx); - - module.Attributes &= DotNetDirectoryFlags.ILOnly; + + module.Attributes &= ~DotNetDirectoryFlags.ILOnly; if (is32Bit) { module.PEKind = OptionalHeaderMagic.Pe32; @@ -41,13 +55,13 @@ private static NativeMethodBody CreateDummyBody(bool isVoid, bool is32Bit) | MethodImplAttributes.PreserveSig; method.DeclaringType.Methods.Remove(method); module.GetOrCreateModuleType().Methods.Add(method); - + return method.NativeMethodBody = new NativeMethodBody(method); } private static CodeSegment GetNewCodeSegment(IPEImage image) { - var methodTable = image.DotNetDirectory.Metadata + var methodTable = image.DotNetDirectory!.Metadata! .GetStream() .GetTable(TableIndex.Method); var row = methodTable.First(r => (r.ImplAttributes & MethodImplAttributes.Native) != 0); @@ -99,18 +113,18 @@ public void NativeMethodBodyImportedSymbolShouldEndUpInImportsDirectory() 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ed worl" 0x6c, 0x64, 0x21, 0x00 // "d!" }; - + // Fix up reference to ucrtbased.dll!puts var ucrtbased = new ImportedModule("ucrtbased.dll"); var puts = new ImportedSymbol(0x4fc, "puts"); ucrtbased.Symbols.Add(puts); - + body.AddressFixups.Add(new AddressFixup( 0xD, AddressFixupType.Relative32BitAddress, puts )); // Serialize module to PE image. - var module = body.Owner.Module; + var module = body.Owner.Module!; var image = module.ToPEImage(); // Verify import is added to PE image. @@ -136,24 +150,24 @@ public void Native32BitMethodShouldResultInBaseRelocation() /* 19: */ 0x5D, // pop ebp /* 1A: */ 0xC3, // ret }; - + // Fix up reference to ucrtbased.dll!puts var ucrtbased = new ImportedModule("ucrtbased.dll"); var puts = new ImportedSymbol(0x4fc, "puts"); ucrtbased.Symbols.Add(puts); - + body.AddressFixups.Add(new AddressFixup( 0xD, AddressFixupType.Absolute32BitAddress, puts )); // Serialize module to PE image. - var module = body.Owner.Module; + var module = body.Owner.Module!; var image = module.ToPEImage(); // Verify import is added to PE image. Assert.Contains(image.Imports, m => m.Name == ucrtbased.Name && m.Symbols.Any(s => s.Name == puts.Name)); - + // Verify relocation is added. var segment = GetNewCodeSegment(image); Assert.Contains(image.Relocations, r => @@ -183,7 +197,7 @@ public void DuplicateImportedSymbolsShouldResultInSameImportInImage() 0x67, 0x65, 0x64, 0x20, 0x77, 0x6f, 0x72, // "ed worl" 0x6c, 0x64, 0x21, 0x00 // "d!" }; - + // Add reference to ucrtbased.dll!puts at offset 0xD. var ucrtbased1 = new ImportedModule("ucrtbased.dll"); var puts1 = new ImportedSymbol(0x4fc, "puts"); @@ -191,7 +205,7 @@ public void DuplicateImportedSymbolsShouldResultInSameImportInImage() body.AddressFixups.Add(new AddressFixup( 0xD, AddressFixupType.Relative32BitAddress, puts1 )); - + // Add second (duplicated) reference to ucrtbased.dll!puts at offset 0x20. var ucrtbased2 = new ImportedModule("ucrtbased.dll"); var puts2 = new ImportedSymbol(0x4fc, "puts"); @@ -201,7 +215,7 @@ public void DuplicateImportedSymbolsShouldResultInSameImportInImage() )); // Serialize module to PE image. - var module = body.Owner.Module; + var module = body.Owner.Module!; var image = module.ToPEImage(); // Verify import is added to PE image. @@ -212,5 +226,101 @@ public void DuplicateImportedSymbolsShouldResultInSameImportInImage() Assert.NotNull(importedSymbol); Assert.Equal(puts1.Name, importedSymbol.Name); } + + [Fact] + public void ReadNativeMethodShouldResultInReferenceWithRightContents() + { + // Create native body. + var body = CreateDummyBody(false, false); + body.Code = new byte[] + { + 0xb8, 0x39, 0x05, 0x00, 0x00, // mov rax, 1337 + 0xc3 // ret + }; + + // Serialize module. + var module = body.Owner.Module!; + using var stream = new MemoryStream(); + module.Write(stream); + + // Reload and look up native method. + var newModule = ModuleDefinition.FromBytes(stream.ToArray()); + var method = newModule.GetAllTypes().SelectMany(t => t.Methods).First(m => m.IsNative); + + // Verify if code behind the entry address is consistent. + var reference = method.MethodBody?.Address; + Assert.NotNull(reference); + Assert.True(reference.CanRead); + + byte[] newBuffer = new byte[body.Code.Length]; + reference.CreateReader().ReadBytes(newBuffer, 0, newBuffer.Length); + Assert.Equal(body.Code, newBuffer); + } + + [SkippableTheory] + [InlineData( + true, + new byte[] {0xB8, 0x00, 0x00, 0x00, 0x00}, // mov eax, message + 1u, AddressFixupType.Absolute32BitAddress, + 6u)] + [InlineData( + false, + new byte[] {0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // mov rax, message + 2u, AddressFixupType.Absolute64BitAddress, + 11u)] + public void NativeBodyWithLocalSymbols(bool is32Bit, byte[] movInstruction, uint fixupOffset, AddressFixupType fixupType, uint symbolOffset) + { + Skip.IfNot(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), NonWindowsPlatform); + + // Create native body. + var code = new List(movInstruction); + code.AddRange(new byte[] + { + 0xc3, // ret + + // message: + 0x48, 0x00, 0x65, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0x6f, 0x00, 0x2c, 0x00, 0x20, 0x00, // "Hello, " + 0x77, 0x00, 0x6f, 0x00, 0x72, 0x00, 0x6c, 0x00, 0x64, 0x00, 0x21, 0x00, 0x00, 0x00 // "world!." + }); + + var body = CreateDummyBody(false, is32Bit); + body.Code = code.ToArray(); + + // Define local symbol. + var messageSymbol = new NativeLocalSymbol(body, symbolOffset); + + // Fixup address in mov instruction. + body.AddressFixups.Add(new AddressFixup(fixupOffset, fixupType, messageSymbol)); + + // Update main to call native method, convert the returned pointer to a String, and write to stdout. + var module = body.Owner.Module; + body.Owner!.Signature!.ReturnType = body.Owner.Module!.CorLibTypeFactory.IntPtr; + var stringConstructor = new MemberReference( + module!.CorLibTypeFactory.String.Type, + ".ctor", + MethodSignature.CreateInstance( + module.CorLibTypeFactory.Void, + module.CorLibTypeFactory.Char.MakePointerType()) + ); + var writeLine = new MemberReference( + new TypeReference(module, module.CorLibTypeFactory.CorLibScope, "System", "Console"), + "WriteLine", + MethodSignature.CreateStatic( + module.CorLibTypeFactory.Void, + module.CorLibTypeFactory.String) + ); + + var instructions = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions; + instructions.Clear(); + instructions.Add(CilOpCodes.Call, body.Owner); + instructions.Add(CilOpCodes.Newobj, stringConstructor); + instructions.Add(CilOpCodes.Call, writeLine); + instructions.Add(CilOpCodes.Ret); + + // Verify. + _fixture + .GetRunner() + .RebuildAndRun(module, "StringPointer.exe", $"Hello, world!{Environment.NewLine}"); + } } -} \ No newline at end of file +} diff --git a/test/AsmResolver.DotNet.Tests/FieldDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/FieldDefinitionTest.cs index 355ef7fb9..28737ee06 100644 --- a/test/AsmResolver.DotNet.Tests/FieldDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/FieldDefinitionTest.cs @@ -73,7 +73,7 @@ public void PersistentFieldSignature() var field = (FieldDefinition) module.LookupMember( typeof(SingleField).GetField(nameof(SingleField.IntField)).MetadataToken); - field.Signature = FieldSignature.CreateInstance(module.CorLibTypeFactory.Byte); + field.Signature = new FieldSignature(module.CorLibTypeFactory.Byte); var newField = RebuildAndLookup(field); diff --git a/test/AsmResolver.DotNet.Tests/MetadataResolverTest.cs b/test/AsmResolver.DotNet.Tests/MetadataResolverTest.cs index f4053192f..48de4c707 100644 --- a/test/AsmResolver.DotNet.Tests/MetadataResolverTest.cs +++ b/test/AsmResolver.DotNet.Tests/MetadataResolverTest.cs @@ -161,8 +161,10 @@ public void ResolveStringEmptyField() var module = new ModuleDefinition("SomeModule.dll"); var stringType = new TypeReference(module.CorLibTypeFactory.CorLibScope, "System", "String"); - var emptyField = new MemberReference(stringType, "Empty", - FieldSignature.CreateStatic(module.CorLibTypeFactory.String)); + var emptyField = new MemberReference( + stringType, + "Empty", + new FieldSignature(module.CorLibTypeFactory.String)); var definition = _fwResolver.ResolveField(emptyField); @@ -222,6 +224,29 @@ public void MaliciousExportedTypeLoop() Assert.Null(reference.Resolve()); } + [Fact] + public void ResolveToOlderNetVersion() + { + // https://github.com/Washi1337/AsmResolver/issues/321 + + var mainApp = ModuleDefinition.FromBytes(Properties.Resources.DifferentNetVersion_MainApp); + var library = ModuleDefinition.FromBytes(Properties.Resources.DifferentNetVersion_Library); + + mainApp.MetadataResolver.AssemblyResolver.AddToCache(library.Assembly!, library.Assembly!); + + var definition = library + .TopLevelTypes.First(t => t.Name == "MyClass") + .Methods.First(m => m.Name == "ThrowMe"); + + var reference = (IMethodDescriptor) mainApp.ManagedEntrypointMethod!.CilMethodBody!.Instructions.First( + i => i.OpCode == CilOpCodes.Callvirt && ((IMethodDescriptor) i.Operand)?.Name == "ThrowMe") + .Operand!; + + var resolved = reference.Resolve(); + Assert.NotNull(resolved); + Assert.Equal(definition, resolved); + } + [Fact] public void ResolveMethodWithoutHideBySig() { diff --git a/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs index 695089208..cb3e996f2 100644 --- a/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/MethodDefinitionTest.cs @@ -6,6 +6,7 @@ using AsmResolver.DotNet.Code.Cil; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.TestCases.Events; +using AsmResolver.DotNet.TestCases.Generics; using AsmResolver.DotNet.TestCases.Methods; using AsmResolver.DotNet.TestCases.Properties; using AsmResolver.PE.DotNet; @@ -500,5 +501,22 @@ public void FillUpExportGapsWithDummyExports() Assert.Equal(ordinal1, image.Exports.BaseOrdinal); } + + [Theory] + [InlineData("NonGenericMethodInNonGenericType", + "System.Void AsmResolver.DotNet.TestCases.Generics.NonGenericType::NonGenericMethodInNonGenericType()")] + [InlineData("GenericMethodInNonGenericType", + "System.Void AsmResolver.DotNet.TestCases.Generics.NonGenericType::GenericMethodInNonGenericType()")] + [InlineData("GenericMethodWithConstraints", + "System.Void AsmResolver.DotNet.TestCases.Generics.NonGenericType::GenericMethodWithConstraints()")] + public void MethodFullNameTests(string methodName, string expectedFullName) + { + var module = ModuleDefinition.FromFile(typeof(NonGenericType).Assembly.Location); + var method = module + .TopLevelTypes.First(t => t.Name == nameof(NonGenericType)) + .Methods.First(m => m.Name == methodName); + + Assert.Equal(expectedFullName, method.FullName); + } } } diff --git a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs index 13fd59cf7..d085b63cc 100644 --- a/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/ModuleDefinitionTest.cs @@ -22,6 +22,7 @@ namespace AsmResolver.DotNet.Tests { public class ModuleDefinitionTest { + private static readonly SignatureComparer Comparer = new(); private const string NonWindowsPlatform = "Test loads a module from a base address, which is only supported on Windows."; private static ModuleDefinition Rebuild(ModuleDefinition module) @@ -343,9 +344,17 @@ public void DetectTargetNetCore() [Fact] public void DetectTargetStandard() { - var module = ModuleDefinition.FromFile(typeof(ISegment).Assembly.Location); + var module = ModuleDefinition.FromFile(typeof(TestCases.Types.Class).Assembly.Location); Assert.Contains(DotNetRuntimeInfo.NetStandard, module.OriginalTargetRuntime.Name); Assert.Equal(2, module.OriginalTargetRuntime.Version.Major); } + + [Fact] + public void NewModuleShouldContainSingleReferenceToCorLib() + { + var module = new ModuleDefinition("SomeModule", KnownCorLibs.NetStandard_v2_0_0_0); + var reference = Assert.Single(module.AssemblyReferences); + Assert.Equal(KnownCorLibs.NetStandard_v2_0_0_0, reference, Comparer); + } } } diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs index bb68c3ab3..fbf27fbcb 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.Designer.cs @@ -129,6 +129,48 @@ public class Resources { } } + public static byte[] HelloWorld_WithAttribute { + get { + object obj = ResourceManager.GetObject("HelloWorld_WithAttribute", resourceCulture); + return ((byte[])(obj)); + } + } + + 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[] HelloWorld_UnusualNestedTypeRefOrder { + get { + object obj = ResourceManager.GetObject("HelloWorld_UnusualNestedTypeRefOrder", resourceCulture); + return ((byte[])(obj)); + } + } + public static byte[] Assembly1_Forwarder { get { object obj = ResourceManager.GetObject("Assembly1_Forwarder", resourceCulture); @@ -192,6 +234,20 @@ public class Resources { } } + public static byte[] DifferentNetVersion_MainApp { + get { + object obj = ResourceManager.GetObject("DifferentNetVersion_MainApp", resourceCulture); + return ((byte[])(obj)); + } + } + + public static byte[] DifferentNetVersion_Library { + get { + object obj = ResourceManager.GetObject("DifferentNetVersion_Library", resourceCulture); + return ((byte[])(obj)); + } + } + public static byte[] CallManagedExport_X86 { get { object obj = ResourceManager.GetObject("CallManagedExport_X86", resourceCulture); diff --git a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx index df11e42bb..e919e0c39 100644 --- a/test/AsmResolver.DotNet.Tests/Properties/Resources.resx +++ b/test/AsmResolver.DotNet.Tests/Properties/Resources.resx @@ -54,6 +54,24 @@ ..\Resources\HelloWorld_Forwarder.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\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\HelloWorld.UnusualNestedTypeRefOrder.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 @@ -81,6 +99,12 @@ ..\Resources\FieldRvaTest.exe;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\DifferentNetVersion.MainApp.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\DifferentNetVersion.Library.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + ..\Resources\CallManagedExport.x86.exe;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 f4c8ef7c9..d7c2cdc0d 100644 --- a/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs +++ b/test/AsmResolver.DotNet.Tests/ReferenceImporterTest.cs @@ -4,6 +4,7 @@ using System.Linq; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.DotNet.TestCases.Fields; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; using Xunit; @@ -11,24 +12,24 @@ 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; - + public ReferenceImporterTest() { _module = new ModuleDefinition("SomeModule.dll"); _importer = new ReferenceImporter(_module); } - + [Fact] public void ImportNewAssemblyShouldAddToModule() { var result = _importer.ImportScope(_dummyAssembly); - - Assert.Equal(_dummyAssembly, result, _comparer); + + Assert.Equal(_dummyAssembly, result, Comparer); Assert.Contains(result, _module.AssemblyReferences); } @@ -38,10 +39,10 @@ public void ImportExistingAssemblyShouldUseExistingAssembly() _module.AssemblyReferences.Add(_dummyAssembly); int count = _module.AssemblyReferences.Count; - + var copy = new AssemblyReference(_dummyAssembly); var result = _importer.ImportScope(copy); - + Assert.Same(_dummyAssembly, result); Assert.Equal(count, _module.AssemblyReferences.Count); } @@ -52,7 +53,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); } @@ -61,7 +62,7 @@ public void ImportAlreadyImportedTypeShouldUseSameInstance() { var type = new TypeReference(_dummyAssembly, "SomeNamespace", "SomeName"); var importedType = _importer.ImportType(type); - + var result = _importer.ImportType(importedType); Assert.Same(importedType, result); @@ -73,20 +74,20 @@ public void ImportTypeDefFromDifferentModuleShouldReturnTypeRef() var assembly = new AssemblyDefinition("ExternalAssembly", new Version(1, 2, 3, 4)); assembly.Modules.Add(new ModuleDefinition("ExternalAssembly.dll")); var definition = new TypeDefinition("SomeNamespace", "SomeName", TypeAttributes.Public); - assembly.ManifestModule.TopLevelTypes.Add(definition); + assembly.ManifestModule!.TopLevelTypes.Add(definition); var result = _importer.ImportType(definition); Assert.IsAssignableFrom(result); - Assert.Equal(definition, result, _comparer); + Assert.Equal(definition, result, Comparer); } - + [Fact] public void ImportTypeDefInSameModuleShouldReturnSameInstance() { var definition = new TypeDefinition("SomeNamespace", "SomeName", TypeAttributes.Public); _module.TopLevelTypes.Add(definition); - + var importedType = _importer.ImportType(definition); Assert.Same(definition, importedType); @@ -100,9 +101,40 @@ 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); + 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] @@ -114,7 +146,7 @@ public void ImportSimpleTypeFromReflectionShouldResultInTypeRef() Assert.IsAssignableFrom(result); Assert.Equal(type.FullName, result.FullName); - Assert.Equal(type.Assembly.GetName().Name, result.Scope.Name); + Assert.Equal(type.Assembly.GetName().Name, result.Scope?.Name); } [Fact] @@ -127,7 +159,7 @@ public void ImportArrayTypeShouldResultInTypeSpecWithSzArray() Assert.IsAssignableFrom(result); Assert.IsAssignableFrom(((TypeSpecification) result).Signature); } - + [Fact] public void ImportCorLibTypeAsSignatureShouldResultInCorLibTypeSignature() { @@ -170,7 +202,7 @@ public void ImportMethodFromExternalModuleShouldResultInMemberRef() var result = _importer.ImportMethod(method); - Assert.Equal(method, result, _comparer); + Assert.Equal(method, result, Comparer); Assert.Same(_module, result.Module); } @@ -179,7 +211,7 @@ public void ImportMethodFromSameModuleShouldResultInSameInstance() { var type = new TypeDefinition(null, "Type", TypeAttributes.Public); _module.TopLevelTypes.Add(type); - + var method = new MethodDefinition("Method", MethodAttributes.Public | MethodAttributes.Static, MethodSignature.CreateStatic(_module.CorLibTypeFactory.Void)); type.Methods.Add(method); @@ -192,11 +224,11 @@ public void ImportMethodFromSameModuleShouldResultInSameInstance() [Fact] public void ImportMethodFromGenericTypeThroughReflectionShouldIncludeGenericParamSig() { - var method = typeof(List).GetMethod("Add"); + var method = typeof(List).GetMethod("Add")!; var result = _importer.ImportMethod(method); - Assert.IsAssignableFrom(result.Signature.ParameterTypes[0]); + Assert.IsAssignableFrom(result.Signature?.ParameterTypes[0]); var genericParameter = (GenericParameterSignature) result.Signature.ParameterTypes[0]; Assert.Equal(0, genericParameter.Index); Assert.Equal(GenericParameterType.Type, genericParameter.ParameterType); @@ -206,7 +238,7 @@ public void ImportMethodFromGenericTypeThroughReflectionShouldIncludeGenericPara public void ImportGenericMethodFromReflectionShouldResultInMethodSpec() { var method = typeof(Enumerable) - .GetMethod("Empty") + .GetMethod("Empty")! .MakeGenericMethod(typeof(string)); var result = _importer.ImportMethod(method); @@ -217,19 +249,21 @@ public void ImportGenericMethodFromReflectionShouldResultInMethodSpec() Assert.Equal(new TypeSignature[] { _module.CorLibTypeFactory.String - }, specification.Signature.TypeArguments, _comparer); + }, specification.Signature?.TypeArguments, Comparer); } [Fact] public void ImportFieldFromExternalModuleShouldResultInMemberRef() { var type = new TypeReference(_dummyAssembly, null, "Type"); - var field = new MemberReference(type, "Field", - FieldSignature.CreateStatic(_module.CorLibTypeFactory.String)); + var field = new MemberReference( + type, + "Field", + new FieldSignature(_module.CorLibTypeFactory.String)); var result = _importer.ImportField(field); - Assert.Equal(field, result, _comparer); + Assert.Equal(field, result, Comparer); Assert.Same(_module, result.Module); } @@ -238,9 +272,12 @@ public void ImportFieldFromSameModuleShouldResultInSameInstance() { var type = new TypeDefinition(null, "Type", TypeAttributes.Public); _module.TopLevelTypes.Add(type); - - var field = new FieldDefinition("Method", FieldAttributes.Public | FieldAttributes.Static, - FieldSignature.CreateStatic(_module.CorLibTypeFactory.Void)); + + var field = new FieldDefinition( + "Field", + FieldAttributes.Public | FieldAttributes.Static, + _module.CorLibTypeFactory.Int32); + type.Fields.Add(field); var result = _importer.ImportField(field); @@ -251,13 +288,203 @@ public void ImportFieldFromSameModuleShouldResultInSameInstance() [Fact] public void ImportFieldFromReflectionShouldResultInMemberRef() { - var field = typeof(string).GetField("Empty"); + var field = typeof(string).GetField("Empty")!; var result = _importer.ImportField(field); - + Assert.Equal(field.Name, result.Name); - Assert.Equal(field.DeclaringType.FullName, result.DeclaringType.FullName); - Assert.Equal(field.FieldType.FullName, ((FieldSignature) result.Signature).FieldType.FullName); + 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); + } + + [Fact] + public void ImportInstanceFieldByReflectionShouldConstructValidFieldSignature() + { + // https://github.com/Washi1337/AsmResolver/issues/307 + + var module = ModuleDefinition.FromFile(typeof(SingleField).Assembly.Location); + var field = module.GetAllTypes() + .First(t => t.Name == nameof(SingleField)) + .Fields + .First(f => f.Name == nameof(SingleField.IntField)); + + var fieldInfo = typeof(SingleField).GetField(nameof(SingleField.IntField))!; + + var importer = new ReferenceImporter(module); + var imported = importer.ImportField(fieldInfo); + var resolved = imported.Resolve(); + + Assert.NotNull(resolved); + Assert.Equal(field, Assert.IsAssignableFrom(resolved), Comparer); } } -} \ No newline at end of file +} diff --git a/test/AsmResolver.DotNet.Tests/Resources/ActualLibrary.dll b/test/AsmResolver.DotNet.Tests/Resources/ActualLibrary.dll index 09112a3f8..ffcf8958c 100644 Binary files a/test/AsmResolver.DotNet.Tests/Resources/ActualLibrary.dll and b/test/AsmResolver.DotNet.Tests/Resources/ActualLibrary.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/DifferentNetVersion.Library.dll b/test/AsmResolver.DotNet.Tests/Resources/DifferentNetVersion.Library.dll new file mode 100644 index 000000000..b1e05392b Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/DifferentNetVersion.Library.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/DifferentNetVersion.MainApp.dll b/test/AsmResolver.DotNet.Tests/Resources/DifferentNetVersion.MainApp.dll new file mode 100644 index 000000000..e484ca0fd Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/DifferentNetVersion.MainApp.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/ForwarderLibrary.dll b/test/AsmResolver.DotNet.Tests/Resources/ForwarderLibrary.dll index 67599b738..a0376b479 100644 Binary files a/test/AsmResolver.DotNet.Tests/Resources/ForwarderLibrary.dll and b/test/AsmResolver.DotNet.Tests/Resources/ForwarderLibrary.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/ForwarderRefTest.exe b/test/AsmResolver.DotNet.Tests/Resources/ForwarderRefTest.exe index f994e3cde..99942219e 100644 Binary files a/test/AsmResolver.DotNet.Tests/Resources/ForwarderRefTest.exe and b/test/AsmResolver.DotNet.Tests/Resources/ForwarderRefTest.exe differ 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.DotNet.Tests/Resources/HelloWorld.UnusualNestedTypeRefOrder.exe b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.UnusualNestedTypeRefOrder.exe new file mode 100644 index 000000000..c70040376 Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.UnusualNestedTypeRefOrder.exe differ diff --git a/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.WithAttribute.dll b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.WithAttribute.dll new file mode 100644 index 000000000..1631efffe Binary files /dev/null and b/test/AsmResolver.DotNet.Tests/Resources/HelloWorld.WithAttribute.dll differ diff --git a/test/AsmResolver.DotNet.Tests/Signatures/GenericContextTest.cs b/test/AsmResolver.DotNet.Tests/Signatures/GenericContextTest.cs index aa9a55beb..2554c775c 100644 --- a/test/AsmResolver.DotNet.Tests/Signatures/GenericContextTest.cs +++ b/test/AsmResolver.DotNet.Tests/Signatures/GenericContextTest.cs @@ -272,8 +272,7 @@ public void ParseGenericFromField() var genericParameter = new GenericParameterSignature(GenericParameterType.Type, 0); - var field = new FieldDefinition("Field", FieldAttributes.Private, - FieldSignature.CreateStatic(genericParameter)); + var field = new FieldDefinition("Field", FieldAttributes.Private, genericParameter); var member = new MemberReference(typeSpecification, field.Name, field.Signature); @@ -292,8 +291,7 @@ public void ParseGenericFromNotGenericField() var type = new TypeDefinition("", "Test type", TypeAttributes.Public); var notGenericSignature = new TypeDefOrRefSignature(type); - var field = new FieldDefinition("Field", FieldAttributes.Private, - FieldSignature.CreateStatic(notGenericSignature)); + var field = new FieldDefinition("Field", FieldAttributes.Private, notGenericSignature); var member = new MemberReference(type, field.Name, field.Signature); diff --git a/test/AsmResolver.DotNet.Tests/Signatures/MethodSignatureTest.cs b/test/AsmResolver.DotNet.Tests/Signatures/MethodSignatureTest.cs new file mode 100644 index 000000000..0b271d859 --- /dev/null +++ b/test/AsmResolver.DotNet.Tests/Signatures/MethodSignatureTest.cs @@ -0,0 +1,47 @@ +using AsmResolver.DotNet.Signatures; +using Xunit; + +namespace AsmResolver.DotNet.Tests.Signatures +{ + public class MethodSignatureTest + { + private readonly ModuleDefinition _module; + + public MethodSignatureTest() + { + _module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld); + } + + [Fact] + public void MakeInstanceShouldHaveHasThisFlagSet() + { + var signature = MethodSignature.CreateInstance(_module.CorLibTypeFactory.Void); + Assert.True(signature.HasThis); + Assert.False(signature.IsGeneric); + } + + [Fact] + public void MakeStaticShouldNotHaveHasThisFlagSet() + { + var signature = MethodSignature.CreateStatic(_module.CorLibTypeFactory.Void); + Assert.False(signature.HasThis); + Assert.False(signature.IsGeneric); + } + + [Fact] + public void MakeGenericInstanceShouldHaveHasThisAndGenericFlagSet() + { + var signature = MethodSignature.CreateInstance(_module.CorLibTypeFactory.Void, 1); + Assert.True(signature.HasThis); + Assert.True(signature.IsGeneric); + } + + [Fact] + public void MakeGenericStaticShouldNotHaveHasThisAndGenericFlagSet() + { + var signature = MethodSignature.CreateStatic(_module.CorLibTypeFactory.Void, 1); + Assert.False(signature.HasThis); + Assert.True(signature.IsGeneric); + } + } +} diff --git a/test/AsmResolver.DotNet.Tests/Signatures/SignatureComparerTest.cs b/test/AsmResolver.DotNet.Tests/Signatures/SignatureComparerTest.cs index 80e67f070..5ae715869 100644 --- a/test/AsmResolver.DotNet.Tests/Signatures/SignatureComparerTest.cs +++ b/test/AsmResolver.DotNet.Tests/Signatures/SignatureComparerTest.cs @@ -1,6 +1,8 @@ using System; +using System.Linq; using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; +using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables.Rows; using Xunit; @@ -114,5 +116,70 @@ public void MatchPropertySignature() Assert.Equal(signature1, signature2, _comparer); } + + [Fact] + public void NestedTypesWithSameNameButDifferentDeclaringTypeShouldNotMatch() + { + var nestedTypes = ModuleDefinition.FromFile(typeof(SignatureComparerTest).Assembly.Location) + .GetAllTypes().First(t => t.Name == nameof(SignatureComparerTest)) + .NestedTypes.First(t => t.Name == nameof(NestedTypes)); + + var firstType = nestedTypes.NestedTypes + .First(t => t.Name == nameof(NestedTypes.FirstType)).NestedTypes + .First(t => t.Name == nameof(NestedTypes.FirstType.TypeWithCommonName)); + var secondType = nestedTypes.NestedTypes + .First(t => t.Name == nameof(NestedTypes.SecondType)).NestedTypes + .First(t => t.Name == nameof(NestedTypes.SecondType.TypeWithCommonName)); + + Assert.NotEqual(firstType, secondType, _comparer); + } + + [Fact] + public void MatchForwardedNestedTypes() + { + var module = ModuleDefinition.FromBytes(Properties.Resources.ForwarderRefTest); + var forwarder = ModuleDefinition.FromBytes(Properties.Resources.ForwarderLibrary).Assembly!; + var library = ModuleDefinition.FromBytes(Properties.Resources.ActualLibrary).Assembly!; + + module.MetadataResolver.AssemblyResolver.AddToCache(forwarder, forwarder); + module.MetadataResolver.AssemblyResolver.AddToCache(library, library); + forwarder.ManifestModule!.MetadataResolver.AssemblyResolver.AddToCache(library, library); + + var referencedTypes = module.ManagedEntrypointMethod!.CilMethodBody!.Instructions + .Where(i => i.OpCode.Code == CilCode.Call) + .Select(i => ((IMethodDefOrRef) i.Operand!).DeclaringType) + .Where(t => t.Name == "MyNestedClass") + .ToArray(); + + var type1 = referencedTypes[0]!; + var type2 = referencedTypes[1]!; + + var resolvedType1 = type1.Resolve(); + var resolvedType2 = type2.Resolve(); + + Assert.Equal(type1, resolvedType1, _comparer); + Assert.Equal(type2, resolvedType2, _comparer); + + Assert.NotEqual(type1, type2, _comparer); + Assert.NotEqual(type1, resolvedType2, _comparer); // Fails + Assert.NotEqual(type2, resolvedType1, _comparer); // Fails + } + + private class NestedTypes + { + public class FirstType + { + public class TypeWithCommonName + { + } + + } + public class SecondType + { + public class TypeWithCommonName + { + } + } + } } } diff --git a/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs b/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs index 1f10fbc54..c79e85572 100644 --- a/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs +++ b/test/AsmResolver.DotNet.Tests/Signatures/TypeNameParserTest.cs @@ -272,5 +272,32 @@ public void ReadTypeInCorLibAssemblyWithoutScope() var actual = TypeNameParser.Parse(_module, $"{ns}.{name}"); Assert.Equal(expected, actual, _comparer); } + + [Fact] + public void ReadCorLibTypeShouldNotUpdateScopesOfUnderlyingTypes() + { + // https://github.com/Washi1337/AsmResolver/issues/263 + + var scope = _module.CorLibTypeFactory.Object.Type.Scope; + TypeNameParser.Parse(_module, + "System.Object, System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.Same(scope, _module.CorLibTypeFactory.Object.Type.Scope); + } + + [Fact] + public void ReadTypeShouldReuseScopeInstanceWhenAvailable() + { + var type = TypeNameParser.Parse(_module, + "System.Array, System.Runtime, Version=4.2.2.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + Assert.Contains(type.Scope!.GetAssembly(), _module.AssemblyReferences); + } + + [Fact] + public void ReadTypeShouldUseNewScopeInstanceIfNotImportedYet() + { + var type = TypeNameParser.Parse(_module, + "SomeNamespace.SomeType, SomeAssembly, Version=1.2.3.4, Culture=neutral, PublicKeyToken=0123456789abcdef"); + Assert.DoesNotContain(type.Scope!.GetAssembly(), _module.AssemblyReferences); + } } } diff --git a/test/AsmResolver.DotNet.Tests/TokenAllocatorTest.cs b/test/AsmResolver.DotNet.Tests/TokenAllocatorTest.cs index 91a297084..d298a9c1a 100644 --- a/test/AsmResolver.DotNet.Tests/TokenAllocatorTest.cs +++ b/test/AsmResolver.DotNet.Tests/TokenAllocatorTest.cs @@ -98,10 +98,8 @@ public void AssignTokenOfNextMemberShouldPreserve() // Create two dummy fields. var fieldType = module.CorLibTypeFactory.Object; - var field1 = new FieldDefinition("NonAssignedField", FieldAttributes.Static, - FieldSignature.CreateStatic(fieldType)); - var field2 = new FieldDefinition("AssignedField", FieldAttributes.Static, - FieldSignature.CreateStatic(fieldType)); + var field1 = new FieldDefinition("NonAssignedField", FieldAttributes.Static, fieldType); + var field2 = new FieldDefinition("AssignedField", FieldAttributes.Static, fieldType); // Add both. var moduleType = module.GetOrCreateModuleType(); @@ -141,7 +139,34 @@ public void Issue187() allocator.AssignNextAvailableToken(method); targetModule.GetOrCreateModuleConstructor(); - var image = targetModule.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveAll)); + _ = targetModule.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveAll)); } + + [Fact] + public void Issue252() + { + // https://github.com/Washi1337/AsmResolver/issues/252 + + var module = ModuleDefinition.FromFile(typeof(TokenAllocatorTest).Assembly.Location); + var asmResRef = module.AssemblyReferences.First(a => a.Name == "AsmResolver.DotNet"); + + var reference = new TypeReference(module, asmResRef, "AsmResolver.DotNet", "MethodDefinition"); + var reference2 = new TypeReference(module, asmResRef, "AsmResolver.DotNet", "ModuleDefinition"); + + module.TokenAllocator.AssignNextAvailableToken(reference); + module.TokenAllocator.AssignNextAvailableToken(reference2); + + var image = module.ToPEImage(new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveAll)); + var newModule = ModuleDefinition.FromImage(image); + + var newReference = (TypeReference) newModule.LookupMember(reference.MetadataToken); + var newReference2 = (TypeReference) newModule.LookupMember(reference2.MetadataToken); + + Assert.Equal(reference.Namespace, newReference.Namespace); + Assert.Equal(reference.Name, newReference.Name); + Assert.Equal(reference2.Namespace, newReference2.Namespace); + Assert.Equal(reference2.Name, newReference2.Name); + } + } } diff --git a/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs b/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs index 451d5a373..ac2834c36 100644 --- a/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs +++ b/test/AsmResolver.DotNet.Tests/TypeDefinitionTest.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using AsmResolver.DotNet.Signatures; using AsmResolver.DotNet.Signatures.Types; using AsmResolver.DotNet.TestCases.CustomAttributes; using AsmResolver.DotNet.TestCases.Events; @@ -20,6 +21,8 @@ namespace AsmResolver.DotNet.Tests { public class TypeDefinitionTest { + private static readonly SignatureComparer Comparer = new(); + private TypeDefinition RebuildAndLookup(TypeDefinition type) { var stream = new MemoryStream(); @@ -547,5 +550,26 @@ public void InvalidMetadataLoopInBaseTypeShouldNotCrashIsValueType() Assert.False(typeB.IsValueType); Assert.False(typeB.IsEnum); } + + [Fact] + public void AddTypeWithCorLibBaseTypeToAssemblyWithCorLibTypeReferenceInAttribute() + { + // https://github.com/Washi1337/AsmResolver/issues/263 + + var module = ModuleDefinition.FromBytes(Properties.Resources.HelloWorld_WithAttribute); + var corlib = module.CorLibTypeFactory; + + var type = new TypeDefinition(null, "Test", TypeAttributes.Class, corlib.Object.Type); + module.TopLevelTypes.Add(type); + + var scope = corlib.Object.Scope; + + var newType = RebuildAndLookup(type); + Assert.Equal(newType.BaseType, type.BaseType, Comparer); + + Assert.Same(scope, corlib.Object.Scope); + var reference = Assert.IsAssignableFrom(corlib.Object.Scope!.GetAssembly()); + Assert.Same(module, reference.Module); + } } } diff --git a/test/AsmResolver.DotNet.Tests/TypeReferenceTest.cs b/test/AsmResolver.DotNet.Tests/TypeReferenceTest.cs index 0a2b835fb..a0a5ae2e5 100644 --- a/test/AsmResolver.DotNet.Tests/TypeReferenceTest.cs +++ b/test/AsmResolver.DotNet.Tests/TypeReferenceTest.cs @@ -1,11 +1,16 @@ -using System.Linq; +using System; +using AsmResolver.DotNet.Signatures; +using AsmResolver.DotNet.Signatures.Types; using AsmResolver.PE.DotNet.Metadata.Tables; +using AsmResolver.PE.DotNet.Metadata.Tables.Rows; using Xunit; namespace AsmResolver.DotNet.Tests { public class TypeReferenceTest { + private static readonly SignatureComparer Comparer = new(); + [Fact] public void ReadAssemblyRefScope() { @@ -13,7 +18,7 @@ public void ReadAssemblyRefScope() var typeRef = (TypeReference) module.LookupMember(new MetadataToken(TableIndex.TypeRef, 13)); Assert.Equal("mscorlib", typeRef.Scope.Name); } - + [Fact] public void ReadName() { @@ -21,7 +26,7 @@ public void ReadName() var typeRef = (TypeReference) module.LookupMember(new MetadataToken(TableIndex.TypeRef, 13)); Assert.Equal("Console", typeRef.Name); } - + [Fact] public void ReadNamespace() { @@ -29,5 +34,23 @@ public void ReadNamespace() var typeRef = (TypeReference) module.LookupMember(new MetadataToken(TableIndex.TypeRef, 13)); Assert.Equal("System", typeRef.Namespace); } + + [Fact] + public void CorLibTypeToTypeSignatureShouldReturnCorLibTypeSignature() + { + var module = new ModuleDefinition("SomeModule"); + var reference = new TypeReference(module, module.CorLibTypeFactory.CorLibScope, "System", "Object"); + var signature = Assert.IsAssignableFrom(reference.ToTypeSignature()); + Assert.Equal(ElementType.Object, signature.ElementType); + } + + [Fact] + public void NonCorLibTypeToTypeSignatureShouldReturnTypeDefOrRef() + { + var module = new ModuleDefinition("SomeModule"); + var reference = new TypeReference(module, module.CorLibTypeFactory.CorLibScope, "System", "Array"); + var signature = Assert.IsAssignableFrom(reference.ToTypeSignature()); + Assert.Equal(signature.Type, reference, Comparer); + } } -} \ No newline at end of file +} diff --git a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj index 600b80c66..ba56b3821 100644 --- a/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj +++ b/test/AsmResolver.PE.File.Tests/AsmResolver.PE.File.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -9,9 +9,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive 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/AsmResolver.PE.Tests.csproj b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj index e2f4044ef..2e5602e90 100644 --- a/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj +++ b/test/AsmResolver.PE.Tests/AsmResolver.PE.Tests.csproj @@ -1,12 +1,9 @@ - + netcoreapp3.1 - false - true - enable @@ -19,10 +16,10 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.PE.Tests/DotNet/Cil/CilInstructionTest.cs b/test/AsmResolver.PE.Tests/DotNet/Cil/CilInstructionTest.cs new file mode 100644 index 000000000..68573748b --- /dev/null +++ b/test/AsmResolver.PE.Tests/DotNet/Cil/CilInstructionTest.cs @@ -0,0 +1,64 @@ +using AsmResolver.PE.DotNet.Cil; +using Xunit; + +namespace AsmResolver.PE.Tests.DotNet.Cil +{ + public class CilInstructionTest + { + [Theory] + [InlineData(CilCode.Ldc_I4, 1234, 1234)] + [InlineData(CilCode.Ldc_I4_M1, null, -1)] + [InlineData(CilCode.Ldc_I4_0, null, 0)] + [InlineData(CilCode.Ldc_I4_1, null, 1)] + [InlineData(CilCode.Ldc_I4_2, null, 2)] + [InlineData(CilCode.Ldc_I4_3, null, 3)] + [InlineData(CilCode.Ldc_I4_4, null, 4)] + [InlineData(CilCode.Ldc_I4_5, null, 5)] + [InlineData(CilCode.Ldc_I4_6, null, 6)] + [InlineData(CilCode.Ldc_I4_7, null, 7)] + [InlineData(CilCode.Ldc_I4_8, null, 8)] + [InlineData(CilCode.Ldc_I4_S, (sbyte) -10, -10)] + public void GetLdcI4ConstantTest(CilCode code, object operand, int expectedValue) + { + var instruction = new CilInstruction(code.ToOpCode(), operand); + Assert.Equal(expectedValue, instruction.GetLdcI4Constant()); + } + + [Fact] + public void ReplaceWithOperand() + { + var instruction = new CilInstruction(CilOpCodes.Ldstr, "Hello, world!"); + + Assert.NotNull(instruction.Operand); + + instruction.ReplaceWith(CilOpCodes.Ldc_I4, 1337); + + Assert.Equal(CilOpCodes.Ldc_I4, instruction.OpCode); + Assert.Equal(1337, instruction.Operand); + } + + [Fact] + public void ReplaceWithoutOperandShouldRemoveOperand() + { + var instruction = new CilInstruction(CilOpCodes.Ldstr, "Hello, world!"); + + Assert.NotNull(instruction.Operand); + + instruction.ReplaceWith(CilOpCodes.Ldnull); + + Assert.Equal(CilOpCodes.Ldnull, instruction.OpCode); + Assert.Null(instruction.Operand); + } + + [Fact] + public void ReplaceWithNop() + { + var instruction = new CilInstruction(CilOpCodes.Ldstr, "Hello, world!"); + + instruction.ReplaceWithNop(); + + Assert.Equal(CilOpCodes.Nop, instruction.OpCode); + Assert.Null(instruction.Operand); + } + } +} 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.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj index 233b2df5b..c6ae10b54 100644 --- a/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj +++ b/test/AsmResolver.PE.Win32Resources.Tests/AsmResolver.PE.Win32Resources.Tests.csproj @@ -2,24 +2,19 @@ netcoreapp3.1 - false - AsmResolver.PE.Win32Resources.Tests - - 8 - warnings - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj new file mode 100644 index 000000000..18d5ed468 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/AsmResolver.Symbols.Pdb.Tests.csproj @@ -0,0 +1,42 @@ + + + + net6.0 + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + True + True + Resources.resx + + + + + + + + diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Dbi/DbiStreamTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Dbi/DbiStreamTest.cs new file mode 100644 index 000000000..889913627 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Dbi/DbiStreamTest.cs @@ -0,0 +1,213 @@ +using System; +using System.IO; +using System.Linq; +using AsmResolver.IO; +using AsmResolver.PE.File.Headers; +using AsmResolver.Symbols.Pdb.Metadata.Dbi; +using AsmResolver.Symbols.Pdb.Msf; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Metadata.Dbi; + +public class DbiStreamTest +{ + private DbiStream GetDbiStream(bool rebuild) + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var dbiStream = DbiStream.FromReader(file.Streams[DbiStream.StreamIndex].CreateReader()); + + if (rebuild) + { + using var stream = new MemoryStream(); + dbiStream.Write(new BinaryStreamWriter(stream)); + dbiStream = DbiStream.FromReader(ByteArrayDataSource.CreateReader(stream.ToArray())); + } + + return dbiStream; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Header(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(1u, dbiStream.Age); + Assert.Equal(DbiAttributes.None, dbiStream.Attributes); + Assert.Equal(MachineType.I386, dbiStream.Machine); + Assert.Equal(14, dbiStream.BuildMajorVersion); + Assert.Equal(29, dbiStream.BuildMinorVersion); + Assert.True(dbiStream.IsNewVersionFormat); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ModuleNames(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new[] + { + "* CIL *", + "C:\\Users\\Admin\\source\\repos\\AsmResolver\\test\\TestBinaries\\Native\\SimpleDll\\Release\\dllmain.obj", + "C:\\Users\\Admin\\source\\repos\\AsmResolver\\test\\TestBinaries\\Native\\SimpleDll\\Release\\pch.obj", + "* Linker Generated Manifest RES *", + "Import:KERNEL32.dll", + "KERNEL32.dll", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\sehprolg4.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\gs_cookie.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\gs_report.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\gs_support.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\guard_support.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\loadcfg.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\dyn_tls_init.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\ucrt_detection.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\cpu_disp.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\chandler4gs.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\secchk.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\argv_mode.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\default_local_stdio_options.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\tncleanup.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\dll_dllmain.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\initializers.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\utility.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\ucrt_stubs.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\utility_desktop.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\initsect.obj", + "D:\\a\\_work\\1\\s\\Intermediate\\vctools\\msvcrt.nativeproj_110336922\\objr\\x86\\x86_exception_filter.obj", + "VCRUNTIME140.dll", + "Import:VCRUNTIME140.dll", + "Import:api-ms-win-crt-runtime-l1-1-0.dll", + "api-ms-win-crt-runtime-l1-1-0.dll", + "* Linker *", + }, dbiStream.Modules.Select(m => m.ModuleName?.Value)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SectionContributions(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new (ushort, uint)[] + { + (1, 1669053862), (16, 2162654757), (20, 1635644926), (20, 3159649454), (20, 1649652954), (20, 3877379438), + (20, 4262788820), (20, 199934614), (8, 4235719287), (8, 1374843914), (9, 4241735292), (9, 2170796787), + (19, 1300950661), (19, 3968158929), (18, 3928463356), (18, 3928463356), (18, 2109213706), (22, 1457516325), + (22, 3939645857), (22, 1393694582), (22, 546064581), (22, 1976627334), (22, 513172946), (22, 25744891), + (22, 1989765812), (22, 2066266302), (22, 3810887196), (22, 206965504), (22, 647717352), (22, 3911072265), + (22, 3290064241), (12, 3928463356), (24, 2717331243), (24, 3687876222), (25, 2318145338), (25, 2318145338), + (6, 542071654), (15, 1810708069), (10, 3974941622), (14, 1150179208), (17, 2709606169), (13, 2361171624), + (28, 0), (28, 0), (28, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), + (23, 3467414241), (23, 4079273803), (26, 1282639619), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), + (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (5, 0), (28, 0), (28, 0), (28, 0), (27, 0), (29, 0), (29, 0), + (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (30, 0), (10, 2556510175), (21, 2556510175), + (21, 2556510175), (21, 2556510175), (21, 2556510175), (21, 2556510175), (21, 2556510175), (21, 2556510175), + (21, 2556510175), (20, 2556510175), (8, 4117779887), (31, 0), (11, 525614319), (31, 0), (31, 0), (31, 0), + (31, 0), (31, 0), (25, 2556510175), (25, 2556510175), (25, 2556510175), (25, 2556510175), (20, 3906165615), + (20, 1185345766), (20, 407658226), (22, 2869884627), (27, 0), (30, 0), (5, 0), (27, 0), (4, 0), (4, 0), + (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (5, 0), (28, 0), (28, 0), (28, 0), + (27, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (30, 0), (28, 0), (28, 0), + (28, 0), (27, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (29, 0), (30, 0), (4, 0), + (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (4, 0), (5, 0), (7, 4096381681), + (22, 454268333), (14, 1927129959), (23, 1927129959), (20, 0), (8, 0), (19, 0), (18, 0), (18, 0), (22, 0), + (24, 0), (10, 0), (14, 0), (2, 0), (31, 0), (3, 0), (3, 0) + }, dbiStream.SectionContributions.Select(x => (x.ModuleIndex, x.DataCrc))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SectionMaps(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new (ushort, ushort, ushort, ushort, ushort, ushort, uint, uint)[] + { + (0x010d, 0x0000, 0x0000, 0x0001, 0xffff, 0xffff, 0x00000000, 0x00000ce8), + (0x0109, 0x0000, 0x0000, 0x0002, 0xffff, 0xffff, 0x00000000, 0x00000834), + (0x010b, 0x0000, 0x0000, 0x0003, 0xffff, 0xffff, 0x00000000, 0x00000394), + (0x0109, 0x0000, 0x0000, 0x0004, 0xffff, 0xffff, 0x00000000, 0x000000f8), + (0x0109, 0x0000, 0x0000, 0x0005, 0xffff, 0xffff, 0x00000000, 0x0000013c), + (0x0208, 0x0000, 0x0000, 0x0000, 0xffff, 0xffff, 0x00000000, 0xffffffff), + }, + dbiStream.SectionMaps.Select(m => ((ushort) + m.Attributes, m.LogicalOverlayNumber, m.Group, m.Frame, + m.SectionName, m.ClassName, m.Offset, m.SectionLength))); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SourceFiles(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + string[][] firstThreeActualFileLists = dbiStream.SourceFiles + .Take(3) + .Select(x => x + .Select(y => y.ToString()) + .ToArray() + ).ToArray(); + + Assert.Equal(new[] + { + Array.Empty(), + new[] + { + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\pch.h", + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\dllmain.cpp", + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\Release\SimpleDll.pch", + }, + new[] + { + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winuser.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\basetsd.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winbase.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\stralign.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\guiddef.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared\winerror.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\corecrt_wstring.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\processthreadsapi.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\winnt.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\ctype.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\string.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\corecrt_memory.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um\memoryapi.h", + @"C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt\corecrt_memcpy_s.h", + @"C:\Users\Admin\source\repos\AsmResolver\test\TestBinaries\Native\SimpleDll\Release\SimpleDll.pch", + } + }, + firstThreeActualFileLists); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ExtraDebugIndices(bool rebuild) + { + var dbiStream = GetDbiStream(rebuild); + + Assert.Equal(new ushort[] + { + 0x7, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xB, 0xFFFF, 0xFFFF, 0xFFFF, 0xD, 0xFFFF + }, dbiStream.ExtraStreamIndices); + } + + [Fact] + public void SizeCalculation() + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var infoStream = DbiStream.FromReader(file.Streams[DbiStream.StreamIndex].CreateReader()); + + uint calculatedSize = infoStream.GetPhysicalSize(); + + using var stream = new MemoryStream(); + infoStream.Write(new BinaryStreamWriter(stream)); + + Assert.Equal(stream.Length, calculatedSize); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Info/InfoStreamTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Info/InfoStreamTest.cs new file mode 100644 index 000000000..03a19f01b --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/Info/InfoStreamTest.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; +using AsmResolver.IO; +using AsmResolver.Symbols.Pdb.Metadata.Info; +using AsmResolver.Symbols.Pdb.Msf; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Metadata.Info; + +public class InfoStreamTest +{ + private static InfoStream GetInfoStream(bool rebuild) + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var infoStream = InfoStream.FromReader(file.Streams[InfoStream.StreamIndex].CreateReader()); + + if (rebuild) + { + using var stream = new MemoryStream(); + infoStream.Write(new BinaryStreamWriter(stream)); + infoStream = InfoStream.FromReader(ByteArrayDataSource.CreateReader(stream.ToArray())); + } + + return infoStream; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Header(bool rebuild) + { + var infoStream = GetInfoStream(rebuild); + + Assert.Equal(InfoStreamVersion.VC70, infoStream.Version); + Assert.Equal(1u, infoStream.Age); + Assert.Equal(Guid.Parse("205dc366-d8f8-4175-8e06-26dd76722df5"), infoStream.UniqueId); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NameTable(bool rebuild) + { + var infoStream = GetInfoStream(rebuild); + + Assert.Equal(new Dictionary + { + ["/UDTSRCLINEUNDONE"] = 48, + ["/src/headerblock"] = 46, + ["/LinkInfo"] = 5, + ["/TMCache"] = 6, + ["/names"] = 12 + }, infoStream.StreamIndices); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void FeatureCodes(bool rebuild) + { + var infoStream = GetInfoStream(rebuild); + + Assert.Equal(new[] {PdbFeature.VC140}, infoStream.Features); + } + + [Fact] + public void SizeCalculation() + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + var infoStream = InfoStream.FromReader(file.Streams[InfoStream.StreamIndex].CreateReader()); + + uint calculatedSize = infoStream.GetPhysicalSize(); + + using var stream = new MemoryStream(); + infoStream.Write(new BinaryStreamWriter(stream)); + + Assert.Equal(stream.Length, calculatedSize); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Metadata/PdbHashTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/PdbHashTest.cs new file mode 100644 index 000000000..5ca3e791f --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Metadata/PdbHashTest.cs @@ -0,0 +1,18 @@ +using AsmResolver.Symbols.Pdb.Metadata; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Metadata; + +public class PdbHashTest +{ + [Theory] + [InlineData("/UDTSRCLINEUNDONE", 0x23296bb2)] + [InlineData("/src/headerblock", 0x2b237ecd)] + [InlineData("/LinkInfo", 0x282209ed)] + [InlineData("/TMCache", 0x2621d5e9)] + [InlineData("/names", 0x6d6cfc21)] + public void HashV1(string value, uint expected) + { + Assert.Equal(expected, PdbHash.ComputeV1(value)); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Msf/MsfFileTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Msf/MsfFileTest.cs new file mode 100644 index 000000000..b17cd4b36 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Msf/MsfFileTest.cs @@ -0,0 +1,27 @@ +using System.IO; +using System.Linq; +using AsmResolver.Symbols.Pdb.Msf; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Msf; + +public class MsfFileTest +{ + [Fact] + public void RoundTrip() + { + var file = MsfFile.FromBytes(Properties.Resources.SimpleDllPdb); + + using var stream = new MemoryStream(); + file.Write(stream); + + var newFile = MsfFile.FromBytes(stream.ToArray()); + + Assert.Equal(file.BlockSize, newFile.BlockSize); + Assert.Equal(file.Streams.Count, newFile.Streams.Count); + Assert.All(Enumerable.Range(0, file.Streams.Count), i => + { + Assert.Equal(file.Streams[i].CreateReader().ReadToEnd(), newFile.Streams[i].CreateReader().ReadToEnd());; + }); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Msf/MsfStreamDataSourceTest.cs b/test/AsmResolver.Symbols.Pdb.Tests/Msf/MsfStreamDataSourceTest.cs new file mode 100644 index 000000000..c61fa8dc1 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Msf/MsfStreamDataSourceTest.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using AsmResolver.IO; +using AsmResolver.Symbols.Pdb.Msf; +using Xunit; + +namespace AsmResolver.Symbols.Pdb.Tests.Msf; + +public class MsfStreamDataSourceTest +{ + [Fact] + public void EmptyStream() + { + var source = new MsfStreamDataSource(0, 0x200, Array.Empty()); + + byte[] buffer = new byte[0x1000]; + int readCount = source.ReadBytes(0, buffer, 0, buffer.Length); + Assert.Equal(0, readCount); + Assert.All(buffer, b => Assert.Equal(0, b)); + } + + [Theory] + [InlineData(0x200, 0x200)] + [InlineData(0x200, 0x100)] + public void StreamWithOneBlock(int blockSize, int actualSize) + { + byte[] block = new byte[blockSize]; + for (int i = 0; i < blockSize; i++) + block[i] = (byte) (i & 0xFF); + + var source = new MsfStreamDataSource((ulong) actualSize, (uint) blockSize, new[] {block}); + + byte[] buffer = new byte[0x1000]; + int readCount = source.ReadBytes(0, buffer, 0, buffer.Length); + Assert.Equal(actualSize, readCount); + Assert.Equal(block.Take(actualSize), buffer.Take(actualSize)); + } + + [Theory] + [InlineData(0x200, 0x400)] + [InlineData(0x200, 0x300)] + public void StreamWithTwoBlocks(int blockSize, int actualSize) + { + byte[] block1 = new byte[blockSize]; + for (int i = 0; i < blockSize; i++) + block1[i] = (byte) 'A'; + + byte[] block2 = new byte[blockSize]; + for (int i = 0; i < blockSize; i++) + block2[i] = (byte) 'B'; + + var source = new MsfStreamDataSource((ulong) actualSize, (uint) blockSize, new[] {block1, block2}); + + byte[] buffer = new byte[0x1000]; + int readCount = source.ReadBytes(0, buffer, 0, buffer.Length); + Assert.Equal(actualSize, readCount); + Assert.Equal(block1.Concat(block2).Take(actualSize), buffer.Take(actualSize)); + } + + [Theory] + [InlineData(0x200, 0x400)] + public void ReadInMiddleOfBlock(int blockSize, int actualSize) + { + byte[] block1 = new byte[blockSize]; + for (int i = 0; i < blockSize; i++) + block1[i] = (byte) ((i*2) & 0xFF); + + byte[] block2 = new byte[blockSize]; + for (int i = 0; i < blockSize; i++) + block2[i] = (byte) ((i * 2 + 1) & 0xFF); + + var source = new MsfStreamDataSource((ulong) actualSize, (uint) blockSize, new[] {block1, block2}); + + byte[] buffer = new byte[blockSize]; + int readCount = source.ReadBytes((ulong) blockSize / 4, buffer, 0, blockSize); + Assert.Equal(blockSize, readCount); + Assert.Equal(block1.Skip(blockSize / 4).Concat(block2).Take(blockSize), buffer); + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Properties/Resources.Designer.cs b/test/AsmResolver.Symbols.Pdb.Tests/Properties/Resources.Designer.cs new file mode 100644 index 000000000..b115b2929 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Properties/Resources.Designer.cs @@ -0,0 +1,55 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AsmResolver.Symbols.Pdb.Tests.Properties { + using System; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static System.Resources.ResourceManager resourceMan; + + private static System.Globalization.CultureInfo resourceCulture; + + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { + get { + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("AsmResolver.Symbols.Pdb.Tests.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static byte[] SimpleDllPdb { + get { + object obj = ResourceManager.GetObject("SimpleDllPdb", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Properties/Resources.resx b/test/AsmResolver.Symbols.Pdb.Tests/Properties/Resources.resx new file mode 100644 index 000000000..64f81d46b --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Properties/Resources.resx @@ -0,0 +1,24 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + ..\Resources\SimpleDll.pdb;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Resources/.gitignore b/test/AsmResolver.Symbols.Pdb.Tests/Resources/.gitignore new file mode 100644 index 000000000..bd46a47b5 --- /dev/null +++ b/test/AsmResolver.Symbols.Pdb.Tests/Resources/.gitignore @@ -0,0 +1 @@ +!*.pdb diff --git a/test/AsmResolver.Symbols.Pdb.Tests/Resources/SimpleDll.pdb b/test/AsmResolver.Symbols.Pdb.Tests/Resources/SimpleDll.pdb new file mode 100644 index 000000000..2a5f3d4ad Binary files /dev/null and b/test/AsmResolver.Symbols.Pdb.Tests/Resources/SimpleDll.pdb differ diff --git a/test/AsmResolver.Tests/AsmResolver.Tests.csproj b/test/AsmResolver.Tests/AsmResolver.Tests.csproj index e333759d2..82f9cb9c6 100644 --- a/test/AsmResolver.Tests/AsmResolver.Tests.csproj +++ b/test/AsmResolver.Tests/AsmResolver.Tests.csproj @@ -1,19 +1,16 @@ - + netcoreapp3.1 - false - true - enable - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/test/AsmResolver.Tests/Collections/BitListTest.cs b/test/AsmResolver.Tests/Collections/BitListTest.cs new file mode 100644 index 000000000..402f8ec2e --- /dev/null +++ b/test/AsmResolver.Tests/Collections/BitListTest.cs @@ -0,0 +1,111 @@ +using System.Linq; +using AsmResolver.Collections; +using Xunit; + +namespace AsmResolver.Tests.Collections +{ + public class BitListTest + { + [Fact] + public void Add() + { + var list = new BitList + { + true, + false, + true, + true, + false, + }; + + Assert.Equal(new[] + { + true, + false, + true, + true, + false + }, list.ToArray()); + } + + [Fact] + public void Insert() + { + var list = new BitList + { + true, + false, + true, + true, + false, + }; + + list.Insert(1, true); + + Assert.Equal(new[] + { + true, + true, + false, + true, + true, + false + }, list.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void InsertIntoLarge(bool parity) + { + var list = new BitList(); + for (int i = 0; i < 100; i++) + list.Add(i % 2 == 0 == parity); + + list.Insert(0, !parity); + + Assert.Equal(101, list.Count); + bool[] expected = Enumerable.Range(0, 101).Select(i => i % 2 == 1 == parity).ToArray(); + Assert.Equal(expected, list.ToArray()); + } + + [Fact] + public void RemoveAt() + { + var list = new BitList + { + true, + false, + true, + true, + false, + }; + + list.RemoveAt(3); + + Assert.Equal(new[] + { + true, + false, + true, + false, + }, list.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RemoveAtLarge(bool parity) + { + var list = new BitList(); + for (int i = 0; i < 100; i++) + list.Add(i % 2 == 0 == parity); + + list.RemoveAt(0); + + Assert.Equal(99, list.Count); + bool[] expected = Enumerable.Range(0, 99).Select(i => i % 2 == 1 == parity).ToArray(); + Assert.Equal(expected, list.ToArray()); + } + } +} diff --git a/test/AsmResolver.Tests/Collections/RefListTest.cs b/test/AsmResolver.Tests/Collections/RefListTest.cs index 8c7b5f201..5f7c0adae 100644 --- a/test/AsmResolver.Tests/Collections/RefListTest.cs +++ b/test/AsmResolver.Tests/Collections/RefListTest.cs @@ -1,3 +1,4 @@ +using System; using AsmResolver.Collections; using Xunit; @@ -11,6 +12,47 @@ public void EmptyList() Assert.Empty(new RefList()); } + [Fact] + public void SetCapacityToCount() + { + var list = new RefList + { + 1, + 2, + 3 + }; + + list.Capacity = 3; + Assert.True(list.Capacity >= 3); + } + + [Fact] + public void SetCapacityToLargerThanCount() + { + var list = new RefList + { + 1, + 2, + 3 + }; + + list.Capacity = 6; + Assert.True(list.Capacity >= 6); + } + + [Fact] + public void SetCapacityToSmallerThanCountShouldThrow() + { + var list = new RefList + { + 1, + 2, + 3 + }; + + Assert.Throws(() => list.Capacity = 2); + } + [Fact] public void AddItemShouldUpdateVersion() { diff --git a/test/AsmResolver.Tests/IO/DataSourceSliceTest.cs b/test/AsmResolver.Tests/IO/DataSourceSliceTest.cs new file mode 100644 index 000000000..937e79993 --- /dev/null +++ b/test/AsmResolver.Tests/IO/DataSourceSliceTest.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using AsmResolver.IO; +using Xunit; + +namespace AsmResolver.Tests.IO +{ + public class DataSourceSliceTest + { + private readonly IDataSource _source = new ByteArrayDataSource(new byte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + }); + + [Fact] + public void EmptySlice() + { + var slice = new DataSourceSlice(_source, 0, 0); + Assert.Equal(0ul, slice.Length); + } + + [Fact] + public void SliceStart() + { + var slice = new DataSourceSlice(_source, 0, 5); + Assert.Equal(5ul, slice.Length); + Assert.All(Enumerable.Range(0, 5), i => Assert.Equal(slice[(ulong) i], _source[(ulong) i])); + Assert.Throws(() => slice[5]); + } + + [Fact] + public void SliceMiddle() + { + var slice = new DataSourceSlice(_source, 3, 5); + Assert.Equal(5ul, slice.Length); + Assert.All(Enumerable.Range(3, 5), i => Assert.Equal(slice[(ulong) i], _source[(ulong) i])); + Assert.Throws(() => slice[3 - 1]); + Assert.Throws(() => slice[3 + 5]); + } + + [Fact] + public void SliceEnd() + { + var slice = new DataSourceSlice(_source, 5, 5); + Assert.Equal(5ul, slice.Length); + Assert.All(Enumerable.Range(5, 5), i => Assert.Equal(slice[(ulong) i], _source[(ulong) i])); + Assert.Throws(() => slice[5 - 1]); + } + + [Fact] + public void ReadSlicedShouldReadUpToSliceAmountOfBytes() + { + var slice = new DataSourceSlice(_source, 3, 5); + + byte[] data1 = new byte[7]; + int originalCount = _source.ReadBytes(3, data1, 0, data1.Length); + Assert.Equal(7, originalCount); + + byte[] data2 = new byte[3]; + int newCount = slice.ReadBytes(3, data2, 0, data2.Length); + Assert.Equal(3, newCount); + Assert.Equal(data1.Take(3), data2.Take(3)); + + byte[] data3 = new byte[7]; + int newCount2 = slice.ReadBytes(3, data3, 0, data3.Length); + Assert.Equal(5, newCount2); + Assert.Equal(data1.Take(5), data3.Take(5)); + } + } +} diff --git a/test/AsmResolver.Tests/IO/MemoryStreamReaderPoolTest.cs b/test/AsmResolver.Tests/IO/MemoryStreamReaderPoolTest.cs new file mode 100644 index 000000000..b59f492d6 --- /dev/null +++ b/test/AsmResolver.Tests/IO/MemoryStreamReaderPoolTest.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Linq; +using AsmResolver.IO; +using Xunit; + +namespace AsmResolver.Tests.IO +{ + public class MemoryStreamReaderPoolTest + { + private readonly MemoryStreamWriterPool _pool = new(); + + [Fact] + public void RentShouldStartWithEmptyStream() + { + using (var rent = _pool.Rent()) + { + Assert.Equal(0u, rent.Writer.Length); + rent.Writer.WriteInt64(0x0123456789abcdef); + Assert.Equal(8u, rent.Writer.Length); + } + + using (var rent = _pool.Rent()) + { + Assert.Equal(0u, rent.Writer.Length); + } + } + + [Fact] + public void RentBeforeDisposeShouldUseNewBackendStream() + { + using var rent1 = _pool.Rent(); + using var rent2 = _pool.Rent(); + Assert.NotSame(rent1.Writer.BaseStream, rent2.Writer.BaseStream); + } + + [Fact] + public void RentAfterDisposeShouldReuseBackendStream() + { + Stream stream; + using (var rent = _pool.Rent()) + { + stream = rent.Writer.BaseStream; + } + + using (var rent = _pool.Rent()) + { + Assert.Same(stream, rent.Writer.BaseStream); + } + } + + [Fact] + public void RentAfterDisposeShouldReuseBackendStream2() + { + var rent1 = _pool.Rent(); + var rent2 = _pool.Rent(); + var stream2 = rent2.Writer.BaseStream; + var rent3 = _pool.Rent(); + + rent2.Dispose(); + var rent4 = _pool.Rent(); + + Assert.Same(stream2, rent4.Writer.BaseStream); + } + + [Fact] + public void GetFinalData() + { + byte[] value = Enumerable.Range(0, 255) + .Select(x => (byte) x) + .ToArray(); + + using var rent = _pool.Rent(); + rent.Writer.WriteBytes(value); + rent.Writer.WriteBytes(value); + Assert.Equal(value.Concat(value), rent.GetData()); + } + + [Fact] + public void UseAfterDisposeShouldThrow() + { + Assert.Throws(() => + { + var rent = _pool.Rent(); + rent.Dispose(); + rent.Writer.WriteInt64(0); + }); + + Assert.Throws(() => + { + var rent = _pool.Rent(); + rent.Dispose(); + return rent.GetData(); + }); + } + + [Fact] + public void DisposeTwiceShouldNotReturnTwice() + { + var rent1 = _pool.Rent(); + var stream = rent1.Writer.BaseStream; + + rent1.Dispose(); + rent1.Dispose(); + + var rent2 = _pool.Rent(); + var rent3 = _pool.Rent(); + + Assert.Same(stream, rent2.Writer.BaseStream); + Assert.NotSame(stream, rent3.Writer.BaseStream); + } + + [Fact] + public void GetDataShouldNotResultInSameInstance() + { + byte[] data1; + + using (var rent1 = _pool.Rent()) + { + rent1.Writer.WriteInt64(0x0123456789abcdef); + data1 = rent1.GetData(); + byte[] data2 = rent1.GetData(); + Assert.Equal(data1, data2); + Assert.NotSame(data1, data2); + } + + using (var rent2 = _pool.Rent()) + { + rent2.Writer.WriteInt64(0x0123456789abcdef); + byte[] data3 = rent2.GetData(); + Assert.Equal(data1, data3); + Assert.NotSame(data1, data3); + } + } + } +} 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); diff --git a/test/AsmResolver.Workspaces.DotNet.Tests/AbstractInheritanceTest.cs b/test/AsmResolver.Workspaces.DotNet.Tests/AbstractInheritanceTest.cs new file mode 100644 index 000000000..d79ae442f --- /dev/null +++ b/test/AsmResolver.Workspaces.DotNet.Tests/AbstractInheritanceTest.cs @@ -0,0 +1,46 @@ +using AsmResolver.DotNet; +using AsmResolver.Workspaces.DotNet.TestCases; +using System.Linq; +using Xunit; + +namespace AsmResolver.Workspaces.DotNet.Tests +{ + public class AbstractInheritanceTest : IClassFixture + { + private readonly TestCasesFixture _fixture; + + public AbstractInheritanceTest(TestCasesFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void InterfaceImplementation() + { + var module = _fixture.WorkspacesAssembly.ManifestModule; + var abstractType = (TypeDefinition)module.LookupMember(typeof(MyAboveAbstractClass).MetadataToken); + var abstractMethod = abstractType.Methods.First(m => m.Name == nameof(MyAboveAbstractClass.TestAboveAbstract)); + + var implType1 = (TypeDefinition)module.LookupMember(typeof(MyDerivedAboveClass).MetadataToken); + var implMethod1 = implType1.Methods.First(m => m.Name == nameof(MyAboveAbstractClass.TestAboveAbstract)); + + var implType2 = (TypeDefinition)module.LookupMember(typeof(MyDerivedInbetweenClass).MetadataToken); + var implMethod2 = implType2.Methods.First(m => m.Name == nameof(MyAboveAbstractClass.TestAboveAbstract)); + + var implType3 = (TypeDefinition)module.LookupMember(typeof(MyDerivedClassGeneric).MetadataToken); + var implMethod3 = implType3.Methods.First(m => m.Name == nameof(MyAboveAbstractClass.TestAboveAbstract)); + + + var workspace = new DotNetWorkspace(); + workspace.Assemblies.Add(_fixture.WorkspacesAssembly); + workspace.Analyze(); + + var node = workspace.Index.GetOrCreateNode(abstractMethod); + var implMethods = node.BackwardRelations.GetObjects(DotNetRelations.ImplementationMethod); + + Assert.Contains(implMethod1, implMethods); + Assert.Contains(implMethod2, implMethods); + Assert.Contains(implMethod3, implMethods); + } + } +} diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj index 261ad5bf0..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.CustomAttributes/AsmResolver.DotNet.TestCases.CustomAttributes.csproj @@ -1,7 +1,7 @@ - - - - netstandard2.0 - - - + + + + netstandard2.0 + + + diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Events/AsmResolver.DotNet.TestCases.Events.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Events/AsmResolver.DotNet.TestCases.Events.csproj index 261ad5bf0..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Events/AsmResolver.DotNet.TestCases.Events.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Events/AsmResolver.DotNet.TestCases.Events.csproj @@ -1,7 +1,7 @@ - - - - netstandard2.0 - - - + + + + netstandard2.0 + + + diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Fields/AsmResolver.DotNet.TestCases.Fields.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Fields/AsmResolver.DotNet.TestCases.Fields.csproj index d2a210cda..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Fields/AsmResolver.DotNet.TestCases.Fields.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Fields/AsmResolver.DotNet.TestCases.Fields.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Generics/AsmResolver.DotNet.TestCases.Generics.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Generics/AsmResolver.DotNet.TestCases.Generics.csproj index d2a210cda..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Generics/AsmResolver.DotNet.TestCases.Generics.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Generics/AsmResolver.DotNet.TestCases.Generics.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/AsmResolver.DotNet.TestCases.Methods.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/AsmResolver.DotNet.TestCases.Methods.csproj index 08f7f347e..a17fd4c4a 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/AsmResolver.DotNet.TestCases.Methods.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Methods/AsmResolver.DotNet.TestCases.Methods.csproj @@ -1,11 +1,11 @@ - - - - netstandard2.0 - - - - - - - + + + + netstandard2.0 + + + + + + + diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.NestedClasses/AsmResolver.DotNet.TestCases.NestedClasses.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.NestedClasses/AsmResolver.DotNet.TestCases.NestedClasses.csproj index d2a210cda..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.NestedClasses/AsmResolver.DotNet.TestCases.NestedClasses.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.NestedClasses/AsmResolver.DotNet.TestCases.NestedClasses.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Properties/AsmResolver.DotNet.TestCases.Properties.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Properties/AsmResolver.DotNet.TestCases.Properties.csproj index 261ad5bf0..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Properties/AsmResolver.DotNet.TestCases.Properties.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Properties/AsmResolver.DotNet.TestCases.Properties.csproj @@ -1,7 +1,7 @@ - - - - netstandard2.0 - - - + + + + netstandard2.0 + + + diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Resources/AsmResolver.DotNet.TestCases.Resources.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Resources/AsmResolver.DotNet.TestCases.Resources.csproj index d22f09dba..da037df75 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Resources/AsmResolver.DotNet.TestCases.Resources.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Resources/AsmResolver.DotNet.TestCases.Resources.csproj @@ -2,7 +2,6 @@ netstandard2.0 - 8 true @@ -25,8 +24,8 @@ - - + + diff --git a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Types/AsmResolver.DotNet.TestCases.Types.csproj b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Types/AsmResolver.DotNet.TestCases.Types.csproj index 261ad5bf0..27560206d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Types/AsmResolver.DotNet.TestCases.Types.csproj +++ b/test/TestBinaries/DotNet/AsmResolver.DotNet.TestCases.Types/AsmResolver.DotNet.TestCases.Types.csproj @@ -1,7 +1,7 @@ - - - - netstandard2.0 - - - + + + + netstandard2.0 + + + diff --git a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyAboveAbstractClass.cs b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyAboveAbstractClass.cs new file mode 100644 index 000000000..c8cba7945 --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyAboveAbstractClass.cs @@ -0,0 +1,7 @@ +namespace AsmResolver.Workspaces.DotNet.TestCases +{ + public abstract class MyAboveAbstractClass + { + public abstract int TestAboveAbstract(); + } +} diff --git a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyClassGeneric.cs b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyClassGeneric.cs index 90bc72e68..5a1e5406d 100644 --- a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyClassGeneric.cs +++ b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyClassGeneric.cs @@ -1,6 +1,6 @@ namespace AsmResolver.Workspaces.DotNet.TestCases { - public abstract class MyClassGeneric : IMyInterfaceGeneric + public abstract class MyClassGeneric : MyInbetweenAbstractClass, IMyInterfaceGeneric { /// void IMyInterfaceGeneric.Explicit() diff --git a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedAboveClass.cs b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedAboveClass.cs new file mode 100644 index 000000000..cb86cb86e --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedAboveClass.cs @@ -0,0 +1,7 @@ +namespace AsmResolver.Workspaces.DotNet.TestCases +{ + public class MyDerivedAboveClass : MyAboveAbstractClass + { + public override int TestAboveAbstract() => 1; + } +} diff --git a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedClassGeneric.cs b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedClassGeneric.cs index f870c5574..dc67bc6e3 100644 --- a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedClassGeneric.cs +++ b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedClassGeneric.cs @@ -11,7 +11,9 @@ public override int GenericMethod(float x) public override bool Implicit() => true; public new virtual int Shadowed(int x) => 1; - + + public override int TestAboveAbstract() => 3; + /// public override int ImplicitP { get; set; } = 1; diff --git a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedInbetweenClass.cs b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedInbetweenClass.cs new file mode 100644 index 000000000..f669d19df --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyDerivedInbetweenClass.cs @@ -0,0 +1,7 @@ +namespace AsmResolver.Workspaces.DotNet.TestCases +{ + public class MyDerivedInbetweenClass : MyInbetweenAbstractClass + { + public override int TestAboveAbstract() => 2; + } +} diff --git a/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyInbetweenAbstractClass.cs b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyInbetweenAbstractClass.cs new file mode 100644 index 000000000..4ad9bc4d4 --- /dev/null +++ b/test/TestBinaries/DotNet/AsmResolver.Workspaces.DotNet.TestCases/MyInbetweenAbstractClass.cs @@ -0,0 +1,6 @@ +namespace AsmResolver.Workspaces.DotNet.TestCases +{ + public abstract class MyInbetweenAbstractClass : MyAboveAbstractClass + { + } +} diff --git a/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj b/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj index a2a134785..21604ba0d 100644 --- a/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj +++ b/test/TestBinaries/DotNet/HelloWorld/HelloWorld.csproj @@ -10,6 +10,6 @@ AnyCPU - + diff --git a/test/TestBinaries/DotNet/HelloWorld/Program.cs b/test/TestBinaries/DotNet/HelloWorld/Program.cs index 848fe59f9..575ec4cc4 100644 --- a/test/TestBinaries/DotNet/HelloWorld/Program.cs +++ b/test/TestBinaries/DotNet/HelloWorld/Program.cs @@ -9,4 +9,4 @@ private static void Main(string[] args) Console.WriteLine("Hello, World!"); } } -} \ No newline at end of file +} diff --git a/test/TestBinaries/DotNet/TheAnswer/TheAnswer.csproj b/test/TestBinaries/DotNet/TheAnswer/TheAnswer.csproj index d67905bb3..264964033 100644 --- a/test/TestBinaries/DotNet/TheAnswer/TheAnswer.csproj +++ b/test/TestBinaries/DotNet/TheAnswer/TheAnswer.csproj @@ -5,7 +5,7 @@ net47;netcoreapp3.1 - + diff --git a/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.dll b/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.dll index 09112a3f8..ffcf8958c 100644 Binary files a/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.dll and b/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.dll differ diff --git a/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.il b/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.il index 34e8a8e9e..2fb8b287a 100644 --- a/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.il +++ b/test/TestBinaries/DotNet/TypeForwarding/ActualLibrary.il @@ -6,12 +6,38 @@ } .class public abstract sealed MyClass - extends [mscorlib] System.Object + extends [mscorlib] System.Object { .method public static hidebysig void StaticMethod(class IMyModel argument) { ldarg.0 call instance void IMyModel::MyMethod() - ret + ret } -} \ No newline at end of file + + .class nested public abstract sealed MyNestedClass + extends [mscorlib] System.Object + { + .method public static hidebysig void NestedStaticMethod() + { + ldstr "MyClass+MyNestedClass::NestedStaticMethod" + call void [mscorlib] System.Console::WriteLine(string) + ret + } + } +} + +.class public abstract sealed MyClass2 + extends [mscorlib] System.Object +{ + .class nested public abstract sealed MyNestedClass + extends [mscorlib] System.Object + { + .method public static hidebysig void NestedStaticMethod() + { + ldstr "MyClass2+MyNestedClass::NestedStaticMethod" + call void [mscorlib] System.Console::WriteLine(string) + ret + } + } +} diff --git a/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.dll b/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.dll index 67599b738..a0376b479 100644 Binary files a/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.dll and b/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.dll differ diff --git a/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.il b/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.il index 081e1c656..c27dd7dbc 100644 --- a/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.il +++ b/test/TestBinaries/DotNet/TypeForwarding/ForwarderLibrary.il @@ -12,4 +12,19 @@ .class extern forwarder MyClass { .assembly extern ActualLibrary -} \ No newline at end of file +} + +.class extern forwarder MyNestedClass +{ + .class extern MyClass +} + +.class extern forwarder MyClass2 +{ + .assembly extern ActualLibrary +} + +.class extern forwarder MyNestedClass +{ + .class extern MyClass2 +} diff --git a/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.exe b/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.exe index f994e3cde..99942219e 100644 Binary files a/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.exe and b/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.exe differ diff --git a/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.il b/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.il index abbac6f56..67c5c3611 100644 --- a/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.il +++ b/test/TestBinaries/DotNet/TypeForwarding/ForwarderRefTest.il @@ -5,7 +5,7 @@ } .class public auto ansi MyModel - extends [mscorlib] System.Object + extends [mscorlib] System.Object implements [ForwarderLibrary] IMyModel { .method public hidebysig specialname rtspecialname void .ctor() @@ -15,7 +15,7 @@ ret } - .method public virtual hidebysig void MyMethod() + .method public virtual hidebysig void MyMethod() { ldstr "Hello, world!" call void [mscorlib] System.Console::WriteLine(string) @@ -29,5 +29,8 @@ newobj void MyModel::.ctor() call void [ForwarderLibrary] MyClass::StaticMethod(class [ForwarderLibrary] IMyModel argument) + + call void [ForwarderLibrary] MyClass/MyNestedClass::NestedStaticMethod() + call void [ForwarderLibrary] MyClass2/MyNestedClass::NestedStaticMethod() ret -} \ No newline at end of file +}