From 8747055beb7e9144001dd31372deaa7305a6487c Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Fri, 4 Sep 2026 14:22:39 +0200 Subject: [PATCH] Recover a StartupUri, and leave a type's own Name property alone Two things #2253 costs an exported application, both found by decompiling one that was built from published source. StartupUri is written in App.xaml but never reaches the BAML: the markup compiler turns the attribute into an assignment inside InitializeComponent, and the project decompiler deletes the generated members. The exported application then builds and opens no window. The assignment is read back out of the generated method and written to the document root again. Where the compiler emits no app.baml at all - App.xaml carrying nothing but attributes - there is no document to write it on, and the export has no ApplicationDefinition either; that is a larger gap of its own. x:Name is recorded as the runtime name property of an element, which is FrameworkElement.Name for everything WPF - a property of the framework. Any property called "Name" on a type of the assembly being decompiled was written back as the directive, so came back as x:Name: the name gets registered and the property stays unset, which still compiles and quietly means something else. The directive is now written only for a name the element does not declare itself. Assisted-by: Claude:claude-opus-5:Claude Code --- .../Handlers/Records/PropertyHandler.cs | 28 +++- .../Rewrite/StartupUriRewritePass.cs | 130 +++++++++++++++++ ICSharpCode.BamlDecompiler/XamlDecompiler.cs | 2 + .../ILSpy.BamlDecompiler.Tests.csproj | 2 + .../RuntimeNamePropertyTests.cs | 131 ++++++++++++++++++ ILSpy.BamlDecompiler.Tests/StartupUriTests.cs | 130 +++++++++++++++++ 6 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs create mode 100644 ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs create mode 100644 ILSpy.BamlDecompiler.Tests/StartupUriTests.cs diff --git a/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs b/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs index a469bdf35..214557049 100644 --- a/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs +++ b/ICSharpCode.BamlDecompiler/Handlers/Records/PropertyHandler.cs @@ -49,11 +49,37 @@ namespace ICSharpCode.BamlDecompiler.Handlers 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); } } + + /// + /// Whether is the name of as + /// x:Name means it, so that the directive can be written instead of the property. + /// + /// 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). + /// + /// + 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; + } } } \ No newline at end of file diff --git a/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs b/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs new file mode 100644 index 000000000..af5dd1c87 --- /dev/null +++ b/ICSharpCode.BamlDecompiler/Rewrite/StartupUriRewritePass.cs @@ -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 +{ + /// + /// Recovers the StartupUri of an application from the code the markup compiler generated for it. + /// + /// 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. + /// + /// + 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)); + } + + /// + /// The string assigned to a StartupUri property in InitializeComponent, if there is one. + /// The generated code reads + /// StartupUri = new Uri("MainWindow.xaml", UriKind.Relative), so the string wanted is + /// the last one loaded before the call to the setter. + /// + 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); + } + } +} diff --git a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs index 7a1e842c2..d0f37ad09 100644 --- a/ICSharpCode.BamlDecompiler/XamlDecompiler.cs +++ b/ICSharpCode.BamlDecompiler/XamlDecompiler.cs @@ -40,6 +40,8 @@ namespace ICSharpCode.BamlDecompiler { static readonly IRewritePass[] rewritePasses = new IRewritePass[] { new XClassRewritePass(), + // After the x:Class pass, which is what establishes the type of the document. + new StartupUriRewritePass(), new MarkupExtensionRewritePass(), new AttributeRewritePass(), new ConnectionIdRewritePass(), diff --git a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj index 949c7f36c..97dde68e0 100644 --- a/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj +++ b/ILSpy.BamlDecompiler.Tests/ILSpy.BamlDecompiler.Tests.csproj @@ -47,6 +47,8 @@ + + diff --git a/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs b/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.cs new file mode 100644 index 000000000..0ac542d4d --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests/RuntimeNamePropertyTests.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; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.PortableExecutable; + +using ICSharpCode.BamlDecompiler; +using ICSharpCode.BamlDecompiler.Handlers; +using ICSharpCode.BamlDecompiler.Xaml; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; + +using NUnit.Framework; + +namespace ILSpy.BamlDecompiler.Tests +{ + /// + /// A type of the assembly being decompiled with a CLR property of its own called "Name". + /// + public class HelperWithItsOwnName + { + public string Name { get; set; } + } + + /// + /// x:Name is the directive that registers a name for an element, and it is recorded as the + /// runtime name property of that element - FrameworkElement.Name for everything WPF, a property + /// of the framework rather than of the assembly being decompiled. Writing the directive for a + /// type's own property of the same name registers a name and leaves the property unset, which + /// still compiles and silently means something else (issue #2253). + /// + [TestFixture] + public class RuntimeNamePropertyTests + { + static ICompilation compilation; + + [OneTimeSetUp] + public void LoadTestAssembly() + { + string location = typeof(RuntimeNamePropertyTests).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()); + compilation = new BamlDecompilerTypeSystem(file, resolver); + } + + static XamlType XamlTypeOf(Type type) + { + var definition = compilation.FindType(type).GetDefinition(); + return new XamlType(definition.ParentModule, definition.ParentModule.FullAssemblyName, + definition.Namespace, definition.Name) { + ResolvedType = definition + }; + } + + /// + /// A property of , the way the decompiler resolves one from + /// the type a BAML attribute record names as its owner. + /// + static XamlProperty PropertyOf(Type declaringType, string propertyName) + { + var declaring = XamlTypeOf(declaringType); + return new XamlProperty(declaring, propertyName) { + ResolvedMember = declaring.ResolvedType.GetDefinition() + .GetProperties(p => p.Name == propertyName).FirstOrDefault() + }; + } + + [Test] + public void ATypesOwnNamePropertyIsNotTheRuntimeName() + { + // : the property belongs to the assembly being + // decompiled, and the value has to reach it. + var property = PropertyOf(typeof(HelperWithItsOwnName), "Name"); + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))), + Is.False); + } + + [Test] + public void ANameInheritedFromTheFrameworkIsTheRuntimeName() + { + // : the document names the control as the owner of + // the attribute, because that is the element it sits on, but the property comes from + // the framework type the control derives from. + var property = new XamlProperty(XamlTypeOf(typeof(HelperWithItsOwnName)), "Name") { + ResolvedMember = compilation.FindType(typeof(MemberInfo)).GetDefinition() + .GetProperties(p => p.Name == "Name").First() + }; + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))), + Is.True); + } + + [Test] + public void AnElementFromOutsideTheAssemblyKeepsItsProperty() + { + var property = PropertyOf(typeof(Uri), "Name"); + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(Uri))), Is.False); + } + + [Test] + public void APropertyOfAnotherNameIsNeverTheRuntimeName() + { + var property = PropertyOf(typeof(Uri), "Title"); + + Assert.That(PropertyHandler.IsRuntimeNameOfElement(property, XamlTypeOf(typeof(HelperWithItsOwnName))), + Is.False); + } + } +} diff --git a/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs b/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs new file mode 100644 index 000000000..1e28c7b72 --- /dev/null +++ b/ILSpy.BamlDecompiler.Tests/StartupUriTests.cs @@ -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.IO; +using System.Reflection.PortableExecutable; + +using ICSharpCode.BamlDecompiler; +using ICSharpCode.BamlDecompiler.Baml; +using ICSharpCode.Decompiler.Metadata; + +using NUnit.Framework; + +namespace ILSpy.BamlDecompiler.Tests +{ + /// + /// StartupUri is written in App.xaml but compiled into App.g.cs, not into the BAML: the markup + /// compiler turns the attribute into an assignment inside InitializeComponent. The project + /// exporter deletes the generated members, so without recovering the assignment the exported + /// application builds and then opens no window (issue #2253). + /// + public class TestApplication + { + public Uri StartupUri { get; set; } + } + + /// + /// What the markup compiler generates for an Application with a StartupUri. + /// + public class AppWithStartupUri : TestApplication + { + public void InitializeComponent() + { + StartupUri = new Uri("MainWindow.xaml", UriKind.Relative); + Uri resourceLocator = new Uri("/Demo;component/app.xaml", UriKind.Relative); + GC.KeepAlive(resourceLocator); + } + } + + /// + /// The same without one, which must stay without one. + /// + public class AppWithoutStartupUri : TestApplication + { + public void InitializeComponent() + { + Uri resourceLocator = new Uri("/Demo;component/app.xaml", UriKind.Relative); + GC.KeepAlive(resourceLocator); + } + } + + [TestFixture] + public class StartupUriTests + { + 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(StartupUriTests).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(); + } + + /// + /// A document whose root is of this assembly, which is what + /// makes the BAML decompiler treat it as the code-behind class of the document. + /// + static string DecompileDocumentOf(string typeName) + { + return Decompile(CreateBaml( + new AssemblyInfoRecord { AssemblyId = 0, AssemblyFullName = "ILSpy.BamlDecompiler.Tests" }, + new TypeInfoRecord { TypeId = 0, AssemblyId = 0, TypeFullName = "ILSpy.BamlDecompiler.Tests." + typeName }, + new ElementStartRecord { TypeId = 0 }, + new ElementEndRecord())); + } + + [Test] + public void TheStartupUriOfTheGeneratedCodeComesBackAsAnAttribute() + { + string xaml = DecompileDocumentOf(nameof(AppWithStartupUri)); + + Assert.That(xaml, Does.Contain(@"StartupUri=""MainWindow.xaml"""), xaml); + } + + [Test] + public void NoStartupUriIsInventedForADocumentThatHasNone() + { + string xaml = DecompileDocumentOf(nameof(AppWithoutStartupUri)); + + Assert.That(xaml, Does.Not.Contain("StartupUri=\""), xaml); + } + } +}