rubrapack 매뉴얼←↑→

32 복합 파일 속의 MSI 데이터베이스

Microsoft 는 Windows Installer 표의 뜻은 문서로 내지만(Microsoft Learn, "Database Tables"), 데이터베이스가 복합 파일 안에 어떻게 놓이는지는 내지 않는다. 이 문서에서 배치에 관한 모든 것은 msi.dll 로 데이터베이스를 만들고, 스트림을 바이트 단위로 다시 읽고, 이 규칙으로 쓴 데이터베이스가 열리고 내보내지고 설치되는지 확인해서 알아냈다. 따로 표시하지 않으면 [observed] 다.

32.1 스트림#

표 하나가 루트 저장소 바로 아래의 스트림 하나다. 보통 패키지에는 하위 저장소가 없다.

스트림 이름#

T 라는 표는 U+4840 뒤에 T 를 UTF-16 단위 하나에 두 글자씩 압축해 붙인 이름으로 저장한다:

  1. 다음 64글자 알파벳의 각 글자를 0..63 으로 바꾼다: 0-9 -> 0..9, A-Z -> 10..35, a-z -> 36..61, . -> 62, _ -> 63.
  2. 알파벳 글자 둘이 이어지면 단위 하나 0x3800 + first + (second << 6) 가 된다.
  3. 알파벳 글자 하나(마지막 글자, 또는 뒤에 알파벳 밖의 글자가 오는 글자)는 0x4800 + value 가 된다.
  4. 그 밖의 글자는 그대로 쓴다.

압축한 이름(표는 U+4840 접두 포함)은 CFB 이름 한도 31단위 안에 들어야 하므로, 표 이름은 알파벳 글자 60개까지 쓸 수 있다. 풀 때는 이 단계를 거꾸로 밟는다.

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

다른 스트림:

스트림이름
Binary/Icon 칸(OBJECT 열)<Table>.<key> 를 같은 방식으로 압축하되 U+4840 접두 없이, 예 Binary.Logo(복합 키는 . 로 잇는다)
안에 넣은 캐비닛Media.Cabinet 의 # 뒤 이름(예 cab1.cab), 압축, 접두 없음
요약 정보\x05SummaryInformation - 압축하지 않는다(요약 정보 참고)

32.2 문자열 풀: _StringPool 과 _StringData#

모든 표의 모든 문자열은 공유 풀에 한 번씩만 살고, 표의 칸은 문자열 id 를 담는다.

_StringPool:

u32  header       아래 31비트: 데이터베이스 코드 페이지; 비트 31: 문자열 참조가 3바이트
그다음 문자열 id 1, 2, 3, ... 마다(id 0 은 "문자열 없음"이고 저장하지 않는다):
u16  length       _StringData 속 바이트 길이
u16  refs         비트 0-14: 참조 수; 비트 15: 문자열에 0x80 이상인 바이트가 있을 때만 켜짐

코드 페이지#

데이터베이스 코드 페이지는 풀 머리의 아래 비트다: 한 번도 정하지 않았으면 0, 아니면 _ForceCodepage 가져오기로 정한 값. 65001(UTF-8)은 요즘 Windows 에서 모든 표에 통한다: 한국어, 일본어, 보조 평면 (예 U+20000, U+1F600)의 파일·폴더 이름, 레지스트리 키와 값, 바로가기 이름, 제품 이름이 모두 정확한 UTF-16 글자로 설치되고 대화창에도 보인다 - ANSI 코드 페이지가 한국어를 담지 못하는 시스템 로캘(en-US, 1252)에서도 그렇다. 그래서 따로 유니코드 설치 프로그램이 필요 없다. UTF-16 코드 페이지(1200)는 여기서 쓸 수 없다: 풀은 한 바이트 단위의 문자열을 담는다. [observed] Windows Installer 는 이름을 정규화하지 않는다: 분해형 한글(U+1100 U+1161)로 이름 붙인 폴더는 바로 그 코드 단위로 설치되고, U+AC00 으로 이름 붙인 폴더를 대신하지 않고 그 옆에 생긴다. [observed]

32.3 시스템 표#

표열키
_TablesName(문자열)Name
_ColumnsTable(문자열), Number(i2, 1부터), Name(문자열), Type(i2)Table, Number

_Tables 는 자기 자신과 _Columns 를 싣지 않는다. 행이 없는 표는 스트림이 아예 없고 _Tables· _Columns 행만 있다. _Validation 은 있다면 보통 표다.

열 형식 비트(_Columns.Type)#

