rubrapack 매뉴얼←↑→

36 캐비닛, MSZIP, deflate

36.1 캐비닛 배치 [spec: MS-CAB]#

CFHEADER (예약 필드가 없으면 36바이트):
  "MSCF", u32 0, u32 캐비닛 크기, u32 0, u32 첫 CFFILE 오프셋, u32 0,
  u8 부판 3, u8 주판 1, u16 폴더 수, u16 파일 수, u16 플래그(0),
  u16 세트 id, u16 캐비닛 번호(0)
CFFOLDER (각 8바이트):
  u32 폴더의 첫 CFDATA 오프셋, u16 CFDATA 블록 수, u16 압축
  (0 = 없음, 1 = MSZIP)
CFFILE (파일마다):
  u32 압축 전 크기, u32 폴더의 압축 전 데이터 속 오프셋, u16 폴더 번호,
  u16 날짜, u16 시각, u16 속성, NUL 로 끝나는 이름
CFDATA (블록마다):
  u32 체크섬, u16 압축 바이트 수, u16 압축 전 바이트 수(최대 32768), 데이터

블록 체크섬 [spec]#

checksum(bytes, seed):
  바이트 속 온전한 리틀엔디언 u32 를 모두 seed 에 xor 한다;
  남은 1~3바이트는 u32 하나를 이루되 남은 바이트 가운데 첫째가 쓰인 자리 가운데 가장 높은 곳에 온다
  (3개 남음: b0<<16 | b1<<8 | b2; 2개: b0<<8 | b1; 1개: b0). 그것도 xor 한다.
블록 체크섬 = checksum(저장된 대로의 4바이트 "압축 크기, 압축 전 크기",
                       checksum(블록 데이터, 0))

체크섬 0 은 "계산하지 않음"을 뜻하고 판독기는 그것을 받는다.

36.2 MSZIP [spec: MS-MCI]#

압축 형식이 1 인 폴더의 CFDATA 블록마다 다음이 든다:

"CK" (43 4B), 그다음 이 블록의 압축 전 바이트(최대 32768)에 대한 온전한 원시 deflate 스트림
(RFC 1951) 하나, 마지막 블록으로 끝남.

압축 해제기는 같은 폴더의 앞 블록들을 사전으로 간직하므로, 인코더는 앞 블록을 되돌아 가리켜도 된다. 그러지 않는 인코더(블록마다 따로 압축)도 올바른 MSZIP 을 만든다. rubrapack 은 블록마다 앞 블록의 압축 전 32 KiB 를 미리 준 사전으로 쓴다(섞인 자료에서 블록을 따로 압축할 때보다 약 9% 작다). 그 사전은 출력이 아니라 입력이므로 블록을 여러 스레드에서 동시에 압축할 수 있고, 스레드 수와 상관없이 바이트가 같다. 폴더는 65535블록이 되기 전에 파일 경계에서 새로 시작하고, 사전은 폴더를 넘지 않는다. Windows 는 이것을 모두 풀어낸다: 폴더 열두 개짜리 패키지가 설치·복구·제거되고 모든 파일이 바이트까지 같다. [observed]

36.3 LZX [spec: MS-PATCH 2, MS-CAB]#

압축 형식이 3 | w << 8(w = 15 .. 21)인 폴더는 창이 2^w 바이트인 LZX 스트림 하나이고, 출력 32768바이트씩 프레임으로 잘린다(마지막은 더 짧다). 프레임 하나가 CFDATA 블록 하나다. 창, 되풀이 오프셋 셋, 부호 길이는 프레임을 넘어 이어지고, 프레임마다 끝에서 비트 스트림을 16비트 경계로 채운다.

rubrapack 은 프레임마다 verbatim 블록 하나(더 작으면 무압축 블록)를 쓰고, 일치는 프레임을 넘지 않으며, 새 일치와 비슷하게 길면 되풀이 오프셋을 쓰고, 한 걸음 미뤄 보는(lazy) 일치를 쓰며, E8 변환은 쓰지 않는다. 읽기는 모든 블록 형식과 E8 변환을 다룬다. Windows 의 expand.exe 와 msiexec 가 그 캐비닛을 풀고, rubrapack 은 makecab.exe 가 E8 변환을 켜고 만든 캐비닛을 푼다. [observed]

36.4 deflate 인코더 간단히 [spec: RFC 1951]#

  1. 일치 찾기(LZ77): 다음 3바이트를 해시하고, 32 KiB 안에서 같은 해시를 가진 앞 자리들의 체인을 따라가 가장 긴 일치(3..258바이트)를 고른다. 체인을 길게 따라갈수록 압축이 좋아진다. 다음 자리도 해 보고 그 일치가 더 길면 리터럴을 내보내는 "게으른" 단계가 눈에 띄게 도움이 된다.
  2. 블록: 기호를 수만 개까지 묶는다. 블록마다 세 가지 인코딩의 크기를 계산해 가장 작은 것을 쓴다:
    • 고정 허프만 부호(리터럴/길이 0-143: 8비트, 144-255: 9, 256-279: 7, 280-287: 8; 거리 5비트);
    • 동적 허프만 부호: 부호 길이는 15비트로 제한(부호 길이 알파벳은 7). package-merge 가 제한된 최적 길이를 준다. HLIT, HDIST, HCLEN, 16 17 18 0 8 7 9 6 10 5 11 4 12 3 13 2 14 1 15 순서의 부호 길이 부호, 그다음 런렝스로 부호화한 길이들(16 = 앞 것 3-6번 반복, 17 = 0 을 3-10개, 18 = 0 을 11-138개)을 보낸다;
    • 저장(바이트 정렬한 LEN, NLEN, 원 바이트; 저장 블록 하나에 최대 65535).
  3. 허프만 부호는 최상위 비트부터, 그 밖의 것은 최하위 비트부터 쓴다.
  4. 늘 기호 둘 이상에 부호 길이를 준다(외톨이 기호는 가짜 기호와 짝지운다). 그래야 모든 부호가 완전하다 - 불완전한 부호를 거부하는 디코더가 있다.

인코더는 독립된 구현(예: zlib 라이브러리의 원시 deflate, window bits -15)으로 출력을 풀어 보고, 그것으로 지은 MSI 는 설치해서 설치된 파일의 해시를 비교해 확인한다(출력을 Windows 와 대조하기).

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

튜토리얼 첫 패키지의 캐비닛은 스트림 cab1.cab, 6776 바이트다. CFHEADER:

오프셋바이트필드값
0x004d 53 43 46서명MSCF
0x0878 1a 00 00캐비닛 크기6776
0x102c 00 00 00첫 CFFILE44
0x1803 01판1.3
0x1A01 00폴더 수1
0x1C01 00파일 수1
0x1E00 00플래그0
0x2000 00묶음 번호0

0x24 의 CFFOLDER: 42 00 00 00 01 00 01 00 - 첫 CFDATA 는 66, 블록 1개, 압축 1(MSZIP).

CFFILE 항목 - 이름은 File 표의 키다:

오프셋크기폴더 안 오프셋폴더날짜속성이름
0x2C17920001980-01-010x20Hello

66 의 첫 CFDATA: 체크섬 80 35 aa c0, 압축 6702 바이트가 풀면 17920 바이트. 데이터는 43 4b ed 5c 0d 74 로 시작한다: CK, 그리고 deflate. 첫 deflate 바이트 ed = 11101101 의 가장 낮은 비트가 BFINAL = 1, 다음 두 비트가 BTYPE = 2(동적 부호)다.