Browse Source

Write XAML a XAML parser can read back

Two of the defects reported on issue #2253 come from the decompiler
writing text that means something else when it is read again.

A markup extension is written as a single attribute value, and its
grammar gives ',' '=' '{' '}' and the quote characters a meaning. Values
went out unquoted, so an argument carrying any of them was read back as
further name/value pairs: {DXBinding Expr='Price - Prev > 0 ? ...'}, the
reported case, no longer compiles at all (MC3042, MC3045). Values without
such a character stay unquoted, because quoting them would rewrite every
document that never needed it.

A clr-namespace declaration names the CLR namespace it maps, but nothing
read that name out of it, so no lookup by namespace could match a
declaration the document itself had made. Every type in such a namespace
then got a second prefix declared on the element that used it. The
assembly is the second half of the same lookup, and there the document
records the name it was written against while a well-known type carries
the assembly it resolves to now - "mscorlib" against
"System.Private.CoreLib" - so the two are also accepted as the same when
the recorded assembly forwards the type.

Assisted-by: Claude:claude-opus-5:Claude Code
pull/4108/head
Siegfried Pammer 2 weeks ago
parent
commit
d8dfbec04b
  1. 12
      ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs
  2. 35
      ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs
  3. 2
      ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs
  4. 4
      ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
  5. 79
      ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
  6. 2
      ICSharpCode.BamlDecompiler/XamlContext.cs
  7. 10
      ICSharpCode.BamlDecompiler/XmlnsDictionary.cs
  8. 6
      ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml
  9. 2
      ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml
  10. 2
      ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj
  11. 133
      ILSpy.BamlDecompiler.Tests/MarkupExtensionQuotingTests.cs
  12. 118
      ILSpy.BamlDecompiler.Tests/XmlnsDeclarationPlacementTests.cs

12
ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs

@ -58,12 +58,20 @@ namespace ICSharpCode.BamlDecompiler.Handlers @@ -58,12 +58,20 @@ namespace ICSharpCode.BamlDecompiler.Handlers
foreach (var asmId in record.AssemblyIds)
{
var assembly = ctx.Baml.ResolveAssembly(asmId);
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace));
// A clr-namespace declaration names its CLR namespace itself. Leaving that unread
// means no lookup by namespace can ever find the declaration the document made,
// and every type in it gets a second prefix of its own (issue #2253).
XamlUtils.TryParseClrNamespace(record.XmlNamespace, out string declaredClrNamespace);
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace, declaredClrNamespace) {
Assembly = assembly.Assembly
});
if (assembly.Assembly?.IsMainModule == true)
{
foreach (var clrNs in ResolveCLRNamespaces(assembly.Assembly, record.XmlNamespace))
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace, clrNs));
ctx.XmlNs.Add(new NamespaceMap(record.Prefix, assembly.FullAssemblyName, record.XmlNamespace, clrNs) {
Assembly = assembly.Assembly
});
}
}

35
ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs

@ -31,6 +31,13 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -31,6 +31,13 @@ namespace ICSharpCode.BamlDecompiler.Xaml
{
public string XmlnsPrefix { get; set; }
public string FullAssemblyName { get; set; }
/// <summary>
/// The assembly <see cref="FullAssemblyName"/> resolves to, where it could be resolved.
/// The name is the one the document was written against, which is not always the name of
/// the assembly the types actually come from.
/// </summary>
public IModule Assembly { get; set; }
public string XMLNamespace { get; set; }
public string CLRNamespace { get; set; }
@ -47,6 +54,34 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -47,6 +54,34 @@ namespace ICSharpCode.BamlDecompiler.Xaml
CLRNamespace = clrNs;
}
/// <summary>
/// Whether <paramref name="map"/> is the declaration to use for a type named
/// <paramref name="typeName"/> in <paramref name="clrNs"/> of
/// <paramref name="fullAssemblyName"/>.
/// </summary>
public static bool Matches(NamespaceMap map, string fullAssemblyName, string clrNs, string typeName)
{
if (map.CLRNamespace != clrNs)
return false;
if (map.FullAssemblyName == fullAssemblyName)
return true;
// The document records the assembly it was written against, while a well-known type
// carries the assembly it resolves to now - "mscorlib" against "System.Private.CoreLib"
// on .NET, say. The two name the same type when the recorded assembly forwards it, and
// then the declaration the document made is the one to use.
return typeName != null && ForwardsOrDeclares(map.Assembly, clrNs, typeName);
}
static bool ForwardsOrDeclares(IModule assembly, string clrNs, string typeName)
{
if (assembly == null)
return false;
var name = new TopLevelTypeName(clrNs, typeName);
if (assembly.GetTypeDefinition(name) != null)
return true;
return assembly.MetadataFile?.GetTypeForwarder(new FullTypeName(name)).IsNil == false;
}
public override string ToString() => $"{XmlnsPrefix}:[{FullAssemblyName}|{CLRNamespace ?? XMLNamespace}]";
}
}

