diff --git a/libs/UIX.DecompXml/DecompileContext.cs b/libs/UIX.DecompXml/DecompileContext.cs new file mode 100644 index 0000000..ac76040 --- /dev/null +++ b/libs/UIX.DecompXml/DecompileContext.cs @@ -0,0 +1,176 @@ +using Humanizer; +using Microsoft.Iris.Asm; +using Microsoft.Iris.Asm.Models; +using Microsoft.Iris.Markup; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Xml.Linq; + +namespace Microsoft.Iris.DecompXml; + +internal class DecompileContext +{ + private readonly MarkupLoadResult _loadResult; + private readonly MarkupLoadResult _dataTableLoadResult; + private readonly Dictionary _namespaces; + private readonly HashSet _usedNamespacePrefixes; + private readonly Dictionary _uriAliasMap; + private Instruction[] _instructions; + private Disassembler.RawConstantInfo[] _constants; + + public DecompileContext(MarkupLoadResult loadResult, MarkupLoadResult dataTableLoadResult = null) + { + _loadResult = loadResult; + + _dataTableLoadResult = dataTableLoadResult + ?? loadResult.BinaryDataTable?.SharedDependenciesTableWithBinaryDataTable?.FirstOrDefault() as MarkupLoadResult; + + _uriAliasMap = new() + { + [_loadResult.Uri] = "me", + ["http://schemas.microsoft.com/2007/uix"] = null + }; + + _namespaces = new() + { + ["me"] = "Me", + [""] = "http://schemas.microsoft.com/2007/uix" + }; + + _usedNamespacePrefixes = []; + + _loadResult.FullLoad(); + if (_loadResult.Status == LoadResultStatus.Error) + throw new Exception($"Failed to load '{_loadResult.ErrorContextUri}'"); + + if (UseSharedDataTable) + { + _dataTableLoadResult.FullLoad(); + if (_dataTableLoadResult.Status == LoadResultStatus.Error) + throw new Exception($"Failed to load '{_dataTableLoadResult.ErrorContextUri}'"); + } + + GenerateNamespaces(); + + _instructions = ObjectSection.Decode(_loadResult.ObjectSection) + .OfType() + .ToArray(); + + _constants = Disassembler.EnumerateConstantInfo(_loadResult).ToArray(); + } + + public Instruction[] Instructions => _instructions; + + public MarkupLoadResult LoadResult => _loadResult; + + public MarkupImportTables ImportTables => _loadResult.ImportTables; + + [MemberNotNullWhen(true, nameof(_dataTableLoadResult))] + private bool UseSharedDataTable => _dataTableLoadResult is not null; + + public Disassembler.RawConstantInfo GetConstant(Operand op) => _constants[((OperandReference)op).Index]; + + public TypeSchema GetImportedType(Operand op) => ImportTables.TypeImports[(ushort)op.Value]; + + public MethodSchema GetImportedMethod(Operand op) => ImportTables.MethodImports[(ushort)op.Value]; + + public PropertySchema GetImportedProperty(Operand op) => ImportTables.PropertyImports[(ushort)op.Value]; + + public IEnumerable> GetUsedNamespaces() + { + return _namespaces + .Where(p => _usedNamespacePrefixes.Contains(p.Key) && !string.IsNullOrEmpty(p.Key)); + } + + public string MapNamespaceToPrefix(string uri) + { + _uriAliasMap.TryGetValue(uri, out string prefix); + _usedNamespacePrefixes.Add(prefix); + return prefix; + } + + public QualifiedTypeName GetQualifiedName(TypeSchema schema) + { + var prefix = MapNamespaceToPrefix(schema.Owner.Uri); + return new(prefix, schema.Name); + } + + public XName GetXName(TypeSchema schema) + { + var prefix = MapNamespaceToPrefix(schema.Owner.Uri); + var ns = _namespaces[prefix ?? ""]; + return ns + schema.Name; + } + + private void GenerateNamespaces() + { + foreach (var typeImport in _loadResult.ImportTables.TypeImports) + { + var typeName = typeImport.Name; + var uri = typeImport.Owner.Uri; + if (!_uriAliasMap.TryGetValue(uri, out var namespacePrefix)) + { + var baseNamespacePrefix = uri; + + var ownerUri = uri; + var schemeLength = uri.IndexOf("://"); + if (schemeLength > 0) + { + var scheme = uri[..schemeLength]; + if (scheme == "assembly") + { + // Assume 'host' is an assembly name and path represents a C# namespace + var path = uri[(schemeLength + 3)..]; + var nsIndex = path.IndexOf('/'); + if (nsIndex >= 0) + { + var importedNamespace = path[(nsIndex + 1)..]; + baseNamespacePrefix = importedNamespace.Split('.', '/', '\\', '!')[^1]; + + System.Reflection.AssemblyName assemblyName = new(path[..nsIndex]); + uri = $"assembly://{assemblyName.Name}/{importedNamespace}"; + } + else + { + // No namespace was specified, assume we're importing the whole assembly + System.Reflection.AssemblyName assemblyName = new(path); + baseNamespacePrefix = assemblyName.Name; + + uri = $"assembly://{assemblyName.Name}/"; + } + } + else + { + // Assume the URI represents a file, + // skip the extension + baseNamespacePrefix = uri.Split('.', '/', '\\', '!')[^2]; + } + } + + baseNamespacePrefix = baseNamespacePrefix.Camelize(); + namespacePrefix = baseNamespacePrefix; + + // Some imports, such as assembly imports, require additional parsing + // and might change the URI that actually gets imported. + if (!_namespaces.ContainsValue(uri)) + { + // Prevent similar imports from generating the same prefix + int duplicateCount = 0; + while (_namespaces.ContainsKey(namespacePrefix)) + namespacePrefix = $"{baseNamespacePrefix}{++duplicateCount}"; + + _namespaces.Add(namespacePrefix, uri); + } + + if (!_uriAliasMap.ContainsKey(uri)) + _uriAliasMap.Add(uri, namespacePrefix); + + // Ensure that the original, un-normalized URI is saved too + if (!_uriAliasMap.ContainsKey(ownerUri)) + _uriAliasMap.Add(ownerUri, namespacePrefix); + } + } + } +} diff --git a/libs/UIX.DecompXml/Decompiler.cs b/libs/UIX.DecompXml/Decompiler.cs index df2f07f..cd73476 100644 --- a/libs/UIX.DecompXml/Decompiler.cs +++ b/libs/UIX.DecompXml/Decompiler.cs @@ -1,11 +1,8 @@ -using Humanizer; -using Microsoft.Iris.Asm; -using Microsoft.Iris.Asm.Models; +using Microsoft.Iris.Asm; using Microsoft.Iris.DecompXml.Mock; using Microsoft.Iris.Markup; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Xml; @@ -15,35 +12,12 @@ namespace Microsoft.Iris.DecompXml; public class Decompiler { - internal readonly MarkupLoadResult _loadResult; - private readonly MarkupLoadResult _dataTableLoadResult; - private readonly XNamespace _nsUix = XNamespace.Get("http://schemas.microsoft.com/2007/uix"); - private readonly Dictionary _namespaces; - private readonly HashSet _usedNamespacePrefixes; - private readonly Dictionary _uriAliasMap; - private Instruction[] _instructions; - private Disassembler.RawConstantInfo[] _constants; + private static readonly XNamespace _nsUix = XNamespace.Get("http://schemas.microsoft.com/2007/uix"); + private readonly DecompileContext _context; - private Decompiler(MarkupLoadResult loadResult, MarkupLoadResult dataTableLoadResult = null) + private Decompiler(DecompileContext context) { - _loadResult = loadResult; - - _dataTableLoadResult = dataTableLoadResult - ?? loadResult.BinaryDataTable?.SharedDependenciesTableWithBinaryDataTable?.FirstOrDefault() as MarkupLoadResult; - - _uriAliasMap = new() - { - [_loadResult.Uri] = "me", - ["http://schemas.microsoft.com/2007/uix"] = null - }; - - _namespaces = new() - { - ["me"] = "Me", - [""] = "http://schemas.microsoft.com/2007/uix" - }; - - _usedNamespacePrefixes = []; + _context = context; } public static Decompiler Load(LoadResult loadResult, LoadResult dataTableLoadResult = null) @@ -51,51 +25,30 @@ public class Decompiler if (loadResult is not MarkupLoadResult markupLoadResult) throw new ArgumentException($"Disassembly can only be performed on markup. Expected '{nameof(MarkupLoadResult)}', got '{loadResult?.GetType().Name}'.", nameof(loadResult)); - if (dataTableLoadResult is MarkupLoadResult dataTableMarkupLoadResult) - return new(markupLoadResult, dataTableMarkupLoadResult); - - if (dataTableLoadResult is not null) + if (dataTableLoadResult is not MarkupLoadResult and not null) throw new ArgumentException($"Data table must be markup. Expected '{nameof(MarkupLoadResult)}', got '{dataTableLoadResult?.GetType().Name}'.", nameof(dataTableLoadResult)); - - return new(markupLoadResult); + + DecompileContext context = new(markupLoadResult, (MarkupLoadResult)dataTableLoadResult); + return new(context); } public XDocument Decompile() { - _loadResult.FullLoad(); - if (_loadResult.Status == LoadResultStatus.Error) - throw new Exception($"Failed to load '{_loadResult.ErrorContextUri}'"); - - if (UseSharedDataTable) - { - _dataTableLoadResult.FullLoad(); - if (_dataTableLoadResult.Status == LoadResultStatus.Error) - throw new Exception($"Failed to load '{_dataTableLoadResult.ErrorContextUri}'"); - } - - GetNamespaces(); - - _instructions = ObjectSection.Decode(_loadResult.ObjectSection) - .OfType() - .ToArray(); - - _constants = Disassembler.EnumerateConstantInfo(_loadResult).ToArray(); - XElement xRoot = new(_nsUix + "UIX", new XAttribute("xmlns", _nsUix)); - foreach (var export in _loadResult.ExportTable.Cast()) + foreach (var export in _context.LoadResult.ExportTable.Cast()) { var name = export.Name; var baseType = export.MarkupTypeBase; - var baseTypeName = GetQualifiedName(baseType); + var baseTypeName = _context.GetQualifiedName(baseType); XElement xExport = new(_nsUix + export.MarkupType.ToString(), new XAttribute("Name", name), new XAttribute("Base", baseTypeName)); var initPropsOffset = export.InitializePropertiesOffset; - var initPropsBody = _instructions + var initPropsBody = _context.Instructions .SkipWhile(i => i.Offset < initPropsOffset) .OrderBy(i => i.Offset) .TakeWhile(i => i.OpCode is not (OpCode.ReturnValue or OpCode.ReturnVoid)) @@ -115,8 +68,7 @@ public class Decompiler } else if (instruction.OpCode is OpCode.PushConstant) { - var constantOperand = (OperandReference)instruction.Operands.First(); - var constant = _constants[constantOperand.Index]; + var constant = _context.GetConstant(instruction.Operands.First()); stack.Push(constant); } else if (instruction.OpCode is OpCode.PushNull) @@ -125,13 +77,13 @@ public class Decompiler } else if (instruction.OpCode is OpCode.ConstructObject) { - var type = _loadResult.ImportTables.TypeImports[(ushort)instruction.Operands.ElementAt(0).Value]; - var xObj = new XElement(GetXName(type)); + var type = _context.GetImportedType(instruction.Operands.ElementAt(0)); + var xObj = new XElement(_context.GetXName(type)); stack.Push(new IrisObject(xObj, type)); } else if (instruction.OpCode is OpCode.MethodInvokeStatic) { - var method = _loadResult.ImportTables.MethodImports[(ushort)instruction.Operands.First().Value]; + var method = _context.GetImportedMethod(instruction.Operands.First()); int parameterCount = method.ParameterTypes.Length; object[] parameters = new object[parameterCount]; @@ -144,28 +96,28 @@ public class Decompiler } else if (instruction.OpCode is OpCode.PropertyInitialize) { - var property = _loadResult.ImportTables.PropertyImports[(ushort)instruction.Operands.ElementAt(0).Value]; + var property = _context.GetImportedProperty(instruction.Operands.ElementAt(0)); var value = stack.Pop(); var target = stack.Pop(); var xTarget = (XElement)ToXmlFriendlyObject(target); - PropertyAssignOnXElement(xTarget, property, IrisObject.Create(value, property.PropertyType, this)); + PropertyAssignOnXElement(xTarget, property, IrisObject.Create(value, property.PropertyType, _context)); stack.Push(new IrisObject(xTarget, property.Owner)); } else if (instruction.OpCode is OpCode.PropertyDictionaryAdd) { - var targetProperty = _loadResult.ImportTables.PropertyImports[(ushort)instruction.Operands.ElementAt(0).Value]; + var targetProperty = _context.GetImportedProperty(instruction.Operands.ElementAt(0)); - var keyReference = (OperandReference)instruction.Operands.ElementAt(1); - var key = _constants[keyReference.Index].Value.ToString(); + var keyReference = instruction.Operands.ElementAt(1); + var key = _context.GetConstant(keyReference).Value.ToString(); var value = stack.Pop(); var targetInstance = stack.Peek() as XElement; - PropertyDictionaryAddOnXElement(targetInstance, targetProperty, IrisObject.Create(value, null, this), key); + PropertyDictionaryAddOnXElement(targetInstance, targetProperty, IrisObject.Create(value, null, _context), key); } } @@ -175,11 +127,11 @@ public class Decompiler XDocument xDoc = new(xRoot); // Add all namespaces to root element - var xNamespaceDeclarations = _namespaces - .Where(p => _usedNamespacePrefixes.Contains(p.Key) && !string.IsNullOrEmpty(p.Key)) - .Select(p => new XAttribute(XNamespace.Xmlns + p.Key, p.Value)); + var xNamespaceDeclarations = _context.GetUsedNamespaces() + .Select(p => new XAttribute(XNamespace.Xmlns + p.Key, p.Value)) + .ToArray(); - xDoc.Root.Add(xNamespaceDeclarations.ToArray()); + xDoc.Root.Add(xNamespaceDeclarations); return xDoc; } @@ -202,96 +154,6 @@ public class Decompiler return sb.ToString(); } - private void GetNamespaces() - { - foreach (var typeImport in _loadResult.ImportTables.TypeImports) - { - var typeName = typeImport.Name; - var uri = typeImport.Owner.Uri; - if (!_uriAliasMap.TryGetValue(uri, out var namespacePrefix)) - { - var baseNamespacePrefix = uri; - - var ownerUri = uri; - var schemeLength = uri.IndexOf("://"); - if (schemeLength > 0) - { - var scheme = uri[..schemeLength]; - if (scheme == "assembly") - { - // Assume 'host' is an assembly name and path represents a C# namespace - var path = uri[(schemeLength + 3)..]; - var nsIndex = path.IndexOf('/'); - if (nsIndex >= 0) - { - var importedNamespace = path[(nsIndex + 1)..]; - baseNamespacePrefix = importedNamespace.Split('.', '/', '\\', '!')[^1]; - - System.Reflection.AssemblyName assemblyName = new(path[..nsIndex]); - uri = $"assembly://{assemblyName.Name}/{importedNamespace}"; - } - else - { - // No namespace was specified, assume we're importing the whole assembly - System.Reflection.AssemblyName assemblyName = new(path); - baseNamespacePrefix = assemblyName.Name; - - uri = $"assembly://{assemblyName.Name}/"; - } - } - else - { - // Assume the URI represents a file, - // skip the extension - baseNamespacePrefix = uri.Split('.', '/', '\\', '!')[^2]; - } - } - - baseNamespacePrefix = baseNamespacePrefix.Camelize(); - namespacePrefix = baseNamespacePrefix; - - // Some imports, such as assembly imports, require additional parsing - // and might change the URI that actually gets imported. - if (!_namespaces.ContainsValue(uri)) - { - // Prevent similar imports from generating the same prefix - int duplicateCount = 0; - while (_namespaces.ContainsKey(namespacePrefix)) - namespacePrefix = $"{baseNamespacePrefix}{++duplicateCount}"; - - _namespaces.Add(namespacePrefix, uri); - } - - if (!_uriAliasMap.ContainsKey(uri)) - _uriAliasMap.Add(uri, namespacePrefix); - - // Ensure that the original, un-normalized URI is saved too - if (!_uriAliasMap.ContainsKey(ownerUri)) - _uriAliasMap.Add(ownerUri, namespacePrefix); - } - }; - } - - internal string MapNamespaceToPrefix(string uri) - { - _uriAliasMap.TryGetValue(uri, out string prefix); - _usedNamespacePrefixes.Add(prefix); - return prefix; - } - - internal QualifiedTypeName GetQualifiedName(TypeSchema schema) - { - var prefix = MapNamespaceToPrefix(schema.Owner.Uri); - return new(prefix, schema.Name); - } - - private XName GetXName(TypeSchema schema) - { - var prefix = MapNamespaceToPrefix(schema.Owner.Uri); - var ns = _namespaces[prefix ?? ""]; - return ns + schema.Name; - } - private static XElement GetOrCreateElement(XElement parent, XName name) { var elem = parent.Element(name); @@ -317,7 +179,7 @@ public class Decompiler string str => str, IStringEncodable strEnc => strEnc.EncodeString(), null => "{null}", - IrisExpression expr => '{' + expr.Decompile(this) + '}', + IrisExpression expr => '{' + expr.Decompile(_context) + '}', XElement xElem => xElem, @@ -366,7 +228,7 @@ public class Decompiler switch (xValue) { case string strValue: - xDictionaryEntry = new(GetXName(value.Type)); + xDictionaryEntry = new(_context.GetXName(value.Type)); xDictionaryEntry.SetAttributeValue(value.Type.Name, strValue); break; case XElement xValueELem: @@ -381,7 +243,4 @@ public class Decompiler return xDictionaryEntry; } - - [MemberNotNullWhen(true, nameof(_dataTableLoadResult))] - private bool UseSharedDataTable => _dataTableLoadResult is not null; } diff --git a/libs/UIX.DecompXml/Mock/IrisConstantExpression.cs b/libs/UIX.DecompXml/Mock/IrisConstantExpression.cs index 493904a..42da0e2 100644 --- a/libs/UIX.DecompXml/Mock/IrisConstantExpression.cs +++ b/libs/UIX.DecompXml/Mock/IrisConstantExpression.cs @@ -19,10 +19,10 @@ internal class IrisConstantExpression : IrisExpression, IReturnValueProvider public TypeSchema ReturnType => TypeSchema; - public override string Decompile(Decompiler decompiler) + public override string Decompile(DecompileContext context) { if (TypeSchema.IsEnum) - return $"{decompiler.GetQualifiedName(TypeSchema)}.{Value}"; + return $"{context.GetQualifiedName(TypeSchema)}.{Value}"; return Value switch { diff --git a/libs/UIX.DecompXml/Mock/IrisExpression.cs b/libs/UIX.DecompXml/Mock/IrisExpression.cs index 6e373f6..cde3919 100644 --- a/libs/UIX.DecompXml/Mock/IrisExpression.cs +++ b/libs/UIX.DecompXml/Mock/IrisExpression.cs @@ -6,7 +6,7 @@ namespace Microsoft.Iris.DecompXml.Mock; internal class IrisExpression : Expression { - public virtual string Decompile(Decompiler decompiler) => ToString(); + public virtual string Decompile(DecompileContext context) => ToString(); public static Expression ToExpression(object p) { diff --git a/libs/UIX.DecompXml/Mock/IrisMethodCallExpression.cs b/libs/UIX.DecompXml/Mock/IrisMethodCallExpression.cs index 2d5cdf3..e3ed376 100644 --- a/libs/UIX.DecompXml/Mock/IrisMethodCallExpression.cs +++ b/libs/UIX.DecompXml/Mock/IrisMethodCallExpression.cs @@ -29,11 +29,11 @@ internal class IrisMethodCallExpression : IrisExpression, IArgumentProvider, IRe public Expression GetArgument(int index) => _arguments[index]; - public override string Decompile(Decompiler decompiler) + public override string Decompile(DecompileContext context) { StringBuilder sb = new(); - var qfn = decompiler.GetQualifiedName(Method.Owner); + var qfn = context.GetQualifiedName(Method.Owner); sb.Append(qfn); sb.Append('.'); sb.Append(Method.Name); @@ -47,7 +47,7 @@ internal class IrisMethodCallExpression : IrisExpression, IArgumentProvider, IRe string exprToString(Expression x) { return x is IrisExpression irisExpr - ? irisExpr.Decompile(decompiler) + ? irisExpr.Decompile(context) : x.ToString(); } } diff --git a/libs/UIX.DecompXml/Mock/IrisObject.cs b/libs/UIX.DecompXml/Mock/IrisObject.cs index 3684812..31a5f7e 100644 --- a/libs/UIX.DecompXml/Mock/IrisObject.cs +++ b/libs/UIX.DecompXml/Mock/IrisObject.cs @@ -1,15 +1,11 @@ using Microsoft.Iris.Asm; using Microsoft.Iris.Markup; -using System; -using System.Collections.Generic; -using System.Text; -using System.Xml.Linq; namespace Microsoft.Iris.DecompXml.Mock; internal record IrisObject(object Object, TypeSchema Type) { - public static IrisObject Create(object objIn, TypeSchema type, Decompiler decompiler) + public static IrisObject Create(object objIn, TypeSchema type, DecompileContext context) { object obj = objIn; @@ -31,7 +27,7 @@ internal record IrisObject(object Object, TypeSchema Type) type ??= hasReturnValue.ReturnType; } - type ??= Disassembler.GuessTypeSchema(obj.GetType(), decompiler._loadResult); + type ??= Disassembler.GuessTypeSchema(obj.GetType(), context.LoadResult); return new(obj, type); }