// 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. namespace WixToolset.Dtf.WindowsInstaller { using System; using System.IO; using System.Collections.Generic; using System.Globalization; using System.Diagnostics.CodeAnalysis; /// /// Accesses a Windows Installer database. /// ///

/// The method must be called before the Database is closed to write out all /// persistent changes. If the Commit method is not called, the installer performs an implicit /// rollback upon object destruction. ///

/// The client can use the following procedure for data access: /// Obtain a Database object using one of the Database constructors. /// Initiate a query using a SQL string by calling the /// method of the Database. /// Set query parameters in a and execute the database /// query by calling the method of the . This /// produces a result that can be fetched or updated. /// Call the method of the View repeatedly to return /// Records. /// Update database rows of a Record object obtained by the Fetch method using /// one of the methods of the View. /// Release the query and any unfetched records by calling the /// method of the View. /// Persist any database updates by calling the Commit method of the Database. /// /// ///

public partial class Database : InstallerHandle { private string filePath; private DatabaseOpenMode openMode; private SummaryInfo summaryInfo; private TableCollection tables; private IList deleteOnClose; /// /// Opens an existing database in read-only mode. /// /// Path to the database file. /// the database could not be created/opened ///

/// Because this constructor initiates database access, it cannot be used with a /// running installation. ///

/// The Database object should be d after use. /// It is best that the handle be closed manually as soon as it is no longer /// needed, as leaving lots of unused handles open can degrade performance. ///

/// Win32 MSI API: /// MsiOpenDatabase ///

public Database(string filePath) : this(filePath, DatabaseOpenMode.ReadOnly) { } /// /// Opens an existing database with another database as output. /// /// Path to the database to be read. /// Open mode for the database /// Database object representing the created or opened database /// the database could not be created/opened ///

/// When a database is opened as the output of another database, the summary information stream /// of the output database is actually a read-only mirror of the original database and thus cannot /// be changed. Additionally, it is not persisted with the database. To create or modify the /// summary information for the output database it must be closed and re-opened. ///

/// The Database object should be d after use. /// It is best that the handle be closed manually as soon as it is no longer /// needed, as leaving lots of unused handles open can degrade performance. ///

/// The database is opened in mode, and will be /// automatically commited when it is closed. ///

/// Win32 MSI API: /// MsiOpenDatabase ///

public Database(string filePath, string outputPath) : this((IntPtr) Database.Open(filePath, outputPath), true, outputPath, DatabaseOpenMode.CreateDirect) { } /// /// Opens an existing database or creates a new one. /// /// Path to the database file. If an empty string /// is supplied, a temporary database is created that is not persisted. /// Open mode for the database /// the database could not be created/opened ///

/// Because this constructor initiates database access, it cannot be used with a /// running installation. ///

/// The database object should be d after use. /// The finalizer will close the handle if it is still open, however due to the nondeterministic /// nature of finalization it is best that the handle be closed manually as soon as it is no /// longer needed, as leaving lots of unused handles open can degrade performance. ///

/// A database opened in or /// mode will be automatically commited when it is /// closed. However a database opened in or /// mode must have the method /// called before it is closed, otherwise no changes will be persisted. ///

/// Win32 MSI API: /// MsiOpenDatabase ///

public Database(string filePath, DatabaseOpenMode mode) : this((IntPtr) Database.Open(filePath, mode), true, filePath, mode) { } /// /// Creates a new database from an MSI handle. /// /// Native MSI database handle. /// True if the handle should be closed /// when the database object is disposed /// Path of the database file, if known /// Mode the handle was originally opened in protected internal Database( IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode) : base(handle, ownsHandle) { this.filePath = filePath; this.openMode = openMode; } /// /// Gets the file path the Database was originally opened from, or null if not known. /// public String FilePath { get { return this.filePath; } } /// /// Gets the open mode for the database. /// public DatabaseOpenMode OpenMode { get { return this.openMode; } } /// /// Gets a boolean value indicating whether this database was opened in read-only mode. /// ///

/// Win32 MSI API: /// MsiGetDatabaseState ///

public bool IsReadOnly { get { if (RemotableNativeMethods.RemotingEnabled) { return true; } int state = NativeMethods.MsiGetDatabaseState((int) this.Handle); return state != 1; } } /// /// Gets the collection of tables in the Database. /// public TableCollection Tables { get { if (this.tables == null) { this.tables = new TableCollection(this); } return this.tables; } } /// /// Gets or sets the code page of the Database. /// /// error exporting/importing the codepage data /// the Database handle is invalid ///

/// Getting or setting the code page is a slow operation because it involves an export or import /// of the codepage data to/from a temporary file. ///

public int CodePage { get { string tempFile = Path.GetTempFileName(); StreamReader reader = null; try { this.Export("_ForceCodepage", tempFile); reader = File.OpenText(tempFile); reader.ReadLine(); // Skip column name record. reader.ReadLine(); // Skip column defn record. string codePageLine = reader.ReadLine(); return Int32.Parse(codePageLine.Split('\t')[0], CultureInfo.InvariantCulture.NumberFormat); } finally { if (reader != null) reader.Close(); File.Delete(tempFile); } } set { string tempFile = Path.GetTempFileName(); StreamWriter writer = null; try { writer = File.AppendText(tempFile); writer.WriteLine(""); writer.WriteLine(""); writer.WriteLine("{0}\t_ForceCodepage", value); writer.Close(); writer = null; this.Import(tempFile); } finally { if (writer != null) writer.Close(); File.Delete(tempFile); } } } /// /// Gets the SummaryInfo object for this database that can be used to examine and modify properties /// to the summary information stream. /// /// the Database handle is invalid ///

/// The object returned from this property does not need to be explicitly persisted or closed. /// Any modifications will be automatically saved when the database is committed. ///

/// Win32 MSI API: /// MsiGetSummaryInformation ///

public SummaryInfo SummaryInfo { get { if (this.summaryInfo == null || this.summaryInfo.IsClosed) { lock (this.Sync) { if (this.summaryInfo == null || this.summaryInfo.IsClosed) { int summaryInfoHandle; int maxProperties = this.IsReadOnly ? 0 : SummaryInfo.MAX_PROPERTIES; uint ret = RemotableNativeMethods.MsiGetSummaryInformation((int) this.Handle, null, (uint) maxProperties, out summaryInfoHandle); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } this.summaryInfo = new SummaryInfo((IntPtr) summaryInfoHandle, true); } } } return this.summaryInfo; } } /// /// Creates a new Database object from an integer database handle. /// ///

/// This method is only provided for interop purposes. A Database object /// should normally be obtained from or /// a public Database constructor. ///

/// Integer database handle /// true to close the handle when this object is disposed public static Database FromHandle(IntPtr handle, bool ownsHandle) { return new Database( handle, ownsHandle, null, NativeMethods.MsiGetDatabaseState((int) handle) == 1 ? DatabaseOpenMode.Direct : DatabaseOpenMode.ReadOnly); } /// /// Schedules a file or directory for deletion after the database handle is closed. /// /// File or directory path to be deleted. All files and subdirectories /// under a directory are deleted. ///

/// Once an item is scheduled, it cannot be unscheduled. ///

/// The items cannot be deleted if the Database object is auto-disposed by the /// garbage collector; the handle must be explicitly closed. ///

/// Files which are read-only or otherwise locked cannot be deleted, /// but they will not cause an exception to be thrown. ///

public void DeleteOnClose(string path) { if (this.deleteOnClose == null) { this.deleteOnClose = new List(); } this.deleteOnClose.Add(path); } /// /// Merges another database with this database. /// /// The database to be merged into this database /// Optional name of table to contain the names of the tables containing /// merge conflicts, the number of conflicting rows within the table, and a reference to the table /// with the merge conflict. /// merge failed due to a schema difference or data conflict /// the Database handle is invalid ///

/// Merge does not copy over embedded cabinet files or embedded transforms from the /// reference database into the target database. Embedded data streams that are listed in the /// Binary table or Icon table are copied from the reference database to the target database. /// Storage embedded in the reference database are not copied to the target database. ///

/// The Merge method merges the data of two databases. These databases must have the same /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists /// if the data in any row in the first database differs from the data in the corresponding row /// of the second database. Corresponding rows are in the same table of both databases and have /// the same primary key in both databases. The tables of non-conflicting databases must have /// the same number of primary keys, same number of columns, same column types, same column names, /// and the same data in rows with identical primary keys. Temporary columns however don't matter /// in the column count and corresponding tables can have a different number of temporary columns /// without creating conflict as long as the persistent columns match. ///

/// If the number, type, or name of columns in corresponding tables are different, the /// schema of the two databases are incompatible and the installer will stop processing tables /// and the merge fails. The installer checks that the two databases have the same schema before /// checking for row merge conflicts. If the schemas are incompatible, the databases have be /// modified. ///

/// If the data in particular rows differ, this is a row merge conflict, the merge fails /// and creates a new table with the specified name. The first column of this table is the name /// of the table having the conflict. The second column gives the number of rows in the table /// having the conflict. ///

/// Win32 MSI API: /// MsiDatabaseMerge ///

[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public void Merge(Database otherDatabase, string errorTable) { if (otherDatabase == null) { throw new ArgumentNullException("otherDatabase"); } uint ret = NativeMethods.MsiDatabaseMerge((int) this.Handle, (int) otherDatabase.Handle, errorTable); if (ret != 0) { if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) { throw new MergeException(this, errorTable); } else if (ret == (uint) NativeMethods.Error.DATATYPE_MISMATCH) { throw new MergeException("Schema difference between the two databases."); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } } /// /// Merges another database with this database. /// /// The database to be merged into this database /// merge failed due to a schema difference or data conflict /// the Database handle is invalid ///

/// MsiDatabaseMerge does not copy over embedded cabinet files or embedded transforms from /// the reference database into the target database. Embedded data streams that are listed in /// the Binary table or Icon table are copied from the reference database to the target database. /// Storage embedded in the reference database are not copied to the target database. ///

/// The Merge method merges the data of two databases. These databases must have the same /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists /// if the data in any row in the first database differs from the data in the corresponding row /// of the second database. Corresponding rows are in the same table of both databases and have /// the same primary key in both databases. The tables of non-conflicting databases must have /// the same number of primary keys, same number of columns, same column types, same column names, /// and the same data in rows with identical primary keys. Temporary columns however don't matter /// in the column count and corresponding tables can have a different number of temporary columns /// without creating conflict as long as the persistent columns match. ///

/// If the number, type, or name of columns in corresponding tables are different, the /// schema of the two databases are incompatible and the installer will stop processing tables /// and the merge fails. The installer checks that the two databases have the same schema before /// checking for row merge conflicts. If the schemas are incompatible, the databases have be /// modified. ///

/// Win32 MSI API: /// MsiDatabaseMerge ///

public void Merge(Database otherDatabase) { this.Merge(otherDatabase, null); } /// /// Checks whether a table exists and is persistent in the database. /// /// The table to the checked /// true if the table exists and is persistent in the database; false otherwise /// the table is unknown /// the Database handle is invalid ///

/// To check whether a table exists regardless of persistence, /// use . ///

/// Win32 MSI API: /// MsiDatabaseIsTablePersistent ///

public bool IsTablePersistent(string table) { if (String.IsNullOrEmpty(table)) { throw new ArgumentNullException("table"); } uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.Handle, table); if (ret == 3) // MSICONDITION_ERROR { throw new InstallerException(); } return ret == 1; } /// /// Checks whether a table contains a persistent column with a given name. /// /// The table to the checked /// The name of the column to be checked /// true if the column exists in the table; false if the column is temporary or does not exist. /// the View could not be executed /// the Database handle is invalid ///

/// To check whether a column exists regardless of persistence, /// use . ///

public bool IsColumnPersistent(string table, string column) { if (String.IsNullOrEmpty(table)) { throw new ArgumentNullException("table"); } if (String.IsNullOrEmpty(column)) { throw new ArgumentNullException("column"); } using (View view = this.OpenView( "SELECT `Number` FROM `_Columns` WHERE `Table` = '{0}' AND `Name` = '{1}'", table, column)) { view.Execute(); using (Record rec = view.Fetch()) { return (rec != null); } } } /// /// Gets the count of all rows in the table. /// /// Name of the table whose rows are to be counted /// The count of all rows in the table /// the View could not be executed /// the Database handle is invalid public int CountRows(string table) { return this.CountRows(table, null); } /// /// Gets the count of all rows in the table that satisfy a given condition. /// /// Name of the table whose rows are to be counted /// Conditional expression, such as could be placed on the end of a SQL WHERE clause /// The count of all rows in the table satisfying the condition /// the SQL WHERE syntax is invalid /// the View could not be executed /// the Database handle is invalid public int CountRows(string table, string where) { if (String.IsNullOrEmpty(table)) { throw new ArgumentNullException("table"); } // to support temporary tables like _Streams, run the query even if the table isn't persistent TableInfo tableInfo = this.Tables[table]; string primaryKeys = tableInfo == null ? "*" : String.Concat("`", tableInfo.PrimaryKeys[0], "`"); int count; try { using (View view = this.OpenView( "SELECT {0} FROM `{1}`{2}", primaryKeys, table, (where != null && where.Length != 0 ? " WHERE " + where : ""))) { view.Execute(); for (count = 0; ; count++) { // Avoid creating unnecessary Record objects by not calling View.Fetch(). int recordHandle; uint ret = RemotableNativeMethods.MsiViewFetch((int)view.Handle, out recordHandle); if (ret == (uint)NativeMethods.Error.NO_MORE_ITEMS) { break; } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } RemotableNativeMethods.MsiCloseHandle(recordHandle); } } } catch (BadQuerySyntaxException) { // table was missing count = 0; } return count; } /// /// Finalizes the persistent form of the database. All persistent data is written /// to the writeable database, and no temporary columns or rows are written. /// /// the Database handle is invalid ///

/// For a database open in mode, this method has no effect. ///

/// For a database open in or /// mode, it is not necessary to call this method because the database will be automatically committed /// when it is closed. However this method may be called at any time to persist the current state of tables /// loaded into memory. ///

/// For a database open in or /// mode, no changes will be persisted until this method is called. If the database object is closed without /// calling this method, the database file remains unmodified. ///

/// Win32 MSI API: /// MsiDatabaseCommit ///

public void Commit() { if (this.summaryInfo != null && !this.summaryInfo.IsClosed) { this.summaryInfo.Persist(); this.summaryInfo.Close(); this.summaryInfo = null; } uint ret = NativeMethods.MsiDatabaseCommit((int) this.Handle); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Copies the structure and data from a specified table to a text archive file. /// /// Name of the table to be exported /// Path to the file to be created /// the file path is invalid /// the Database handle is invalid ///

/// Win32 MSI API: /// MsiDatabaseExport ///

public void Export(string table, string exportFilePath) { if (table == null) { throw new ArgumentNullException("table"); } FileInfo file = new FileInfo(exportFilePath); uint ret = NativeMethods.MsiDatabaseExport((int) this.Handle, table, file.DirectoryName, file.Name); if (ret != 0) { if (ret == (uint) NativeMethods.Error.BAD_PATHNAME) { throw new FileNotFoundException(null, exportFilePath); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } } /// /// Imports a database table from a text archive file, dropping any existing table. /// /// Path to the file to be imported. /// The table name is specified within the file. /// the file path is invalid /// the Database handle is invalid ///

/// Win32 MSI API: /// MsiDatabaseImport ///

public void Import(string importFilePath) { if (String.IsNullOrEmpty(importFilePath)) { throw new ArgumentNullException("importFilePath"); } FileInfo file = new FileInfo(importFilePath); uint ret = NativeMethods.MsiDatabaseImport((int) this.Handle, file.DirectoryName, file.Name); if (ret != 0) { if (ret == (uint) NativeMethods.Error.BAD_PATHNAME) { throw new FileNotFoundException(null, importFilePath); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } } /// /// Exports all database tables, streams, and summary information to archive files. /// /// Path to the directory where archive files will be created /// the directory path is invalid /// the Database handle is invalid ///

/// The directory will be created if it does not already exist. ///

/// Win32 MSI API: /// MsiDatabaseExport ///

public void ExportAll(string directoryPath) { if (String.IsNullOrEmpty(directoryPath)) { throw new ArgumentNullException("directoryPath"); } if (!Directory.Exists(directoryPath)) { Directory.CreateDirectory(directoryPath); } this.Export("_SummaryInformation", Path.Combine(directoryPath, "_SummaryInformation.idt")); using (View view = this.OpenView("SELECT `Name` FROM `_Tables`")) { view.Execute(); foreach (Record rec in view) using (rec) { string table = (string) rec[1]; this.Export(table, Path.Combine(directoryPath, table + ".idt")); } } if (!Directory.Exists(Path.Combine(directoryPath, "_Streams"))) { Directory.CreateDirectory(Path.Combine(directoryPath, "_Streams")); } using (View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`")) { view.Execute(); foreach (Record rec in view) using (rec) { string stream = (string) rec[1]; if (stream.EndsWith("SummaryInformation", StringComparison.Ordinal)) continue; int i = stream.IndexOf('.'); if (i >= 0) { if (File.Exists(Path.Combine( directoryPath, Path.Combine(stream.Substring(0, i), stream.Substring(i + 1) + ".ibd")))) { continue; } } rec.GetStream(2, Path.Combine(directoryPath, Path.Combine("_Streams", stream))); } } } /// /// Imports all database tables, streams, and summary information from archive files. /// /// Path to the directory from which archive files will be imported /// the directory path is invalid /// the Database handle is invalid ///

/// Win32 MSI API: /// MsiDatabaseImport ///

public void ImportAll(string directoryPath) { if (String.IsNullOrEmpty(directoryPath)) { throw new ArgumentNullException("directoryPath"); } if (File.Exists(Path.Combine(directoryPath, "_SummaryInformation.idt"))) { this.Import(Path.Combine(directoryPath, "_SummaryInformation.idt")); } string[] idtFiles = Directory.GetFiles(directoryPath, "*.idt"); foreach (string file in idtFiles) { if (Path.GetFileName(file) != "_SummaryInformation.idt") { this.Import(file); } } if (Directory.Exists(Path.Combine(directoryPath, "_Streams"))) { View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`"); Record rec = null; try { view.Execute(); string[] streamFiles = Directory.GetFiles(Path.Combine(directoryPath, "_Streams")); foreach (string file in streamFiles) { rec = this.CreateRecord(2); rec[1] = Path.GetFileName(file); rec.SetStream(2, file); view.Insert(rec); rec.Close(); rec = null; } } finally { if (rec != null) rec.Close(); view.Close(); } } } /// /// Creates a new record object with the requested number of fields. /// /// Required number of fields, which may be 0. /// The maximum number of fields in a record is limited to 65535. /// A new record object that can be used with the database. ///

/// This method is equivalent to directly calling the /// constructor in all cases outside of a custom action context. When in a /// custom action session, this method allows creation of a record that can /// work with a database other than the session database. ///

/// The Record object should be d after use. /// It is best that the handle be closed manually as soon as it is no longer /// needed, as leaving lots of unused handles open can degrade performance. ///

/// Win32 MSI API: /// MsiCreateRecord ///

public Record CreateRecord(int fieldCount) { int hRecord = RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, (int) this.Handle); return new Record((IntPtr) hRecord, true, (View) null); } /// /// Returns the file path of this database, or the handle value if a file path was not specified. /// public override string ToString() { if (this.FilePath != null) { return this.FilePath; } else { return "#" + ((int) this.Handle).ToString(CultureInfo.InvariantCulture); } } /// /// Closes the database handle. After closing a handle, further method calls may throw . /// /// If true, the method has been called directly or /// indirectly by a user's code, so managed and unmanaged resources will be /// disposed. If false, only unmanaged resources will be disposed. protected override void Dispose(bool disposing) { if (!this.IsClosed && (this.OpenMode == DatabaseOpenMode.CreateDirect || this.OpenMode == DatabaseOpenMode.Direct)) { // Always commit a direct-opened database before closing. // This avoids unexpected corruption of the database. this.Commit(); } base.Dispose(disposing); if (disposing) { if (this.summaryInfo != null) { this.summaryInfo.Close(); this.summaryInfo = null; } if (this.deleteOnClose != null) { foreach (string path in this.deleteOnClose) { try { if (Directory.Exists(path)) { Directory.Delete(path, true); } else { if (File.Exists(path)) File.Delete(path); } } catch (IOException) { } catch (UnauthorizedAccessException) { } } this.deleteOnClose = null; } } } private static int Open(string filePath, string outputPath) { if (String.IsNullOrEmpty(filePath)) { throw new ArgumentNullException("filePath"); } if (String.IsNullOrEmpty(outputPath)) { throw new ArgumentNullException("outputPath"); } int hDb; uint ret = NativeMethods.MsiOpenDatabase(filePath, outputPath, out hDb); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return hDb; } private static int Open(string filePath, DatabaseOpenMode mode) { if (String.IsNullOrEmpty(filePath)) { throw new ArgumentNullException("filePath"); } if (Path.GetExtension(filePath).Equals(".msp", StringComparison.Ordinal)) { const int DATABASEOPENMODE_PATCH = 32; int patchMode = (int) mode | DATABASEOPENMODE_PATCH; mode = (DatabaseOpenMode) patchMode; } int hDb; uint ret = NativeMethods.MsiOpenDatabase(filePath, (IntPtr) mode, out hDb); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode( ret, String.Format(CultureInfo.InvariantCulture, "Database=\"{0}\"", filePath)); } return hDb; } /// /// Returns the value of the specified property. /// /// Name of the property to retrieve. public string ExecutePropertyQuery(string property) { IList values = this.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", property); return (values.Count > 0 ? values[0] : null); } } }