diff --git a/UIXC/Commands/DecompileCommand.cs b/UIXC/Commands/DecompileCommand.cs index a4cfef6..3db669a 100644 --- a/UIXC/Commands/DecompileCommand.cs +++ b/UIXC/Commands/DecompileCommand.cs @@ -1,5 +1,6 @@ using Microsoft.Iris.Asm; using Microsoft.Iris.Debug; +using Microsoft.Iris.DecompXml; using Microsoft.Iris.Markup; using Spectre.Console; using Spectre.Console.Cli; @@ -77,42 +78,54 @@ public class DecompileCommand : CompilerCommandBase } } + Func decompilerMethod; + if (settings.Language == SourceLanguage.Asm) { - foreach (var input in enumeratedInputs.Where(f => !f.EndsWith(".uixa"))) + decompilerMethod = loadResult => { - try - { - var uibLoadResult = LoadIrisFile(input, settings); - if (uibLoadResult.Status != LoadResultStatus.Success) - throw new Exception($"Failed to load UIB source from {uibLoadResult.ErrorContextUri}"); - - var disassembler = Disassembler.Load(uibLoadResult); - var asm = disassembler.Write(); - - var output = GetOutputPath(settings, input); - File.WriteAllText(output, asm); - - AnsiConsole.MarkupLineInterpolated($"[green]Decompiled to '{output}'[/]"); - success = true; - } - catch (Exception ex) - { - AnsiConsole.WriteException(ex); - success = false; - } - } + var disassembler = Disassembler.Load(loadResult); + return disassembler.Write(); + }; } else if (settings.Language == SourceLanguage.Xml) { - AnsiConsole.MarkupLine("[red]UIX XML is not currently a supported decompilation target.[/]"); + decompilerMethod = loadResult => + { + var decompiler = Decompiler.Load(loadResult); + return decompiler.DecompileToSource(); + }; } else { AnsiConsole.MarkupLine($"[red]{settings.Language} is not currently a supported decompilation target.[/]"); + goto done; } - done: + foreach (var input in enumeratedInputs)//.Where(f => !f.EndsWith(settings.Language.GetExtension()))) + { + try + { + var uibLoadResult = LoadIrisFile(input, settings); + if (uibLoadResult.Status != LoadResultStatus.Success) + throw new Exception($"Failed to load UIB source from {uibLoadResult.ErrorContextUri}"); + + var decompiledSource = decompilerMethod(uibLoadResult); + + var output = GetOutputPath(settings, input); + File.WriteAllText(output, decompiledSource); + + AnsiConsole.MarkupLineInterpolated($"[green]Decompiled to '{output}'[/]"); + success = true; + } + catch (Exception ex) + { + AnsiConsole.WriteException(ex); + success = false; + } + } + + done: StopErrorReporting(); Report?.Render(AnsiConsole.Console); AnsiConsole.WriteLine(); diff --git a/UIXC/Properties/launchSettings.json b/UIXC/Properties/launchSettings.json index cde3180..3b316ad 100644 --- a/UIXC/Properties/launchSettings.json +++ b/UIXC/Properties/launchSettings.json @@ -2,7 +2,9 @@ "profiles": { "UIXC": { "commandName": "Project", - "commandLineArgs": "decompile C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources_uixa -t C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources_uixa\\_DATATABLE.uixa", + "commandLineArgs": "decompile C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources\\AboutDialog.uix -l xml -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources_xml -t C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources\\_DATATABLE.uib", + + //"commandLineArgs": "decompile C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources -l xml -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources_xml -t C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources\\_DATATABLE.uib", //"commandLineArgs": "decompile C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources\\_DATATABLE.UIB -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources_uixa", //"commandLineArgs": "compile C:\\Users\\jjask\\Documents\\Dump\\Zune\\ZuneShellResources\\CreatePassport.uixa -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o C:\\Users\\jjask\\Documents\\Dump\\Zune", //"commandLineArgs": "compile C:\\Users\\jjask\\Documents\\Dump\\Zune\\_DATATABLE.uixa -A ZuneShell.dll -A UIXControls.dll -A ZuneDBApi.dll -o C:\\Users\\jjask\\Documents\\Dump\\Zune", diff --git a/UIXC/UIXC.csproj b/UIXC/UIXC.csproj index 29dab41..870377e 100644 --- a/UIXC/UIXC.csproj +++ b/UIXC/UIXC.csproj @@ -13,6 +13,7 @@ + diff --git a/ZuneUIXTools.sln b/ZuneUIXTools.sln index d42d6c0..cb0de30 100644 --- a/ZuneUIXTools.sln +++ b/ZuneUIXTools.sln @@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UIX.Test", "libs\UIX.Test\U EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXC", "UIXC\UIXC.csproj", "{F918CB09-0AE6-420E-B75C-852C0666483C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIX.DecompXml", "libs\UIX.DecompXml\UIX.DecompXml.csproj", "{3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -155,6 +157,18 @@ Global {F918CB09-0AE6-420E-B75C-852C0666483C}.Release|x64.Build.0 = Release|Any CPU {F918CB09-0AE6-420E-B75C-852C0666483C}.Release|x86.ActiveCfg = Release|Any CPU {F918CB09-0AE6-420E-B75C-852C0666483C}.Release|x86.Build.0 = Release|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Debug|x64.ActiveCfg = Debug|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Debug|x64.Build.0 = Debug|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Debug|x86.ActiveCfg = Debug|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Debug|x86.Build.0 = Debug|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|Any CPU.Build.0 = Release|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x64.ActiveCfg = Release|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x64.Build.0 = Release|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x86.ActiveCfg = Release|Any CPU + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -167,6 +181,7 @@ Global {CB2B7609-C7B8-4E2A-82D8-DD631B70B621} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} {D311AFFC-CA27-4349-8B01-D5CF0E5FC2B6} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} {C401799F-64DA-41E2-BC37-BE3C304FAE19} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} + {3AE2DF12-C52B-48DB-9120-BCA32BB04FB2} = {470F14DD-0489-4BE5-BB4B-421EDB4021AC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4FFAF324-5FC4-4AAF-A0D7-BAA0D81C2B4C} diff --git a/libs/MicrosoftIris b/libs/MicrosoftIris index 72f338a..9d76811 160000 --- a/libs/MicrosoftIris +++ b/libs/MicrosoftIris @@ -1 +1 @@ -Subproject commit 72f338a729e248f3f152f9c999530504e62ad14e +Subproject commit 9d7681113332178a41f306509694e02441329a56 diff --git a/libs/UIX.Asm/UIX.Asm.csproj b/libs/UIX.Asm/UIX.Asm.csproj index 1fc6b0a..f8d7049 100644 --- a/libs/UIX.Asm/UIX.Asm.csproj +++ b/libs/UIX.Asm/UIX.Asm.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/libs/UIX.DecompXml/Decompiler.cs b/libs/UIX.DecompXml/Decompiler.cs new file mode 100644 index 0000000..2842462 --- /dev/null +++ b/libs/UIX.DecompXml/Decompiler.cs @@ -0,0 +1,223 @@ +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.Text; +using System.Xml; +using System.Xml.Linq; + +namespace Microsoft.Iris.DecompXml; + +public class Decompiler +{ + private readonly MarkupLoadResult _loadResult; + private readonly MarkupLoadResult _dataTableLoadResult; + private readonly Dictionary _namespaces; + private readonly Dictionary _uriAliasMap; + + private Decompiler(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" + }; + } + + public static Decompiler Load(LoadResult loadResult, LoadResult dataTableLoadResult = null) + { + 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) + throw new ArgumentException($"Data table must be markup. Expected '{nameof(MarkupLoadResult)}', got '{dataTableLoadResult?.GetType().Name}'.", nameof(dataTableLoadResult)); + + return new(markupLoadResult); + } + + 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(); + + XNamespace nsUix = XNamespace.Get("http://schemas.microsoft.com/2007/uix"); + XElement xRoot = new(nsUix + "UIX", new XAttribute("xmlns", nsUix)); + + foreach (var export in _loadResult.ExportTable.Cast()) + { + var name = export.Name; + + var baseType = export.MarkupTypeBase; + var baseTypeName = GetQualifiedName(baseType); + + XElement xExport = new(nsUix + export.MarkupType.ToString(), + new XAttribute("Name", name), + new XAttribute("Base", baseTypeName)); + + var propertyElements = new XElement[export.Properties.Length]; + for (int i = 0; i < export.Properties.Length; i++) + { + PropertySchema property = export.Properties[i]; + var typeName = GetXName(property.PropertyType); + + // TODO: Decode object section to get default property assignments + XElement xProperty = new(typeName, + new XAttribute("Name", property.Name)); + + propertyElements[i] = xProperty; + } + + XElement xProperties = new(nsUix + "Properties", propertyElements); + xExport.Add(xProperties); + + xRoot.Add(xExport); + } + + XDocument xDoc = new(xRoot); + + // Add all namespaces to root element + foreach (var pair in _namespaces) + { + var prefix = pair.Key; + var ns = pair.Value; + + if (string.IsNullOrEmpty(prefix)) + continue; + + xDoc.Root.SetAttributeValue(XNamespace.Xmlns + prefix, ns); + } + return xDoc; + } + + public string DecompileToSource() + { + var xmlDoc = Decompile(); + + XmlWriterSettings writerSettings = new() + { + Indent = true, + NamespaceHandling = NamespaceHandling.OmitDuplicates, + }; + + StringBuilder sb = new(); + using (XmlWriter writer = XmlWriter.Create(sb, writerSettings)) + { + xmlDoc.WriteTo(writer); + } + 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); + } + }; + } + + private QualifiedTypeName GetQualifiedName(TypeSchema schema) + { + _uriAliasMap.TryGetValue(schema.Owner.Uri, out string prefix); + return new(prefix, schema.Name); + } + + private XName GetXName(TypeSchema schema) + { + _uriAliasMap.TryGetValue(schema.Owner.Uri, out string prefix); + var ns = _namespaces[prefix ?? ""]; + return ns + schema.Name; + } + + [MemberNotNullWhen(true, nameof(_dataTableLoadResult))] + private bool UseSharedDataTable => _dataTableLoadResult is not null; +} diff --git a/libs/UIX.DecompXml/UIX.DecompXml.csproj b/libs/UIX.DecompXml/UIX.DecompXml.csproj new file mode 100644 index 0000000..27b7420 --- /dev/null +++ b/libs/UIX.DecompXml/UIX.DecompXml.csproj @@ -0,0 +1,18 @@ + + + + netstandard2.0;net8.0 + 12 + Microsoft.Iris.DecompXml + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + +