// 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.Text; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Diagnostics.CodeAnalysis; /// /// The Record object is a container for holding and transferring a variable number of values. /// Fields within the record are numerically indexed and can contain strings, integers, streams, /// and null values. Record fields are indexed starting with 1. Field 0 is a special format field. /// ///

/// Most methods on the Record class have overloads that allow using either a number /// or a name to designate a field. However note that field names only exist when the /// Record is directly returned from a query on a database. For other records, attempting /// to access a field by name will result in an InvalidOperationException. ///

public class Record : InstallerHandle { private View view; private bool isFormatStringInvalid; /// /// IsFormatStringInvalid is set from several View methods that invalidate the FormatString /// and used to determine behavior during Record.ToString(). /// internal protected bool IsFormatStringInvalid { set { this.isFormatStringInvalid = value; } get { return this.isFormatStringInvalid; } } /// /// 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. ///

/// 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(int fieldCount) : this((IntPtr) RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, 0), true, (View) null) { } /// /// Creates a new record object, providing values for an arbitrary number of fields. /// /// The values of the record fields. The parameters should be of type Int16, Int32 or String ///

/// 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(params object[] fields) : this(fields.Length) { for (int i = 0; i < fields.Length; i++) { this[i + 1] = fields[i]; } } internal Record(IntPtr handle, bool ownsHandle, View view) : base(handle, ownsHandle) { this.view = view; } /// /// Gets the number of fields in a record. /// ///

/// Win32 MSI API: /// MsiRecordGetFieldCount ///

public int FieldCount { get { return (int) RemotableNativeMethods.MsiRecordGetFieldCount((int) this.Handle); } } /// /// Gets or sets field 0 of the Record, which is the format string. /// public string FormatString { get { return this.GetString(0); } set { this.SetString(0, value); } } /// /// Gets or sets a record field value. /// /// Specifies the name of the field of the Record to get or set. /// The name does not match any known field of the Record. ///

/// When getting a field, the type of the object returned depends on the type of the Record field. /// The object will be one of: Int16, Int32, String, Stream, or null. ///

/// When setting a field, the type of the object provided will be converted to match the View /// query that returned the record, or if Record was not returned from a view then the type of /// the object provided will determine the type of the Record field. The object should be one of: /// Int16, Int32, String, Stream, or null. ///

public object this[string fieldName] { get { int field = this.FindColumn(fieldName); return this[field]; } set { int field = this.FindColumn(fieldName); this[field] = value; } } /// /// Gets or sets a record field value. /// /// Specifies the field of the Record to get or set. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Record fields are indexed starting with 1. Field 0 is a special format field. ///

/// When getting a field, the type of the object returned depends on the type of the Record field. /// The object will be one of: Int16, Int32, String, Stream, or null. If the Record was returned /// from a View, the type will match that of the field from the View query. Otherwise, the type /// will match the type of the last value set for the field. ///

/// When setting a field, the type of the object provided will be converted to match the View /// query that returned the Record, or if Record was not returned from a View then the type of /// the object provided will determine the type of the Record field. The object should be one of: /// Int16, Int32, String, Stream, or null. ///

/// The type-specific getters and setters are slightly more efficient than this property, since /// they don't have to do the extra work to infer the value's type every time. ///

/// Win32 MSI APIs: /// MsiRecordGetInteger, /// MsiRecordGetString, /// MsiRecordSetInteger, /// MsiRecordSetString ///

public object this[int field] { get { if (field == 0) { return this.GetString(0); } else { Type valueType = null; if (this.view != null) { this.CheckRange(field); valueType = this.view.Columns[field - 1].Type; } if (valueType == null || valueType == typeof(String)) { return this.GetString(field); } else if (valueType == typeof(Stream)) { return this.IsNull(field) ? null : new RecordStream(this, field); } else { int? value = this.GetNullableInteger(field); return value.HasValue ? (object) value.Value : null; } } } set { if (field == 0) { if (value == null) { value = String.Empty; } this.SetString(0, value.ToString()); } else if (value == null) { this.SetNullableInteger(field, null); } else { Type valueType = value.GetType(); if (valueType == typeof(Int32) || valueType == typeof(Int16)) { this.SetInteger(field, (int) value); } else if (valueType.IsSubclassOf(typeof(Stream))) { this.SetStream(field, (Stream) value); } else { this.SetString(field, value.ToString()); } } } } /// /// Creates a new Record object from an integer record handle. /// ///

/// This method is only provided for interop purposes. A Record object /// should normally be obtained by calling /// other methods. ///

The handle will be closed when this object is disposed or finalized.

///

/// Integer record handle /// true to close the handle when this object is disposed or finalized public static Record FromHandle(IntPtr handle, bool ownsHandle) { return new Record(handle, ownsHandle, (View) null); } /// /// Sets all fields in a record to null. /// ///

/// Win32 MSI API: /// MsiRecordClearData ///

public void Clear() { uint ret = RemotableNativeMethods.MsiRecordClearData((int) this.Handle); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Reports whether a record field is null. /// /// Specifies the field to check. /// True if the field is null, false otherwise. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordIsNull ///

public bool IsNull(int field) { this.CheckRange(field); return RemotableNativeMethods.MsiRecordIsNull((int) this.Handle, (uint) field); } /// /// Reports whether a record field is null. /// /// Specifies the field to check. /// True if the field is null, false otherwise. /// The field name does not match any /// of the named fields in the Record. public bool IsNull(string fieldName) { int field = this.FindColumn(fieldName); return this.IsNull(field); } /// /// Gets the length of a record field. The count does not include the terminating null. /// /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// The returned data size is 0 if the field is null, non-existent, /// or an internal object pointer. The method also returns 0 if the handle is not a valid /// Record handle. ///

/// If the data is in integer format, the property returns 2 or 4. ///

/// If the data is in string format, the property returns the character count /// (not including the NULL terminator). ///

/// If the data is in stream format, the property returns the byte count. ///

/// Win32 MSI API: /// MsiRecordDataSize ///

public int GetDataSize(int field) { this.CheckRange(field); return (int) RemotableNativeMethods.MsiRecordDataSize((int) this.Handle, (uint) field); } /// /// Gets the length of a record field. The count does not include the terminating null. /// /// Specifies the field to check. /// The field name does not match any /// of the named fields in the Record. ///

The returned data size is 0 if the field is null, non-existent, /// or an internal object pointer. The method also returns 0 if the handle is not a valid /// Record handle. ///

/// If the data is in integer format, the property returns 2 or 4. ///

/// If the data is in string format, the property returns the character count /// (not including the NULL terminator). ///

/// If the data is in stream format, the property returns the byte count. ///

public int GetDataSize(string fieldName) { int field = this.FindColumn(fieldName); return this.GetDataSize(field); } /// /// Gets a field value as an integer. /// /// Specifies the field to retrieve. /// Integer value of the field, or 0 if the field is null. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordGetInteger ///

/// [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")] public int GetInteger(int field) { this.CheckRange(field); int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field); if (value == Int32.MinValue) // MSI_NULL_INTEGER { return 0; } return value; } /// /// Gets a field value as an integer. /// /// Specifies the field to retrieve. /// Integer value of the field, or 0 if the field is null. /// The field name does not match any /// of the named fields in the Record. /// [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")] public int GetInteger(string fieldName) { int field = this.FindColumn(fieldName); return this.GetInteger(field); } /// /// Gets a field value as an integer. /// /// Specifies the field to retrieve. /// Integer value of the field, or null if the field is null. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordGetInteger ///

/// public int? GetNullableInteger(int field) { this.CheckRange(field); int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field); if (value == Int32.MinValue) // MSI_NULL_INTEGER { return null; } return value; } /// /// Gets a field value as an integer. /// /// Specifies the field to retrieve. /// Integer value of the field, or null if the field is null. /// The field name does not match any /// of the named fields in the Record. /// public int? GetNullableInteger(string fieldName) { int field = this.FindColumn(fieldName); return this.GetInteger(field); } /// /// Sets the value of a field to an integer. /// /// Specifies the field to set. /// new value of the field /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordSetInteger ///

/// public void SetInteger(int field, int value) { this.CheckRange(field); uint ret = RemotableNativeMethods.MsiRecordSetInteger((int) this.Handle, (uint) field, value); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Sets the value of a field to an integer. /// /// Specifies the field to set. /// new value of the field /// The field name does not match any /// of the named fields in the Record. /// public void SetInteger(string fieldName, int value) { int field = this.FindColumn(fieldName); this.SetInteger(field, value); } /// /// Sets the value of a field to a nullable integer. /// /// Specifies the field to set. /// new value of the field /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordSetInteger ///

/// public void SetNullableInteger(int field, int? value) { this.CheckRange(field); uint ret = RemotableNativeMethods.MsiRecordSetInteger( (int) this.Handle, (uint) field, value.HasValue ? (int) value : Int32.MinValue); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Sets the value of a field to a nullable integer. /// /// Specifies the field to set. /// new value of the field /// The field name does not match any /// of the named fields in the Record. /// public void SetNullableInteger(string fieldName, int? value) { int field = this.FindColumn(fieldName); this.SetNullableInteger(field, value); } /// /// Gets a field value as a string. /// /// Specifies the field to retrieve. /// String value of the field, or an empty string if the field is null. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordGetString ///

public string GetString(int field) { this.CheckRange(field); StringBuilder buf = new StringBuilder(String.Empty); uint bufSize = 0; uint ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize); if (ret == (uint) NativeMethods.Error.MORE_DATA) { buf.Capacity = (int) ++bufSize; ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize); } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return buf.ToString(); } /// /// Gets a field value as a string. /// /// Specifies the field to retrieve. /// String value of the field, or an empty string if the field is null. /// The field name does not match any /// of the named fields in the Record. public string GetString(string fieldName) { int field = this.FindColumn(fieldName); return this.GetString(field); } /// /// Sets the value of a field to a string. /// /// Specifies the field to set. /// new value of the field /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// Win32 MSI API: /// MsiRecordSetString ///

