main
cs 360 lines 14.9 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.Unbind
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.ComponentModel;
8 using System.Globalization;
9 using System.IO;
10 using System.Linq;
11 using WixToolset.Core.Native.Msi;
12 using WixToolset.Core.WindowsInstaller.Bind;
13 using WixToolset.Data;
14 using WixToolset.Data.WindowsInstaller;
15 using WixToolset.Extensibility.Services;
16
17 internal class UnbindTransformCommand
18 {
19 public UnbindTransformCommand(IMessaging messaging, IBackendHelper backendHelper, IFileSystem fileSystem, IPathResolver pathResolver, FileSystemManager fileSystemManager, string transformFile, string exportBasePath, string intermediateFolder)
20 {
21 this.Messaging = messaging;
22 this.BackendHelper = backendHelper;
23 this.FileSystem = fileSystem;
24 this.PathResolver = pathResolver;
25 this.FileSystemManager = fileSystemManager;
26 this.TransformFile = transformFile;
27 this.ExportBasePath = exportBasePath;
28 this.IntermediateFolder = intermediateFolder;
29
30 this.TableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
31 }
32
33 private IMessaging Messaging { get; }
34
35 private IBackendHelper BackendHelper { get; }
36
37 private IFileSystem FileSystem { get; }
38
39 private IPathResolver PathResolver { get; }
40
41 private FileSystemManager FileSystemManager { get; }
42
43 private string TransformFile { get; }
44
45 private string ExportBasePath { get; }
46
47 private string IntermediateFolder { get; }
48
49 private TableDefinitionCollection TableDefinitions { get; }
50
51 private string EmptyFile { get; set; }
52
53 public WindowsInstallerData Execute()
54 {
55 var transform = new WindowsInstallerData(new SourceLineNumber(this.TransformFile))
56 {
57 Type = OutputType.Transform
58 };
59
60 // get the summary information table
61 using (var summaryInformation = new SummaryInformation(this.TransformFile))
62 {
63 var table = transform.EnsureTable(this.TableDefinitions["_SummaryInformation"]);
64
65 for (var i = 1; 19 >= i; i++)
66 {
67 var value = summaryInformation.GetProperty(i);
68
69 if (0 < value.Length)
70 {
71 var row = table.CreateRow(transform.SourceLineNumbers);
72 row[0] = i;
73 row[1] = value;
74 }
75 }
76 }
77
78 // create a schema msi which hopefully matches the table schemas in the transform
79 var schemaDatabasePath = Path.Combine(this.IntermediateFolder, "schema.msi");
80 var schemaData = this.CreateSchemaData(schemaDatabasePath);
81
82 // Bind the schema msi.
83 this.GenerateDatabase(schemaData);
84
85 var transformViewTable = this.OpenTransformViewForAddedAndModifiedRows(schemaDatabasePath);
86
87 var addedRows = this.CreatePlaceholdersForModifiedRowsAndIndexAddedRows(schemaData, transformViewTable);
88
89 // Re-bind the schema output with the placeholder rows over top the original schema database.
90 this.GenerateDatabase(schemaData);
91
92 this.PopulateTransformFromView(schemaDatabasePath, transform, transformViewTable, addedRows);
93
94 return transform;
95 }
96
97 private WindowsInstallerData CreateSchemaData(string schemaDatabasePath)
98 {
99 var schemaData = new WindowsInstallerData(new SourceLineNumber(schemaDatabasePath))
100 {
101 Type = OutputType.Package,
102 };
103
104 foreach (var tableDefinition in this.TableDefinitions)
105 {
106 // skip unreal tables and the Patch table
107 if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name)
108 {
109 schemaData.EnsureTable(tableDefinition);
110 }
111 }
112
113 return schemaData;
114 }
115
116 private Table OpenTransformViewForAddedAndModifiedRows(string schemaDatabasePath)
117 {
118 // Apply the transform with the ViewTransform option to collect all the modifications.
119 using (var msiDatabase = this.ApplyTransformToSchemaDatabase(schemaDatabasePath, TransformErrorConditions.All | TransformErrorConditions.ViewTransform))
120 {
121 // unbind the database
122 var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystem, this.PathResolver, schemaDatabasePath, msiDatabase, OutputType.Package, null, null, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: true);
123 var transformViewOutput = unbindCommand.Execute();
124
125 return transformViewOutput.Tables["_TransformView"];
126 }
127 }
128
129 private Dictionary<string, Row> CreatePlaceholdersForModifiedRowsAndIndexAddedRows(WindowsInstallerData schemaData, Table transformViewTable)
130 {
131 // Index the added and possibly modified rows (added rows may also appears as modified rows).
132 var addedRows = new Dictionary<string, Row>();
133 var modifiedRows = new Dictionary<string, TableNameWithPrimaryKeys>();
134
135 foreach (var row in transformViewTable.Rows)
136 {
137 var tableName = row.FieldAsString(0);
138 var columnName = row.FieldAsString(1);
139 var primaryKeys = row.FieldAsString(2);
140
141 if ("INSERT" == columnName)
142 {
143 var index = String.Concat(tableName, ':', primaryKeys);
144
145 addedRows.Add(index, null);
146 }
147 else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row
148 {
149 var index = String.Concat(tableName, ':', primaryKeys);
150
151 if (!modifiedRows.ContainsKey(index))
152 {
153 modifiedRows.Add(index, new TableNameWithPrimaryKeys { TableName = tableName, PrimaryKeys = primaryKeys });
154 }
155 }
156 }
157
158 // Create placeholder rows for modified rows to make the transform insert the updated values when its applied.
159 foreach (var kvp in modifiedRows)
160 {
161 var index = kvp.Key;
162 var tableNameWithPrimaryKey = kvp.Value;
163
164 // Ignore added rows.
165 if (!addedRows.ContainsKey(index))
166 {
167 var table = schemaData.Tables[tableNameWithPrimaryKey.TableName];
168 this.CreateRow(table, tableNameWithPrimaryKey.PrimaryKeys, setRequiredFields: true);
169 }
170 }
171
172 return addedRows;
173 }
174
175 private void PopulateTransformFromView(string schemaDatabasePath, WindowsInstallerData transform, Table transformViewTable, Dictionary<string, Row> addedRows)
176 {
177 WindowsInstallerData output;
178 // Apply the transform to the database and retrieve the modifications
179 using (var database = this.ApplyTransformToSchemaDatabase(schemaDatabasePath, TransformErrorConditions.All))
180 {
181
182 // unbind the database
183 var unbindCommand = new UnbindDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystem, this.PathResolver, schemaDatabasePath, database, OutputType.Package, this.ExportBasePath, null, this.IntermediateFolder, enableDemodularization: false, skipSummaryInfo: true);
184 output = unbindCommand.Execute();
185 }
186
187 // index all the rows to easily find modified rows
188 var rows = new Dictionary<string, Row>();
189 foreach (var table in output.Tables)
190 {
191 foreach (var row in table.Rows)
192 {
193 rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row);
194 }
195 }
196
197 // process the _TransformView rows into transform rows
198 foreach (var row in transformViewTable.Rows)
199 {
200 var tableName = row.FieldAsString(0);
201 var columnName = row.FieldAsString(1);
202 var primaryKeys = row.FieldAsString(2);
203
204 var table = transform.EnsureTable(this.TableDefinitions[tableName]);
205
206 if ("CREATE" == columnName) // added table
207 {
208 table.Operation = TableOperation.Add;
209 }
210 else if ("DELETE" == columnName) // deleted row
211 {
212 var deletedRow = this.CreateRow(table, primaryKeys, false);
213 deletedRow.Operation = RowOperation.Delete;
214 }
215 else if ("DROP" == columnName) // dropped table
216 {
217 table.Operation = TableOperation.Drop;
218 }
219 else if ("INSERT" == columnName) // added row
220 {
221 var index = String.Concat(tableName, ':', primaryKeys);
222 var addedRow = rows[index];
223 addedRow.Operation = RowOperation.Add;
224 table.Rows.Add(addedRow);
225 }
226 else if (null != primaryKeys) // modified row
227 {
228 var index = String.Concat(tableName, ':', primaryKeys);
229
230 // the _TransformView table includes information for added rows
231 // that looks like modified rows so it sometimes needs to be ignored
232 if (!addedRows.ContainsKey(index))
233 {
234 var modifiedRow = rows[index];
235
236 // mark the field as modified
237 var indexOfModifiedValue = -1;
238 for (var i = 0; i < modifiedRow.TableDefinition.Columns.Length; ++i)
239 {
240 if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal))
241 {
242 indexOfModifiedValue = i;
243 break;
244 }
245 }
246 modifiedRow.Fields[indexOfModifiedValue].Modified = true;
247
248 // move the modified row into the transform the first time its encountered
249 if (RowOperation.None == modifiedRow.Operation)
250 {
251 modifiedRow.Operation = RowOperation.Modify;
252 table.Rows.Add(modifiedRow);
253 }
254 }
255 }
256 else // added column
257 {
258 var column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal));
259 column.Added = true;
260 }
261 }
262 }
263
264 private Database ApplyTransformToSchemaDatabase(string schemaDatabasePath, TransformErrorConditions transformConditions)
265 {
266 var msiDatabase = new Database(schemaDatabasePath, OpenDatabase.Transact);
267
268 try
269 {
270 // apply the transform
271 msiDatabase.ApplyTransform(this.TransformFile, transformConditions);
272
273 // commit the database to guard against weird errors with streams
274 msiDatabase.Commit();
275 }
276 catch (Win32Exception ex)
277 {
278 if (0x65B == ex.NativeErrorCode)
279 {
280 // this commonly happens when the transform was built
281 // against a database schema different from the internal
282 // table definitions
283 throw new WixException(ErrorMessages.TransformSchemaMismatch());
284 }
285 }
286
287 return msiDatabase;
288 }
289
290 /// <summary>
291 /// Create a deleted or modified row.
292 /// </summary>
293 /// <param name="table">The table containing the row.</param>
294 /// <param name="primaryKeys">The primary keys of the row.</param>
295 /// <param name="setRequiredFields">Option to set all required fields with placeholder values.</param>
296 /// <returns>The new row.</returns>
297 private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields)
298 {
299 var row = table.CreateRow(null);
300
301 var primaryKeyParts = primaryKeys.Split('\t');
302 var primaryKeyPartIndex = 0;
303
304 for (var i = 0; i < table.Definition.Columns.Length; i++)
305 {
306 var columnDefinition = table.Definition.Columns[i];
307
308 if (columnDefinition.PrimaryKey)
309 {
310 if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
311 {
312 row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture);
313 }
314 else
315 {
316 row[i] = primaryKeyParts[primaryKeyPartIndex++];
317 }
318 }
319 else if (setRequiredFields)
320 {
321 if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
322 {
323 row[i] = 1;
324 }
325 else if (ColumnType.Object == columnDefinition.Type)
326 {
327 if (null == this.EmptyFile)
328 {
329 this.EmptyFile = Path.Combine(this.IntermediateFolder, ".empty");
330 using (var fileStream = this.FileSystem.OpenFile(null, this.EmptyFile, FileMode.Create, FileAccess.Write, FileShare.None))
331 {
332 }
333 }
334
335 row[i] = this.EmptyFile;
336 }
337 else
338 {
339 row[i] = "1";
340 }
341 }
342 }
343
344 return row;
345 }
346
347 private void GenerateDatabase(WindowsInstallerData data)
348 {
349 var command = new GenerateDatabaseCommand(this.Messaging, this.BackendHelper, this.FileSystem, this.FileSystemManager, data, data.SourceLineNumbers.FileName, this.TableDefinitions, this.IntermediateFolder, keepAddedColumns: true, suppressAddingValidationRows: true, useSubdirectory: false);
350 command.Execute();
351 }
352
353 private class TableNameWithPrimaryKeys
354 {
355 public string TableName { get; set; }
356
357 public string PrimaryKeys { get; set; }
358 }
359 }
360 }