비트뜻
0x00FF너비: 문자열 최대 길이(0 = 제한 없음), 또는 정수의 2 / 4
0x0100유효
0x0200지역화 가능
0x0400비이진(글자면 켜짐, OBJECT 면 꺼짐)
0x0800문자열(글자와 OBJECT 면 켜짐)
0x1000null 허용
0x2000기본 키의 일부

흔한 SQL 열 형식에 msi.dll 이 쓰는 값:

SQLType
CHAR(72) NOT NULL(키)0x2D48
CHAR(72)(null 허용)0x1D48
CHAR(255) NOT NULL LOCALIZABLE0x0FFF
LONGCHAR NOT NULL LOCALIZABLE0x0F00
SHORT NOT NULL0x0502
SHORT(null 허용)0x1502
SHORT NOT NULL(키)0x2502
LONG NOT NULL0x0104
OBJECT NOT NULL0x0900

정수 열에는 비이진 비트가 없다.

32.4 표 스트림#

문자열 id 를 문자열의 바이트 순서로 매기는 작성기는 행을 문자열 값 순으로 저절로 정렬하게 되고, 이것은 IDT 내보내기가 쓰는 순서이기도 하다.

32.5 변환(.mst)#

변환은 같은 종류의 복합 파일로, 클래스가 {000C1082-0000-0000-C000-000000000046} 이고(데이터베이스는 {000C1084-...}) 차이만 담는다: 자기 문자열 풀, 바뀐 표마다 스트림 하나, 더하거나 바꾼 이진 칸마다 스트림 하나, 요약 정보.

32.6 패치(.msp)#

패치는 클래스가 {000C1086-0000-0000-C000-000000000046} 인 복합 파일이다. 뿌리는 작은 데이터베이스

자리) - 이고, 나르는 파일의 캐비닛이 스트림으로, 변환들이 하위 저장소(파일 안의 저장소, 저마다 변환의 스트림을 담는다)로 들어 있다. 요약 정보: 5 는 원본 목록, 7 은 적용 대상 제품 코드, 8 은 적용 순서대로 적은 변환 저장소(:RP1;:#RP1), 9 는 패치 코드, 15 는 필요한 Windows Installer 최소 판(4 = 3.0).

32.7 IDT 보관 파일(MsiDatabaseExport 가 쓰는 것)#

기준 출력으로 쓸모 있다: 바이트까지 같은 IDT 파일을 내보내는 판독기는 Windows 와 같은 방식으로 데이터베이스를 푸는 것이다.

1줄: TAB 으로 나눈 열 이름
2줄: 열 형식: s/S(문자열), l/L(지역화 문자열), i/I(정수), v/V(이진),
     대문자 = null 허용, 뒤에 너비(0 = 제한 없음; 정수는 2 또는 4)
3줄: [<코드 페이지> TAB] <표 이름> TAB <키 열> [TAB <키 열> ...]
행 : TAB 으로 나눈 칸, 줄 끝은 CR LF

32.8 안전하게 읽기#

풀 길이의 합이 _StringData 크기와 정확히 같은지, 표의 모든 문자열 참조가 쓰이는 id 를 가리키는지, 표 스트림 크기가 행 너비의 배수인지, _Columns 가 표마다 열을 1..n 으로 빈틈없이 번호 매기는지 확인한다.

32.9 실제 예: 튜토리얼의 hello.msi#

튜토리얼 첫 패키지(복합 파일(Compound File Binary, CFB))의 표 File 은 이름이 U+4840 U+430F U+422F 인 스트림이다:

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

_StringPool 은 e9 fd 00 00 로 시작한다: 코드 페이지 65001(UTF-8), 비트 31 은 꺼짐(2 바이트 문자열 참조). 그 뒤는 문자열 번호마다 u16 둘이다. 처음 번호들:

번호바이트길이참조문자열
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개이고, 그 바이트를 이어 붙인 것이 _StringData 의 3706 바이트다.

_Columns 가 표를 설명한다 - File 의 행들, 형식은 위의 비트 표로 풀었다:

#열Type뜻
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

File 스트림 자체는 20 바이트다: 행 하나를 열 순서로 저장했다:

3b 00 0f 00 a6 00 00 46 00 80 05 00 06 00 00 82 01 00 00 80
열저장값
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

문자열 칸은 문자열 번호이고, 정수는 맨 위 비트를 뒤집어 저장한다. 그래서 17920(0x00004600)은 0x80004600 으로 저장되고, 0 은 null 을 뜻하도록 비워 둔다.