public void SetString(int field, string value) { this.CheckRange(field); if (value == null) { value = String.Empty; } uint ret = RemotableNativeMethods.MsiRecordSetString((int) this.Handle, (uint) field, value); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } // If we set the FormatString manually, then it should be valid again if (field == 0) { this.IsFormatStringInvalid = false; } } /// /// Sets the value of a field to a string. /// /// Specifies the field to set. /// new value of the field /// The field name does not match any /// of the named fields in the Record. public void SetString(string fieldName, string value) { int field = this.FindColumn(fieldName); this.SetString(field, value); } /// /// Reads a record stream field into a file. /// /// Specifies the field of the Record to get. /// Specifies the path to the file to contain the stream. /// The field is less than 0 or greater than the /// number of fields in the Record. /// Attempt to extract a storage from a database open /// in read-write mode, or from a database without an associated file path ///

/// This method is capable of directly extracting substorages. To do so, first select both the /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field. /// However, substorages may only be extracted from a database that is open in read-only mode. ///

/// Win32 MSI API: /// MsiRecordReadStream ///

public void GetStream(int field, string filePath) { if (String.IsNullOrEmpty(filePath)) { throw new ArgumentNullException("filePath"); } IList tables = (this.view != null ? this.view.Tables : null); if (tables != null && tables.Count == 1 && tables[0].Name == "_Storages" && field == this.FindColumn("Data")) { if (!this.view.Database.IsReadOnly) { throw new NotSupportedException("Database must be opened read-only to support substorage extraction."); } else if (this.view.Database.FilePath == null) { throw new NotSupportedException("Database must have an associated file path to support substorage extraction."); } else if (this.FindColumn("Name") <= 0) { throw new NotSupportedException("Name column must be part of the Record in order to extract substorage."); } else { Record.ExtractSubStorage(this.view.Database.FilePath, this.GetString("Name"), filePath); } } else { if (!this.IsNull(field)) { Stream readStream = null, writeStream = null; try { readStream = new RecordStream(this, field); writeStream = new FileStream(filePath, FileMode.Create, FileAccess.Write); int count = 512; byte[] buf = new byte[count]; while (count == buf.Length) { if ((count = readStream.Read(buf, 0, buf.Length)) > 0) { writeStream.Write(buf, 0, count); } } } finally { if (readStream != null) readStream.Close(); if (writeStream != null) writeStream.Close(); } } } } /// /// Reads a record stream field into a file. /// /// Specifies the field of the Record to get. /// Specifies the path to the file to contain the stream. /// The field name does not match any /// of the named fields in the Record. /// Attempt to extract a storage from a database open /// in read-write mode, or from a database without an associated file path ///

