diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs new file mode 100644 index 000000000..00df00eeb --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/DuplicateAssemblyReferenceTests.cs @@ -0,0 +1,200 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Tests.Helpers; +using ICSharpCode.Decompiler.TypeSystem; + +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Tests.TypeSystem +{ + /// + /// Obfuscated and merged assemblies can end up referencing two versions of the same + /// assembly. Loading both splits every type they declare into two type definitions that + /// compare unequal, which in turn breaks every signature comparison spanning the two + /// references: a method whose parameter is named through one of them no longer matches the + /// base method that names it through the other. The fixture reproduces that through the + /// symptom it surfaced as in issue #2764 - a genuine override printed as virtual, because + /// found no base member for it. + /// + [TestFixture] + public class DuplicateAssemblyReferenceTests + { + /// + /// Assembled twice, as version 1.0.0.0 and 2.0.0.0. + /// + const string LibraryIL = @" +.assembly extern System.Runtime +{ + .ver 8:0:0:0 +} +.assembly Lib +{ + .ver {VERSION}:0:0:0 +} +.class public auto ansi beforefieldinit Param + extends [System.Runtime]System.Object +{ +} +"; + + /// + /// Declares both versions of Lib as references, the way a merging obfuscator leaves + /// them behind, and names Param through a different one in the base declaration than + /// in the override. Both are the same type to the runtime, which unifies the two + /// references, so Run really does override Base.Run. + /// + const string MainIL = @" +.assembly extern Lib +{ + .ver 2:0:0:0 +} +.assembly extern Lib as Lib_v1 +{ + .ver 1:0:0:0 +} +.assembly extern System.Runtime +{ + .ver 8:0:0:0 +} +.assembly Main { } + +.class public abstract auto ansi beforefieldinit Base + extends [System.Runtime]System.Object +{ + .method family hidebysig specialname rtspecialname instance void .ctor() cil managed + { + ldarg.0 + call instance void [System.Runtime]System.Object::.ctor() + ret + } + .method public hidebysig newslot abstract virtual instance void Run(class [Lib_v1]Param p) cil managed + { + } +} + +.class public auto ansi beforefieldinit Derived + extends Base +{ + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + ldarg.0 + call instance void Base::.ctor() + ret + } + .method public hidebysig virtual instance void Run(class [Lib]Param p) cil managed + { + ret + } +} +"; + + string directory; + string mainAssemblyPath; + + [OneTimeSetUp] + public async Task SetUp() + { + directory = Path.Combine(Path.GetTempPath(), "ILSpy-DuplicateAssemblyReference-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + foreach (var version in new[] { 1, 2 }) + { + await AssembleAsync($"Lib.v{version}", LibraryIL.Replace("{VERSION}", version.ToString())).ConfigureAwait(false); + } + mainAssemblyPath = await AssembleAsync("Main", MainIL).ConfigureAwait(false); + } + + [OneTimeTearDown] + public void TearDown() + { + Directory.Delete(directory, recursive: true); + } + + Task AssembleAsync(string name, string il) + { + string sourceFile = Path.Combine(directory, name + ".il"); + File.WriteAllText(sourceFile, il); + return Tester.AssembleIL(sourceFile, AssemblerOptions.Library); + } + + DecompilerTypeSystem CreateTypeSystem() + { + var mainModule = new PEFile(mainAssemblyPath); + return new DecompilerTypeSystem(mainModule, new VersionedResolver(directory)); + } + + [Test] + public void OnlyTheHighestVersionOfADuplicatedReferenceIsLoaded() + { + var libraries = CreateTypeSystem().Modules + .Where(m => m.AssemblyName == "Lib") + .Select(m => m.FullAssemblyName) + .ToArray(); + + Assert.That(libraries, Is.EqualTo(new[] { "Lib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null" })); + } + + [Test] + public void OverrideDeclaredThroughTheOtherReferenceStaysAnOverride() + { + var decompiler = new CSharpDecompiler(CreateTypeSystem(), new DecompilerSettings()); + + string code = decompiler.DecompileTypeAsString(new FullTypeName("Derived")); + + Assert.That(code, Does.Contain("public override void Run(Param p)")); + } + + /// + /// Hands out a separate file per requested version of Lib, so that the type system has + /// to deal with two candidates for the same assembly name; everything else comes from + /// the runtime the tests execute on. + /// + class VersionedResolver : IAssemblyResolver + { + static readonly string runtimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location); + + readonly string directory; + + public VersionedResolver(string directory) + { + this.directory = directory; + } + + public MetadataFile Resolve(IAssemblyReference reference) + { + string path = reference.Name == "Lib" + ? Path.Combine(directory, $"Lib.v{reference.Version.Major}.dll") + : Path.Combine(runtimeDirectory, reference.Name + ".dll"); + return File.Exists(path) ? new PEFile(path) : null; + } + + public MetadataFile ResolveModule(MetadataFile mainModule, string moduleName) => null; + + public Task ResolveAsync(IAssemblyReference reference) => Task.FromResult(Resolve(reference)); + + public Task ResolveModuleAsync(MetadataFile mainModule, string moduleName) => Task.FromResult(null); + } + } +}