Browse Source

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
pull/4108/head
Siegfried Pammer 2 weeks ago
parent
commit
069366e4c2
  1. 27
      ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs
  2. 91
      ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs
  3. 1
      ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj

27
ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs

@ -40,6 +40,29 @@ namespace ICSharpCode.BamlDecompiler @@ -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<string, TopLevelTypeName> wellKnownProbeTypes = new(StringComparer.OrdinalIgnoreCase) {
["WindowsBase"] = new TopLevelTypeName("System.Windows", "Point"),
["PresentationCore"] = new TopLevelTypeName("System.Windows.Media", "Brush"),
["PresentationFramework"] = new TopLevelTypeName("System.Windows.Controls", "Button")
};
/// <summary>
/// Whether <paramref name="file"/> is the assembly its name claims, rather than a facade
/// standing where it should be.
/// </summary>
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 @@ -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

91
ILSpy.BamlDecompiler.Tests/FacadeAssemblyTests.cs

@ -0,0 +1,91 @@ @@ -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
{
/// <summary>
/// .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.
/// </summary>
[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);
}
}
}

1
ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj

@ -43,6 +43,7 @@ @@ -43,6 +43,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="FacadeAssemblyTests.cs" />
<Compile Include="InvalidXmlCharacterTests.cs" />
<Compile Include="MissingReferencesTests.cs" />
<Compile Include="MarkupExtensionQuotingTests.cs" />

Loading…
Cancel
Save