| 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. |
| 2 | |
| 3 | namespace WixToolset.Core.WindowsInstaller.CommandLine |
| 4 | { |
| 5 | using System; |
| 6 | using System.IO; |
| 7 | using System.Threading; |
| 8 | using System.Threading.Tasks; |
| 9 | using System.Xml.Linq; |
| 10 | using WixToolset.Data; |
| 11 | using WixToolset.Extensibility; |
| 12 | using WixToolset.Extensibility.Data; |
| 13 | using WixToolset.Extensibility.Services; |
| 14 | |
| 15 | internal class DecompilerSubcommand : WindowsInstallerSubcommandBase |
| 16 | { |
| 17 | public DecompilerSubcommand(IServiceProvider serviceProvider) |
| 18 | { |
| 19 | this.ServiceProvider = serviceProvider; |
| 20 | this.Messaging = serviceProvider.GetService<IMessaging>(); |
| 21 | } |
| 22 | |
| 23 | private IServiceProvider ServiceProvider { get; } |
| 24 | |
| 25 | private IMessaging Messaging { get; } |
| 26 | |
| 27 | private string InputPath { get; set; } |
| 28 | |
| 29 | private string DecompileType { get; set; } |
| 30 | |
| 31 | private string IntermediateFolder { get; set; } |
| 32 | |
| 33 | private string OutputPath { get; set; } |
| 34 | |
| 35 | private string ExportBasePath { get; set; } |
| 36 | |
| 37 | private bool SaveAsData { get; set; } |
| 38 | |
| 39 | private bool SuppressCustomTables { get; set; } |
| 40 | |
| 41 | private bool SuppressDroppingEmptyTables { get; set; } |
| 42 | |
| 43 | private bool SuppressRelativeActionSequencing { get; set; } |
| 44 | |
| 45 | private bool SuppressUI { get; set; } |
| 46 | |
| 47 | public override CommandLineHelp GetCommandLineHelp() |
| 48 | { |
| 49 | return new CommandLineHelp("Converts a Windows Installer database back into source code.", "msi decompile [options] {inputfile.msi|inputfile.msm}", new[] |
| 50 | { |
| 51 | new CommandLineHelpSwitch("-data", "Save output as WiX data instead of as a source file."), |
| 52 | new CommandLineHelpSwitch("-sct", "Suppress decompiling custom tables."), |
| 53 | new CommandLineHelpSwitch("-sdet", "Suppress dropping empty tables."), |
| 54 | new CommandLineHelpSwitch("-sras", "Suppress relative action sequencing."), |
| 55 | new CommandLineHelpSwitch("-sui", "Suppress decompiling UI tables."), |
| 56 | new CommandLineHelpSwitch("-type", "Optionally specify the input file type: msi or msm. If not specified, type will be inferred by file extension."), |
| 57 | new CommandLineHelpSwitch("-intermediateFolder", "Optional working folder. If not specified, %TMP% will be used."), |
| 58 | new CommandLineHelpSwitch("-out", "-o", "Optional path for the decompiled output file. If not specified, output path will have the same base name as the input file in the same directory."), |
| 59 | new CommandLineHelpSwitch("-x", "If specified, export embedded binaries and icons to specified folder."), |
| 60 | }); |
| 61 | } |
| 62 | |
| 63 | public override Task<int> ExecuteAsync(CancellationToken cancellationToken) |
| 64 | { |
| 65 | if (String.IsNullOrEmpty(this.InputPath)) |
| 66 | { |
| 67 | this.Messaging.Write(ErrorMessages.FilePathRequired("input MSI or MSM database")); |
| 68 | } |
| 69 | else if (!this.TryCalculateDecompileType(out var decompileType)) |
| 70 | { |
| 71 | this.Messaging.Write(WindowsInstallerBackendErrors.UnknownDecompileType(this.DecompileType, this.InputPath)); |
| 72 | } |
| 73 | else |
| 74 | { |
| 75 | if (String.IsNullOrEmpty(this.IntermediateFolder)) |
| 76 | { |
| 77 | this.IntermediateFolder = Path.GetTempPath(); |
| 78 | } |
| 79 | |
| 80 | if (String.IsNullOrEmpty(this.OutputPath)) |
| 81 | { |
| 82 | var defaultExtension = this.CalculateExtensionFromDecompileType(decompileType); |
| 83 | |
| 84 | this.OutputPath = Path.ChangeExtension(this.InputPath, defaultExtension); |
| 85 | } |
| 86 | |
| 87 | var extensionManager = this.ServiceProvider.GetService<IExtensionManager>(); |
| 88 | var creator = this.ServiceProvider.GetService<ISymbolDefinitionCreator>(); |
| 89 | |
| 90 | var context = this.ServiceProvider.GetService<IWindowsInstallerDecompileContext>(); |
| 91 | context.Extensions = extensionManager.GetServices<IWindowsInstallerDecompilerExtension>(); |
| 92 | context.ExtensionData = extensionManager.GetServices<IExtensionData>(); |
| 93 | context.DecompilePath = this.InputPath; |
| 94 | context.DecompileType = decompileType; |
| 95 | context.IntermediateFolder = this.IntermediateFolder; |
| 96 | context.SymbolDefinitionCreator = creator; |
| 97 | context.OutputPath = this.OutputPath; |
| 98 | |
| 99 | context.ExtractFolder = this.ExportBasePath; |
| 100 | context.SuppressCustomTables = this.SuppressCustomTables; |
| 101 | context.SuppressDroppingEmptyTables = this.SuppressDroppingEmptyTables; |
| 102 | context.SuppressRelativeActionSequencing = this.SuppressRelativeActionSequencing; |
| 103 | context.SuppressUI = this.SuppressUI; |
| 104 | |
| 105 | try |
| 106 | { |
| 107 | var decompiler = this.ServiceProvider.GetService<IWindowsInstallerDecompiler>(); |
| 108 | var result = decompiler.Decompile(context); |
| 109 | |
| 110 | if (!this.Messaging.EncounteredError) |
| 111 | { |
| 112 | Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(context.OutputPath))); |
| 113 | if (this.SaveAsData || result.Document == null) |
| 114 | { |
| 115 | using (var output = WixOutput.Create(context.OutputPath)) |
| 116 | { |
| 117 | result.Data.Save(output); |
| 118 | } |
| 119 | } |
| 120 | else |
| 121 | { |
| 122 | result.Document.Save(context.OutputPath, SaveOptions.OmitDuplicateNamespaces); |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | catch (WixException e) |
| 127 | { |
| 128 | this.Messaging.Write(e.Error); |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | return Task.FromResult(this.Messaging.LastErrorNumber); |
| 133 | } |
| 134 | |
| 135 | public override bool TryParseArgument(ICommandLineParser parser, string argument) |
| 136 | { |
| 137 | if (parser.IsSwitch(argument)) |
| 138 | { |
| 139 | var parameter = argument.Substring(1); |
| 140 | switch (parameter.ToLowerInvariant()) |
| 141 | { |
| 142 | case "intermediatefolder": |
| 143 | this.IntermediateFolder = parser.GetNextArgumentAsDirectoryOrError(argument); |
| 144 | return true; |
| 145 | |
| 146 | case "data": |
| 147 | this.SaveAsData = true; |
| 148 | return true; |
| 149 | |
| 150 | case "o": |
| 151 | case "out": |
| 152 | this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument, "output file"); |
| 153 | return true; |
| 154 | |
| 155 | case "sct": |
| 156 | this.SuppressCustomTables = true; |
| 157 | return true; |
| 158 | |
| 159 | case "sdet": |
| 160 | this.SuppressDroppingEmptyTables = true; |
| 161 | return true; |
| 162 | |
| 163 | case "sras": |
| 164 | this.SuppressRelativeActionSequencing = true; |
| 165 | return true; |
| 166 | |
| 167 | case "sui": |
| 168 | this.SuppressUI = true; |
| 169 | return true; |
| 170 | |
| 171 | case "type": |
| 172 | this.DecompileType = parser.GetNextArgumentOrError(argument); |
| 173 | return true; |
| 174 | |
| 175 | case "x": |
| 176 | // Peek ahead to get the actual value provided on the command-line so the authoring |
| 177 | // matches what they typed on the command-line. |
| 178 | var originalExportBasePath = parser.PeekNextArgument(); |
| 179 | parser.GetNextArgumentAsDirectoryOrError(argument); // ensure we actually got a directory. |
| 180 | |
| 181 | this.ExportBasePath = originalExportBasePath; |
| 182 | return true; |
| 183 | } |
| 184 | } |
| 185 | else if (String.IsNullOrEmpty(this.InputPath)) |
| 186 | { |
| 187 | this.InputPath = argument; |
| 188 | return true; |
| 189 | } |
| 190 | |
| 191 | return false; |
| 192 | } |
| 193 | |
| 194 | private bool TryCalculateDecompileType(out OutputType decompileType) |
| 195 | { |
| 196 | decompileType = OutputType.Unknown; |
| 197 | |
| 198 | if (String.IsNullOrEmpty(this.DecompileType)) |
| 199 | { |
| 200 | this.DecompileType = Path.GetExtension(this.InputPath); |
| 201 | } |
| 202 | |
| 203 | switch (this.DecompileType.ToLowerInvariant()) |
| 204 | { |
| 205 | case "product": |
| 206 | case "package": |
| 207 | case "msi": |
| 208 | case ".msi": |
| 209 | decompileType = OutputType.Package; |
| 210 | break; |
| 211 | |
| 212 | case "mergemodule": |
| 213 | case "module": |
| 214 | case "msm": |
| 215 | case ".msm": |
| 216 | decompileType = OutputType.Module; |
| 217 | break; |
| 218 | |
| 219 | case "transform": |
| 220 | case "mst": |
| 221 | case ".mst": |
| 222 | decompileType = OutputType.Transform; |
| 223 | break; |
| 224 | } |
| 225 | |
| 226 | return decompileType != OutputType.Unknown; |
| 227 | } |
| 228 | |
| 229 | private string CalculateExtensionFromDecompileType(OutputType decompileType) |
| 230 | { |
| 231 | switch (decompileType) |
| 232 | { |
| 233 | case OutputType.Package: |
| 234 | return this.SaveAsData ? ".wixmsi" : ".wxs"; |
| 235 | |
| 236 | case OutputType.Module: |
| 237 | return this.SaveAsData ? ".wixmsm" : ".wxs"; |
| 238 | |
| 239 | case OutputType.Transform: |
| 240 | return ".wixmst"; |
| 241 | |
| 242 | default: |
| 243 | return this.SaveAsData ? ".wixdata" : ".wxs"; |
| 244 | } |
| 245 | } |
| 246 | } |
| 247 | } |