Skip to content

fix: HWP5 저장 시 BMP 밖 문자(char) as u16 캐스팅으로 인한 데이터 손상 수정 - #3767

Closed
kevin9327 wants to merge 3 commits into
edwardkim:develfrom
kevin9327:pr/task-hwp5-integer-bugs
Closed

kevin9327 wants to merge 3 commits into
edwardkim:develfrom
kevin9327:pr/task-hwp5-integer-bugs

Conversation

@kevin9327

@kevin9327 kevin9327 commented Aug 1, 2026 •

Copy link
Copy Markdown
Contributor

문제

HWP5 직렬화 코드에서 char 타입 필드를 HWP5의 16비트 WCHAR 슬롯에 쓸 때
as u16으로 직접 캐스팅하는 패턴이 4개 함수, 10개 필드에 걸쳐 반복돼 있었다.

char는 U+0000~U+10FFFF 전체 유니코드 스칼라 값을 담을 수 있지만, HWPX
파서는 이 필드들을 s.chars().next()로 BMP 제한 없이 채운다(글머리표
기호, 각주/미주 접두/접미 문자, 자동번호 사용자 기호 등은 실제로 이모지 같은
비BMP 문자를 담을 수 있다). 이런 문자를 그대로 as u16 캐스팅하면 상위
비트가 잘려 완전히 다른 문자로 조용히 손상된다. 예: '🔶'(U+1F536) as u16
== 0xF536(전혀 다른 글자) — 에러도, 패닉도 없이 잘못된 값이 저장된다.

같은 클래스의 결함이 byte_writer.rs의 write_hwp_string(문자열 길이
u16::MAX 초과 시 wraparound)에서는 이미 한 번 고쳐졌지만
(test_write_hwp_string_overlong_truncates_instead_of_wrapping), char
타입 필드들에는 같은 수정이 적용되지 않고 남아 있었다.

수정 대상 (4곳, 10개 필드)

  1. src/serializer/doc_info.rs — serialize_bullet: bullet_char,
    check_bullet_char
  2. src/serializer/control.rs — serialize_footnote_shape: user_char,
    prefix_char, suffix_char
  3. src/serializer/control.rs — serialize_auto_number(추정 함수명):
    user_symbol, prefix_char, suffix_char
  4. src/serializer/control.rs — serialize_page_num_pos: user_symbol,
    prefix_char, suffix_char, dash_char

수정 방식

src/serializer/byte_writer.rs에 공용 헬퍼 char_to_wchar()를 추가:
BMP(U+0000~U+FFFF) 안이면 그대로 as u16, 밖이면 대체 문자
U+FFFD(REPLACEMENT CHARACTER)로 치환한다. 4개 함수의 10개 캐스팅 지점을
모두 이 헬퍼 호출로 교체했다.

검증

  • cargo test --lib serializer:: — 488 passed, 0 failed
    (신규 테스트 test_char_to_wchar_bmp_passthrough,
    test_char_to_wchar_astral_replaces_instead_of_truncating 포함)
  • cargo test --lib serializer::byte_writer::tests — 18 passed, 0 failed
  • 디스크 여유 공간 제약(작업 중 ~2GB까지 하락)으로 이번 세션에서는 narrow
    cargo test --lib 필터만 실행했고 풀 cargo build/cargo test --tests는
    실행하지 않았다. 컴파일은 cargo test --lib 과정에서 전체 크레이트를
    포함해 성공적으로 완료됐다.

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com


📌 추가 커밋 안내 — #3763 플레이키 수정을 함께 실었습니다

이 PR 의 변경과 무관하게 CI 가 계속 빨갰습니다. devel(cc38291) 자체가 #3742 머지 이후
tests/batch_axes_contract.rs 의 stdin 헬퍼 때문에 깨져 있고, 그 커밋을 기반으로 하는 모든 PR 이
같은 지점에서 막힙니다.

thread 'batch_global_auth_options_are_rejected_before_consuming_path_stdin' panicked at tests/batch_axes_contract.rs:34:10:
stdin 쓰기 실패: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

재실행으로는 빠져나가지 못했습니다 — 이 PR 도 재실행했지만 같은 지점에서 다시 실패했습니다.
그래서 이 브랜치의 CI 를 읽을 수 있게 하려고 #3766 의 테스트 하네스 수정
(tests/ 3개 파일, 제품 코드 무변경)을 그대로 실었습니다.

