Rust로 작성된 rhwp 파서/모델 모듈을 TypeScript로 포팅하는 스킬. HWP/HWPX 파싱, 바이너리 읽기, 문서 IR 구현 시 사용. "포팅", "마이그레이션", "변환", "TypeScript로 구현" 등의 표현이 나오면 이 스킬을 사용할 것.
Rust HWP 파서 모듈을 TypeScript로 포팅하는 구체적인 방법론.
1. mod.rs (또는 해당 .rs 파일) 전체 읽기
2. public API (pub fn, pub struct) 목록 추출
3. 의존하는 크레이트/모듈 확인
4. 비트 파싱 로직 특별 주의 (HWP는 리틀엔디안 + 비트 필드)
5. 에러 타입 확인
6. 불명확한 필드/비트 의미가 있으면 공식 스펙 문서를 참조한다 (아래 섹션 참조)
스펙 참조 판단 기준: 비트 플래그 의미, 상수값 의미, 레코드 구조가 Rust 코드만으로 파악되지 않을 때. 단, Rust 코드가 충분히 명확하면 스펙을 읽지 않아도 된다. Rust 구현이 항상 최종 기준이다.
Rust → TypeScript 패턴:
// Rust: struct → TS: interface
// interface HwpDocument { ... }
// Rust: enum (데이터 없음) → TS: const enum 또는 number union
const enum RecordTag { ... }
// Rust: enum (데이터 있음) → TS: discriminated union
type Control =
| { type: 'table'; data: TableControl }
| { type: 'image'; data: ImageControl }
// Rust: Option<T> → TS: T | null
// Rust: Vec<T> → TS: T[]
// Rust: HashMap<K,V> → TS: Map<K,V>
// Rust: Result<T, E> → TS: throw HwpError (권장) 또는 { ok: T } | { err: E }
비트 파싱 패턴:
// Rust: (value >> 3) & 0x1F → TS: 동일
// Rust: value.to_le_bytes() → TS: DataView.getUint32(offset, true) ← true = little endian
class ByteReader {
private view: DataView;
private offset: number = 0;
readU8(): number { return this.view.getUint8(this.offset++); }
readU16(): number {
const v = this.view.getUint16(this.offset, true); // little endian
this.offset += 2;
return v;
}
readU32(): number {
const v = this.view.getUint32(this.offset, true);
this.offset += 4;
return v;
}
readI32(): number {
const v = this.view.getInt32(this.offset, true);
this.offset += 4;
return v;
}
}
CFB/OLE 컨테이너 (HWP 바이너리):
cfb를 사용한다FileHeader, DocInfo, BodyText/Section0, BinData/BIN0001 등import * as CFB from 'cfb';
const workbook = CFB.read(buffer, { type: 'buffer' });
const entry = CFB.find(workbook, '/FileHeader');
const data = entry?.content; // Uint8Array
ZIP 컨테이너 (HWPX):
fflate를 사용한다import { unzipSync } from 'fflate';
const files = unzipSync(buffer);
const headerXml = files['header.xml']; // Uint8Array
레코드 구조 (HWP 바이너리 스트림):
[TagID: 10비트][Level: 4비트][Size: 12비트 or 0xFFF][Data]
function readRecord(reader: ByteReader): HwpRecord {
const header = reader.readU32();
const tagId = header & 0x3FF; // 하위 10비트
const level = (header >> 10) & 0xF; // 다음 4비트
let size = (header >> 14) & 0xFFF; // 상위 12비트
if (size === 0xFFF) {
size = reader.readU32(); // 확장 크기
}
const data = reader.readBytes(size);
return { tagId, level, size, data };
}
deflate 압축 해제:
import { inflateRawSync } from 'fflate';
const decompressed = inflateRawSync(compressedData);
구현 순서를 지킨다 (의존성 있음):
1. tags.ts ← 태그 상수 (의존성 없음)
2. byte-reader.ts ← 바이너리 읽기 유틸
3. record.ts ← 레코드 파싱 (byte-reader 의존)
4. model/*.ts ← IR 타입 정의 (의존성 없음, parser와 병렬 가능)
5. cfb-reader.ts ← CFB 컨테이너 (cfb 패키지 의존)
6. header.ts ← FileHeader 파싱 (byte-reader, model 의존)
7. doc-info.ts ← DocInfo 파싱 (record, model 의존)
8. control.ts ← 컨트롤 파싱 (record, model 의존)
9. body-text.ts ← BodyText 파싱 (record, control, model 의존)
10. bin-data.ts ← 바이너리 데이터
11. hwpx/*.ts ← HWPX XML 파싱 (fflate 의존)
12. mod.ts (index) ← 통합 진입점
# Rust 참조 덤프
cd /home/eitetu/git/hwp-workspace/rhwp
cargo build
./target/debug/rhwp dump samples/sample.hwp -s 0 -p 0
# TS 구현 실행
cd /home/eitetu/git/hwp-workspace/hwp-devcode-kr
npx tsx src/test-parse.ts samples/sample.hwp
ViewText 스트림 사용 — 초기 구현에서 제외 가능cfb 패키지는 import * as CFB from 'cfb' 방식으로 importreferences/parser-module-map.mdreferences/rust-to-ts-patterns.mdreferences/hwp-spec-guide.md — 스펙 파일별 담당 영역, 읽는 시점, PDF 참조 방법