Browse Source

Merge pull request #4096 from icsharpcode/fix/1688-xmlns-redefinition

Fix two BAML bugs that make the decompiled XAML unwritable
pull/4101/head
Siegfried Pammer 2 weeks ago committed by GitHub
parent
commit
e0199b0b3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 62
      ICSharpCode.BamlDecompiler/Rewrite/EscapeInvalidXmlCharactersRewritePass.cs
  2. 2
      ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
  3. 54
      ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
  4. 32
      ICSharpCode.BamlDecompiler/XamlContext.cs
  5. 1
      ICSharpCode.BamlDecompiler/XamlDecompiler.cs
  6. 53
      ICSharpCode.Decompiler.Tests/DecompilationErrorRecoveryTests.cs
  7. BIN
      ICSharpCode.Decompiler.Tests/Helpers/TwoStreamEntries.resources
  8. 6
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  9. 18
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
  10. 6
      ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs
  11. 4
      ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml
  12. 42
      ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs
  13. 6
      ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj
  14. 2
      ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
  15. 131
      ILSpy.BamlDecompiler.Tests/InvalidXmlCharacterTests.cs
  16. 115
      ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs

62
ICSharpCode.BamlDecompiler/Rewrite/EscapeInvalidXmlCharactersRewritePass.cs

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
// 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.Linq;
using System.Xml.Linq;
using ICSharpCode.BamlDecompiler.Xaml;
namespace ICSharpCode.BamlDecompiler.Rewrite
{
/// <summary>
/// Escapes attribute values, text and comments that carry characters XML cannot represent -
/// a string record from an obfuscated assembly may hold any byte sequence. Without this the
/// document builds fine and only throws when it is written, taking the resource with it.
/// Names and namespace URIs are escaped where they are built, so this pass only has to cover
/// the content.
/// </summary>
internal class EscapeInvalidXmlCharactersRewritePass : IRewritePass
{
public void Run(XamlContext ctx, XDocument document)
{
foreach (var element in document.Descendants())
{
foreach (var attribute in element.Attributes())
{
// Namespace declarations carry a URI that was escaped when the XNamespace was
// created; rewriting it here would desync it from the names using it.
if (!attribute.IsNamespaceDeclaration)
attribute.Value = XamlUtils.EscapeInvalidXmlCharacters(attribute.Value);
}
}
foreach (var node in document.DescendantNodes().ToList())
{
switch (node)
{
case XText text:
text.Value = XamlUtils.EscapeInvalidXmlCharacters(text.Value);
break;
case XComment comment:
comment.Value = XamlUtils.EscapeInvalidXmlCharacters(comment.Value);
break;
}
}
}
}
}

2
ICSharpCode.BamlDecompiler/Xaml/XamlType.cs

