요약
pull reader로 XML을 다시 쓰거나 훑는 보조 코드 두 곳에 잠복 결함이 있습니다. 둘 다 지금 사용자 문서를 망가뜨리는 경로는 확인되지 않아 우선순위가 낮고, 성격이 비슷해(XML 이벤트를 다루는 작은 도우미) 한 이슈로 묶었습니다.
rewrite_element_attr가 이미 escape된 원래 속성 값을 push_attribute로 다시 escape합니다(& → &amp;). 대상 요소가 숫자 속성만 가진 <hp:sz>라 지금은 무해합니다.
- CLI
hwpx_paths 스캐너가 XML 오류를 만나면 Err(_) => break로 조용히 멈춥니다. census-hwp5 --companion은 이때 앞부분만 담긴 목록을 status: ok로 돌려줍니다.
배경 지식
이미 아는 내용이면 건너뛰어도 됩니다.
1. quick-xml pull reader와 속성 값
Reader는 XML을 이벤트(Start·Empty·Text·End…)로 하나씩 돌려줍니다. 이때 attr.value는 파일에 적힌 그대로의(escape된) 바이트입니다(a&b). 반대로 BytesStart::push_attribute((key, value))는 value를 escape되지 않은 글자로 보고 escape해서 씁니다. 원래 값을 그대로 옮기려면 먼저 unescape하거나, escape하지 않는 API로 넣어야 합니다.
2. layout_hint_patch의 표 높이 보정
HWP5 → HWPX 변환 뒤, hwpforge-convert는 생성된 section XML을 한 번 더 읽어 표의 <hp:sz height>를 HWP5 조판 값으로 고칩니다. 이때 rewrite_element_attr가 그 요소의 속성을 전부 다시 만들어 씁니다.
3. hwpx_paths 스캐너와 census-hwp5
CLI의 analysis/hwpx_paths.rs는 section XML을 decode하지 않고 훑어서 요소 경로·개수 목록을 만듭니다. inspect는 먼저 문서를 decode하므로 깨진 XML이면 거기서 오류가 납니다. census-hwp5 --companion <hwpx>는 decode 없이 이 스캐너만 돌립니다.
재현
A. rewrite_element_attr 이중 escape
main 32d1435의 crates/hwpforge-convert/src/layout_hint_patch.rs test module에 임시 테스트를 넣어 돌렸습니다(커밋하지 않음).
#[test]
fn probe_rewrite_element_attr_escape() {
let xml = r#"<hp:sz width="10" note="a&b<c" height="5"/>"#;
let mut reader = Reader::from_str(xml);
let ev = match reader.read_event().unwrap() { Event::Empty(e) => e.into_owned(), other => panic!("{other:?}") };
let rebuilt = rewrite_element_attr(ev, "height", "9999").unwrap();
let mut writer = Writer::new(Cursor::new(Vec::new()));
writer.write_event(Event::Empty(rebuilt)).unwrap();
println!("{}", String::from_utf8(writer.into_inner().into_inner()).unwrap());
}
in : <hp:sz width="10" note="a&b<c" height="5"/>
out: <hp:sz width="10" note="a&amp;b&lt;c" height="9999"/>
| 속성 |
기대 |
실제 |
height (대상) |
9999 |
9999 |
note (대상 아님) |
a&b<c 그대로 |
a&amp;b&lt;c |
실제 호출은 <hp:tbl> 바로 아래 <hp:sz>뿐이고, 그 속성(width·widthRelTo·height·heightRelTo·protect)은 숫자·열거값이라 &·<가 들어갈 일이 없습니다. 같은 도우미를 다른 요소에 쓰는 순간 드러나는 결함입니다.
B. 스캐너의 조용한 break
-
표 두 개가 든 Markdown을 HWPX로 만듭니다.
| a | b |
| - | - |
| 1 | 2 |
문단
| c | d |
| - | - |
| 3 | 4 |
hwpforge convert t.md -o two.hwpx
-
Contents/section0.xml에서 첫 </hp:tbl> 바로 뒤에 <hp:t>x</hp:T>(닫는 태그 대소문자 불일치)를 넣어 two-broken.hwpx를 만듭니다.
-
hwpforge --json census-hwp5 tests/fixtures/user_samples/sample-memo-basic.hwp --companion two-broken.hwpx -o census.json
-
census.json의 companion.path_inventory를 셉니다. 비교로 hwpforge --json inspect two-broken.hwpx도 돌립니다.
| 입력 |
기대 |
실제 |
정상 two.hwpx |
표 2개 |
status: ok, 목록 27개, tbl 2개 |
two-broken.hwpx, census-hwp5 |
오류 또는 경고 |
status: ok, 목록 14개, tbl 1개(오류 지점 뒤가 없음) |
two-broken.hwpx, inspect |
오류 |
DECODE_FAILED ... expected </hp:t>, but </hp:T> was found |
영향
| 항목 |
A. 이중 escape |
B. 조용한 break |
| 현재 사용자 영향 |
없음(대상 요소에 escape가 필요한 값이 없음) |
census-hwp5 --companion의 결과가 조용히 잘림. 진단 도구라 문서 자체는 바뀌지 않음 |
| 신호 |
없음 |
없음(status: ok) |
| 드러나는 조건 |
이 도우미를 문자열 속성이 있는 요소에 쓸 때 |
companion HWPX가 well-formed가 아닐 때 |
선택지
| 결함 |
안 |
내용 |
장단점 |
| A |
A1 |
원래 속성은 Attribute를 그대로 push_attribute(attr)로 옮기고, 대상 속성만 새 값으로 |
원래 바이트 보존. 가장 작은 변경 |
| A |
A2 |
원래 값을 unescape한 뒤 push_attribute((key, value)) |
동작은 맞지만 unescape 실패 처리가 더 생김 |
| B |
B1 |
오류를 HwpxResult로 올려 HWPX_CENSUS_FAILED로 끝냄 |
fail-closed. 깨진 companion은 census를 못 함 |
| B |
B2 |
그때까지의 목록을 두고 경고 필드(오류 위치·메시지)를 결과에 추가 |
부분 결과를 쓸 수 있음. 출력 스키마가 바뀜 |
관련 위치
요약
pull reader로 XML을 다시 쓰거나 훑는 보조 코드 두 곳에 잠복 결함이 있습니다. 둘 다 지금 사용자 문서를 망가뜨리는 경로는 확인되지 않아 우선순위가 낮고, 성격이 비슷해(XML 이벤트를 다루는 작은 도우미) 한 이슈로 묶었습니다.
rewrite_element_attr가 이미 escape된 원래 속성 값을push_attribute로 다시 escape합니다(&→&amp;). 대상 요소가 숫자 속성만 가진<hp:sz>라 지금은 무해합니다.hwpx_paths스캐너가 XML 오류를 만나면Err(_) => break로 조용히 멈춥니다.census-hwp5 --companion은 이때 앞부분만 담긴 목록을status: ok로 돌려줍니다.배경 지식
이미 아는 내용이면 건너뛰어도 됩니다.
1. quick-xml pull reader와 속성 값
Reader는 XML을 이벤트(Start·Empty·Text·End…)로 하나씩 돌려줍니다. 이때attr.value는 파일에 적힌 그대로의(escape된) 바이트입니다(a&b). 반대로BytesStart::push_attribute((key, value))는value를 escape되지 않은 글자로 보고 escape해서 씁니다. 원래 값을 그대로 옮기려면 먼저 unescape하거나, escape하지 않는 API로 넣어야 합니다.2.
layout_hint_patch의 표 높이 보정HWP5 → HWPX 변환 뒤,
hwpforge-convert는 생성된 section XML을 한 번 더 읽어 표의<hp:sz height>를 HWP5 조판 값으로 고칩니다. 이때rewrite_element_attr가 그 요소의 속성을 전부 다시 만들어 씁니다.3.
hwpx_paths스캐너와census-hwp5CLI의
analysis/hwpx_paths.rs는 section XML을 decode하지 않고 훑어서 요소 경로·개수 목록을 만듭니다.inspect는 먼저 문서를 decode하므로 깨진 XML이면 거기서 오류가 납니다.census-hwp5 --companion <hwpx>는 decode 없이 이 스캐너만 돌립니다.재현
A.
rewrite_element_attr이중 escapemain
32d1435의crates/hwpforge-convert/src/layout_hint_patch.rstest module에 임시 테스트를 넣어 돌렸습니다(커밋하지 않음).height(대상)99999999note(대상 아님)a&b<c그대로a&amp;b&lt;c실제 호출은
<hp:tbl>바로 아래<hp:sz>뿐이고, 그 속성(width·widthRelTo·height·heightRelTo·protect)은 숫자·열거값이라&·<가 들어갈 일이 없습니다. 같은 도우미를 다른 요소에 쓰는 순간 드러나는 결함입니다.B. 스캐너의 조용한
break표 두 개가 든 Markdown을 HWPX로 만듭니다.
hwpforge convert t.md -o two.hwpxContents/section0.xml에서 첫</hp:tbl>바로 뒤에<hp:t>x</hp:T>(닫는 태그 대소문자 불일치)를 넣어two-broken.hwpx를 만듭니다.hwpforge --json census-hwp5 tests/fixtures/user_samples/sample-memo-basic.hwp --companion two-broken.hwpx -o census.jsoncensus.json의companion.path_inventory를 셉니다. 비교로hwpforge --json inspect two-broken.hwpx도 돌립니다.two.hwpxstatus: ok, 목록 27개,tbl2개two-broken.hwpx,census-hwp5status: ok, 목록 14개,tbl1개(오류 지점 뒤가 없음)two-broken.hwpx,inspectDECODE_FAILED ... expected </hp:t>, but </hp:T> was found영향
census-hwp5 --companion의 결과가 조용히 잘림. 진단 도구라 문서 자체는 바뀌지 않음status: ok)선택지
Attribute를 그대로push_attribute(attr)로 옮기고, 대상 속성만 새 값으로push_attribute((key, value))HwpxResult로 올려HWPX_CENSUS_FAILED로 끝냄관련 위치
convert/src/layout_hint_patch.rs:355-388, 호출convert/src/layout_hint_patch.rs:255-268bindings-cli/src/analysis/hwpx_paths.rs:100bindings-cli/src/commands/census_hwp5.rs:162-168tbl1개). A는 main(0.41)에서만 위 테스트로 확인했고, 0.42 빌드에서는 미측정입니다.