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 @@ +