/// This method is capable of directly extracting substorages. To do so, first select both the /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field. /// However, substorages may only be extracted from a database that is open in read-only mode. ///

public void GetStream(string fieldName, string filePath) { int field = this.FindColumn(fieldName); this.GetStream(field, filePath); } /// /// Gets a record stream field. /// /// Specifies the field of the Record to get. /// A Stream that reads the field data. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// This method is not capable of reading substorages. To extract a substorage, /// use . ///

/// Win32 MSI API: /// MsiRecordReadStream ///

public Stream GetStream(int field) { this.CheckRange(field); return this.IsNull(field) ? null : new RecordStream(this, field); } /// /// Gets a record stream field. /// /// Specifies the field of the Record to get. /// A Stream that reads the field data. /// The field name does not match any /// of the named fields in the Record. ///

/// This method is not capable of reading substorages. To extract a substorage, /// use . ///

public Stream GetStream(string fieldName) { int field = this.FindColumn(fieldName); return this.GetStream(field); } /// /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields. /// /// Specifies the field of the Record to set. /// Specifies the path to the file containing the stream. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// The contents of the specified file are read into a stream object. The stream persists if /// the Record is inserted into the Database and the Database is committed. ///

/// To reset the stream to its beginning you must pass in null for filePath. /// Do not pass an empty string, "", to reset the stream. ///

/// Setting a stream with this method is more efficient than setting a field to a /// FileStream object. ///

/// Win32 MSI API: /// MsiRecordsetStream ///

public void SetStream(int field, string filePath) { this.CheckRange(field); if (String.IsNullOrEmpty(filePath)) { throw new ArgumentNullException("filePath"); } uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, filePath); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields. /// /// Specifies the field name of the Record to set. /// Specifies the path to the file containing the stream. /// The field name does not match any /// of the named fields in the Record. ///

/// The contents of the specified file are read into a stream object. The stream persists if /// the Record is inserted into the Database and the Database is committed. /// To reset the stream to its beginning you must pass in null for filePath. /// Do not pass an empty string, "", to reset the stream. ///

/// Setting a stream with this method is more efficient than setting a field to a /// FileStream object. ///

public void SetStream(string fieldName, string filePath) { int field = this.FindColumn(fieldName); this.SetStream(field, filePath); } /// /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields. /// /// Specifies the field of the Record to set. /// Specifies the stream data. /// The field is less than 0 or greater than the /// number of fields in the Record. ///

/// The stream persists if the Record is inserted into the Database and the Database is committed. ///

/// Win32 MSI API: /// MsiRecordsetStream ///

public void SetStream(int field, Stream stream) { this.CheckRange(field); if (stream == null) { uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, null); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } else { Stream writeStream = null; string tempPath = Path.GetTempFileName(); try { writeStream = new FileStream(tempPath, FileMode.Truncate, FileAccess.Write); byte[] buf = new byte[512]; int count; while ((count = stream.Read(buf, 0, buf.Length)) > 0) { writeStream.Write(buf, 0, count); } writeStream.Close(); writeStream = null; uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, tempPath); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } finally { if (writeStream != null) writeStream.Close(); if (File.Exists(tempPath)) { try { File.Delete(tempPath); } catch (IOException) { if (this.view != null) { this.view.Database.DeleteOnClose(tempPath); } } } } } } /// /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields. /// /// Specifies the field name of the Record to set. /// Specifies the stream data. /// The field name does not match any /// of the named fields in the Record. ///

