From 069366e4c21036d5ff6d8f8537a74cf8a3abacbf Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 4 Sep 2026 13:59:22 +0200 Subject: [PATCH] Do not let a facade stand in for a well-known BAML assembly .NET ships a WindowsBase facade on every platform. It resolves under the name BAML means, so no synthetic stand-in was substituted for it, and it carries none of the WPF types, because those live in the WindowsDesktop runtime pack. System.Windows.Point and Size then resolved to nothing and the whole resource was lost with a NullReferenceException - ten of the BAML entries in one DevExpress theme assembly, on any machine without WPF, which is every Linux and macOS user and every CI run. A well-known assembly now counts as resolved only if it defines a type it is expected to have, so a facade gives way to the stand-in the way an assembly that does not resolve at all does. Nothing else changes: over 1158 documents of that assembly the output is identical, with the ten that used to be lost added back. Assisted-by: Claude:claude-opus-5:Claude Code --- .../BamlDecompilerTypeSystem.cs | 27 ++++++ .../FacadeAssemblyTests.cs | 91 +++++++++++++++++++ .../ILSpy.BamlDecompiler.Tests.csproj | 1 + 3 files changed, 119 insertions(+) create mode 100644 ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs diff --git a/ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs b/ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs index a2e2cc412..79542334a 100644 --- a/ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs +++ b/ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs @@ -40,6 +40,29 @@ namespace ICSharpCode.BamlDecompiler "System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" }; + // A type each of these assemblies must define for the module resolved under its name to be + // the assembly BAML means by it. .NET ships a WindowsBase facade on every platform - it + // resolves everywhere and carries none of the WPF types, because those live in the + // WindowsDesktop runtime pack - and a module like that has to give way to the synthetic + // stand-in the way an assembly that does not resolve at all does. Without this, a document + // using System.Windows.Point or Size is lost outright on a machine without WPF. + static readonly Dictionary wellKnownProbeTypes = new(StringComparer.OrdinalIgnoreCase) { + ["WindowsBase"] = new TopLevelTypeName("System.Windows", "Point"), + ["PresentationCore"] = new TopLevelTypeName("System.Windows.Media", "Brush"), + ["PresentationFramework"] = new TopLevelTypeName("System.Windows.Controls", "Button") + }; + + /// + /// Whether is the assembly its name claims, rather than a facade + /// standing where it should be. + /// + static bool IsTheAssemblyItIsNamedAfter(MetadataFile file) + { + if (!wellKnownProbeTypes.TryGetValue(file.Name, out var probeType)) + return true; + return !file.GetTypeDefinition(probeType).IsNil; + } + // The WPF assemblies whose types serialize under the presentation XML namespace. When one of // these has to be synthesized (e.g. inspecting a WPF binary on a non-Windows machine), the // synthetic module reproduces its XmlnsDefinitionAttribute mapping so known types still emit @@ -120,6 +143,10 @@ namespace ICSharpCode.BamlDecompiler } } } + // A facade standing in for a well-known assembly is worse than nothing: it satisfies the + // name, so no stand-in is synthesized, and then every type BAML expects from it is + // missing. Drop it and let the stand-in below take its place. + referencedAssemblies.RemoveAll(file => !IsTheAssemblyItIsNamedAfter(file)); var mainModuleWithOptions = mainModule.WithOptions(TypeSystemOptions.Default); var referencedAssembliesWithOptions = referencedAssemblies.Select(file => file.WithOptions(TypeSystemOptions.Default)); // Substitute a synthetic stand-in for every well-known BAML assembly that could not be diff --git a/ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs b/ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs new file mode 100644 index 000000000..e2e069c11 --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs @@ -0,0 +1,91 @@ +// 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.IO; +using System.Reflection.PortableExecutable; + +using ICSharpCode.BamlDecompiler; +using ICSharpCode.BamlDecompiler.Baml; +using ICSharpCode.Decompiler.Metadata; + +using NUnit.Framework; + +namespace ILSpy.BamlDecompiler.Tests +{ + /// + /// .NET ships a WindowsBase facade on every platform, so the assembly resolves everywhere - but + /// it carries none of the types BAML means by it, because those live in the WindowsDesktop + /// runtime pack. A well-known type that only exists in the real assembly then resolves to + /// nothing, and the whole resource is lost: ten of the BAML entries in a DevExpress theme + /// assembly are unreadable on a machine without WPF for exactly this reason. + /// + [TestFixture] + public class FacadeAssemblyTests + { + static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type); + + static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member); + + static MemoryStream CreateBaml(params BamlRecord[] records) + { + var version = new BamlDocument.BamlVersion { Major = 0, Minor = 0x60 }; + var document = new BamlDocument { + Signature = "MSBAML", + ReaderVersion = version, + UpdaterVersion = version, + WriterVersion = version + }; + document.Add(new DocumentStartRecord()); + document.AddRange(records); + document.Add(new DocumentEndRecord()); + + var stream = new MemoryStream(); + BamlWriter.WriteDocument(document, stream); + stream.Position = 0; + return stream; + } + + static string Decompile(Stream baml) + { + var location = typeof(FacadeAssemblyTests).Assembly.Location; + using var fileStream = new FileStream(location, FileMode.Open, FileAccess.Read); + var file = new PEFile(location, fileStream, streamOptions: PEStreamOptions.PrefetchEntireImage); + var resolver = new UniversalAssemblyResolver(location, throwOnError: false, + file.DetectTargetFrameworkId(), file.DetectRuntimePack()); + var decompiler = new XamlDecompiler(new BamlDecompilerTypeSystem(file, resolver), + new BamlDecompilerSettings()); + return decompiler.Decompile(baml).Xaml.ToString(); + } + + [Test] + public void ATypeOfTheRealWindowsBaseStillDecompiles() + { + // System.Windows.Size is a well-known BAML type of WindowsBase, and one the facade does + // not have. + string xaml = Decompile(CreateBaml( + new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) }, + new PropertyComplexStartRecord { AttributeId = MemberId(KnownMembers.Button_Content) }, + new ElementStartRecord { TypeId = TypeId(KnownTypes.Size) }, + new ElementEndRecord(), + new PropertyComplexEndRecord(), + new ElementEndRecord())); + + Assert.That(xaml, Does.Contain("Size"), xaml); + } + } +} diff --git a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj index 2d80f6d2e..949c7f36c 100644 --- a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj +++ b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj @@ -43,6 +43,7 @@ +