rubrapack Manual←↑→

32 The MSI database inside the compound file

Microsoft documents the meaning of the Windows Installer tables (Microsoft Learn, "Database Tables"), but not how a database is laid out in the compound file. Everything on this page about the layout was established by creating databases through msi.dll, reading the streams back byte for byte, and checking that databases written from these rules open, export and install. [observed] unless marked otherwise.

32.1 Streams#

Every table is one stream directly under the root storage. There are no sub-storages in an ordinary package.

Stream names#

A table named T is stored under the name U+4840 followed by T packed two characters per UTF-16 unit:

  1. Map each character of this 64-character alphabet to 0..63: 0-9 -> 0..9, A-Z -> 10..35, a-z -> 36..61, . -> 62, _ -> 63.
  2. Two alphabet characters in a row become one unit 0x3800 + first + (second << 6).
  3. A single alphabet character (the last one, or one followed by a character outside the alphabet) becomes 0x4800 + value.
  4. Any other character is written as itself.

The packed name (with the U+4840 prefix for tables) must fit the 31-unit CFB name limit, which allows table names of up to 60 alphabet characters. Unpacking reverses the steps.

Example: _Columns -> U+4840 U+3B3F U+43F2 U+4438 U+45B1.

Other streams:

StreamName
a Binary/Icon cell (OBJECT column)<Table>.<key> packed the same way without the U+4840 prefix, e.g. Binary.Logo (compound keys joined with .)
an embedded cabinetthe name after # in Media.Cabinet (e.g. cab1.cab), packed, no prefix
summary information\x05SummaryInformation - not packed (see Summary information)

32.2 The string pool: _StringPool and _StringData#

All strings of all tables live once in a shared pool; table cells hold string ids.

_StringPool:

u32  header       low 31 bits: the database code page; bit 31: string references are 3 bytes
then per string id 1, 2, 3, ... (id 0 is "no string" and is not stored):
u16  length       byte length in _StringData
u16  refs         bits 0-14: reference count; bit 15: set exactly when the string has a byte >= 0x80

Code page#

The database code page is the pool header's low bits: 0 when never set, otherwise what was set through the _ForceCodepage import. 65001 (UTF-8) works for every table on current Windows: Korean, Japanese and supplementary-plane (e.g. U+20000, U+1F600) file and folder names, registry keys and values, shortcut names and the product name all install with exactly the right UTF-16 text, and dialogs display them - also under a system locale whose ANSI code page cannot hold Korean (en-US, 1252), so no separate Unicode setup program is needed. A UTF-16 code page (1200) is not possible here: the pool holds single-byte-unit strings. [observed] Windows Installer does not normalize names: a folder named with decomposed Hangul (U+1100 U+1161) is installed with exactly those code units, next to - not instead of - one named U+AC00. [observed]

32.3 System tables#

TableColumnsKey
_TablesName (string)Name
_ColumnsTable (string), Number (i2, 1-based), Name (string), Type (i2)Table, Number

_Tables does not list itself or _Columns. A table with no rows has no stream at all, only its _Tables and _Columns rows. _Validation, when present, is an ordinary table.

Column type bits (_Columns.Type)#

BitsMeaning
0x00FFwidth: maximum string length (0 = unlimited), or 2 / 4 for integers
0x0100valid
0x0200localizable
0x0400non-binary (set for text, clear for OBJECT)
0x0800string (set for text and OBJECT)
0x1000nullable
0x2000part of the primary key

Values msi.dll uses for common SQL column types:

SQLType
CHAR(72) NOT NULL (key)0x2D48
CHAR(72) (nullable)0x1D48
CHAR(255) NOT NULL LOCALIZABLE0x0FFF
LONGCHAR NOT NULL LOCALIZABLE0x0F00
SHORT NOT NULL0x0502
SHORT (nullable)0x1502
SHORT NOT NULL (key)0x2502
LONG NOT NULL0x0104
OBJECT NOT NULL0x0900

