Hwp 파일의 레코드 구조 - hallazzang/hwp5-table-extractor GitHub Wiki

Hwp 파일의 몇몇 스트림(DocInfoBodyText/SectionN, DocHistory/VersionLogN)은 연속된 레코드 구조를 가진다.

레코드 하나의 구조는 다음과 같다:

        ┌────────┬─────────┐
Record: │ Header │ Payload │
        └────┬───┴─────────┘
        ┌────┴───┬───────┬──────┐
Header: │ Tag ID │ Level │ Size │
        └────────┴───────┴──────┘

즉 레코드 하나는 헤더와 데이터(앞으로 Payload라고 부르겠다)로 이루어져 있고, 여기서 헤더는 다시 태그 아이디, 레벨, 그리고 크기로 이루어져 있다.

또한, 레코드의 헤더는 다음과 같이 볼 수 있다:

비트 범위 설명
0~9 태그 아이디
10~19 레벨
20~31 크기

여기서 만약 크기가 0xfff(4095)라면 Payload의 크기가 4095 바이트 이상인 경우로, 이후에 나타나는 4바이트를 크기로 인식한다. 즉 Payload의 크기가 4095 바이트 이상인 경우에 레코드는 다음과 같은 구조를 가진다.

        ┌────────┬──────┬─────────┐
Record: │ Header │ Size │ Payload │
        └────┬───┴──────┴─────────┘
        ┌────┴───┬───────┬───────┐
Header: │ Tag ID │ Level │ 0xfff │
        └────────┴───────┴───────┘

예제 코드

실제로 Hwp 파일이 위와 같은 구조를 가지는지 확인해보자.

import struct
import zlib
from io import BytesIO

import olefile

ole = olefile.OleFileIO('test.hwp')
stream = ole.openstream('DocInfo')
stream = BytesIO(zlib.decompress(stream.read(), -15))

while True:
    header = stream.read(4)
    if not header: # 파일이 끝남(EOF)
        break

    header = struct.unpack('<I', header)[0]

    tag_id = header & 0x3ff
    level = (header >> 10) & 0x3ff
    size = (header >> 20) & 0xfff
    if size == 0xfff:
        size = struct.unpack('<I', stream.read(4))[0]

    payload = stream.read(size)

    print('Tag ID: %d, Level: %d, Size: %d(bytes)' % (
        tag_id, level, size
    ))

결과는 아마 다음과 같을 것이다:

Tag ID: 16, Level: 0, Size: 26(bytes)
Tag ID: 17, Level: 0, Size: 64(bytes)
Tag ID: 19, Level: 1, Size: 29(bytes)
Tag ID: 19, Level: 1, Size: 37(bytes)
Tag ID: 19, Level: 1, Size: 29(bytes)
Tag ID: 19, Level: 1, Size: 51(bytes)
Tag ID: 19, Level: 1, Size: 31(bytes)
...

태그 아이디와 레벨에 관해서는 다른 페이지에서 설명한다. (작성 중)