/// <summary> /// Creates the MSI/MSM/PCP database. /// </summary> /// <param name="output">Output to create database for.</param> /// <param name="databaseFile">The database file to create.</param> /// <param name="keepAddedColumns">Whether to keep columns added in a transform.</param> /// <param name="useSubdirectory">Whether to use a subdirectory based on the <paramref name="databaseFile"/> file name for intermediate files.</param> internal void GenerateDatabase(Output output, string databaseFile, bool keepAddedColumns, bool useSubdirectory) { // add the _Validation rows if (!this.suppressAddingValidationRows) { Table validationTable = output.EnsureTable(this.core.TableDefinitions["_Validation"]); foreach (Table table in output.Tables) { if (!table.Definition.IsUnreal) { // add the validation rows for this table table.Definition.AddValidationRows(validationTable); } } } // set the base directory string baseDirectory = this.TempFilesLocation; if (useSubdirectory) { string filename = Path.GetFileNameWithoutExtension(databaseFile); baseDirectory = Path.Combine(baseDirectory, filename); // make sure the directory exists Directory.CreateDirectory(baseDirectory); } try { OpenDatabase type = OpenDatabase.CreateDirect; // set special flag for patch files if (OutputType.Patch == output.Type) { type |= OpenDatabase.OpenPatchFile; } // try to create the database using (Database db = new Database(databaseFile, type)) { // localize the codepage if a value was specified by the localizer if (null != this.Localizer && -1 != this.Localizer.Codepage) { output.Codepage = this.Localizer.Codepage; } // if we're not using the default codepage, import a new one into our // database before we add any tables (or the tables would be added // with the wrong codepage) if (0 != output.Codepage) { this.SetDatabaseCodepage(db, output); } // insert substorages (like transforms inside a patch) if (0 < output.SubStorages.Count) { using (View storagesView = new View(db, "SELECT `Name`, `Data` FROM `_Storages`")) { foreach (SubStorage subStorage in output.SubStorages) { string transformFile = Path.Combine(this.TempFilesLocation, String.Concat(subStorage.Name, ".mst")); // bind the transform if (this.BindTransform(subStorage.Data, transformFile)) { // add the storage using (Record record = new Record(2)) { record.SetString(1, subStorage.Name); record.SetStream(2, transformFile); storagesView.Modify(ModifyView.Assign, record); } } } } // some empty transforms may have been excluded // we need to remove these from the final patch summary information if (OutputType.Patch == output.Type && this.AllowEmptyTransforms) { Table patchSummaryInfo = output.EnsureTable(this.core.TableDefinitions["_SummaryInformation"]); for (int i = patchSummaryInfo.Rows.Count - 1; i >= 0; i--) { Row row = patchSummaryInfo.Rows[i]; if ((int)SummaryInformation.Patch.ProductCodes == (int)row[0]) { if (nonEmptyProductCodes.Count > 0) { string[] productCodes = new string[nonEmptyProductCodes.Count]; nonEmptyProductCodes.CopyTo(productCodes, 0); row[1] = String.Join(";", productCodes); } else { row[1] = Binder.NullString; } } else if ((int)SummaryInformation.Patch.TransformNames == (int)row[0]) { if (nonEmptyTransformNames.Count > 0) { string[] transformNames = new string[nonEmptyTransformNames.Count]; nonEmptyTransformNames.CopyTo(transformNames, 0); row[1] = String.Join(";", transformNames); } else { row[1] = Binder.NullString; } } } } } foreach (Table table in output.Tables) { Table importTable = table; bool hasBinaryColumn = false; // skip all unreal tables other than _Streams if (table.Definition.IsUnreal && "_Streams" != table.Name) { continue; } // Do not put the _Validation table in patches, it is not needed if (OutputType.Patch == output.Type && "_Validation" == table.Name) { continue; } // The only way to import binary data is to copy it to a local subdirectory first. // To avoid this extra copying and perf hit, import an empty table with the same // definition and later import the binary data from source using records. foreach (ColumnDefinition columnDefinition in table.Definition.Columns) { if (ColumnType.Object == columnDefinition.Type) { importTable = new Table(table.Section, table.Definition); hasBinaryColumn = true; break; } } // create the table via IDT import if ("_Streams" != importTable.Name) { try { db.ImportTable(output.Codepage, this.core, importTable, baseDirectory, keepAddedColumns); } catch (WixInvalidIdtException) { // If ValidateRows finds anything it doesn't like, it throws importTable.ValidateRows(); // Otherwise we rethrow the InvalidIdt throw; } } // insert the rows via SQL query if this table contains object fields if (hasBinaryColumn) { StringBuilder query = new StringBuilder("SELECT "); // build the query for the view bool firstColumn = true; foreach (ColumnDefinition columnDefinition in table.Definition.Columns) { if (!firstColumn) { query.Append(","); } query.AppendFormat(" `{0}`", columnDefinition.Name); firstColumn = false; } query.AppendFormat(" FROM `{0}`", table.Name); using (View tableView = db.OpenExecuteView(query.ToString())) { // import each row containing a stream foreach (Row row in table.Rows) { using (Record record = new Record(table.Definition.Columns.Count)) { StringBuilder streamName = new StringBuilder(); bool needStream = false; // the _Streams table doesn't prepend the table name (or a period) if ("_Streams" != table.Name) { streamName.Append(table.Name); } for (int i = 0; i < table.Definition.Columns.Count; i++) { ColumnDefinition columnDefinition = table.Definition.Columns[i]; switch (columnDefinition.Type) { case ColumnType.Localized: case ColumnType.Preserved: case ColumnType.String: if (columnDefinition.IsPrimaryKey) { if (0 < streamName.Length) { streamName.Append("."); } streamName.Append((string)row[i]); } record.SetString(i + 1, (string)row[i]); break; case ColumnType.Number: record.SetInteger(i + 1, Convert.ToInt32(row[i], CultureInfo.InvariantCulture)); break; case ColumnType.Object: if (null != row[i]) { needStream = true; try { record.SetStream(i + 1, (string)row[i]); } catch (Win32Exception e) { if (0xA1 == e.NativeErrorCode) // ERROR_BAD_PATHNAME { throw new WixException(WixErrors.FileNotFound(row.SourceLineNumbers, (string)row[i])); } else { throw new WixException(WixErrors.Win32Exception(e.NativeErrorCode, e.Message)); } } } break; } } // stream names are created by concatenating the name of the table with the values // of the primary key (delimited by periods) // check for a stream name that is more than 62 characters long (the maximum allowed length) if (needStream && MsiInterop.MsiMaxStreamNameLength < streamName.Length) { this.core.OnMessage(WixErrors.StreamNameTooLong(row.SourceLineNumbers, table.Name, streamName.ToString(), streamName.Length)); } else // add the row to the database { tableView.Modify(ModifyView.Assign, record); } } } } // Remove rows from the _Streams table for wixpdbs. if ("_Streams" == table.Name) { table.Rows.Clear(); } } } // we're good, commit the changes to the new MSI db.Commit(); } } catch (IOException) { // TODO: this error message doesn't seem specific enough throw new WixFileNotFoundException(SourceLineNumberCollection.FromFileName(databaseFile), databaseFile); } }
/// <summary> /// Updates database with signatures from external cabinets. /// </summary> /// <param name="databaseFile">Path to MSI database.</param> /// <param name="outputFile">Ouput for updated MSI database.</param> /// <param name="tidy">Clean up files.</param> /// <returns>True if database is updated.</returns> public bool InscribeDatabase(string databaseFile, string outputFile, bool tidy) { // Keeps track of whether we've encountered at least one signed cab or not - we'll throw a warning if no signed cabs were encountered bool foundUnsignedExternals = false; bool shouldCommit = false; FileAttributes attributes = File.GetAttributes(databaseFile); if (FileAttributes.ReadOnly == (attributes & FileAttributes.ReadOnly)) { this.OnMessage(WixErrors.ReadOnlyOutputFile(databaseFile)); return shouldCommit; } using (Database database = new Database(databaseFile, OpenDatabase.Transact)) { // Just use the English codepage, because the tables we're importing only have binary streams / MSI identifiers / other non-localizable content int codepage = 1252; // list of certificates for this database (hash/identifier) Dictionary<string, string> certificates = new Dictionary<string, string>(); // Reset the in-memory tables for this new database Table digitalSignatureTable = new Table(null, this.tableDefinitions["MsiDigitalSignature"]); Table digitalCertificateTable = new Table(null, this.tableDefinitions["MsiDigitalCertificate"]); // If any digital signature records exist that are not of the media type, preserve them if (database.TableExists("MsiDigitalSignature")) { using (View digitalSignatureView = database.OpenExecuteView("SELECT `Table`, `SignObject`, `DigitalCertificate_`, `Hash` FROM `MsiDigitalSignature` WHERE `Table` <> 'Media'")) { while (true) { using (Record digitalSignatureRecord = digitalSignatureView.Fetch()) { if (null == digitalSignatureRecord) { break; } Row digitalSignatureRow = null; digitalSignatureRow = digitalSignatureTable.CreateRow(null); string table = digitalSignatureRecord.GetString(0); string signObject = digitalSignatureRecord.GetString(1); digitalSignatureRow[0] = table; digitalSignatureRow[1] = signObject; digitalSignatureRow[2] = digitalSignatureRecord.GetString(2); if (false == digitalSignatureRecord.IsNull(3)) { // Export to a file, because the MSI API's require us to provide a file path on disk string hashPath = Path.Combine(this.TempFilesLocation, "MsiDigitalSignature"); string hashFileName = string.Concat(table,".", signObject, ".bin"); Directory.CreateDirectory(hashPath); hashPath = Path.Combine(hashPath, hashFileName); using (FileStream fs = File.Create(hashPath)) { int bytesRead; byte[] buffer = new byte[1024 * 4]; while (0 != (bytesRead = digitalSignatureRecord.GetStream(3, buffer, buffer.Length))) { fs.Write(buffer, 0, bytesRead); } } digitalSignatureRow[3] = hashFileName; } } } } } // If any digital certificates exist, extract and preserve them if (database.TableExists("MsiDigitalCertificate")) { using (View digitalCertificateView = database.OpenExecuteView("SELECT * FROM `MsiDigitalCertificate`")) { while (true) { using (Record digitalCertificateRecord = digitalCertificateView.Fetch()) { if (null == digitalCertificateRecord) { break; } string certificateId = digitalCertificateRecord.GetString(1); // get the identifier of the certificate // Export to a file, because the MSI API's require us to provide a file path on disk string certPath = Path.Combine(this.TempFilesLocation, "MsiDigitalCertificate"); Directory.CreateDirectory(certPath); certPath = Path.Combine(certPath, string.Concat(certificateId, ".cer")); using (FileStream fs = File.Create(certPath)) { int bytesRead; byte[] buffer = new byte[1024 * 4]; while (0 != (bytesRead = digitalCertificateRecord.GetStream(2, buffer, buffer.Length))) { fs.Write(buffer, 0, bytesRead); } } // Add it to our "add to MsiDigitalCertificate" table dictionary Row digitalCertificateRow = digitalCertificateTable.CreateRow(null); digitalCertificateRow[0] = certificateId; // Now set the file path on disk where this binary stream will be picked up at import time digitalCertificateRow[1] = string.Concat(certificateId, ".cer"); // Load the cert to get it's thumbprint X509Certificate cert = X509Certificate.CreateFromCertFile(certPath); X509Certificate2 cert2 = new X509Certificate2(cert); certificates.Add(cert2.Thumbprint, certificateId); } } } } using (View mediaView = database.OpenExecuteView("SELECT * FROM `Media`")) { while (true) { using (Record mediaRecord = mediaView.Fetch()) { if (null == mediaRecord) { break; } X509Certificate2 cert2 = null; Row digitalSignatureRow = null; string cabName = mediaRecord.GetString(4); // get the name of the cab // If there is no cabinet or it's an internal cab, skip it. if (String.IsNullOrEmpty(cabName) || cabName.StartsWith("#", StringComparison.Ordinal)) { continue; } string cabId = mediaRecord.GetString(1); // get the ID of the cab string cabPath = Path.Combine(Path.GetDirectoryName(databaseFile), cabName); // If the cabs aren't there, throw an error but continue to catch the other errors if (!File.Exists(cabPath)) { this.OnMessage(WixErrors.WixFileNotFound(cabPath)); continue; } try { // Get the certificate from the cab X509Certificate signedFileCert = X509Certificate.CreateFromSignedFile(cabPath); cert2 = new X509Certificate2(signedFileCert); } catch (System.Security.Cryptography.CryptographicException e) { uint HResult = unchecked((uint)Marshal.GetHRForException(e)); // If the file has no cert, continue, but flag that we found at least one so we can later give a warning if (0x80092009 == HResult) // CRYPT_E_NO_MATCH { foundUnsignedExternals = true; continue; } // todo: exactly which HRESULT corresponds to this issue? // If it's one of these exact platforms, warn the user that it may be due to their OS. if ((5 == Environment.OSVersion.Version.Major && 2 == Environment.OSVersion.Version.Minor) || // W2K3 (5 == Environment.OSVersion.Version.Major && 1 == Environment.OSVersion.Version.Minor)) // XP { this.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFileDownlevelOS(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult))); } else // otherwise, generic error { this.OnMessage(WixErrors.UnableToGetAuthenticodeCertOfFile(cabPath, String.Format(CultureInfo.InvariantCulture, "HRESULT: 0x{0:x8}", HResult))); } } // If we haven't added this cert to the MsiDigitalCertificate table, set it up to be added if (!certificates.ContainsKey(cert2.Thumbprint)) { // generate a stable identifier string certificateGeneratedId = Common.GenerateIdentifier("cer", true, cert2.Thumbprint); // Add it to our "add to MsiDigitalCertificate" table dictionary Row digitalCertificateRow = digitalCertificateTable.CreateRow(null); digitalCertificateRow[0] = certificateGeneratedId; // Export to a file, because the MSI API's require us to provide a file path on disk string certPath = Path.Combine(this.TempFilesLocation, "MsiDigitalCertificate"); Directory.CreateDirectory(certPath); certPath = Path.Combine(certPath, string.Concat(cert2.Thumbprint, ".cer")); File.Delete(certPath); using (BinaryWriter writer = new BinaryWriter(File.Open(certPath, FileMode.Create))) { writer.Write(cert2.RawData); writer.Close(); } // Now set the file path on disk where this binary stream will be picked up at import time digitalCertificateRow[1] = string.Concat(cert2.Thumbprint, ".cer"); certificates.Add(cert2.Thumbprint, certificateGeneratedId); } digitalSignatureRow = digitalSignatureTable.CreateRow(null); digitalSignatureRow[0] = "Media"; digitalSignatureRow[1] = cabId; digitalSignatureRow[2] = certificates[cert2.Thumbprint]; } } } if (digitalCertificateTable.Rows.Count > 0) { database.ImportTable(codepage, (IMessageHandler)this, digitalCertificateTable, this.TempFilesLocation, true); shouldCommit = true; } if (digitalSignatureTable.Rows.Count > 0) { database.ImportTable(codepage, (IMessageHandler)this, digitalSignatureTable, this.TempFilesLocation, true); shouldCommit = true; } // TODO: if we created the table(s), then we should add the _Validation records for them. certificates = null; // If we did find external cabs but none of them were signed, give a warning if (foundUnsignedExternals) { this.OnMessage(WixWarnings.ExternalCabsAreNotSigned(databaseFile)); } if (shouldCommit) { database.Commit(); } } return shouldCommit; }