mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
Merge pull request #4096 from icsharpcode/fix/1688-xmlns-redefinition
Fix two BAML bugs that make the decompiled XAML unwritable
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
@@ -44,6 +44,7 @@ namespace ICSharpCode.BamlDecompiler
|
||||
new AttributeRewritePass(),
|
||||
new ConnectionIdRewritePass(),
|
||||
new DocumentRewritePass(),
|
||||
new EscapeInvalidXmlCharactersRewritePass(),
|
||||
};
|
||||
|
||||
private BamlDecompilerTypeSystem typeSystem;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <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) };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -135,6 +135,12 @@ namespace ILSpy.BamlDecompiler.Tests
|
||||
RunTest("cases/issue1547");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Issue1688()
|
||||
{
|
||||
RunTest("cases/issue1688");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Issue2052()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 @@
|
||||
<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>
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="InvalidXmlCharacterTests.cs" />
|
||||
<Compile Include="MissingReferencesTests.cs" />
|
||||
<Compile Include="XmlNamespaceResolutionTests.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user