@ -69,7 +69,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -69,7 +69,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml
xmlNs = ctx.XmlNs.LookupXmlns(FullAssemblyName, TypeNamespace);
// Sometimes there's no reference to System.Xaml even if x:Type is used
if (xmlNs == null)
xmlNs = ctx.TryGetXmlNamespace(Assembly, TypeNamespace);
xmlNs = XamlContext.TryGetXmlNamespace(Assembly, TypeNamespace, elem);
if (xmlNs == null)
{

54
ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs

@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace ICSharpCode.BamlDecompiler.Xaml
@ -37,6 +38,59 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -37,6 +38,59 @@ namespace ICSharpCode.BamlDecompiler.Xaml
return value;
}
/// <summary>
/// Escapes the characters XML cannot carry - obfuscators put them into BAML strings, and
/// XML 1.0 has no representation for them at all, not even a numeric character reference.
/// The escapes are spelled the way the C# output spells them, so one convention covers
/// both languages: the short form where C# has one, "\uXXXX" otherwise.
/// Characters XML can carry - tab, newline, astral characters - are left untouched, and a
/// literal backslash is not doubled, because XAML itself has no escape syntax to undo.
/// </summary>
public static string EscapeInvalidXmlCharacters(string value)
{
if (string.IsNullOrEmpty(value))
return value;
StringBuilder escaped = null;
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]))
{
escaped?.Append(c).Append(value[i + 1]);
i++;
continue;
}
if (XmlConvert.IsXmlChar(c))
{
escaped?.Append(c);
continue;
}
escaped ??= new StringBuilder(value.Length).Append(value, 0, i);
escaped.Append(EscapeChar(c));
}
return escaped?.ToString() ?? value;
}
static string EscapeChar(char c)
{
switch (c)
{
case '\0':
return "\\0";
case '\a':
return "\\a";
case '\b':
return "\\b";
case '\f':
return "\\f";
case '\v':
return "\\v";
default:
return "\\u" + ((int)c).ToString("x4");
}
}
public static string ToString(this XamlContext ctx, XElement elem, XamlType type)
{
type.ResolveNamespace(elem, ctx);

32
ICSharpCode.BamlDecompiler/XamlContext.cs

@ -184,7 +184,12 @@ namespace ICSharpCode.BamlDecompiler @@ -184,7 +184,12 @@ namespace ICSharpCode.BamlDecompiler
return null;
if (!xmlnsMap.TryGetValue(xmlns, out var ns))
xmlnsMap[xmlns] = ns = XNamespace.Get(xmlns);
{
// Every XNamespace is created here, so escaping the URI once keeps the xmlns
// declaration and the names that use it in sync. Doing it later is not possible:
// the URI is baked into every name built from this namespace.
xmlnsMap[xmlns] = ns = XNamespace.Get(XamlUtils.EscapeInvalidXmlCharacters(xmlns));
}
return ns;
}
@ -192,7 +197,7 @@ namespace ICSharpCode.BamlDecompiler @@ -192,7 +197,7 @@ namespace ICSharpCode.BamlDecompiler
public const string KnownNamespace_Presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
public const string KnownNamespace_PresentationOptions = "http://schemas.microsoft.com/winfx/2006/xaml/presentation/options";
public string TryGetXmlNamespace(IModule assembly, string typeNamespace)
public static string TryGetXmlNamespace(IModule assembly, string typeNamespace, XElement context = null)
{
if (assembly == null)
return null;
@ -214,12 +219,35 @@ namespace ICSharpCode.BamlDecompiler @@ -214,12 +219,35 @@ namespace ICSharpCode.BamlDecompiler
possibleXmlNs.Add(xmlNs);
}
// An assembly may map one CLR namespace to several XML namespaces; PresentationFramework
// for example maps its namespaces to both the winfx/2006 and the netfx/2007 presentation
// namespace. Whenever the document itself declares one of the candidates, that one has to
// win: picking a different candidate for an element whose start tag carries the xmlns
// declaration redefines the prefix within that tag, which is not valid XML.
var declared = possibleXmlNs.Where(ns => IsDeclaredIn(context, ns)).ToList();
if (declared.Count > 0)
possibleXmlNs = new HashSet<string>(declared);
if (possibleXmlNs.Contains(KnownNamespace_Presentation))
return KnownNamespace_Presentation;
return possibleXmlNs.FirstOrDefault();
}
static bool IsDeclaredIn(XElement context, string xmlNamespace)
{
for (var elem = context; elem != null; elem = elem.Parent)
{
foreach (var attr in elem.Attributes())
{
if (attr.IsNamespaceDeclaration && attr.Value == xmlNamespace)
return true;
}
}
return false;
}
public XName GetKnownNamespace(string name, string xmlNamespace, XElement context = null)
{
var xNs = GetXmlNamespace(xmlNamespace);

1
ICSharpCode.BamlDecompiler/XamlDecompiler.cs

@ -44,6 +44,7 @@ namespace ICSharpCode.BamlDecompiler @@ -44,6 +44,7 @@ namespace ICSharpCode.BamlDecompiler
new AttributeRewritePass(),
new ConnectionIdRewritePass(),
new DocumentRewritePass(),
new EscapeInvalidXmlCharactersRewritePass(),
};
private BamlDecompilerTypeSystem typeSystem;

53
ICSharpCode.Decompiler.Tests/DecompilationErrorRecoveryTests.cs