/// The stream persists if the Record is inserted into the Database and the Database is committed. ///

public void SetStream(string fieldName, Stream stream) { int field = this.FindColumn(fieldName); this.SetStream(field, stream); } /// /// Gets a formatted string representation of the Record. /// /// A formatted string representation of the Record. ///

/// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record. ///

/// Win32 MSI API: /// MsiFormatRecord ///

/// /// public override string ToString() { return this.ToString((IFormatProvider) null); } /// /// Gets a formatted string representation of the Record, optionally using a Session to format properties. /// /// an optional Session instance that will be used to lookup any /// properties in the Record's format string /// A formatted string representation of the Record. ///

/// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record. ///

/// Win32 MSI API: /// MsiFormatRecord ///

/// /// public string ToString(IFormatProvider provider) { if (this.IsFormatStringInvalid) // Format string is invalid { // TODO: return all values by default? return String.Empty; } InstallerHandle session = provider as InstallerHandle; int sessionHandle = session != null ? (int) session.Handle : 0; StringBuilder buf = new StringBuilder(String.Empty); uint bufSize = 1; uint ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize); if (ret == (uint) NativeMethods.Error.MORE_DATA) { bufSize++; buf = new StringBuilder((int) bufSize); ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize); } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return buf.ToString(); } /// /// Gets a formatted string representation of the Record. /// /// String to be used to format the data in the Record, /// instead of the Record's format string. /// A formatted string representation of the Record. ///

/// Win32 MSI API: /// MsiFormatRecord ///

[Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " + "property separately before calling the ToString() override that takes no parameters.")] public string ToString(string format) { return this.ToString(format, null); } /// /// Gets a formatted string representation of the Record, optionally using a Session to format properties. /// /// String to be used to format the data in the Record, /// instead of the Record's format string. /// an optional Session instance that will be used to lookup any /// properties in the Record's format string /// A formatted string representation of the Record. ///

/// Win32 MSI API: /// MsiFormatRecord ///

/// /// [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " + "property separately before calling the ToString() override that takes just a format provider.")] public string ToString(string format, IFormatProvider provider) { if (format == null) { return this.ToString(provider); } else if (format.Length == 0) { return String.Empty; } else { string savedFormatString = (string) this[0]; try { this.FormatString = format; return this.ToString(provider); } finally { this.FormatString = savedFormatString; } } } [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] private static void ExtractSubStorage(string databaseFile, string storageName, string extractFile) { IStorage storage; NativeMethods.STGM openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_DENY_WRITE; int hr = NativeMethods.StgOpenStorage(databaseFile, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0, out storage); if (hr != 0) { Marshal.ThrowExceptionForHR(hr); } try { openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_EXCLUSIVE; IStorage subStorage = storage.OpenStorage(storageName, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0); try { IStorage newStorage; openMode = NativeMethods.STGM.CREATE | NativeMethods.STGM.READWRITE | NativeMethods.STGM.SHARE_EXCLUSIVE; hr = NativeMethods.StgCreateDocfile(extractFile, (uint) openMode, 0, out newStorage); if (hr != 0) { Marshal.ThrowExceptionForHR(hr); } try { subStorage.CopyTo(0, IntPtr.Zero, IntPtr.Zero, newStorage); newStorage.Commit(0); } finally { Marshal.ReleaseComObject(newStorage); } } finally { Marshal.ReleaseComObject(subStorage); } } finally { Marshal.ReleaseComObject(storage); } } private int FindColumn(string fieldName) { if (this.view == null) { throw new InvalidOperationException(); } ColumnCollection columns = this.view.Columns; for (int i = 0; i < columns.Count; i++) { if (columns[i].Name == fieldName) { return i + 1; } } throw new ArgumentOutOfRangeException("fieldName"); } private void CheckRange(int field) { if (field < 0 || field > this.FieldCount) { throw new ArgumentOutOfRangeException("field"); } } } }