main
cs 411 lines 16.7 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.Bind
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.ComponentModel;
8 using System.IO;
9 using System.Linq;
10 using System.Text;
11 using WixToolset.Core.Native.Msi;
12 using WixToolset.Data;
13 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Extensibility.Data;
15 using WixToolset.Extensibility.Services;
16
17 internal class GenerateDatabaseCommand
18 {
19 private const string IdtsSubFolder = "_idts";
20
21 public GenerateDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IFileSystem fileSystem, FileSystemManager fileSystemManager, WindowsInstallerData data, string outputPath, TableDefinitionCollection tableDefinitions, string intermediateFolder, bool keepAddedColumns, bool suppressAddingValidationRows, bool useSubdirectory)
22 {
23 this.Messaging = messaging;
24 this.BackendHelper = backendHelper;
25 this.FileSystem = fileSystem;
26 this.FileSystemManager = fileSystemManager;
27 this.Data = data;
28 this.OutputPath = outputPath;
29 this.TableDefinitions = tableDefinitions;
30 this.IntermediateFolder = intermediateFolder;
31 this.KeepAddedColumns = keepAddedColumns;
32 this.SuppressAddingValidationRows = suppressAddingValidationRows;
33 this.UseSubDirectory = useSubdirectory;
34 }
35
36 private IBackendHelper BackendHelper { get; }
37
38 private IFileSystem FileSystem { get; }
39
40 private FileSystemManager FileSystemManager { get; }
41
42 /// <summary>
43 /// Whether to keep columns added in a transform.
44 /// </summary>
45 private bool KeepAddedColumns { get; }
46
47 private IMessaging Messaging { get; }
48
49 private WindowsInstallerData Data { get; }
50
51 private string OutputPath { get; }
52
53 private TableDefinitionCollection TableDefinitions { get; }
54
55 private string IntermediateFolder { get; }
56
57 public List<ITrackedFile> GeneratedTemporaryFiles { get; } = new List<ITrackedFile>();
58
59 /// <summary>
60 /// Whether to use a subdirectory based on the database file name for intermediate files.
61 /// </summary>
62 private bool SuppressAddingValidationRows { get; }
63
64 private bool UseSubDirectory { get; }
65
66 public void Execute()
67 {
68 // Add the _Validation rows.
69 if (!this.SuppressAddingValidationRows)
70 {
71 this.AddValidationRows();
72 }
73
74 var baseDirectory = this.IntermediateFolder;
75
76 if (this.UseSubDirectory)
77 {
78 var filename = Path.GetFileNameWithoutExtension(this.OutputPath);
79 baseDirectory = Path.Combine(baseDirectory, filename);
80 }
81
82 var idtFolder = Path.Combine(baseDirectory, IdtsSubFolder);
83
84 var type = OpenDatabase.CreateDirect;
85
86 if (OutputType.Patch == this.Data.Type)
87 {
88 type |= OpenDatabase.OpenPatchFile;
89 }
90
91 try
92 {
93 Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath));
94
95 Directory.CreateDirectory(idtFolder);
96
97 using (var db = new Database(this.OutputPath, type))
98 {
99 // If we're not using the default codepage, import a new one into our
100 // database before we add any tables (or the tables would be added
101 // with the wrong codepage).
102 if (0 != this.Data.Codepage)
103 {
104 this.SetDatabaseCodepage(db, this.Data.Codepage, idtFolder);
105 }
106
107 this.ImportTables(db, idtFolder);
108
109 // Insert substorages (usually transforms inside a patch or instance transforms in a package).
110 this.ImportSubStorages(db);
111
112 // We're good, commit the changes to the new database.
113 db.Commit();
114 }
115 }
116 catch (IOException e)
117 {
118 // TODO: this error message doesn't seem specific enough
119 throw new WixException(ErrorMessages.FileNotFound(new SourceLineNumber(this.OutputPath), this.OutputPath), e);
120 }
121 }
122
123 private void AddValidationRows()
124 {
125 var validationTable = this.Data.EnsureTable(this.TableDefinitions["_Validation"]);
126
127 // Add the validation rows for real tables and columns.
128 foreach (var table in this.Data.Tables.Where(t => !t.Definition.Unreal))
129 {
130 foreach (var columnDef in table.Definition.Columns.Where(c => !c.Unreal))
131 {
132 var row = validationTable.CreateRow(null);
133
134 row[0] = table.Name;
135
136 row[1] = columnDef.Name;
137
138 if (columnDef.Nullable)
139 {
140 row[2] = "Y";
141 }
142 else
143 {
144 row[2] = "N";
145 }
146
147 if (columnDef.MinValue.HasValue)
148 {
149 row[3] = columnDef.MinValue.Value;
150 }
151
152 if (columnDef.MaxValue.HasValue)
153 {
154 row[4] = columnDef.MaxValue.Value;
155 }
156
157 row[5] = columnDef.KeyTable;
158
159 if (columnDef.KeyColumn.HasValue)
160 {
161 row[6] = columnDef.KeyColumn.Value;
162 }
163
164 if (ColumnCategory.Unknown != columnDef.Category)
165 {
166 row[7] = columnDef.Category.ToString();
167 }
168
169 row[8] = columnDef.Possibilities;
170
171 row[9] = columnDef.Description;
172 }
173 }
174 }
175
176 private void ImportTables(Database db, string idtDirectory)
177 {
178 foreach (var table in this.Data.Tables)
179 {
180 var importTable = table;
181 var hasBinaryColumn = false;
182
183 // Skip all unreal tables other than _Streams.
184 if (table.Definition.Unreal && "_Streams" != table.Name)
185 {
186 continue;
187 }
188
189 // Do not put the _Validation table in patches, it is not needed.
190 if (OutputType.Patch == this.Data.Type && "_Validation" == table.Name)
191 {
192 continue;
193 }
194
195 // The only way to import binary data is to copy it to a local subdirectory first.
196 // To avoid this extra copying and perf hit, import an empty table with the same
197 // definition and later import the binary data from source using records.
198 foreach (var columnDefinition in table.Definition.Columns)
199 {
200 if (ColumnType.Object == columnDefinition.Type)
201 {
202 importTable = new Table(table.Definition);
203 hasBinaryColumn = true;
204 break;
205 }
206 }
207
208 // Create the table via IDT import.
209 if ("_Streams" != importTable.Name)
210 {
211 try
212 {
213 var command = new CreateIdtFileCommand(this.Messaging, importTable, this.Data.Codepage, idtDirectory, this.KeepAddedColumns);
214 command.Execute();
215
216 var trackIdt = this.BackendHelper.TrackFile(command.IdtPath, TrackedFileType.Temporary);
217 this.GeneratedTemporaryFiles.Add(trackIdt);
218
219 db.Import(command.IdtPath);
220 }
221 catch (WixInvalidIdtException)
222 {
223 // If ValidateRows finds anything it doesn't like, it throws
224 importTable.ValidateRows();
225
226 // Otherwise we rethrow the InvalidIdt
227 throw;
228 }
229 }
230
231 // insert the rows via SQL query if this table contains object fields
232 if (hasBinaryColumn)
233 {
234 var query = new StringBuilder("SELECT ");
235
236 // Build the query for the view.
237 var firstColumn = true;
238 foreach (var columnDefinition in table.Definition.Columns)
239 {
240 if (columnDefinition.Unreal)
241 {
242 continue;
243 }
244
245 if (!firstColumn)
246 {
247 query.Append(",");
248 }
249
250 query.AppendFormat(" `{0}`", columnDefinition.Name);
251 firstColumn = false;
252 }
253 query.AppendFormat(" FROM `{0}`", table.Name);
254
255 using (var tableView = db.OpenExecuteView(query.ToString()))
256 {
257 // Import each row containing a stream
258 foreach (var row in table.Rows)
259 {
260 using (var record = new Record(table.Definition.Columns.Length))
261 {
262 // Stream names are created by concatenating the name of the table with the values
263 // of the primary key (delimited by periods).
264 var streamName = new StringBuilder();
265
266 // the _Streams table doesn't prepend the table name (or a period)
267 if ("_Streams" != table.Name)
268 {
269 streamName.Append(table.Name);
270 }
271
272 var needStream = false;
273
274 for (var i = 0; i < table.Definition.Columns.Length; i++)
275 {
276 var columnDefinition = table.Definition.Columns[i];
277
278 if (columnDefinition.Unreal)
279 {
280 continue;
281 }
282
283 switch (columnDefinition.Type)
284 {
285 case ColumnType.Localized:
286 case ColumnType.Preserved:
287 case ColumnType.String:
288 var str = row.FieldAsString(i);
289
290 if (columnDefinition.PrimaryKey)
291 {
292 if (0 < streamName.Length)
293 {
294 streamName.Append(".");
295 }
296
297 streamName.Append(str);
298 }
299
300 record.SetString(i + 1, str);
301 break;
302 case ColumnType.Number:
303 record.SetInteger(i + 1, row.FieldAsInteger(i));
304 break;
305
306 case ColumnType.Object:
307 var path = row.FieldAsString(i);
308 if (null != path)
309 {
310 needStream = true;
311 try
312 {
313 record.SetStream(i + 1, path);
314 }
315 catch (Win32Exception e)
316 {
317 if (0xA1 == e.NativeErrorCode) // ERROR_BAD_PATHNAME
318 {
319 throw new WixException(ErrorMessages.FileNotFound(row.SourceLineNumbers, path));
320 }
321 else
322 {
323 throw new WixException(ErrorMessages.Win32Exception(e.NativeErrorCode, e.Message));
324 }
325 }
326 }
327 break;
328 }
329 }
330
331 // check for a stream name that is more than 62 characters long (the maximum allowed length)
332 if (needStream && Database.MsiMaxStreamNameLength < streamName.Length)
333 {
334 this.Messaging.Write(ErrorMessages.StreamNameTooLong(row.SourceLineNumbers, table.Name, streamName.ToString(), streamName.Length));
335 }
336 else // add the row to the database
337 {
338 tableView.Modify(ModifyView.Assign, record);
339 }
340 }
341 }
342 }
343
344 // Remove rows from the _Streams table for wixpdbs.
345 if ("_Streams" == table.Name)
346 {
347 table.Rows.Clear();
348 }
349 }
350 }
351 }
352
353 private void ImportSubStorages(Database db)
354 {
355 if (0 < this.Data.SubStorages.Count)
356 {
357 using (var storagesView = new View(db, "SELECT `Name`, `Data` FROM `_Storages`"))
358 {
359 foreach (var subStorage in this.Data.SubStorages)
360 {
361 var transformFile = Path.Combine(this.IntermediateFolder, String.Concat(subStorage.Name, ".mst"));
362
363 // Bind the transform.
364 var command = new BindTransformCommand(this.Messaging, this.BackendHelper, this.FileSystem, this.FileSystemManager, this.IntermediateFolder, subStorage.Data, transformFile, this.TableDefinitions);
365 command.Execute();
366
367 if (this.Messaging.EncounteredError)
368 {
369 continue;
370 }
371
372 // Add the storage to the database.
373 using (var record = new Record(2))
374 {
375 record.SetString(1, subStorage.Name);
376 record.SetStream(2, transformFile);
377 storagesView.Modify(ModifyView.Assign, record);
378 }
379 }
380 }
381 }
382 }
383
384 private void SetDatabaseCodepage(Database db, int codepage, string idtFolder)
385 {
386 // Write out the _ForceCodepage IDT file.
387 var idtPath = Path.Combine(idtFolder, "_ForceCodepage.idt");
388 using (var idtFile = new StreamWriter(idtPath, false, Encoding.ASCII))
389 {
390 idtFile.WriteLine(); // dummy column name record
391 idtFile.WriteLine(); // dummy column definition record
392 idtFile.Write(codepage);
393 idtFile.WriteLine("\t_ForceCodepage");
394 }
395
396 var trackIdt = this.BackendHelper.TrackFile(idtPath, TrackedFileType.Temporary);
397 this.GeneratedTemporaryFiles.Add(trackIdt);
398
399 // Try to import the table into the MSI.
400 try
401 {
402 db.Import(idtPath);
403 }
404 catch (WixInvalidIdtException)
405 {
406 // The IDT should always be generated correctly, so an invalid code page was given.
407 throw new WixException(ErrorMessages.IllegalCodepage(codepage));
408 }
409 }
410 }
411 }