@ -17,11 +17,14 @@ @@ -17,11 +17,14 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.IL;
using ICSharpCode.Decompiler.IL.Transforms;
@ -125,5 +128,55 @@ namespace ICSharpCode.Decompiler.Tests @@ -125,5 +128,55 @@ namespace ICSharpCode.Decompiler.Tests
}
}
/// <summary>
/// A .resources container holds every BAML stream of an assembly. One entry the decompiler
/// cannot write - obfuscated BAML that produces characters XML cannot carry, say - must not
/// take the entries next to it down: they are unrelated pages of an unrelated type.
/// </summary>
[Test]
public void FailingResourceEntryKeepsTheOtherEntriesOfTheContainer()
{
string location = typeof(DecompilationErrorRecoveryTests).Assembly.Location;
using var stream = new FileStream(location, FileMode.Open, FileAccess.Read);
var module = new PEFile(location, stream, streamOptions: PEStreamOptions.PrefetchEntireImage);
var decompiler = new EntryFailingProjectDecompiler(
new UniversalAssemblyResolver(location, throwOnError: false, module.DetectTargetFrameworkId()));
var items = decompiler.WriteResources(module).ToList();
using (Assert.EnterMultipleScope())
{
Assert.That(items.Select(i => i.FileName), Does.Contain("good.baml"),
"the entry after the failing one is still written");
Assert.That(decompiler.Errors, Has.Count.EqualTo(1), "the failure is reported to the caller");
Assert.That(decompiler.Errors[0].ToString(), Does.Contain("bad.baml"),
"and names the entry that failed");
}
}
/// <summary>
/// Writes every resource entry as a project item, except the one named "bad.baml", which
/// throws the way a resource handler does when it cannot produce a file.
/// </summary>
sealed class EntryFailingProjectDecompiler : WholeProjectDecompiler
{
public EntryFailingProjectDecompiler(IAssemblyResolver assemblyResolver)
: base(assemblyResolver)
{
// Entries this fixture does not override still get written to disk.
TargetDirectory = Directory.CreateTempSubdirectory("ILSpyResourceRecovery").FullName;
}
public IEnumerable<ProjectItemInfo> WriteResources(MetadataFile module)
=> WriteResourceFilesInProject(module);
protected override IEnumerable<ProjectItemInfo> WriteResourceToFile(string fileName, string resourceName, Stream entryStream)
{
if (resourceName == "bad.baml")
throw new NotSupportedException("cannot write bad.baml");
return new[] { new ProjectItemInfo("Page", fileName) };
}
}
}
}

BIN
ICSharpCode.Decompiler.Tests/Helpers/TwoStreamEntries.resources

Binary file not shown.

6
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -93,6 +93,12 @@ @@ -93,6 +93,12 @@
<PackageReference Include="System.Resources.Extensions" />
</ItemGroup>
<ItemGroup>
<!-- A .resources container holding two stream entries, so this assembly itself is the module
WholeProjectDecompiler's per-entry resource path can be exercised against. -->
<EmbeddedResource Include="Helpers\TwoStreamEntries.resources" LogicalName="TwoStreamEntries.g.resources" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ICSharpCode.ILSpyX\ICSharpCode.ILSpyX.csproj" />
<ProjectReference Include="..\ICSharpCode.Decompiler\ICSharpCode.Decompiler.csproj" />

18
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs

@ -506,7 +506,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -506,7 +506,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
List<ProjectItemInfo> items;
try
{
items = WriteResourceFileInProject(r).ToList();
items = WriteResourceFileInProject(module, r).ToList();
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
@ -522,7 +522,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -522,7 +522,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
}
}
IEnumerable<ProjectItemInfo> WriteResourceFileInProject(Resource r)
IEnumerable<ProjectItemInfo> WriteResourceFileInProject(MetadataFile module, Resource r)
{
Stream? stream = r.TryOpenStream();
if (stream == null)
@ -549,8 +549,18 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler @@ -549,8 +549,18 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
}
Stream entryStream = (Stream)value!;
entryStream.Position = 0;
individualResources.AddRange(
WriteResourceToFile(fileName, name, entryStream));
try
{
individualResources.AddRange(
WriteResourceToFile(fileName, name, entryStream));
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
// One entry nobody can decode - a BAML stream carrying characters XML
// cannot represent, say - costs that entry, not every other entry
// sharing the container with it.
RecordError(ex as DecompilerException ?? new DecompilerException(module, $"Error writing resource '{name}'", ex));
}
}
decodedIntoIndividualFiles = true;
}

6
ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs

@ -135,6 +135,12 @@ namespace ILSpy.BamlDecompiler.Tests @@ -135,6 +135,12 @@ namespace ILSpy.BamlDecompiler.Tests
RunTest("cases/issue1547");
}
[Test]
public void Issue1688()
{
RunTest("cases/issue1688");
}
[Test]
public void Issue2052()
{

4
ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
<ContextMenu x:Class="ILSpy.BamlDecompiler.Tests.Cases.Issue1688" xmlns="http://schemas.microsoft.com/netfx/2007/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:cases="clr-namespace:ILSpy.BamlDecompiler.Tests.Cases">
<MenuItem Header="Assign Place" Click="Click_AssignPlace" />
<MenuItem Header="Assign Move" Click="Click_AssignMove" />
</ContextMenu>

42
ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs

@ -0,0 +1,42 @@ @@ -0,0 +1,42 @@
// 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.Windows;
using System.Windows.Controls;
namespace ILSpy.BamlDecompiler.Tests.Cases
{
/// <summary>
/// Interaction logic for Issue1688.xaml
/// </summary>
public partial class Issue1688 : ContextMenu
{
public Issue1688()
{
InitializeComponent();
}
void Click_AssignPlace(object sender, RoutedEventArgs e)
{
}
void Click_AssignMove(object sender, RoutedEventArgs e)
{
}
}
}

6
ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj

@ -66,6 +66,9 @@ @@ -66,6 +66,9 @@
</Compile>
<Compile Include="Cases\CustomControl.cs" />
<Compile Include="Cases\Issue1547.xaml.cs" />
<Compile Include="Cases\Issue1688.xaml.cs">
<DependentUpon>Issue1688.xaml</DependentUpon>
</Compile>
<Compile Include="Cases\Issue2097.xaml.cs" />
<Compile Include="Cases\Issue2116.xaml.cs" />
<Compile Include="Cases\Issue3318.xaml.cs" />
@ -102,6 +105,9 @@ @@ -102,6 +105,9 @@
<Page Include="Cases\Issue1547.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Cases\Issue1688.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Cases\Issue2052.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>

2
ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj

@ -43,7 +43,9 @@ @@ -43,7 +43,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="InvalidXmlCharacterTests.cs" />
<Compile Include="MissingReferencesTests.cs" />
<Compile Include="XmlNamespaceResolutionTests.cs" />
</ItemGroup>
</Project>

131
ILSpy.BamlDecompiler.Tests/InvalidXmlCharacterTests.cs

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
// 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>
/// Obfuscators put characters into BAML strings that XML cannot carry at all - not even as a
/// numeric character reference. They have to be escaped before they reach the XDocument;
/// otherwise writing the decompiled XAML throws and the whole resource is lost.
/// </summary>
[TestFixture]
public class InvalidXmlCharacterTests
{
static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type);
static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member);
/// <summary>
/// Builds a BAML stream out of <paramref name="records"/>, wrapped in the document
/// start/end records and the header the reader insists on.
/// </summary>
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;
}
/// <summary>
/// Decompiles against this test assembly as the main module: the BAML built here refers
/// only to well-known WPF types, which the decompiler resolves without it.
/// </summary>
static string Decompile(Stream baml)
{
var location = typeof(InvalidXmlCharacterTests).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 ControlCharacterInPropertyValue_IsEscaped()
{
string xaml = Decompile(CreateBaml(
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new PropertyRecord {
AttributeId = MemberId(KnownMembers.Button_Content),
Value = "a\u0018b"
},
new ElementEndRecord()));
Assert.That(xaml, Does.Contain(@"Content=""a\u0018b"""));
}
[Test]
public void ControlCharacterInNamespaceUri_IsEscaped()
{
// The URI ends up both in the xmlns declaration and in the namespace of every element
// name, so it cannot be repaired after the document has been built.
string xaml = Decompile(CreateBaml(
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new XmlnsPropertyRecord {
Prefix = "obf",
XmlNamespace = "clr-namespace:Obfuscated\u0018Namespace",
AssemblyIds = new ushort[0]
},
new ElementEndRecord()));
Assert.That(xaml, Does.Contain(@"xmlns:obf=""clr-namespace:Obfuscated\u0018Namespace"""));
}
[Test]
public void CharactersXmlCanCarry_AreLeftAlone()
{
// Tab, newline and astral characters are valid XML content; escaping them would
// change the output of every ordinary document.
string xaml = Decompile(CreateBaml(
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new PropertyRecord {
AttributeId = MemberId(KnownMembers.Button_Content),
Value = "tab\tastral\U0001F600"
},
new ElementEndRecord()));
Assert.Multiple(() => {
Assert.That(xaml, Does.Contain("\U0001F600"), "the surrogate pair stays intact");
Assert.That(xaml, Does.Not.Contain("\\u"), "nothing valid gets escaped");
});
}
}
}

