| 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 | } |