Integer columns do not have the non-binary bit.

32.4 Table streams#

A writer that assigns string ids in byte order of the strings gets rows sorted by string value for free, which is also the order an IDT export uses.

32.5 Transforms (.mst)#

A transform is a compound file of the same kind, of class {000C1082-0000-0000-C000-000000000046} (a database is {000C1084-...}), that holds only differences: a string pool of its own, a stream per changed table, a stream per binary cell it adds or changes, and summary information.

32.6 Patches (.msp)#

A patch is a compound file of class {000C1086-0000-0000-C000-000000000046}. Its root is a small database - MsiPatchMetadata (display name, classification, whether it can be removed) and MsiPatchSequence (its family and place in it) - with the cabinet of the files it carries as a stream, and transforms as substorages (storages inside the file, each holding a transform's streams). Summary information: 5 the source list, 7 the product codes it applies to, 8 the transform storages in the order they apply (:RP1;:#RP1), 9 the patch code, 15 the minimum Windows Installer (4 = version 3.0).

32.7 IDT archive files (what MsiDatabaseExport writes)#

Useful as a reference output: a reader that exports byte-identical IDT files decodes the database the way Windows does.

line 1: column names separated by TAB
line 2: column types: s/S (string), l/L (localizable string), i/I (integer), v/V (binary),
        upper case = nullable, followed by the width (0 = unlimited; 2 or 4 for integers)
line 3: [<code page> TAB] <table name> TAB <key column> [TAB <key column> ...]
rows  : cells separated by TAB, lines end with CR LF

32.8 Reading safely#

Check that the pool lengths add up to exactly the size of _StringData, that every string reference in a table points to a used id, that table stream sizes are a multiple of the row width, and that _Columns numbers each table's columns 1..n without gaps.

32.9 Worked example: the tutorial's hello.msi#

The table File of the tutorial's first package (see Compound File Binary (CFB)) is the stream named U+4840 U+430F U+422F:

0x4840  (table)
F i: 0x3800 + 15 + (44 << 6) = 0x430F
l e: 0x3800 + 47 + (40 << 6) = 0x422F

_StringPool starts with e9 fd 00 00: the code page 65001 (UTF-8), bit 31 clear (2-byte string references). Then two u16 per string id; the first ids:

IdBytesLengthRefsString
109 00 01 0091#cab1.cab
201 00 01 0011.
301 00 01 00111
405 00 03 00531.0.0
507 00 01 00711.2.3.4
604 00 02 00421033

170 strings in all; their bytes, back to back, are the 3706 bytes of _StringData.

_Columns describes the table - its rows for File, the type decoded with the bit table above:

#ColumnTypeMeaning
1File0x2D48key, string, non-binary, valid, width 72
2Component_0x0D48string, non-binary, valid, width 72
3FileName0x0FFFstring, non-binary, localizable, valid, width 255
4FileSize0x0104valid, width 4
5Version0x1D48nullable, string, non-binary, valid, width 72
6Language0x1D14nullable, string, non-binary, valid, width 20
7Attributes0x1502nullable, non-binary, valid, width 2
8Sequence0x0104valid, width 4

The File stream itself is 20 bytes: one row, stored column by column:

3b 00 0f 00 a6 00 00 46 00 80 05 00 06 00 00 82 01 00 00 80
ColumnStored
File3b 00 -> id 59 Hello
Component_0f 00 -> id 15 C_185f8db32271fe25f561
FileNamea6 00 -> id 166 hello.exe
FileSize00 46 00 80 -> 0x80004600 ^ 0x80000000 = 17920
Version05 00 -> id 5 1.2.3.4
Language06 00 -> id 6 1033
Attributes00 82 -> 0x8200 ^ 0x8000 = 512
Sequence01 00 00 80 -> 0x80000001 ^ 0x80000000 = 1

String cells are string ids; integers are stored with the top bit flipped, so 17920 (0x00004600) is stored 0x80004600 and 0 stays free to mean null.