115
ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
// 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.Reflection.PortableExecutable;
using System.Xml.Linq;
using ICSharpCode.BamlDecompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using NUnit.Framework;
using ILSpy.BamlDecompiler.Tests;
// This assembly plays the role of an assembly that maps one CLR namespace to two XML namespaces,
// the way PresentationFramework maps its namespaces to both presentation namespaces.
[assembly: System.Windows.Markup.XmlnsDefinition(XmlNamespaceResolutionTests.Winfx2006Presentation, XmlNamespaceResolutionTests.TestClrNamespace)]
[assembly: System.Windows.Markup.XmlnsDefinition(XmlNamespaceResolutionTests.Netfx2007Presentation, XmlNamespaceResolutionTests.TestClrNamespace)]
namespace System.Windows.Markup
{
/// <summary>
/// Stand-in for the WPF attribute of the same name, which is unavailable on platforms without
/// WPF. The BAML decompiler matches it by full name in metadata, so the declaring assembly does
/// not matter.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
internal sealed class XmlnsDefinitionAttribute : Attribute
{
public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace)
{
XmlNamespace = xmlNamespace;
ClrNamespace = clrNamespace;
}
public string XmlNamespace { get; }
public string ClrNamespace { get; }
}
}
namespace ILSpy.BamlDecompiler.Tests
{
/// <summary>
/// Tests for the fallback used when neither the BAML xmlns records nor the PI mappings name an
/// XML namespace for a type: which of the assembly's XmlnsDefinition mappings is picked.
/// </summary>
[TestFixture]
public class XmlNamespaceResolutionTests
{
public const string Winfx2006Presentation = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
public const string Netfx2007Presentation = "http://schemas.microsoft.com/netfx/2007/xaml/presentation";
public const string TestClrNamespace = "ILSpy.BamlDecompiler.Tests";
static IModule GetTestAssemblyModule()
{
var location = typeof(XmlNamespaceResolutionTests).Assembly.Location;
using var stream = new FileStream(location, FileMode.Open, FileAccess.Read);
var file = new PEFile(location, stream, streamOptions: PEStreamOptions.PrefetchEntireImage);
var resolver = new UniversalAssemblyResolver(location, throwOnError: false,
file.DetectTargetFrameworkId(), file.DetectRuntimePack());
return new BamlDecompilerTypeSystem(file, resolver).MainModule;
}
[Test]
public void PrefersPresentationNamespace_WhenDocumentDeclaresNothing()
{
var xmlNs = XamlContext.TryGetXmlNamespace(GetTestAssemblyModule(), TestClrNamespace);
Assert.That(xmlNs, Is.EqualTo(Winfx2006Presentation));
}
[Test]
public void PrefersNamespaceDeclaredByDocument_OverPresentationNamespace()
{
// Issue #1688: the document binds the default prefix to the netfx/2007 presentation
// namespace. Resolving its elements to the winfx/2006 one made the root start tag both
// declare and redefine the default prefix, which XmlWriter rejects.
var root = new XElement(XName.Get("Root", Netfx2007Presentation),
new XAttribute("xmlns", Netfx2007Presentation));
var xmlNs = XamlContext.TryGetXmlNamespace(GetTestAssemblyModule(), TestClrNamespace, root);
Assert.That(xmlNs, Is.EqualTo(Netfx2007Presentation));
}
[Test]
public void PrefersNamespaceDeclaredByAncestor()
{
var child = new XElement(XName.Get("Child", Netfx2007Presentation));
new XElement(XName.Get("Root", Netfx2007Presentation),
new XAttribute("xmlns", Netfx2007Presentation), child);
var xmlNs = XamlContext.TryGetXmlNamespace(GetTestAssemblyModule(), TestClrNamespace, child);
Assert.That(xmlNs, Is.EqualTo(Netfx2007Presentation));
}
}
}
Loading…
Cancel
Save