2
ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs

@ -43,7 +43,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -43,7 +43,7 @@ namespace ICSharpCode.BamlDecompiler.Xaml
if (value is XamlExtension)
sb.Append(((XamlExtension)value).ToString(ctx, ctxElement));
else
sb.Append(value.ToString());
sb.Append(XamlUtils.QuoteMarkupExtensionValue(value.ToString()));
}
public string ToString(XamlContext ctx, XElement ctxElement)

4
ICSharpCode.BamlDecompiler/Xaml/XamlType.cs

@ -64,9 +64,9 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -64,9 +64,9 @@ namespace ICSharpCode.BamlDecompiler.Xaml
string xmlNs = null;
if (elem.Annotation<XmlnsScope>() != null)
xmlNs = elem.Annotation<XmlnsScope>().LookupXmlns(FullAssemblyName, TypeNamespace);
xmlNs = elem.Annotation<XmlnsScope>().LookupXmlns(FullAssemblyName, TypeNamespace, TypeName);
if (xmlNs == null)
xmlNs = ctx.XmlNs.LookupXmlns(FullAssemblyName, TypeNamespace);
xmlNs = ctx.XmlNs.LookupXmlns(FullAssemblyName, TypeNamespace, TypeName);
// Sometimes there's no reference to System.Xaml even if x:Type is used
if (xmlNs == null)
xmlNs = XamlContext.TryGetXmlNamespace(Assembly, TypeNamespace, elem);

79
ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs

@ -20,6 +20,7 @@ @@ -20,6 +20,7 @@
THE SOFTWARE.
*/
using System;
using System.IO;
using System.Text;
using System.Xml;
@ -29,6 +30,84 @@ namespace ICSharpCode.BamlDecompiler.Xaml @@ -29,6 +30,84 @@ namespace ICSharpCode.BamlDecompiler.Xaml
{
internal static class XamlUtils
{
static readonly char[] markupExtensionSpecialCharacters = { ',', '=', '\'', '"', '\\' };
/// <summary>
/// Quotes an argument of a markup extension if the parser reading the document again would
/// take part of it for grammar: ',' and '=' separate arguments from one another, a quote
/// character starts a quoted value, '\' escapes whatever follows it, and whitespace at
/// either end is dropped. A value carrying none of those is left as it is, because quoting
/// every value would rewrite every document that never needed it.
/// <para>
/// Braces are grammar only where they are unbalanced: a stray '{' opens an extension and a
/// stray '}' closes the surrounding one, while a matched pair inside a value ("{0:C}",
/// "Element[{ns}Name]") is read as text and stays unquoted. A value beginning with '{' is
/// a nested extension that is already written as one, so it is left alone; the "{}" that
/// escapes a leading brace is not, because inside an extension it would open one.
/// </para>
/// </summary>
public static string QuoteMarkupExtensionValue(string value)
{
if (value == null)
return null;
if (value.StartsWith("{", StringComparison.Ordinal) && !value.StartsWith("{}", StringComparison.Ordinal))
{
return value; // a nested markup extension, already written as one
}
if (value.Length > 0
&& !value.StartsWith("{}", StringComparison.Ordinal)
&& value.IndexOfAny(markupExtensionSpecialCharacters) < 0
&& BracesAreBalanced(value)
&& !char.IsWhiteSpace(value[0])
&& !char.IsWhiteSpace(value[value.Length - 1]))
{
return value;
}
var quoted = new StringBuilder(value.Length + 2);
quoted.Append('\'');
foreach (char c in value)
{
if (c == '\'' || c == '\\')
quoted.Append('\\');
quoted.Append(c);
}
quoted.Append('\'');
return quoted.ToString();
}
static bool BracesAreBalanced(string value)
{
int depth = 0;
foreach (char c in value)
{
if (c == '{')
depth++;
else if (c == '}' && --depth < 0)
return false;
}
return depth == 0;
}
/// <summary>
/// Reads the CLR namespace out of a "clr-namespace:Some.Namespace;assembly=Some.Assembly"
/// declaration. Such a declaration names its CLR namespace itself; the other form of XML
/// namespace ("http://...") maps to CLR namespaces through XmlnsDefinition attributes
/// instead, and has none of its own.
/// </summary>
public static bool TryParseClrNamespace(string xmlNamespace, out string clrNamespace)
{
const string prefix = "clr-namespace:";
clrNamespace = null;
if (xmlNamespace == null || !xmlNamespace.StartsWith(prefix, StringComparison.Ordinal))
return false;
clrNamespace = xmlNamespace.Substring(prefix.Length);
int assembly = clrNamespace.IndexOf(';');
if (assembly >= 0)
clrNamespace = clrNamespace.Substring(0, assembly);
return true;
}
public static string Escape(string value)
{
if (value.Length == 0)

2
ICSharpCode.BamlDecompiler/XamlContext.cs

@ -126,7 +126,7 @@ namespace ICSharpCode.BamlDecompiler @@ -126,7 +126,7 @@ namespace ICSharpCode.BamlDecompiler
}
var clrNs = type.Namespace;
var xmlNs = XmlNs.LookupXmlns(fullAssemblyName, clrNs);
var xmlNs = XmlNs.LookupXmlns(fullAssemblyName, clrNs, type.Name);
typeMap[id] = xamlType = new XamlType(assembly, fullAssemblyName, clrNs, type.Name, GetXmlNamespace(xmlNs)) {
ResolvedType = type

10
ICSharpCode.BamlDecompiler/XmlnsDictionary.cs

@ -40,11 +40,11 @@ namespace ICSharpCode.BamlDecompiler @@ -40,11 +40,11 @@ namespace ICSharpCode.BamlDecompiler
Element = elem;
}
public string LookupXmlns(string fullAssemblyName, string clrNs)
public string LookupXmlns(string fullAssemblyName, string clrNs, string typeName = null)
{
foreach (var ns in this)
{
if (fullAssemblyName == ns.FullAssemblyName && ns.CLRNamespace == clrNs)
if (NamespaceMap.Matches(ns, fullAssemblyName, clrNs, typeName))
return ns.XMLNamespace;
}
@ -119,11 +119,11 @@ namespace ICSharpCode.BamlDecompiler @@ -119,11 +119,11 @@ namespace ICSharpCode.BamlDecompiler
return null;
}
public string LookupXmlns(string fullAssemblyName, string clrNs)
public string LookupXmlns(string fullAssemblyName, string clrNs, string typeName = null)
{
foreach (var map in piMappings)
{
if (fullAssemblyName == map.Value.FullAssemblyName && map.Value.CLRNamespace == clrNs)
if (NamespaceMap.Matches(map.Value, fullAssemblyName, clrNs, typeName))
return map.Key;
}
@ -132,7 +132,7 @@ namespace ICSharpCode.BamlDecompiler @@ -132,7 +132,7 @@ namespace ICSharpCode.BamlDecompiler
{
foreach (var ns in scope)
{
if (fullAssemblyName == ns.FullAssemblyName && ns.CLRNamespace == clrNs)
if (NamespaceMap.Matches(ns, fullAssemblyName, clrNs, typeName))
return ns.XMLNamespace;
}

6
ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml

@ -7,17 +7,17 @@ @@ -7,17 +7,17 @@
<TextBlock Width="100" Text="{Binding Path=Element[{http://planetsNS}DiameterKM].Value}" />
<TextBlock Width="100" Text="{Binding Path=Attribute[Name].Value}" />
<TextBlock Text="{Binding Path=Element[{http://planetsNS}Details].Value}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Date: {0:dddd, MMMM dd}}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat='Date: {0:dddd, MMMM dd}'}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Time: {0:HH:mm}}" />
</StackPanel>
</DataTemplate>
</ResourceDictionary>
</FrameworkElement.Resources>
<TextBlock Text="{Binding Path=ActualWidth, StringFormat=Window width: {0:#,#.0}}" />
<TextBlock Text="{Binding Path=ActualWidth, StringFormat='Window width: {0:#,#.0}'}" />
<TextBlock Text="{Binding Path=ActualHeight, StringFormat=Window height: {0:C}}" />
<WrapPanel Margin="10">
<TextBlock Text="Width: " />
<TextBlock Text="{Binding ActualWidth, StringFormat={}{0:#,#.0}}" />
<TextBlock Text="{Binding ActualWidth, StringFormat='{}{0:#,#.0}'}" />
<StackPanel Margin="10">
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture=de-DE, StringFormat=German date: {0:D}}" />
<TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture=en-US, StringFormat=American date: {0:D}}" />

2
ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<Label xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="{Binding Blub}" Content="{Binding Path=Blah, StringFormat={}{0} items}">
<Label xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" DataContext="{Binding Blub}" Content="{Binding Path=Blah, StringFormat='{}{0} items'}">
<FrameworkElement.Style>
<Style />
</FrameworkElement.Style>

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

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

133
ILSpy.BamlDecompiler.Tests/MarkupExtensionQuotingTests.cs

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
// 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.BamlDecompiler.Xaml;
using ICSharpCode.Decompiler.Metadata;
using NUnit.Framework;
namespace ILSpy.BamlDecompiler.Tests
{
/// <summary>
/// A markup extension is written back as text that the XAML parser reads again, and its
/// grammar gives ',' '=' '{' '}' and the quote characters a meaning. A value carrying one of
/// them has to be quoted, or the parser splits it into name/value pairs that do not exist -
/// the reported case is DevExpress' {DXBinding Expr='...'}, whose expressions are full of
/// commas and equals signs.
/// </summary>
[TestFixture]
public class MarkupExtensionQuotingTests
{
static ushort TypeId(KnownTypes type) => unchecked((ushort)-(short)type);
static ushort MemberId(KnownMembers member) => unchecked((ushort)-(short)member);
[TestCase("plain", ExpectedResult = "plain", TestName = "AnOrdinaryValueIsLeftAlone")]
[TestCase("with space", ExpectedResult = "with space", TestName = "InteriorWhitespaceNeedsNoQuotes")]
[TestCase("a, b", ExpectedResult = "'a, b'", TestName = "CommaSeparatesArguments")]
[TestCase("a = b", ExpectedResult = "'a = b'", TestName = "EqualsStartsANamedArgument")]
[TestCase("{}{0} items", ExpectedResult = "'{}{0} items'", TestName = "AnEscapedLeadingBraceIsQuoted")]
[TestCase("{x:Static local:Thing.Value}", ExpectedResult = "{x:Static local:Thing.Value}", TestName = "ANestedExtensionStaysOne")]
[TestCase("Element[{http://ns}Name].Value", ExpectedResult = "Element[{http://ns}Name].Value", TestName = "BracesInsideAValueAreNotGrammar")]
[TestCase("Date: {0:dddd, MMMM dd}", ExpectedResult = "'Date: {0:dddd, MMMM dd}'", TestName = "ACommaInsideBracesStillSeparates")]
[TestCase("a}b", ExpectedResult = "'a}b'", TestName = "AStrayClosingBraceWouldEndTheExtension")]
[TestCase("a{b", ExpectedResult = "'a{b'", TestName = "AStrayOpeningBraceWouldStartAnExtension")]
[TestCase(" padded ", ExpectedResult = "' padded '", TestName = "EdgeWhitespaceWouldBeTrimmed")]
[TestCase("", ExpectedResult = "''", TestName = "AnEmptyValueNeedsToStayEmpty")]
[TestCase("it's", ExpectedResult = @"'it\'s'", TestName = "TheQuoteCharacterIsEscaped")]
[TestCase(@"back\slash", ExpectedResult = @"'back\\slash'", TestName = "TheEscapeCharacterIsEscaped")]
public string QuotingRules(string value)
{
return XamlUtils.QuoteMarkupExtensionValue(value);
}
/// <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(MarkupExtensionQuotingTests).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();
}
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>
/// Builds "&lt;Button Content="{StaticResource &lt;value&gt;}" /&gt;", whose resource key is
/// the single argument of the extension.
/// </summary>
static string DecompileExtensionArgument(string value)
{
return Decompile(CreateBaml(
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new StringInfoRecord { StringId = 0, Value = value },
new PropertyWithExtensionRecord {
AttributeId = MemberId(KnownMembers.Button_Content),
Flags = (ushort)KnownTypes.StaticResourceExtension,
ValueId = 0
},
new ElementEndRecord()));
}
[Test]
public void AnArgumentCarryingACommaIsQuoted()
{
string xaml = DecompileExtensionArgument("ctor, arg = with comma");
Assert.That(xaml, Does.Contain("{StaticResource 'ctor, arg = with comma'}"));
}
[Test]
public void AnOrdinaryArgumentIsNotQuoted()
{
// Quoting everything would change every document ILSpy prints today.
string xaml = DecompileExtensionArgument("MyResourceKey");
Assert.That(xaml, Does.Contain("{StaticResource MyResourceKey}"));
}
}
}

118
ILSpy.BamlDecompiler.Tests/XmlnsDeclarationPlacementTests.cs

@ -0,0 +1,118 @@ @@ -0,0 +1,118 @@
// 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>
/// A document that already binds a prefix to a CLR namespace must keep using it. The document
/// records the assembly the way it was written - "mscorlib" - while a well-known type carries
/// the assembly it actually resolves to, which is the implementation assembly of whatever
/// runtime ILSpy runs on. Comparing the two by name never matches, and every use of such a
/// type then declared a second prefix for a namespace the root already had (issue #2253).
/// </summary>
[TestFixture]
public class XmlnsDeclarationPlacementTests
{
const string MscorlibFullName = "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
const string SystemNamespaceXmlns = "clr-namespace:System;assembly=mscorlib";
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(XmlnsDeclarationPlacementTests).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();
}
/// <summary>
/// Builds a document whose root binds "sys" to the System namespace of mscorlib and then
/// puts a System.String into the tree.
/// </summary>
static string DecompileDocumentUsingStringUnderAPrefixedRoot()
{
return Decompile(CreateBaml(
new AssemblyInfoRecord { AssemblyId = 0, AssemblyFullName = MscorlibFullName },
new ElementStartRecord { TypeId = TypeId(KnownTypes.Button) },
new XmlnsPropertyRecord {
Prefix = "sys",
XmlNamespace = SystemNamespaceXmlns,
AssemblyIds = new ushort[] { 0 }
},
new PropertyComplexStartRecord { AttributeId = MemberId(KnownMembers.Button_Content) },
new ElementStartRecord { TypeId = TypeId(KnownTypes.String) },
new ElementEndRecord(),
new PropertyComplexEndRecord(),
new ElementEndRecord()));
}
[Test]
public void ThePrefixTheDocumentDeclaredIsTheOneThatGetsUsed()
{
string xaml = DecompileDocumentUsingStringUnderAPrefixedRoot();
Assert.That(xaml, Does.Contain("<sys:String"), xaml);
}
[Test]
public void NoSecondPrefixIsDeclaredForANamespaceTheRootAlreadyBinds()
{
string xaml = DecompileDocumentUsingStringUnderAPrefixedRoot();
Assert.Multiple(() => {
Assert.That(xaml, Does.Not.Contain("xmlns:system="), xaml);
Assert.That(xaml, Does.Not.Contain("System.Private.CoreLib"), xaml);
});
}
}
}
Loading…
Cancel
Save