Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions ICSharpCode.BamlDecompiler/BamlDecompilerTypeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,29 @@ public class BamlDecompilerTypeSystem : SimpleCompilation, IDecompilerTypeSystem
"System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
};

// A type each of these assemblies must define for the module resolved under its name to be
// the assembly BAML means by it. .NET ships a WindowsBase facade on every platform - it
// resolves everywhere and carries none of the WPF types, because those live in the
// WindowsDesktop runtime pack - and a module like that has to give way to the synthetic
// stand-in the way an assembly that does not resolve at all does. Without this, a document
// using System.Windows.Point or Size is lost outright on a machine without WPF.
static readonly Dictionary<string, TopLevelTypeName> wellKnownProbeTypes = new(StringComparer.OrdinalIgnoreCase) {
["WindowsBase"] = new TopLevelTypeName("System.Windows", "Point"),
["PresentationCore"] = new TopLevelTypeName("System.Windows.Media", "Brush"),
["PresentationFramework"] = new TopLevelTypeName("System.Windows.Controls", "Button")
};

/// <summary>
/// Whether <paramref name="file"/> is the assembly its name claims, rather than a facade
/// standing where it should be.
/// </summary>
static bool IsTheAssemblyItIsNamedAfter(MetadataFile file)
{
if (!wellKnownProbeTypes.TryGetValue(file.Name, out var probeType))
return true;
return !file.GetTypeDefinition(probeType).IsNil;
}

// The WPF assemblies whose types serialize under the presentation XML namespace. When one of
// these has to be synthesized (e.g. inspecting a WPF binary on a non-Windows machine), the
// synthetic module reproduces its XmlnsDefinitionAttribute mapping so known types still emit
Expand Down Expand Up @@ -120,6 +143,10 @@ public BamlDecompilerTypeSystem(MetadataFile mainModule, IAssemblyResolver assem
}
}
}
// A facade standing in for a well-known assembly is worse than nothing: it satisfies the
// name, so no stand-in is synthesized, and then every type BAML expects from it is
// missing. Drop it and let the stand-in below take its place.
referencedAssemblies.RemoveAll(file => !IsTheAssemblyItIsNamedAfter(file));
var mainModuleWithOptions = mainModule.WithOptions(TypeSystemOptions.Default);
var referencedAssembliesWithOptions = referencedAssemblies.Select(file => file.WithOptions(TypeSystemOptions.Default));
// Substitute a synthetic stand-in for every well-known BAML assembly that could not be
Expand Down
28 changes: 27 additions & 1 deletion ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,37 @@ XAttribute ConstructXAttribute()
if (xamlProp.IsAttachedTo(elemType))
return new XAttribute(xamlProp.ToXName(ctx, parent.Xaml, true), value);

if (xamlProp.PropertyName == "Name" && elemType.ResolvedType.GetDefinition()?.ParentModule.IsMainModule == true)
if (IsRuntimeNameOfElement(xamlProp, elemType))
return new XAttribute(ctx.GetKnownNamespace("Name", XamlContext.KnownNamespace_Xaml), value);

return new XAttribute(xamlProp.ToXName(ctx, parent.Xaml, false), value);
}
}

/// <summary>
/// Whether <paramref name="property"/> is the name of <paramref name="elementType"/> as
/// x:Name means it, so that the directive can be written instead of the property.
/// <para>
/// x:Name is recorded as the runtime name property of the element, which is
/// FrameworkElement.Name for everything WPF - a property of the framework, not of the
/// assembly being decompiled. A type of that assembly declaring a property of its own called
/// "Name" is an ordinary property: writing the directive for it registers a name and leaves
/// the property unset, which still compiles and silently means something else (issue #2253).
/// </para>
/// </summary>
internal static bool IsRuntimeNameOfElement(XamlProperty property, XamlType elementType)
{
if (property.PropertyName != "Name")
return false;
if (elementType?.ResolvedType.GetDefinition()?.ParentModule.IsMainModule != true)
return false;
// The type that declares the property, not the one the document names as the owner of
// the attribute: a control of the assembly being decompiled inherits Name from the
// framework, and the document names the control. Only a Name the type declares itself
// is a property of its own rather than the runtime name.
var declaringType = property.ResolvedMember?.DeclaringTypeDefinition
?? property.DeclaringType?.ResolvedType?.GetDefinition();
return declaringType?.ParentModule.IsMainModule != true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,20 @@ public BamlElement Translate(XamlContext ctx, BamlNode node, BamlElement parent)
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
});
}
}

Expand Down
130 changes: 130 additions & 0 deletions ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Xml.Linq;

using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.TypeSystem;

namespace ICSharpCode.BamlDecompiler.Rewrite
{
/// <summary>
/// Recovers the StartupUri of an application from the code the markup compiler generated for it.
/// <para>
/// StartupUri is written in App.xaml, but it does not reach the BAML: the markup compiler turns
/// the attribute into an assignment inside InitializeComponent. The project decompiler deletes
/// the generated members, so a document decompiled without this would build into an application
/// that starts and shows nothing.
/// </para>
/// </summary>
internal class StartupUriRewritePass : IRewritePass
{
const string StartupUriPropertyName = "StartupUri";

public void Run(XamlContext ctx, XDocument document)
{
var root = document.Elements().FirstOrDefault()?.Elements().FirstOrDefault();
if (root == null || root.Attribute(StartupUriPropertyName) != null)
return;
// The type of the document, which the x:Class pass has recorded by the time this runs.
if (ctx.XClassNames.FirstOrDefault() is not string className)
return;
var typeDefinition = ctx.TypeSystem.MainModule.GetTypeDefinition(new FullTypeName(className).TopLevelTypeName);
if (typeDefinition == null)
return;

string startupUri = FindAssignedStartupUri(typeDefinition);
if (startupUri != null)
root.Add(new XAttribute(StartupUriPropertyName, startupUri));
}

/// <summary>
/// The string assigned to a StartupUri property in InitializeComponent, if there is one.
/// The generated code reads
/// <c>StartupUri = new Uri("MainWindow.xaml", UriKind.Relative)</c>, so the string wanted is
/// the last one loaded before the call to the setter.
/// </summary>
static string FindAssignedStartupUri(ITypeDefinition typeDefinition)
{
var method = typeDefinition.Methods.FirstOrDefault(
m => m.Name == "InitializeComponent" && m.Parameters.Count == 0);
if (method?.MetadataToken.IsNil != false)
return null;
var module = typeDefinition.ParentModule?.MetadataFile;
if (module == null)
return null;

try
{
var metadata = module.Metadata;
var methodDefinition = metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken);
if (methodDefinition.RelativeVirtualAddress == 0)
return null;
var body = module.GetMethodBody(methodDefinition.RelativeVirtualAddress);
var reader = body.GetILReader();
string lastLoadedString = null;
while (reader.RemainingBytes > 0)
{
var opCode = reader.DecodeOpCode();
switch (opCode)
{
case ILOpCode.Ldstr:
lastLoadedString = metadata.GetUserString(
MetadataTokens.UserStringHandle(reader.ReadInt32()));
break;
case ILOpCode.Call:
case ILOpCode.Callvirt:
var target = MetadataTokens.EntityHandle(reader.ReadInt32());
if (lastLoadedString != null && IsStartupUriSetter(metadata, target))
return lastLoadedString;
break;
default:
ILParser.SkipOperand(ref reader, opCode);
break;
}
}
}
catch (BadImageFormatException)
{
// A method body nobody can read says nothing about the StartupUri.
}
return null;
}

static bool IsStartupUriSetter(MetadataReader metadata, EntityHandle handle)
{
StringHandle name;
switch (handle.Kind)
{
case HandleKind.MethodDefinition:
name = metadata.GetMethodDefinition((MethodDefinitionHandle)handle).Name;
break;
case HandleKind.MemberReference:
name = metadata.GetMemberReference((MemberReferenceHandle)handle).Name;
break;
default:
return false;
}
return metadata.StringComparer.Equals(name, "set_" + StartupUriPropertyName);
}
}
}
35 changes: 35 additions & 0 deletions ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ internal class NamespaceMap
{
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; }

Expand All @@ -47,6 +54,34 @@ public NamespaceMap(string prefix, string fullAssemblyName, string xmlNs, string
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 changes: 1 addition & 1 deletion ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static void WriteObject(StringBuilder sb, XamlContext ctx, XElement ctxElement,
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)
Expand Down
4 changes: 2 additions & 2 deletions ICSharpCode.BamlDecompiler/Xaml/XamlType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ public void ResolveNamespace(XElement elem, XamlContext ctx)

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);
Expand Down
79 changes: 79 additions & 0 deletions ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

using System;
using System.IO;
using System.Text;
using System.Xml;
Expand All @@ -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)
Expand Down
Loading
Loading