main
cs 247 lines 9.74 KB
Raw
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 }