@joebigelow / wix-1 / commits / 188952d8

Implement transform decompiling and use it in patch filter tests

Rob Mensching committed Sep 27, 2022 at 01:47 UTC 188952d84f5789128ddd32e7adf09e60899af43a
17 files changed +390 -177
src/api/wix/WixToolset.Extensibility/Data/IWindowsInstallerDecompileResult.cs
+6
@@ -5,12 +5,18 @@ namespace WixToolset.Extensibility.Data
5 using System.Collections.Generic;
6 using System.Xml.Linq;
7 using WixToolset.Data;
8 + using WixToolset.Data.WindowsInstaller;
9
10 /// <summary>
11 /// The result from decompiling a Windows Installer database.
12 /// </summary>
13 public interface IWindowsInstallerDecompileResult
14 {
15 + /// <summary>
16 + /// Decompiled <c>WindowsInstallerData</c>.
17 + /// </summary>
18 + WindowsInstallerData Data { get; set; }
19 +
20 /// <summary>
21 /// Decompiled document.
22 /// </summary>
src/test/burn/WixToolsetTest.BurnE2E/SlipstreamTests.cs
+1 -1
@@ -266,7 +266,7 @@ namespace WixToolsetTest.BurnE2E
266 packageBv1.VerifyTestRegistryRootDeleted();
267 }
268
269 - [RuntimeFact(Skip = "https://github.com/wixtoolset/issues/issues/6402")]
269 + [RuntimeFact]
270 public void CanAutomaticallyPredetermineSlipstreamPatchesAtBuildTime()
271 {
272 var testRegistryValueA = "PackageA";
src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs
+1 -1
@@ -94,7 +94,7 @@ namespace WixToolset.Core.WindowsInstaller.Bind
94 var exportBasePath = Path.Combine(this.IntermediateFolder, stageFolder);
95 var extractFilesFolder = Path.Combine(exportBasePath, "File");
96
97 - var command = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.PathResolver, path, OutputType.Product, exportBasePath, extractFilesFolder, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: false);
97 + var command = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.PathResolver, path, null, OutputType.Product, exportBasePath, extractFilesFolder, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: false);
98 data = command.Execute();
99 }
100
src/wix/WixToolset.Core.WindowsInstaller/CommandLine/DecompilerSubcommand.cs
+47 -4
@@ -34,6 +34,8 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
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; }
@@ -47,13 +49,14 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
49 return new CommandLineHelp("Converts a Windows Installer database back into source code.", "msi decompile [options] inputfile", new[]
50 {
51 new CommandLineHelpSwitch("-cub", "Optional path to a custom validation .CUBe file."),
52 + new CommandLineHelpSwitch("-data", "Save output as data instead of as a source file."),
53 new CommandLineHelpSwitch("-sct", "Suppress decompiling custom tables."),
54 new CommandLineHelpSwitch("-sdet", "Suppress dropping empty tables."),
55 new CommandLineHelpSwitch("-sras", "Suppress relative action sequencing."),
56 new CommandLineHelpSwitch("-sui", "Suppress decompiling UI tables."),
57 new CommandLineHelpSwitch("-type", "Optional specify the input file type: msi or msm. If not specified, type will be inferred by file extension."),
58 new CommandLineHelpSwitch("-intermediateFolder", "Optional working folder. If not specified %TMP% will be used."),
56 - new CommandLineHelpSwitch("-out", "-o", "Path to output the decompiled .wxs file. If not specified, outputs next to inputfile"),
59 + new CommandLineHelpSwitch("-out", "-o", "Path to output the decompiled output file. If not specified, outputs next to inputfile"),
60 new CommandLineHelpSwitch("-x", "Folder to export embedded binaries and icons to."),
61 });
62 }
@@ -77,7 +80,9 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
80
81 if (String.IsNullOrEmpty(this.OutputPath))
82 {
80 - this.OutputPath = Path.ChangeExtension(this.InputPath, ".wxs");
83 + var defaultExtension = this.CalculateExtensionFromDecompileType(decompileType);
84 +
85 + this.OutputPath = Path.ChangeExtension(this.InputPath, defaultExtension);
86 }
87
88 var extensionManager = this.ServiceProvider.GetService<IExtensionManager>();
@@ -92,7 +97,7 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
97 context.SymbolDefinitionCreator = creator;
98 context.OutputPath = this.OutputPath;
99
95 - context.ExtractFolder = this.ExportBasePath ?? this.IntermediateFolder;
100 + context.ExtractFolder = this.ExportBasePath;
101 context.SuppressCustomTables = this.SuppressCustomTables;
102 context.SuppressDroppingEmptyTables = this.SuppressDroppingEmptyTables;
103 context.SuppressRelativeActionSequencing = this.SuppressRelativeActionSequencing;
@@ -106,7 +111,17 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
111 if (!this.Messaging.EncounteredError)
112 {
113 Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(context.OutputPath)));
109 - result.Document.Save(context.OutputPath, SaveOptions.OmitDuplicateNamespaces);
114 + if (this.SaveAsData || result.Document == null)
115 + {
116 + using (var output = WixOutput.Create(context.OutputPath))
117 + {
118 + result.Data.Save(output);
119 + }
120 + }
121 + else
122 + {
123 + result.Document.Save(context.OutputPath, SaveOptions.OmitDuplicateNamespaces);
124 + }
125 }
126 }
127 catch (WixException e)
@@ -129,6 +144,10 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
144 this.IntermediateFolder = parser.GetNextArgumentAsDirectoryOrError(argument);
145 return true;
146
147 + case "data":
148 + this.SaveAsData = true;
149 + return true;
150 +
151 case "o":
152 case "out":
153 this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument, "output file");
@@ -197,9 +216,33 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
216 case ".msm":
217 decompileType = OutputType.Module;
218 break;
219 +
220 + case "transform":
221 + case "mst":
222 + case ".mst":
223 + decompileType = OutputType.Transform;
224 + break;
225 }
226
227 return decompileType != OutputType.Unknown;
228 }
229 +
230 + private string CalculateExtensionFromDecompileType(OutputType decompileType)
231 + {
232 + switch (decompileType)
233 + {
234 + case OutputType.Product:
235 + return this.SaveAsData ? ".wixmsi" : ".wxs";
236 +
237 + case OutputType.Module:
238 + return this.SaveAsData ? ".wixmsm" : ".wxs";
239 +
240 + case OutputType.Transform:
241 + return ".wixmst";
242 +
243 + default:
244 + return this.SaveAsData ? ".wixdata" : ".wxs";
245 + }
246 + }
247 }
248 }
src/wix/WixToolset.Core.WindowsInstaller/CommandLine/TransformSubcommand.cs
+1 -1
@@ -379,7 +379,7 @@ namespace WixToolset.Core.WindowsInstaller.CommandLine
379 {
380 if (!DataLoader.TryLoadWindowsInstallerData(path, out var data))
381 {
382 - var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.PathResolver, path, OutputType.Product, this.ExportBasePath, null, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: false);
382 + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.PathResolver, path, null, OutputType.Product, this.ExportBasePath, null, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: false);
383 data = unbindCommand.Execute();
384 }
385
src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
+24 -18
@@ -21,12 +21,13 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
21 {
22 private static readonly Regex Modularization = new Regex(@"\.[0-9A-Fa-f]{8}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{12}");
23
24 - public UnbindDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IPathResolver pathResolver, string databasePath, OutputType outputType, string exportBasePath, string extractFilesFolder, string intermediateFolder, bool enableDemodularization, bool skipSummaryInfo)
24 + public UnbindDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IPathResolver pathResolver, string databasePath, Database database, OutputType outputType, string exportBasePath, string extractFilesFolder, string intermediateFolder, bool enableDemodularization, bool skipSummaryInfo)
25 {
26 this.Messaging = messaging;
27 this.BackendHelper = backendHelper;
28 this.PathResolver = pathResolver;
29 this.DatabasePath = databasePath;
30 + this.Database = database;
31 this.OutputType = outputType;
32 this.ExportBasePath = exportBasePath;
33 this.ExtractFilesFolder = extractFilesFolder;
@@ -45,21 +46,21 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
46
47 private Database Database { get; set; }
48
48 - public string DatabasePath { get; }
49 + private string DatabasePath { get; }
50
50 - public OutputType OutputType { get; }
51 + private OutputType OutputType { get; }
52
52 - public string ExportBasePath { get; }
53 + private string ExportBasePath { get; }
54
54 - public string ExtractFilesFolder { get; }
55 + private string ExtractFilesFolder { get; }
56
56 - public string IntermediateFolder { get; }
57 + private string IntermediateFolder { get; }
58
58 - public bool EnableDemodularization { get; }
59 + private bool EnableDemodularization { get; }
60
60 - public bool SkipSummaryInfo { get; }
61 + private bool SkipSummaryInfo { get; }
62
62 - public TableDefinitionCollection TableDefinitions { get; }
63 + private TableDefinitionCollection TableDefinitions { get; }
64
65 public bool AdminImage { get; private set; }
66
@@ -77,22 +78,24 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
78 Type = this.OutputType
79 };
80
81 + Database database = null;
82 try
83 {
82 - using (var database = new Database(this.DatabasePath, OpenDatabase.ReadOnly))
84 + if (this.Database == null)
85 {
86 + database = new Database(this.DatabasePath, OpenDatabase.ReadOnly);
87 this.Database = database;
88 + }
89
86 - Directory.CreateDirectory(this.IntermediateFolder);
90 + Directory.CreateDirectory(this.IntermediateFolder);
91
88 - data.Codepage = this.GetCodePage();
92 + data.Codepage = this.GetCodePage();
93
90 - var modularizationGuid = this.ProcessTables(data, exportedFiles);
94 + var modularizationGuid = this.ProcessTables(data, exportedFiles);
95
92 - var summaryInfo = this.ProcessSummaryInfo(data, modularizationGuid);
96 + var summaryInfo = this.ProcessSummaryInfo(data, modularizationGuid);
97
94 - this.UpdateUnrealFileColumns(this.DatabasePath, data, summaryInfo, exportedFiles);
95 - }
98 + this.UpdateUnrealFileColumns(this.DatabasePath, data, summaryInfo, exportedFiles);
99 }
100 catch (Win32Exception e)
101 {
@@ -103,6 +106,10 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
106
107 throw;
108 }
109 + finally
110 + {
111 + database?.Dispose();
112 + }
113
114 this.AdminImage = adminImage;
115 this.Data = data;
@@ -542,9 +549,8 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
549 source = Path.Combine(this.ExtractFilesFolder, fileRow.File);
550 }
551 }
545 - else
552 + else if (componentDirectoryIndex.TryGetValue(fileRow.Component, out var directoryId)) // this can happen when unbinding an invalid MSI file or MST file with select table modifications.
553 {
547 - var directoryId = componentDirectoryIndex[fileRow.Component];
554 var relativeFileLayoutPath = this.PathResolver.GetFileSourcePath(directories, directoryId, fileRow.FileName, compressed: false, useLongName: summaryInformation.LongFilenames);
555
556 source = Path.Combine(databaseFolder, relativeFileLayoutPath);
src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindTransformCommand.cs
+178 -136
@@ -1,11 +1,8 @@
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 -#if TODO_KEEP_FOR_PATCH_UNBINDING_CONSIDERATION_IN_FUTURE
4 -
3 namespace WixToolset.Core.WindowsInstaller.Unbind
4 {
5 using System;
8 - using System.Collections;
6 using System.Collections.Generic;
7 using System.ComponentModel;
8 using System.Globalization;
@@ -19,10 +16,11 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
16
17 internal class UnbindTransformCommand
18 {
22 - public UnbindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, FileSystemManager fileSystemManager, string transformFile, string exportBasePath, string intermediateFolder)
19 + public UnbindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, IPathResolver pathResolver, FileSystemManager fileSystemManager, string transformFile, string exportBasePath, string intermediateFolder)
20 {
21 this.Messaging = messaging;
22 this.BackendHelper = backendHelper;
23 + this.PathResolver = pathResolver;
24 this.FileSystemManager = fileSystemManager;
25 this.TransformFile = transformFile;
26 this.ExportBasePath = exportBasePath;
@@ -35,6 +33,8 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
33
34 private IBackendHelper BackendHelper { get; }
35
36 + private IPathResolver PathResolver { get; }
37 +
38 private FileSystemManager FileSystemManager { get; }
39
40 private string TransformFile { get; }
@@ -49,8 +49,10 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
49
50 public WindowsInstallerData Execute()
51 {
52 - var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile));
53 - transform.Type = OutputType.Transform;
52 + var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile))
53 + {
54 + Type = OutputType.Transform
55 + };
56
57 // get the summary information table
58 using (var summaryInformation = new SummaryInformation(this.TransformFile))
@@ -71,181 +73,215 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
73 }
74
75 // create a schema msi which hopefully matches the table schemas in the transform
74 - var schemaOutput = new WindowsInstallerData(null);
76 + var schemaDatabasePath = Path.Combine(this.IntermediateFolder, "schema.msi");
77 + var schemaData = this.CreateSchemaData(schemaDatabasePath);
78 +
79 + // Bind the schema msi.
80 + this.GenerateDatabase(schemaData);
81 +
82 + var transformViewTable = this.OpenTransformViewForAddedAndModifiedRows(schemaDatabasePath);
83 +
84 + var addedRows = this.CreatePlaceholdersForModifiedRowsAndIndexAddedRows(schemaData, transformViewTable);
85 +
86 + // Re-bind the schema output with the placeholder rows over top the original schema database.
87 + this.GenerateDatabase(schemaData);
88 +
89 + this.PopulateTransformFromView(schemaDatabasePath, transform, transformViewTable, addedRows);
90 +
91 + return transform;
92 + }
93 +
94 + private WindowsInstallerData CreateSchemaData(string schemaDatabasePath)
95 + {
96 + var schemaData = new WindowsInstallerData(new SourceLineNumber(schemaDatabasePath))
97 + {
98 + Type = OutputType.Product,
99 + };
100 +
101 foreach (var tableDefinition in this.TableDefinitions)
102 {
103 // skip unreal tables and the Patch table
104 if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name)
105 {
80 - schemaOutput.EnsureTable(tableDefinition);
106 + schemaData.EnsureTable(tableDefinition);
107 }
108 }
109
84 - var addedRows = new Dictionary<string, Row>();
85 - Table transformViewTable;
86 -
87 - // Bind the schema msi.
88 - var msiDatabaseFile = Path.Combine(this.IntermediateFolder, "schema.msi");
89 - this.GenerateDatabase(schemaOutput, msiDatabaseFile);
110 + return schemaData;
111 + }
112
91 - // Apply the transform to the database and retrieve the modifications.
92 - using (var msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact))
113 + private Table OpenTransformViewForAddedAndModifiedRows(string schemaDatabasePath)
114 + {
115 + // Apply the transform with the ViewTransform option to collect all the modifications.
116 + using (var msiDatabase = this.ApplyTransformToSchemaDatabase(schemaDatabasePath, TransformErrorConditions.All | TransformErrorConditions.ViewTransform))
117 {
94 - // apply the transform with the ViewTransform option to collect all the modifications
95 - msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform);
96 -
118 // unbind the database
98 - var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: true);
119 + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.PathResolver, schemaDatabasePath, msiDatabase, OutputType.Product, null, null, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: true);
120 var transformViewOutput = unbindCommand.Execute();
121
101 - // index the added and possibly modified rows (added rows may also appears as modified rows)
102 - transformViewTable = transformViewOutput.Tables["_TransformView"];
103 - var modifiedRows = new Hashtable();
104 - foreach (var row in transformViewTable.Rows)
105 - {
106 - var tableName = (string)row[0];
107 - var columnName = (string)row[1];
108 - var primaryKeys = (string)row[2];
109 -
110 - if ("INSERT" == columnName)
111 - {
112 - var index = String.Concat(tableName, ':', primaryKeys);
122 + return transformViewOutput.Tables["_TransformView"];
123 + }
124 + }
125
114 - addedRows.Add(index, null);
115 - }
116 - else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row
117 - {
118 - var index = String.Concat(tableName, ':', primaryKeys);
126 + private Dictionary<string, Row> CreatePlaceholdersForModifiedRowsAndIndexAddedRows(WindowsInstallerData schemaData, Table transformViewTable)
127 + {
128 + // Index the added and possibly modified rows (added rows may also appears as modified rows).
129 + var addedRows = new Dictionary<string, Row>();
130 + var modifiedRows = new Dictionary<string, TableNameWithPrimaryKeys>();
131
120 - modifiedRows[index] = row;
121 - }
122 - }
132 + foreach (var row in transformViewTable.Rows)
133 + {
134 + var tableName = row.FieldAsString(0);
135 + var columnName = row.FieldAsString(1);
136 + var primaryKeys = row.FieldAsString(2);
137
124 - // create placeholder rows for modified rows to make the transform insert the updated values when its applied
125 - foreach (Row row in modifiedRows.Values)
138 + if ("INSERT" == columnName)
139 {
127 - var tableName = (string)row[0];
128 - var columnName = (string)row[1];
129 - var primaryKeys = (string)row[2];
140 + var index = String.Concat(tableName, ':', primaryKeys);
141
142 + addedRows.Add(index, null);
143 + }
144 + else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row
145 + {
146 var index = String.Concat(tableName, ':', primaryKeys);
147
133 - // ignore information for added rows
134 - if (!addedRows.ContainsKey(index))
148 + if (!modifiedRows.ContainsKey(index))
149 {
136 - var table = schemaOutput.Tables[tableName];
137 - this.CreateRow(table, primaryKeys, true);
150 + modifiedRows.Add(index, new TableNameWithPrimaryKeys { TableName = tableName, PrimaryKeys = primaryKeys });
151 }
152 }
153 }
154
142 - // Re-bind the schema output with the placeholder rows.
143 - this.GenerateDatabase(schemaOutput, msiDatabaseFile);
144 -
145 - // apply the transform to the database and retrieve the modifications
146 - using (var msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact))
155 + // Create placeholder rows for modified rows to make the transform insert the updated values when its applied.
156 + foreach (var kvp in modifiedRows)
157 {
148 - try
149 - {
150 - // apply the transform
151 - msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All);
158 + var index = kvp.Key;
159 + var tableNameWithPrimaryKey = kvp.Value;
160
153 - // commit the database to guard against weird errors with streams
154 - msiDatabase.Commit();
155 - }
156 - catch (Win32Exception ex)
161 + // Ignore added rows.
162 + if (!addedRows.ContainsKey(index))
163 {
158 - if (0x65B == ex.NativeErrorCode)
159 - {
160 - // this commonly happens when the transform was built
161 - // against a database schema different from the internal
162 - // table definitions
163 - throw new WixException(ErrorMessages.TransformSchemaMismatch());
164 - }
164 + var table = schemaData.Tables[tableNameWithPrimaryKey.TableName];
165 + this.CreateRow(table, tableNameWithPrimaryKey.PrimaryKeys, setRequiredFields: true);
166 }
167 + }
168 +
169 + return addedRows;
170 + }
171 +
172 + private void PopulateTransformFromView(string schemaDatabasePath, WindowsInstallerData transform, Table transformViewTable, Dictionary<string, Row> addedRows)
173 + {
174 + WindowsInstallerData output;
175 + // Apply the transform to the database and retrieve the modifications
176 + using (var database = this.ApplyTransformToSchemaDatabase(schemaDatabasePath, TransformErrorConditions.All))
177 + {
178
179 // unbind the database
168 - var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: true);
169 - var output = unbindCommand.Execute();
180 + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.PathResolver, schemaDatabasePath, database, OutputType.Product, this.ExportBasePath, null, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: true);
181 + output = unbindCommand.Execute();
182 + }
183
171 - // index all the rows to easily find modified rows
172 - var rows = new Dictionary<string, Row>();
173 - foreach (var table in output.Tables)
184 + // index all the rows to easily find modified rows
185 + var rows = new Dictionary<string, Row>();
186 + foreach (var table in output.Tables)
187 + {
188 + foreach (var row in table.Rows)
189 {
175 - foreach (var row in table.Rows)
176 - {
177 - rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row);
178 - }
190 + rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row);
191 }
192 + }
193
181 - // process the _TransformView rows into transform rows
182 - foreach (var row in transformViewTable.Rows)
183 - {
184 - var tableName = (string)row[0];
185 - var columnName = (string)row[1];
186 - var primaryKeys = (string)row[2];
194 + // process the _TransformView rows into transform rows
195 + foreach (var row in transformViewTable.Rows)
196 + {
197 + var tableName = row.FieldAsString(0);
198 + var columnName = row.FieldAsString(1);
199 + var primaryKeys = row.FieldAsString(2);
200
188 - var table = transform.EnsureTable(this.TableDefinitions[tableName]);
201 + var table = transform.EnsureTable(this.TableDefinitions[tableName]);
202
190 - if ("CREATE" == columnName) // added table
191 - {
192 - table.Operation = TableOperation.Add;
193 - }
194 - else if ("DELETE" == columnName) // deleted row
195 - {
196 - var deletedRow = this.CreateRow(table, primaryKeys, false);
197 - deletedRow.Operation = RowOperation.Delete;
198 - }
199 - else if ("DROP" == columnName) // dropped table
200 - {
201 - table.Operation = TableOperation.Drop;
202 - }
203 - else if ("INSERT" == columnName) // added row
204 - {
205 - var index = String.Concat(tableName, ':', primaryKeys);
206 - var addedRow = rows[index];
207 - addedRow.Operation = RowOperation.Add;
208 - table.Rows.Add(addedRow);
209 - }
210 - else if (null != primaryKeys) // modified row
203 + if ("CREATE" == columnName) // added table
204 + {
205 + table.Operation = TableOperation.Add;
206 + }
207 + else if ("DELETE" == columnName) // deleted row
208 + {
209 + var deletedRow = this.CreateRow(table, primaryKeys, false);
210 + deletedRow.Operation = RowOperation.Delete;
211 + }
212 + else if ("DROP" == columnName) // dropped table
213 + {
214 + table.Operation = TableOperation.Drop;
215 + }
216 + else if ("INSERT" == columnName) // added row
217 + {
218 + var index = String.Concat(tableName, ':', primaryKeys);
219 + var addedRow = rows[index];
220 + addedRow.Operation = RowOperation.Add;
221 + table.Rows.Add(addedRow);
222 + }
223 + else if (null != primaryKeys) // modified row
224 + {
225 + var index = String.Concat(tableName, ':', primaryKeys);
226 +
227 + // the _TransformView table includes information for added rows
228 + // that looks like modified rows so it sometimes needs to be ignored
229 + if (!addedRows.ContainsKey(index))
230 {
212 - var index = String.Concat(tableName, ':', primaryKeys);
231 + var modifiedRow = rows[index];
232
214 - // the _TransformView table includes information for added rows
215 - // that looks like modified rows so it sometimes needs to be ignored
216 - if (!addedRows.ContainsKey(index))
233 + // mark the field as modified
234 + var indexOfModifiedValue = -1;
235 + for (var i = 0; i < modifiedRow.TableDefinition.Columns.Length; ++i)
236 {
218 - var modifiedRow = rows[index];
219 -
220 - // mark the field as modified
221 - var indexOfModifiedValue = -1;
222 - for (var i = 0; i < modifiedRow.TableDefinition.Columns.Length; ++i)
237 + if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal))
238 {
224 - if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal))
225 - {
226 - indexOfModifiedValue = i;
227 - break;
228 - }
239 + indexOfModifiedValue = i;
240 + break;
241 }
230 - modifiedRow.Fields[indexOfModifiedValue].Modified = true;
242 + }
243 + modifiedRow.Fields[indexOfModifiedValue].Modified = true;
244
232 - // move the modified row into the transform the first time its encountered
233 - if (RowOperation.None == modifiedRow.Operation)
234 - {
235 - modifiedRow.Operation = RowOperation.Modify;
236 - table.Rows.Add(modifiedRow);
237 - }
245 + // move the modified row into the transform the first time its encountered
246 + if (RowOperation.None == modifiedRow.Operation)
247 + {
248 + modifiedRow.Operation = RowOperation.Modify;
249 + table.Rows.Add(modifiedRow);
250 }
251 }
240 - else // added column
241 - {
242 - var column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal));
243 - column.Added = true;
244 - }
252 + }
253 + else // added column
254 + {
255 + var column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal));
256 + column.Added = true;
257 }
258 }
259 + }
260
248 - return transform;
261 + private Database ApplyTransformToSchemaDatabase(string schemaDatabasePath, TransformErrorConditions transformConditions)
262 + {
263 + var msiDatabase = new Database(schemaDatabasePath, OpenDatabase.Transact);
264 +
265 + try
266 + {
267 + // apply the transform
268 + msiDatabase.ApplyTransform(this.TransformFile, transformConditions);
269 +
270 + // commit the database to guard against weird errors with streams
271 + msiDatabase.Commit();
272 + }
273 + catch (Win32Exception ex)
274 + {
275 + if (0x65B == ex.NativeErrorCode)
276 + {
277 + // this commonly happens when the transform was built
278 + // against a database schema different from the internal
279 + // table definitions
280 + throw new WixException(ErrorMessages.TransformSchemaMismatch());
281 + }
282 + }
283 +
284 + return msiDatabase;
285 }
286
287 /// <summary>
@@ -305,11 +341,17 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
341 return row;
342 }
343
308 - private void GenerateDatabase(WindowsInstallerData output, string databaseFile)
344 + private void GenerateDatabase(WindowsInstallerData data)
345 {
310 - var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystemManager, output, databaseFile, this.TableDefinitions, this.IntermediateFolder, keepAddedColumns: true, suppressAddingValidationRows: true, useSubdirectory: false);
346 + var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystemManager, data, data.SourceLineNumbers.FileName, this.TableDefinitions, this.IntermediateFolder, keepAddedColumns: true, suppressAddingValidationRows: true, useSubdirectory: false);
347 command.Execute();
348 }
349 +
350 + private class TableNameWithPrimaryKeys
351 + {
352 + public string TableName { get; set; }
353 +
354 + public string PrimaryKeys { get; set; }
355 + }
356 }
357 }
315 -#endif
src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerDecompileResult.cs
+3
@@ -5,10 +5,13 @@ namespace WixToolset.Core.WindowsInstaller
5 using System.Collections.Generic;
6 using System.Xml.Linq;
7 using WixToolset.Data;
8 + using WixToolset.Data.WindowsInstaller;
9 using WixToolset.Extensibility.Data;
10
11 internal class WindowsInstallerDecompileResult : IWindowsInstallerDecompileResult
12 {
13 + public WindowsInstallerData Data { get; set; }
14 +
15 public XDocument Document { get; set; }
16
17 public IList<string> ExtractedFilePaths { get; set; }
src/wix/WixToolset.Core.WindowsInstaller/WindowsInstallerDecompiler.cs
+34 -3
@@ -3,13 +3,15 @@
3 namespace WixToolset.Core.WindowsInstaller
4 {
5 using System;
6 - using System.ComponentModel.Design;
6 + using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 + using WixToolset.Core.WindowsInstaller.Bind;
10 using WixToolset.Core.WindowsInstaller.Decompile;
11 using WixToolset.Core.WindowsInstaller.Unbind;
12 using WixToolset.Data;
13 using WixToolset.Data.WindowsInstaller;
14 + using WixToolset.Extensibility;
15 using WixToolset.Extensibility.Data;
16 using WixToolset.Extensibility.Services;
17
@@ -22,12 +24,15 @@ namespace WixToolset.Core.WindowsInstaller
24 {
25 this.ServiceProvider = serviceProvider;
26 this.Messaging = serviceProvider.GetService<IMessaging>();
27 + this.ExtensionManager = serviceProvider.GetService<IExtensionManager>();
28 }
29
30 private IServiceProvider ServiceProvider { get; }
31
32 private IMessaging Messaging { get; }
33
34 + private IExtensionManager ExtensionManager { get; }
35 +
36 public IWindowsInstallerDecompileResult Decompile(IWindowsInstallerDecompileContext context)
37 {
38 if (context.SymbolDefinitionCreator == null)
@@ -62,7 +67,7 @@ namespace WixToolset.Core.WindowsInstaller
67 private IWindowsInstallerDecompileResult Execute(IWindowsInstallerDecompileContext context)
68 {
69 // Delete the directory and its files to prevent cab extraction failure due to an existing file.
65 - if (Directory.Exists(context.ExtractFolder))
70 + if (!String.IsNullOrEmpty(context.ExtractFolder) && Directory.Exists(context.ExtractFolder))
71 {
72 Directory.Delete(context.ExtractFolder, true);
73 }
@@ -71,11 +76,23 @@ namespace WixToolset.Core.WindowsInstaller
76
77 var pathResolver = context.ServiceProvider.GetService<IPathResolver>();
78
79 + if (context.DecompileType == OutputType.Transform)
80 + {
81 + return this.DecompileTransform(context, backendHelper, pathResolver);
82 + }
83 + else
84 + {
85 + return this.DecompileDatabase(context, backendHelper, pathResolver);
86 + }
87 + }
88 +
89 + private IWindowsInstallerDecompileResult DecompileDatabase(IWindowsInstallerDecompileContext context, IWindowsInstallerBackendHelper backendHelper, IPathResolver pathResolver)
90 + {
91 var extractFilesFolder = context.SuppressExtractCabinets || (String.IsNullOrEmpty(context.CabinetExtractFolder) && String.IsNullOrEmpty(context.ExtractFolder)) ? null :
92 String.IsNullOrEmpty(context.CabinetExtractFolder) ? Path.Combine(context.ExtractFolder, "File") : context.CabinetExtractFolder;
93
94 var outputType = context.TreatProductAsModule ? OutputType.Module : context.DecompileType;
78 - var unbindCommand = new UnbindDatabaseCommand(this.Messaging, backendHelper, pathResolver, context.DecompilePath, outputType, context.ExtractFolder, extractFilesFolder, context.IntermediateFolder, enableDemodularization: true, skipSummaryInfo: false);
95 + var unbindCommand = new UnbindDatabaseCommand(this.Messaging, backendHelper, pathResolver, context.DecompilePath, null, outputType, context.ExtractFolder, extractFilesFolder, context.IntermediateFolder, enableDemodularization: true, skipSummaryInfo: false);
96 var output = unbindCommand.Execute();
97 var extractedFilePaths = unbindCommand.ExportedFiles;
98
@@ -84,10 +101,24 @@ namespace WixToolset.Core.WindowsInstaller
101 var document = decompiler.Decompile(output);
102
103 var result = context.ServiceProvider.GetService<IWindowsInstallerDecompileResult>();
104 + result.Data = output;
105 result.Document = document;
106 result.Platform = GetPlatformFromOutput(output);
107 result.ExtractedFilePaths = extractedFilePaths.ToList();
108 + return result;
109 + }
110 +
111 + private IWindowsInstallerDecompileResult DecompileTransform(IWindowsInstallerDecompileContext context, IWindowsInstallerBackendHelper backendHelper, IPathResolver pathResolver)
112 + {
113 + var fileSystemExtensions = this.ExtensionManager.GetServices<IFileSystemExtension>();
114
115 + var fileSystemManager = new FileSystemManager(fileSystemExtensions);
116 +
117 + var unbindCommand = new UnbindTransformCommand(this.Messaging, backendHelper, pathResolver, fileSystemManager, context.DecompilePath, context.ExtractFolder, context.IntermediateFolder);
118 + var output = unbindCommand.Execute();
119 +
120 + var result = context.ServiceProvider.GetService<IWindowsInstallerDecompileResult>();
121 + result.Data = output;
122 return result;
123 }
124
src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs
+75 -11
@@ -18,6 +18,8 @@ namespace WixToolsetTest.CoreIntegration
18 using WixToolset.Core.TestPackage;
19 using WixToolset.Data;
20 using WixToolset.Data.Burn;
21 + using WixToolset.Data.WindowsInstaller;
22 + using WixToolset.Dtf.Compression.Cab;
23 using Xunit;
24
25 public class PatchFixture : IDisposable
@@ -395,9 +397,9 @@ namespace WixToolsetTest.CoreIntegration
397 }
398
399 [Fact]
398 - public void CanBuildPatchWithFiltering()
400 + public void CanBuildPatchWithFileFiltering()
401 {
400 - var sourceFolder = TestData.Get(@"TestData", "PatchFamilyFilter");
402 + var sourceFolder = TestData.Get(@"TestData", "PatchFamilyFileFilter");
403
404 using (var fs = new DisposableFileSystem())
405 {
@@ -406,23 +408,50 @@ namespace WixToolsetTest.CoreIntegration
408
409 var patchPath = BuildMsp("Patch1.msp", sourceFolder, tempFolderPatch, "1.0.1", bindpaths: new[] { Path.GetDirectoryName(this.templateBaselinePdb), Path.GetDirectoryName(this.templateUpdatePdb) });
410
409 - var doc = GetExtractPatchXml(patchPath);
410 - WixAssert.StringEqual("{11111111-2222-3333-4444-555555555555}", doc.Root.Element(TargetProductCodeName).Value);
411 -
412 - var names = Query.GetSubStorageNames(patchPath);
413 - WixAssert.CompareLineByLine(new[] { "#RTM.1", "RTM.1" }, names);
411 + var mainTransform = ExtractBaselinePatch(patchPath, "RTM.1", baseFolder);
412 + Assert.Null(mainTransform.Tables["Registry"]);
413 + var fileRow = mainTransform.Tables["File"].Rows.Single();
414 + Assert.Equal("a.txt", fileRow.FieldAsString(0));
415 + Assert.Equal(152, fileRow.FieldAsInteger(3));
416
415 - var cab = Path.Combine(baseFolder, "foo.cab");
416 - Query.ExtractStream(patchPath, "foo.cab", cab);
417 + var pairedTransform = ExtractBaselinePatch(patchPath, "#RTM.1", baseFolder);
418 + fileRow = mainTransform.Tables["File"].Rows.Single();
419 + Assert.Equal("a.txt", fileRow.FieldAsString(0));
420 + Assert.Equal(152, fileRow.FieldAsInteger(3));
421
418 - var files = Query.GetCabinetFiles(cab);
422 + var files = ExtractFilesFromPatchCabinet(patchPath, "foo.cab", baseFolder);
423 var file = files.Single();
420 - WixAssert.StringEqual("a.txt", file.Name);
424 var contents = file.OpenText().ReadToEnd();
425 WixAssert.StringEqual("This is A v1.0.1 from the '.update-data' folder in 'PatchTemplatePackage'.\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.\r\n", contents);
426 }
427 }
428
429 + [Fact]
430 + public void CanBuildPatchWithRegistryFiltering()
431 + {
432 + var sourceFolder = TestData.Get(@"TestData", "PatchFamilyRegistryFilter");
433 +
434 + using (var fs = new DisposableFileSystem())
435 + {
436 + var baseFolder = fs.GetFolder();
437 + var tempFolderPatch = Path.Combine(baseFolder, "patch");
438 +
439 + var patchPath = BuildMsp("Patch1.msp", sourceFolder, tempFolderPatch, "1.0.1", bindpaths: new[] { Path.GetDirectoryName(this.templateBaselinePdb), Path.GetDirectoryName(this.templateUpdatePdb) }, warningsAsErrors: false);
440 +
441 + var mainTransform = ExtractBaselinePatch(patchPath, "RTM.1", baseFolder);
442 + Assert.Null(mainTransform.Tables["File"]);
443 + var row = mainTransform.Tables["Registry"].Rows.Single();
444 + Assert.Equal("regUty0zLJ5uYhRlGzmOzENKmnAtno", row.FieldAsString(0));
445 + Assert.Equal("1.0.1", row.FieldAsString(4));
446 +
447 + var pairedTransform = ExtractBaselinePatch(patchPath, "#RTM.1", baseFolder);
448 + Assert.Null(pairedTransform.Tables["File"]);
449 +
450 + var files = ExtractFilesFromPatchCabinet(patchPath, "foo.cab", baseFolder);
451 + Assert.Empty(files);
452 + }
453 + }
454 +
455 private static string BuildMsi(string outputName, string sourceFolder, string baseFolder, string defineV, string defineA, string defineB, IEnumerable<string> bindpaths = null)
456 {
457 var extensionPath = Path.GetFullPath(new Uri(typeof(ExampleExtensionFactory).Assembly.CodeBase).LocalPath);
@@ -552,6 +581,41 @@ namespace WixToolsetTest.CoreIntegration
581 Assert.Equal(0, proc.ExitCode);
582 }
583
584 + private static WindowsInstallerData DecompileMst(string transformPath, string baseFolder)
585 + {
586 + var outputPath = Path.ChangeExtension(transformPath, ".wixmst");
587 +
588 + var args = new List<string>
589 + {
590 + "msi", "decompile",
591 + transformPath,
592 + "-intermediateFolder", Path.Combine(baseFolder),
593 + "-o", outputPath,
594 + };
595 +
596 + var result = WixRunner.Execute(args.ToArray());
597 +
598 + result.AssertSuccess();
599 +
600 + return WindowsInstallerData.Load(outputPath);
601 + }
602 +
603 + private static WindowsInstallerData ExtractBaselinePatch(string patchPath, string substorageName, string baseFolder)
604 + {
605 + var mstPath = Path.Combine(baseFolder, substorageName, substorageName + ".mst");
606 + Query.ExtractSubStorage(patchPath, substorageName, mstPath);
607 +
608 + return DecompileMst(mstPath, baseFolder);
609 + }
610 +
611 + private static CabFileInfo[] ExtractFilesFromPatchCabinet(string patchPath, string cabinetName, string baseFolder)
612 + {
613 + var cab = Path.Combine(baseFolder, cabinetName);
614 + Query.ExtractStream(patchPath, cabinetName, cab);
615 +
616 + return Query.GetCabinetFiles(cab);
617 + }
618 +
619 private static XDocument GetExtractPatchXml(string path)
620 {
621 var buffer = new StringBuilder(65535);
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFileFilter/.data/Av1.0.0.txt renamed
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFileFilter/.data/Av1.0.1.txt renamed
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFileFilter/.data/Bv1.0.0.txt renamed
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFileFilter/.data/Bv1.0.1.txt renamed
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFileFilter/Package.wxs renamed
+3 -1
@@ -1,4 +1,5 @@
1 -<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
1 +<!-- TODO: DELETE ???
2 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
3 <Package Name="~Test Package" Version="$(var.V)" Manufacturer="Example Corporation" Language="1033" UpgradeCode="e703bf17-4765-444c-91fd-88550fa681d4" Scope="perMachine" ProductCode="e703bf17-4765-444c-91fd-88550fa681d4">
4
5
@@ -26,3 +27,4 @@
27 </ComponentGroup>
28 </Fragment>
29 </Wix>
30 +-->
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFileFilter/Patch.wxs renamed
+1 -1
@@ -10,7 +10,7 @@
10
11 <Fragment>
12 <PatchFamily Id="SamplePatchFamily" Version="$(var.V)" Supersede="yes">
13 - <ComponentRef Id="A" />
13 + <ComponentRef Id="a.txt" />
14 </PatchFamily>
15 </Fragment>
16 </Wix>
src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyRegistryFilter/Patch.wxs new
+16
@@ -0,0 +1,16 @@
1 +<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
2 + <Patch AllowRemoval="yes" Manufacturer="FireGiant" MoreInfoURL="http://www.example.com/" DisplayName="~Test Patch v$(var.V)" Description="~Test Small Update Patch v($var.V)" Classification="Update">
3 +
4 + <Media Id="1" Cabinet="foo.cab">
5 + <PatchBaseline Id="RTM" BaselineFile="Baseline.wixpdb" UpdateFile="Update.wixpdb" />
6 + </Media>
7 +
8 + <PatchFamilyRef Id="SamplePatchFamily" />
9 + </Patch>
10 +
11 + <Fragment>
12 + <PatchFamily Id="SamplePatchFamily" Version="$(var.V)" Supersede="yes">
13 + <ComponentRef Id="regUty0zLJ5uYhRlGzmOzENKmnAtno" />
14 + </PatchFamily>
15 + </Fragment>
16 +</Wix>