diff --git a/ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs b/ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs index 7eb462ffb..ac5d8a268 100644 --- a/ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs +++ b/ICSharpCode.BamlDecompiler/Handlers/Records/XmlnsPropertyHandler.cs @@ -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 + }); } } diff --git a/ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs b/ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs index a81bec5cd..33411ccff 100644 --- a/ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs +++ b/ICSharpCode.BamlDecompiler/Xaml/NamespaceMap.cs @@ -31,6 +31,13 @@ namespace ICSharpCode.BamlDecompiler.Xaml { public string XmlnsPrefix { get; set; } public string FullAssemblyName { get; set; } + + /// + /// The assembly 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. + /// + public IModule Assembly { get; set; } public string XMLNamespace { get; set; } public string CLRNamespace { get; set; } @@ -47,6 +54,34 @@ namespace ICSharpCode.BamlDecompiler.Xaml CLRNamespace = clrNs; } + /// + /// Whether is the declaration to use for a type named + /// in of + /// . + /// + 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}]"; } } \ No newline at end of file diff --git a/ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs b/ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs index cd3e664ca..0d45d7a5b 100644 --- a/ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs +++ b/ICSharpCode.BamlDecompiler/Xaml/XamlExtension.cs @@ -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) diff --git a/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs b/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs index 09d287c18..a5c2be0f3 100644 --- a/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs +++ b/ICSharpCode.BamlDecompiler/Xaml/XamlType.cs @@ -64,9 +64,9 @@ namespace ICSharpCode.BamlDecompiler.Xaml string xmlNs = null; if (elem.Annotation() != null) - xmlNs = elem.Annotation().LookupXmlns(FullAssemblyName, TypeNamespace); + xmlNs = elem.Annotation().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); diff --git a/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs b/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs index 572667f1d..34a8b7ce8 100644 --- a/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs +++ b/ICSharpCode.BamlDecompiler/Xaml/XamlUtils.cs @@ -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 { internal static class XamlUtils { + static readonly char[] markupExtensionSpecialCharacters = { ',', '=', '\'', '"', '\\' }; + + /// + /// 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. + /// + /// 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. + /// + /// + 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; + } + + /// + /// 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. + /// + 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) diff --git a/ICSharpCode.BamlDecompiler/XamlContext.cs b/ICSharpCode.BamlDecompiler/XamlContext.cs index 5fc1752aa..2d6e5c104 100644 --- a/ICSharpCode.BamlDecompiler/XamlContext.cs +++ b/ICSharpCode.BamlDecompiler/XamlContext.cs @@ -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 diff --git a/ICSharpCode.BamlDecompiler/XmlnsDictionary.cs b/ICSharpCode.BamlDecompiler/XmlnsDictionary.cs index 8f90d47a8..fcb5ccbfe 100644 --- a/ICSharpCode.BamlDecompiler/XmlnsDictionary.cs +++ b/ICSharpCode.BamlDecompiler/XmlnsDictionary.cs @@ -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 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 { foreach (var ns in scope) { - if (fullAssemblyName == ns.FullAssemblyName && ns.CLRNamespace == clrNs) + if (NamespaceMap.Matches(ns, fullAssemblyName, clrNs, typeName)) return ns.XMLNamespace; } diff --git a/ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml b/ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml index 004ac0d22..8dd4c16b9 100644 --- a/ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml +++ b/ILSpy.BamlDecompiler.Tests.Windows/Cases/EscapeSequence.xaml @@ -7,17 +7,17 @@ - + - + - + diff --git a/ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml b/ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml index ab059da44..0ba8c34c5 100644 --- a/ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml +++ b/ILSpy.BamlDecompiler.Tests.Windows/Cases/MarkupExtension.xaml @@ -1,4 +1,4 @@ -