// 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.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.PortableExecutable; using System.Threading.Tasks; using ICSharpCode.BamlDecompiler; using ICSharpCode.BamlDecompiler.Baml; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; using NUnit.Framework; namespace ILSpy.BamlDecompiler.Tests { /// /// Verifies that the BAML decompiler survives when the well-known WPF assemblies cannot be /// resolved - the situation on any machine without WPF installed (Linux/macOS). Previously /// threw because it could not find PresentationFramework et al.; /// now those assemblies are substituted by synthetic stand-ins. /// [TestFixture] public class MissingReferencesTests { const string PresentationXmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"; /// /// An assembly resolver that hides the WPF assemblies, reproducing the "no WPF available" /// environment deterministically on every platform (on Windows the real assemblies would /// otherwise be found in the runtime pack). /// sealed class WpfHidingResolver : IAssemblyResolver { static readonly HashSet hidden = new(StringComparer.OrdinalIgnoreCase) { "WindowsBase", "PresentationCore", "PresentationFramework", "PresentationUI", "System.Xaml" }; readonly IAssemblyResolver inner; public WpfHidingResolver(IAssemblyResolver inner) => this.inner = inner; public MetadataFile Resolve(IAssemblyReference reference) => hidden.Contains(reference.Name) ? null : inner.Resolve(reference); public MetadataFile ResolveModule(MetadataFile mainModule, string moduleName) => inner.ResolveModule(mainModule, moduleName); public Task ResolveAsync(IAssemblyReference reference) => hidden.Contains(reference.Name) ? Task.FromResult(null) : inner.ResolveAsync(reference); public Task ResolveModuleAsync(MetadataFile mainModule, string moduleName) => inner.ResolveModuleAsync(mainModule, moduleName); } static BamlDecompilerTypeSystem CreateTypeSystemWithoutWpf() { // This test assembly is a managed module that does not itself reference WPF; combined // with the WPF-hiding resolver, none of the well-known WPF assemblies resolve. var location = typeof(MissingReferencesTests).Assembly.Location; // PrefetchEntireImage reads the whole image into memory, so the file stream is only // needed while the PEFile is being constructed and can be closed right afterwards. using var stream = new FileStream(location, FileMode.Open, FileAccess.Read); var file = new PEFile(location, stream, streamOptions: PEStreamOptions.PrefetchEntireImage); var resolver = new WpfHidingResolver(new UniversalAssemblyResolver(location, throwOnError: false, file.DetectTargetFrameworkId(), file.DetectRuntimePack())); return new BamlDecompilerTypeSystem(file, resolver); } [Test] public void KnownThings_DoesNotThrow_WhenWpfAssembliesAreUnavailable() { var typeSystem = CreateTypeSystemWithoutWpf(); Assert.DoesNotThrow(() => new KnownThings(typeSystem)); } [Test] public void KnownType_ResolvesToSyntheticDefinition_WithCorrectIdentity() { var knownThings = new KnownThings(CreateTypeSystemWithoutWpf()); var button = knownThings.Types(KnownTypes.Button); Assert.Multiple(() => { Assert.That(button.Namespace, Is.EqualTo("System.Windows.Controls")); Assert.That(button.Name, Is.EqualTo("Button")); Assert.That(button.ParentModule.AssemblyName, Is.EqualTo("PresentationFramework")); }); } [Test] public void SyntheticModule_IsBounded_ReturnsNullForUnseededType() { var knownThings = new KnownThings(CreateTypeSystemWithoutWpf()); var module = knownThings.Types(KnownTypes.Button).ParentModule; // A type the decompiler never seeded must stay unresolved, so that references to // non-well-known WPF types keep degrading to UnknownType exactly as before. var unseeded = module.GetTypeDefinition( new TopLevelTypeName("System.Windows.Controls", "ThisTypeDoesNotExist")); Assert.That(unseeded, Is.Null); } [Test] public void SyntheticModule_ExposesPresentationXmlnsDefinition() { var knownThings = new KnownThings(CreateTypeSystemWithoutWpf()); var module = knownThings.Types(KnownTypes.Button).ParentModule; var controlsMapping = module.GetAssemblyAttributes() .Where(a => a.AttributeType.FullName == "System.Windows.Markup.XmlnsDefinitionAttribute") .FirstOrDefault(a => (string)a.FixedArguments[1].Value == "System.Windows.Controls"); Assert.That(controlsMapping, Is.Not.Null, "expected an XmlnsDefinition for System.Windows.Controls"); Assert.That((string)controlsMapping.FixedArguments[0].Value, Is.EqualTo(PresentationXmlns)); } } }