원인 분석은 #3763 에 있습니다. #3766 이 먼저 머지되면 이 커밋은 빈 diff 가 되어 리베이스 때
자연히 떨어져 나갑니다.
리뷰 시 마지막 커밋
test(cli): stdin 계약 테스트의 BrokenPipe 플레이키를 함께 싣는다 (#3763) 는 이 PR 의 본 주제가
아니니 분리해서 보시면 됩니다.

kevin9327 and others added 2 commits August 2, 2026 07:50
글머리표(bullet_char/check_bullet_char), 각주/미주 모양(user_char/prefix_char/
suffix_char), 자동번호(user_symbol/prefix_char/suffix_char), 쪽번호 위치
(user_symbol/prefix_char/suffix_char/dash_char) 필드는 모델에서 `char`
(전체 유니코드 스칼라)로 정의되어 있고 HWPX 파서는 `s.chars().next()`로
BMP 제한 없이 값을 채운다. 하지만 HWP5 직렬화 시 `as u16`으로 그대로
캐스팅해 U+FFFF를 넘는 문자(예: 이모지 🔶 U+1F536)의 상위 비트가 잘려
0xF536 같은 완전히 다른 문자로 조용히 손상된 채 저장됐다.

byte_writer.rs에 char_to_wchar() 헬퍼를 추가해 BMP 밖 문자를 U+FFFD로
치환하도록 doc_info.rs·control.rs의 4개 함수(bullet, footnote_shape,
auto_number, page_num_pos) 10곳을 통일했다. byte_writer.rs의
`write_hwp_string` 오버플로 수정(u16::MAX 초과 문자열 길이 wraparound)과
같은 종류의 결함이었으나 char 타입 필드들에는 적용되지 않고 남아있었다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cargo fmt --all -- --check 가 src/serializer/doc_info.rs:621 에서 실패했다.
w.write_u16(char_to_wchar(..)).unwrap() 체인을 rustfmt 가 요구하는 형태로
줄바꿈한다.

동작 변화 없음. 포맷만 수정.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
이 PR 의 변경과 무관하게 CI 가 계속 빨갛다. devel(cc38291) 자체가 edwardkim#3742 머지
이후 tests/batch_axes_contract.rs 의 stdin 헬퍼 때문에 깨져 있고, 그 커밋을
기반으로 하는 모든 PR 이 같은 지점에서 막힌다.

    thread 'batch_global_auth_options_are_rejected_before_consuming_path_stdin'
    panicked at tests/batch_axes_contract.rs:34:10:
    stdin 쓰기 실패: Os { code: 32, kind: BrokenPipe, message: "Broken pipe" }

재실행으로는 빠져나가지 못한다 — 이 PR 도 재실행했지만 같은 지점에서 다시
실패했다. 이 브랜치의 CI 를 읽을 수 있게 하려고 edwardkim#3766 의 테스트 하네스 수정을
그대로 싣는다.

근인: 이 헬퍼를 쓰는 테스트들의 계약이 "자식이 stdin 을 읽기 전에 거부하고
종료한다" 이다. 자식이 인자 검증에서 즉시 죽으면 파이프의 읽기 끝이 닫히고
부모의 write_all 이 EPIPE 를 받는다. 기능이 의도대로 일찍 거부할수록 더 잘
깨진다. EPIPE 는 오류가 아니라 검증 대상 동작의 정상적인 부산물이므로
ErrorKind::BrokenPipe 만 넘어가고 그 밖의 오류는 그대로 패닉한다.

제품 코드 변경 없음. 테스트 하네스만 고친다.

edwardkim#3766 이 먼저 머지되면 이 커밋은 빈 diff 가 되므로 리베이스 때 떨어져 나간다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@edwardkim edwardkim added this to the v1.0.0 milestone Aug 2, 2026
@edwardkim edwardkim added bug Something isn't working serialization 문서 저장/직렬화: HWPX/HWP 출력, XML/OLE 속성 보존, roundtrip 저장 정합성 hwp5 현대 .hwp(HWP5/OLE) 포맷 처리: 파싱, IR 변환, 컨트롤·표·그림 속성 해석, 호환성 개선 labels Aug 2, 2026
@jangster77
jangster77 self-requested a review August 2, 2026 10:48
@jangster77

Copy link
Copy Markdown
Collaborator

통합 검토 PR #3801이 devel에 merge되었습니다.

이 PR의 contributor 기능 commit은 cherry-pick -x로 통합했으며, 누적 과정에서 확인된 binding
계약과 회귀 테스트 보정도 같은 통합 PR에 포함했습니다. 원 source branch는 수정하거나 삭제하지
않았습니다.

원 PR별 검토 기록과 적용 SHA는
mydocs/pr/archives/pr_3747_3779_kevin_review_impl.md 및 해당 pr_<번호>_review.md에 남겼습니다.
기여해 주셔서 감사합니다.

@jangster77 jangster77 closed this Aug 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working hwp5 현대 .hwp(HWP5/OLE) 포맷 처리: 파싱, IR 변환, 컨트롤·표·그림 속성 해석, 호환성 개선 serialization 문서 저장/직렬화: HWPX/HWP 출력, XML/OLE 속성 보존, roundtrip 저장 정합성

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants