diff --git a/ICSharpCode.BamlDecompiler/Rewrite/EscapeInvalidXmlCharactersRewritePass.cs b/ICSharpCode.BamlDecompiler/Rewrite/EscapeInvalidXmlCharactersRewritePass.cs
new file mode 100644
index 000000000..2f49e4954
--- /dev/null
+++ b/ICSharpCode.BamlDecompiler/Rewrite/EscapeInvalidXmlCharactersRewritePass.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ 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;
+ }
+ }
+ }
+ }
+}
diff --git a/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs b/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
index a0f619408..09d287c18 100644
--- a/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
+++ b/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
@@ -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)
{
diff --git a/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs b/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
index cc26dd7b3..572667f1d 100644
--- a/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
+++ b/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
@@ -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
return value;
}
+ ///
+ /// 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.
+ ///
+ 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);
diff --git a/ICSharpCode.BamlDecompiler/XamlContext.cs b/ICSharpCode.BamlDecompiler/XamlContext.cs
index f35108c40..5fc1752aa 100644
--- a/ICSharpCode.BamlDecompiler/XamlContext.cs
+++ b/ICSharpCode.BamlDecompiler/XamlContext.cs
@@ -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
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
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(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);
diff --git a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs
index 3aecaec2f..7a1e842c2 100644
--- a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs
+++ b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs
@@ -44,6 +44,7 @@ namespace ICSharpCode.BamlDecompiler
new AttributeRewritePass(),
new ConnectionIdRewritePass(),
new DocumentRewritePass(),
+ new EscapeInvalidXmlCharactersRewritePass(),
};
private BamlDecompilerTypeSystem typeSystem;
diff --git a/ICSharpCode.Decompiler.Tests/DecompilationErrorRecoveryTests.cs b/ICSharpCode.Decompiler.Tests/DecompilationErrorRecoveryTests.cs
index c5d28d9b9..3e42c76cc 100644
--- a/ICSharpCode.Decompiler.Tests/DecompilationErrorRecoveryTests.cs
+++ b/ICSharpCode.Decompiler.Tests/DecompilationErrorRecoveryTests.cs
@@ -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
}
}
+ ///
+ /// 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.
+ ///
+ [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");
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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 WriteResources(MetadataFile module)
+ => WriteResourceFilesInProject(module);
+
+ protected override IEnumerable WriteResourceToFile(string fileName, string resourceName, Stream entryStream)
+ {
+ if (resourceName == "bad.baml")
+ throw new NotSupportedException("cannot write bad.baml");
+ return new[] { new ProjectItemInfo("Page", fileName) };
+ }
+ }
+
}
}
diff --git a/ICSharpCode.Decompiler.Tests/Helpers/TwoStreamEntries.resources b/ICSharpCode.Decompiler.Tests/Helpers/TwoStreamEntries.resources
new file mode 100644
index 000000000..a60561492
Binary files /dev/null and b/ICSharpCode.Decompiler.Tests/Helpers/TwoStreamEntries.resources differ
diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index c385749ce..a70de1e14 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -93,6 +93,12 @@
+
+
+
+
+
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
index 9264e109a..01b908530 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
@@ -506,7 +506,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
List 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
}
}
- IEnumerable WriteResourceFileInProject(Resource r)
+ IEnumerable WriteResourceFileInProject(MetadataFile module, Resource r)
{
Stream? stream = r.TryOpenStream();
if (stream == null)
@@ -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;
}
diff --git a/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs b/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs
index 951d3c5c6..65e7abc25 100644
--- a/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs
+++ b/ILSpy.BamlDecompiler.Tests.Windows/BamlTestRunner.cs
@@ -135,6 +135,12 @@ namespace ILSpy.BamlDecompiler.Tests
RunTest("cases/issue1547");
}
+ [Test]
+ public void Issue1688()
+ {
+ RunTest("cases/issue1688");
+ }
+
[Test]
public void Issue2052()
{
diff --git a/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml
new file mode 100644
index 000000000..27b972b5a
--- /dev/null
+++ b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs
new file mode 100644
index 000000000..0393a8681
--- /dev/null
+++ b/ILSpy.BamlDecompiler.Tests.Windows/Cases/Issue1688.xaml.cs
@@ -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
+{
+ ///
+ /// Interaction logic for Issue1688.xaml
+ ///
+ public partial class Issue1688 : ContextMenu
+ {
+ public Issue1688()
+ {
+ InitializeComponent();
+ }
+
+ void Click_AssignPlace(object sender, RoutedEventArgs e)
+ {
+ }
+
+ void Click_AssignMove(object sender, RoutedEventArgs e)
+ {
+ }
+ }
+}
diff --git a/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj b/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj
index 1b7afc7dd..1fa27da82 100644
--- a/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj
+++ b/ILSpy.BamlDecompiler.Tests.Windows/ILSpy.BamlDecompiler.Tests.Windows.csproj
@@ -66,6 +66,9 @@
+
+ Issue1688.xaml
+
@@ -102,6 +105,9 @@
MSBuild:Compile
+
+ MSBuild:Compile
+
MSBuild:Compile
diff --git a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
index 92eb7131d..298a74a03 100644
--- a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
+++ b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
@@ -43,7 +43,9 @@
+
+
diff --git a/ILSpy.BamlDecompiler.Tests/InvalidXmlCharacterTests.cs b/ILSpy.BamlDecompiler.Tests/InvalidXmlCharacterTests.cs
new file mode 100644
index 000000000..36a5ecc9d
--- /dev/null
+++ b/ILSpy.BamlDecompiler.Tests/InvalidXmlCharacterTests.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ [TestFixture]
+ public class InvalidXmlCharacterTests
+ {
+ static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type);
+
+ static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member);
+
+ ///
+ /// Builds a BAML stream out of , wrapped in the document
+ /// start/end records and the header the reader insists on.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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");
+ });
+ }
+ }
+}
diff --git a/ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs b/ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs
new file mode 100644
index 000000000..dd43459f0
--- /dev/null
+++ b/ILSpy.BamlDecompiler.Tests/XmlNamespaceResolutionTests.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ [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
+{
+ ///
+ /// 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.
+ ///
+ [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));
+ }
+ }
+}