diff --git a/.gitignore b/.gitignore index 5465120fc..2c4ecc787 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,16 @@ Cargo.lock /pkg/ *.wasm +# Node +node_modules/ + +# CodeGraph local index +.codegraph/ + +# Local HTTPS certificates +/web/certs/* +!/web/certs/README.md + # Output (렌더링 결과물) /output/ /dist/swift/ diff --git a/CLAUDE.md b/CLAUDE.md index 7e740aff2..ab8201e39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,10 +16,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co | 포맷 | 파서 위치 | 출력 IR | |------|----------|---------| | HWPX (ZIP+XML) | `src/parser/hwpx/` | `Document` | -| HWP5 (OLE 복합) | `src/parser/hwp5/` | `Document` | +| HWP5 (OLE 복합) | `src/parser/` 루트의 HWP5 모듈들 | `Document` | | HWP3 (고전 바이너리) | `src/parser/hwp3/` | `Document` | > 역사적으로 `Document` 모델은 HWP5 형식의 구조를 기반으로 설계되었으며, HWPX는 같은 의미의 XML 포맷이다. HWP3는 고전 포맷이지만 동일한 `Document` IR로 변환한다. +> HWP5 파서는 별도 `src/parser/hwp5/` 디렉터리가 아니라 `body_text.rs`, `doc_info.rs`, `control.rs` 등 `src/parser/` 루트 모듈로 구성되어 있다. **HWP3 파서 규칙**: `src/parser/hwp3/` 내부에서 HWP3 바이너리를 읽어 `Document` IR로 변환하여 반환한다. HWP3 전용 로직은 **반드시 `src/parser/hwp3/` 안에서만** 구현한다. 렌더러(`src/renderer/`), 레이아웃(`src/renderer/layout.rs`), 문서 코어(`src/document_core/`) 등 공통 모듈에 HWP3 전용 분기를 추가하지 않는다. diff --git a/Cargo.toml b/Cargo.toml index 52d46f210..e7ffb1a6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ quick-xml = "0.40" codepage = "0.1.2" embedded-io = "0.7.1" encoding_rs = "0.8.35" -paste = "1.0.15" snafu = "0.9.0" strum = { version = "0.28.0", default-features = false, features = ["derive"] } unicode-segmentation = "1.12.0" diff --git a/bindings/Native/src/lib.rs b/bindings/Native/src/lib.rs index 8d02ef26f..cdf7d8a14 100644 --- a/bindings/Native/src/lib.rs +++ b/bindings/Native/src/lib.rs @@ -62,6 +62,8 @@ pub extern "C" fn rhwp_string_free(ptr: *mut c_char) { } unsafe { + // SAFETY: The native API returns strings allocated with CString::into_raw. + // Callers must pass each returned pointer back exactly once for release. drop(CString::from_raw(ptr)); } } @@ -264,6 +266,8 @@ fn read_utf8(ptr: *const c_char, name: &str) -> Result { } unsafe { + // SAFETY: Null pointers are rejected above. The FFI contract requires + // input strings to be valid, NUL-terminated C strings for this call. CStr::from_ptr(ptr) .to_str() .map(|s| s.to_string()) diff --git a/examples/dump_eqedit_raw.rs b/examples/dump_eqedit_raw.rs index ed62b6ae6..af5fe16ca 100644 --- a/examples/dump_eqedit_raw.rs +++ b/examples/dump_eqedit_raw.rs @@ -48,7 +48,7 @@ fn main() { println!(); } } - if size % 16 != 0 { + if !size.is_multiple_of(16) { println!(); } // 첫 2개 EQEDIT만 diff --git a/examples/inspect_574.rs b/examples/inspect_574.rs index 4afcc2adb..d7dc5c872 100644 --- a/examples/inspect_574.rs +++ b/examples/inspect_574.rs @@ -26,7 +26,7 @@ fn main() { // 본문 default CharShape 비교 (id=9 — section setup paragraph 0.0 의 본문 CS) println!("\n--- [쪽번호 후보] CharShape id=0 (TextBox 사각형 내 \"1\") ---"); - if let Some(cs) = doc.doc_info.char_shapes.get(0) { + if let Some(cs) = doc.doc_info.char_shapes.first() { print_char_shape(0, cs); } println!("\n--- [본문 비교] CharShape id=9 (본문 default ratio=95%) ---"); diff --git a/mydocs/eng/working/archives/task_83_done.md b/mydocs/eng/working/archives/task_83_done.md index 0b2128966..6d7acc8a3 100644 --- a/mydocs/eng/working/archives/task_83_done.md +++ b/mydocs/eng/working/archives/task_83_done.md @@ -11,8 +11,8 @@ Implemented HWP's F5 cell block selection mode. Pressing F5 inside a table cell | `src/wasm_api.rs` | Modified | Added `getTableCellBboxes` API — returns all cell bboxes of a table | | `rhwp-studio/src/core/types.ts` | Modified | Added `CellBbox` interface | | `rhwp-studio/src/core/wasm-bridge.ts` | Modified | Added `getTableCellBboxes()` bridge method | -| `rhwp-studio/src/engine/cursor.ts` | Modified | Cell selection mode state management (enter/exit/move/shift/ctrl/getRange) | -| `rhwp-studio/src/engine/input-handler.ts` | Modified | F5 key handler, arrow/ESC/Shift+click/Ctrl+click handling | +| `rhwp-studio/src/engine/cursor.ts` | Modified | Cell selection mode state management for enter, exit, move, modifier keys, and range lookup | +| `rhwp-studio/src/engine/input-handler.ts` | Modified | F5 key handler plus arrow, escape, shift-click, and modifier-click handling | | `rhwp-studio/src/engine/cell-selection-renderer.ts` | New | Cell range highlight overlay renderer (excluded cell support) | | `rhwp-studio/src/command/types.ts` | Modified | Added `inCellSelectionMode` to EditorContext | | `rhwp-studio/src/main.ts` | Modified | CellSelectionRenderer creation/injection, getContext extension | diff --git a/rhwp-chrome/package-lock.json b/rhwp-chrome/package-lock.json index 4a48f9394..934e32332 100644 --- a/rhwp-chrome/package-lock.json +++ b/rhwp-chrome/package-lock.json @@ -833,9 +833,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -843,6 +843,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -869,9 +870,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -887,8 +888,9 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/rhwp-firefox/package-lock.json b/rhwp-firefox/package-lock.json index 85150930d..a26ab3572 100644 --- a/rhwp-firefox/package-lock.json +++ b/rhwp-firefox/package-lock.json @@ -547,9 +547,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -564,9 +561,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -581,9 +575,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -598,9 +589,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -615,9 +603,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -632,9 +617,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -649,9 +631,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -666,9 +645,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -683,9 +659,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -700,9 +673,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -717,9 +687,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -734,9 +701,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -751,9 +715,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/rhwp-studio/package-lock.json b/rhwp-studio/package-lock.json index cc3a07031..9dcf8c3dd 100644 --- a/rhwp-studio/package-lock.json +++ b/rhwp-studio/package-lock.json @@ -6723,9 +6723,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "dev": true, "license": "MIT", "engines": { diff --git a/rhwp-vscode/package-lock.json b/rhwp-vscode/package-lock.json index 7ea4cb8a7..f8bf898b6 100644 --- a/rhwp-vscode/package-lock.json +++ b/rhwp-vscode/package-lock.json @@ -793,9 +793,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { diff --git a/src/document_core/commands/object_ops.rs b/src/document_core/commands/object_ops.rs index 674838faf..1b0e1f2a4 100644 --- a/src/document_core/commands/object_ops.rs +++ b/src/document_core/commands/object_ops.rs @@ -5293,6 +5293,7 @@ impl DocumentCore { } #[cfg(test)] +#[allow(clippy::items_after_test_module)] mod resize_clamp_tests { use super::*; use crate::model::document::{Document, Section, SectionDef}; diff --git a/src/document_core/commands/text_editing.rs b/src/document_core/commands/text_editing.rs index 49407c9c2..e08d4cdeb 100644 --- a/src/document_core/commands/text_editing.rs +++ b/src/document_core/commands/text_editing.rs @@ -2178,6 +2178,7 @@ impl DocumentCore { } #[cfg(test)] +#[allow(clippy::items_after_test_module)] mod tests { use super::*; diff --git a/src/document_core/converters/common_obj_attr_writer.rs b/src/document_core/converters/common_obj_attr_writer.rs index c50641227..2ce38630b 100644 --- a/src/document_core/converters/common_obj_attr_writer.rs +++ b/src/document_core/converters/common_obj_attr_writer.rs @@ -35,28 +35,28 @@ pub fn serialize_common_obj_attr(common: &CommonObjAttr) -> Vec { } else { pack_common_attr_bits(common) }; - w.write_u32(attr).unwrap(); + w.write_u32(attr); - w.write_u32(common.vertical_offset).unwrap(); - w.write_u32(common.horizontal_offset).unwrap(); - w.write_u32(common.width).unwrap(); - w.write_u32(common.height).unwrap(); - w.write_i32(common.z_order).unwrap(); + w.write_u32(common.vertical_offset); + w.write_u32(common.horizontal_offset); + w.write_u32(common.width); + w.write_u32(common.height); + w.write_i32(common.z_order); - w.write_i16(common.margin.left).unwrap(); - w.write_i16(common.margin.right).unwrap(); - w.write_i16(common.margin.top).unwrap(); - w.write_i16(common.margin.bottom).unwrap(); + w.write_i16(common.margin.left); + w.write_i16(common.margin.right); + w.write_i16(common.margin.top); + w.write_i16(common.margin.bottom); - w.write_u32(common.instance_id).unwrap(); - w.write_i32(common.prevent_page_break).unwrap(); + w.write_u32(common.instance_id); + w.write_i32(common.prevent_page_break); // description (HWP string) - w.write_hwp_string(&common.description).unwrap(); + w.write_hwp_string(&common.description); // 라운드트립 보존: raw_extra 가 있으면 이어붙임 if !common.raw_extra.is_empty() { - w.write_bytes(&common.raw_extra).unwrap(); + w.write_bytes(&common.raw_extra); } w.into_bytes() diff --git a/src/document_core/converters/hwpx_to_hwp.rs b/src/document_core/converters/hwpx_to_hwp.rs index e87756a97..5337b4aa8 100644 --- a/src/document_core/converters/hwpx_to_hwp.rs +++ b/src/document_core/converters/hwpx_to_hwp.rs @@ -8,7 +8,9 @@ //! `control.rs` 등은 변경하지 않는다. //! - **IR 만 만진다**: 진입점은 `&mut Document` 이며, 출력은 IR 필드 갱신뿐. //! - **idempotent**: 같은 IR 에 두 번 호출해도 같은 결과. -//! - **HWP 출처 보호**: `source_format == Hwpx` 일 때만 동작. HWP 출처는 no-op. +//! - **HWP 출처 보호**: `source_format == Hwpx` 또는 `Hwp3` 일 때만 동작. +//! HWP5 출처는 no-op. HWP3 허용은 HWP 저장 어댑터가 필요한 legacy IR 정규화 경로이며, +//! 공통 converter 에 새 HWP3 파서 로직을 섞는 예외가 아니다. //! //! ## 매핑 명세서 //! @@ -1260,6 +1262,9 @@ fn adapt_cell_list_attr(cell: &mut Cell, report: &mut AdapterReport) { /// /// 호출자: `DocumentCore::export_hwp_with_adapter()` (Stage 5 에서 추가). pub fn convert_if_hwpx_source(doc: &mut Document, source_format: FileFormat) -> AdapterReport { + // Guardrail: this converter only materializes HWP-save contracts for HWPX + // and native HWP3 IR. Do not add format-specific parsing here; keep that in + // the parser modules before DocumentCore reaches the export adapter. if !matches!(source_format, FileFormat::Hwpx | FileFormat::Hwp3) { return AdapterReport::new().no_op("source_format != Hwpx/Hwp3"); } @@ -2281,7 +2286,7 @@ mod tests { 0.0, 0.0, 0.0, - f64::from(924.090_027_f32), + f64::from(924.09_f32), 0.0, ])); raw_rendering.extend_from_slice(&matrix([1.0, 0.0, 0.0, 0.0, 1.0, 0.0])); diff --git a/src/document_core/helpers.rs b/src/document_core/helpers.rs index 2e4461b12..19d0d2c54 100644 --- a/src/document_core/helpers.rs +++ b/src/document_core/helpers.rs @@ -1410,7 +1410,7 @@ mod tests { let para = Paragraph { text: "abc".to_string(), char_offsets: vec![0, 1, 2], - controls: vec![Control::Footnote(Box::new(Footnote::default()))], + controls: vec![Control::Footnote(Box::default())], ..Default::default() }; @@ -1424,10 +1424,10 @@ mod tests { text: " ".to_string(), char_offsets: vec![24, 25], controls: vec![ - Control::SectionDef(Box::new(SectionDef::default())), + Control::SectionDef(Box::default()), Control::ColumnDef(ColumnDef::default()), - Control::Footnote(Box::new(Footnote::default())), - Control::Footnote(Box::new(Footnote::default())), + Control::Footnote(Box::default()), + Control::Footnote(Box::default()), ], ..Default::default() }; diff --git a/src/emf/tests.rs b/src/emf/tests.rs index 422542b1f..5ed4d8cf0 100644 --- a/src/emf/tests.rs +++ b/src/emf/tests.rs @@ -160,7 +160,7 @@ fn header_prefix() -> Vec { fn push_record(b: &mut Vec, rt: u32, payload: &[u8]) { let size = 8u32 + payload.len() as u32; - assert!(size % 4 == 0, "record size must be 4-aligned"); + assert!(size.is_multiple_of(4), "record size must be 4-aligned"); b.extend_from_slice(&rt.to_le_bytes()); b.extend_from_slice(&size.to_le_bytes()); b.extend_from_slice(payload); diff --git a/src/model/shape.rs b/src/model/shape.rs index 28bc218de..d63c0f140 100644 --- a/src/model/shape.rs +++ b/src/model/shape.rs @@ -789,10 +789,7 @@ mod tests { #[test] fn test_shape_object_line() { let line = ShapeObject::Line(LineShape::default()); - match line { - ShapeObject::Line(_) => assert!(true), - _ => panic!("Expected Line variant"), - } + assert!(matches!(line, ShapeObject::Line(_))); } #[test] diff --git a/src/model/table/tests.rs b/src/model/table/tests.rs index 14c682cab..6e4192b8b 100644 --- a/src/model/table/tests.rs +++ b/src/model/table/tests.rs @@ -710,7 +710,7 @@ fn test_edit_ops_rebuild_grid() { // delete_column 후 grid 정합성 확인 table.delete_column(1).unwrap(); - assert_eq!(table.cell_grid.len(), 3 * 1); // 3행 × 1열 + assert_eq!(table.cell_grid.len(), 3); // 3행 × 1열 for r in 0..3u16 { let cell = table .cell_at(r, 0) diff --git a/src/paint/json.rs b/src/paint/json.rs index d13caa504..f931515ba 100644 --- a/src/paint/json.rs +++ b/src/paint/json.rs @@ -2407,6 +2407,7 @@ fn form_type_str(value: FormType) -> &'static str { } #[cfg(test)] +#[allow(clippy::items_after_test_module)] mod tests { use super::*; use crate::paint::{ diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d95038c5e..7766bf625 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1310,7 +1310,7 @@ mod tests { assert_eq!(detect_format(&data), FileFormat::Hwp3); let doc = parse_document(&data).expect("Should successfully parse HWP3 sample"); assert!( - doc.sections.len() > 0, + !doc.sections.is_empty(), "Document should have at least one section" ); } diff --git a/src/renderer/composer/tests.rs b/src/renderer/composer/tests.rs index 1f02b3fcf..9b5c95de7 100644 --- a/src/renderer/composer/tests.rs +++ b/src/renderer/composer/tests.rs @@ -224,7 +224,7 @@ fn test_identify_inline_controls_table() { let para = Paragraph { text: "표 앞 텍스트".to_string(), - controls: vec![Control::Table(Box::new(Table::default()))], + controls: vec![Control::Table(Box::default())], ..Default::default() }; diff --git a/src/renderer/equation/parser.rs b/src/renderer/equation/parser.rs index 533e1b810..d206e6fc1 100644 --- a/src/renderer/equation/parser.rs +++ b/src/renderer/equation/parser.rs @@ -2375,7 +2375,6 @@ mod latex_compat_tests { } } - #[test] #[test] fn test_hwpeq_inf_remains_symbol() { let ast = parse("lim _{n→inf}"); diff --git a/src/renderer/layout/integration_tests.rs b/src/renderer/layout/integration_tests.rs index bd382dd29..7c65316fc 100644 --- a/src/renderer/layout/integration_tests.rs +++ b/src/renderer/layout/integration_tests.rs @@ -662,7 +662,7 @@ mod tests { ); // IR delta 정합 검증: 5448 HU = 72.64 px. - let expected_gap = (5448.0_f64 * 96.0 / 7200.0_f64); + let expected_gap = 5448.0_f64 * 96.0 / 7200.0_f64; assert!( (gap_12 - expected_gap).abs() < 0.5, "①→② gap({:.2}) 가 IR vpos delta({:.2}) 와 일치해야 함", @@ -739,8 +739,7 @@ mod tests { // '르' 직전 line 의 글자 y 찾기 (gap 측정용) let prev_line_y = points .iter() - .filter(|(y, _, _)| *y < leporeut_y - 0.5) - .last() + .rfind(|(y, _, _)| *y < leporeut_y - 0.5) .map(|(y, _, _)| *y) .expect("'르' 직전 line 을 찾을 수 없음"); @@ -821,8 +820,7 @@ mod tests { let prev_line_y = points .iter() - .filter(|(y, _, _)| *y < deobureo_y - 0.5) - .last() + .rfind(|(y, _, _)| *y < deobureo_y - 0.5) .map(|(y, _, _)| *y) .expect("'더' 직전 line 을 찾을 수 없음"); diff --git a/src/renderer/layout/table_layout.rs b/src/renderer/layout/table_layout.rs index 6b259d9fe..fec5c1347 100644 --- a/src/renderer/layout/table_layout.rs +++ b/src/renderer/layout/table_layout.rs @@ -4346,7 +4346,7 @@ mod row_cut_tests { let eng = LayoutEngine::new(96.0); let styles = ResolvedStyleSet::default(); let t = table(vec![cell(0, 0, vec![text_para(6, 0)])]); - let r = eng.advance_row_cut(&t, 0, &vec![], 50.0, &styles); + let r = eng.advance_row_cut(&t, 0, &[], 50.0, &styles); assert_eq!(r.end_cut, vec![3]); assert!(!r.fully_consumed); assert!(!r.hit_hard_break); @@ -4358,7 +4358,7 @@ mod row_cut_tests { let eng = LayoutEngine::new(96.0); let styles = ResolvedStyleSet::default(); let t = table(vec![cell(0, 0, vec![text_para(6, 0)])]); - let r = eng.advance_row_cut(&t, 0, &vec![], 500.0, &styles); + let r = eng.advance_row_cut(&t, 0, &[], 500.0, &styles); assert_eq!(r.end_cut, vec![6]); assert!(r.fully_consumed); } @@ -4369,7 +4369,7 @@ mod row_cut_tests { let eng = LayoutEngine::new(96.0); let styles = ResolvedStyleSet::default(); let t = table(vec![cell(0, 0, vec![text_para(6, 0)])]); - let r = eng.advance_row_cut(&t, 0, &vec![], 5.0, &styles); + let r = eng.advance_row_cut(&t, 0, &[], 5.0, &styles); assert_eq!(r.end_cut, vec![1]); assert!(!r.fully_consumed); } @@ -4382,7 +4382,7 @@ mod row_cut_tests { let styles = ResolvedStyleSet::default(); let t = table(vec![cell(0, 0, vec![text_para(3, 0), text_para(2, 1000)])]); // avail 충분해도 리셋에서 정지. - let r = eng.advance_row_cut(&t, 0, &vec![], 1000.0, &styles); + let r = eng.advance_row_cut(&t, 0, &[], 1000.0, &styles); assert_eq!(r.end_cut, vec![3]); assert!(r.hit_hard_break); assert!(!r.fully_consumed); @@ -4402,7 +4402,7 @@ mod row_cut_tests { cell(0, 0, vec![text_para(3, 0)]), cell(0, 1, vec![text_para(6, 0)]), ]); - let r = eng.advance_row_cut(&t, 0, &vec![], 500.0, &styles); + let r = eng.advance_row_cut(&t, 0, &[], 500.0, &styles); assert_eq!(r.end_cut, vec![3, 6]); assert!(r.fully_consumed); assert!((r.consumed_height - 96.0).abs() < 0.5); diff --git a/src/renderer/pagination/tests.rs b/src/renderer/pagination/tests.rs index b84c98989..d3e469da8 100644 --- a/src/renderer/pagination/tests.rs +++ b/src/renderer/pagination/tests.rs @@ -79,7 +79,7 @@ fn test_page_overflow() { 0, ); // 여러 페이지로 분할되어야 함 - assert!(result.pages.len() >= 1); + assert!(!result.pages.is_empty()); } #[test] diff --git a/src/serializer/body_text.rs b/src/serializer/body_text.rs index 6ec527559..5cb36ff01 100644 --- a/src/serializer/body_text.rs +++ b/src/serializer/body_text.rs @@ -340,10 +340,10 @@ fn serialize_para_header_with_mask( // MSB는 위치 기반으로 결정: 현재 스코프의 마지막 문단만 MSB=1 let char_count_raw = char_count | if is_last { 0x80000000 } else { 0 }; - w.write_u32(char_count_raw).unwrap(); - w.write_u32(control_mask).unwrap(); - w.write_u16(para.para_shape_id).unwrap(); - w.write_u8(para.style_id).unwrap(); + w.write_u32(char_count_raw); + w.write_u32(control_mask); + w.write_u16(para.para_shape_id); + w.write_u8(para.style_id); let break_val: u8 = if para.raw_break_type != 0 { para.raw_break_type @@ -356,24 +356,24 @@ fn serialize_para_header_with_mask( ColumnBreakType::None => 0x00, } }; - w.write_u8(break_val).unwrap(); + w.write_u8(break_val); // count 필드는 실제 데이터 기반으로 항상 재생성 (편집 후 불일치 방지) - w.write_u16(num_char_shapes as u16).unwrap(); - w.write_u16(para.range_tags.len() as u16).unwrap(); - w.write_u16(para.line_segs.len() as u16).unwrap(); + w.write_u16(num_char_shapes as u16); + w.write_u16(para.range_tags.len() as u16); + w.write_u16(para.line_segs.len() as u16); // instanceId + 추가 바이트: raw_header_extra에서 복원 // raw_header_extra[0..5] = numCharShapes(2) + numRangeTags(2) + numLineSegs(2) → 건너뜀 // raw_header_extra[6..] = instanceId(4) + (옵션) 변경추적 UINT16 (2, 5.0.3.2 이상) if para.raw_header_extra.len() >= 10 { let extra = ¶.raw_header_extra[6..]; - w.write_bytes(extra).unwrap(); + w.write_bytes(extra); } else { // 새 문단 (HWPX 출처, raw_header_extra 없음): instanceId(4)만 기록. // 한컴 정답지 footnote-01.hwp 의 PARA_HEADER size=22 = 18 (heading) + 4 (instanceId). // 변경추적 UINT16 (size=24 형식) 은 한컴 정답지에 미사용. - w.write_u32(0).unwrap(); + w.write_u32(0); } w.into_bytes() @@ -582,8 +582,8 @@ fn serialize_para_text(para: &Paragraph) -> Vec { fn serialize_para_char_shape(char_shapes: &[CharShapeRef]) -> Vec { let mut w = ByteWriter::new(); for cs in char_shapes { - w.write_u32(cs.start_pos).unwrap(); - w.write_u32(cs.char_shape_id).unwrap(); + w.write_u32(cs.start_pos); + w.write_u32(cs.char_shape_id); } w.into_bytes() } @@ -654,15 +654,15 @@ fn push_field_end_ctrl(code_units: &mut Vec, marker: FieldEndMarker) { fn serialize_para_line_seg(line_segs: &[LineSeg]) -> Vec { let mut w = ByteWriter::new(); for seg in line_segs { - w.write_u32(seg.text_start).unwrap(); - w.write_i32(seg.vertical_pos).unwrap(); - w.write_i32(seg.line_height).unwrap(); - w.write_i32(seg.text_height).unwrap(); - w.write_i32(seg.baseline_distance).unwrap(); - w.write_i32(seg.line_spacing).unwrap(); - w.write_i32(seg.column_start).unwrap(); - w.write_i32(seg.segment_width).unwrap(); - w.write_u32(seg.tag).unwrap(); + w.write_u32(seg.text_start); + w.write_i32(seg.vertical_pos); + w.write_i32(seg.line_height); + w.write_i32(seg.text_height); + w.write_i32(seg.baseline_distance); + w.write_i32(seg.line_spacing); + w.write_i32(seg.column_start); + w.write_i32(seg.segment_width); + w.write_u32(seg.tag); } w.into_bytes() } @@ -673,9 +673,9 @@ fn serialize_para_line_seg(line_segs: &[LineSeg]) -> Vec { fn serialize_para_range_tag(range_tags: &[RangeTag]) -> Vec { let mut w = ByteWriter::new(); for rt in range_tags { - w.write_u32(rt.start).unwrap(); - w.write_u32(rt.end).unwrap(); - w.write_u32(rt.tag).unwrap(); + w.write_u32(rt.start); + w.write_u32(rt.end); + w.write_u32(rt.tag); } w.into_bytes() } @@ -1121,7 +1121,7 @@ mod tests { #[test] fn test_control_char_code() { assert_eq!( - control_char_code_and_id(&Control::SectionDef(Box::new(SectionDef::default()))).0, + control_char_code_and_id(&Control::SectionDef(Box::default())).0, 0x0002 ); assert_eq!( diff --git a/src/serializer/byte_writer.rs b/src/serializer/byte_writer.rs index 986c16524..57e4cc991 100644 --- a/src/serializer/byte_writer.rs +++ b/src/serializer/byte_writer.rs @@ -3,9 +3,6 @@ //! HWP 레코드 내부의 바이너리 필드를 순차적으로 쓰기 위한 버퍼 기반 라이터. //! `ByteReader`의 역방향으로, 리틀 엔디안과 UTF-16LE 문자열을 지원한다. -use byteorder::{LittleEndian, WriteBytesExt}; -use std::io; - /// 바이트 라이터 (버퍼 기반) pub struct ByteWriter { buf: Vec, @@ -23,68 +20,65 @@ impl ByteWriter { } /// u8 쓰기 - pub fn write_u8(&mut self, v: u8) -> io::Result<()> { - self.buf.write_u8(v) + pub fn write_u8(&mut self, v: u8) { + self.buf.push(v); } /// u16 쓰기 (LE) - pub fn write_u16(&mut self, v: u16) -> io::Result<()> { - self.buf.write_u16::(v) + pub fn write_u16(&mut self, v: u16) { + self.buf.extend_from_slice(&v.to_le_bytes()); } /// u32 쓰기 (LE) - pub fn write_u32(&mut self, v: u32) -> io::Result<()> { - self.buf.write_u32::(v) + pub fn write_u32(&mut self, v: u32) { + self.buf.extend_from_slice(&v.to_le_bytes()); } /// i8 쓰기 - pub fn write_i8(&mut self, v: i8) -> io::Result<()> { - self.buf.write_i8(v) + pub fn write_i8(&mut self, v: i8) { + self.buf.push(v as u8); } /// i16 쓰기 (LE) - pub fn write_i16(&mut self, v: i16) -> io::Result<()> { - self.buf.write_i16::(v) + pub fn write_i16(&mut self, v: i16) { + self.buf.extend_from_slice(&v.to_le_bytes()); } /// i32 쓰기 (LE) - pub fn write_i32(&mut self, v: i32) -> io::Result<()> { - self.buf.write_i32::(v) + pub fn write_i32(&mut self, v: i32) { + self.buf.extend_from_slice(&v.to_le_bytes()); } /// f64 쓰기 (LE) - pub fn write_f64(&mut self, v: f64) -> io::Result<()> { - self.buf.write_f64::(v) + pub fn write_f64(&mut self, v: f64) { + self.buf.extend_from_slice(&v.to_le_bytes()); } /// 바이트 슬라이스 쓰기 - pub fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + pub fn write_bytes(&mut self, data: &[u8]) { self.buf.extend_from_slice(data); - Ok(()) } /// HWP 문자열 쓰기 (u16 글자수 + UTF-16LE 바이트) /// /// `ByteReader::read_hwp_string()`의 역방향. /// 형식: [u16 글자수] + [UTF-16LE 바이트 * 글자수] - pub fn write_hwp_string(&mut self, s: &str) -> io::Result<()> { + pub fn write_hwp_string(&mut self, s: &str) { let utf16: Vec = s.encode_utf16().collect(); - self.write_u16(utf16.len() as u16)?; + self.write_u16(utf16.len() as u16); for code_unit in &utf16 { - self.write_u16(*code_unit)?; + self.write_u16(*code_unit); } - Ok(()) } /// ColorRef 쓰기 (4바이트, 0x00BBGGRR 형식) - pub fn write_color_ref(&mut self, color: u32) -> io::Result<()> { + pub fn write_color_ref(&mut self, color: u32) { self.write_u32(color) } /// 0으로 채운 패딩 쓰기 - pub fn write_zeros(&mut self, count: usize) -> io::Result<()> { + pub fn write_zeros(&mut self, count: usize) { self.buf.extend(std::iter::repeat(0u8).take(count)); - Ok(()) } /// 내부 버퍼를 소유권 이전하여 반환 @@ -106,35 +100,35 @@ mod tests { #[test] fn test_write_u8() { let mut w = ByteWriter::new(); - w.write_u8(0x42).unwrap(); + w.write_u8(0x42); assert_eq!(w.into_bytes(), [0x42]); } #[test] fn test_write_u16_le() { let mut w = ByteWriter::new(); - w.write_u16(0x1234).unwrap(); + w.write_u16(0x1234); assert_eq!(w.into_bytes(), [0x34, 0x12]); } #[test] fn test_write_u32_le() { let mut w = ByteWriter::new(); - w.write_u32(0x12345678).unwrap(); + w.write_u32(0x12345678); assert_eq!(w.into_bytes(), [0x78, 0x56, 0x34, 0x12]); } #[test] fn test_write_i8() { let mut w = ByteWriter::new(); - w.write_i8(-1).unwrap(); + w.write_i8(-1); assert_eq!(w.into_bytes(), [0xFF]); } #[test] fn test_write_i16_negative() { let mut w = ByteWriter::new(); - w.write_i16(-100).unwrap(); + w.write_i16(-100); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); assert_eq!(reader.read_i16().unwrap(), -100); @@ -143,7 +137,7 @@ mod tests { #[test] fn test_write_i32_negative() { let mut w = ByteWriter::new(); - w.write_i32(-7200).unwrap(); + w.write_i32(-7200); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); assert_eq!(reader.read_i32().unwrap(), -7200); @@ -152,7 +146,7 @@ mod tests { #[test] fn test_write_bytes() { let mut w = ByteWriter::new(); - w.write_bytes(&[0x01, 0x02, 0x03]).unwrap(); + w.write_bytes(&[0x01, 0x02, 0x03]); assert_eq!(w.into_bytes(), [0x01, 0x02, 0x03]); } @@ -160,7 +154,7 @@ mod tests { fn test_write_hwp_string_korean() { // "한글" → u16 글자수(2) + UTF-16LE let mut w = ByteWriter::new(); - w.write_hwp_string("한글").unwrap(); + w.write_hwp_string("한글"); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); @@ -170,7 +164,7 @@ mod tests { #[test] fn test_write_hwp_string_ascii() { let mut w = ByteWriter::new(); - w.write_hwp_string("ABC").unwrap(); + w.write_hwp_string("ABC"); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); @@ -180,7 +174,7 @@ mod tests { #[test] fn test_write_hwp_string_empty() { let mut w = ByteWriter::new(); - w.write_hwp_string("").unwrap(); + w.write_hwp_string(""); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); @@ -191,7 +185,7 @@ mod tests { fn test_write_hwp_string_mixed() { // 한글 + ASCII 혼합 let mut w = ByteWriter::new(); - w.write_hwp_string("Hello 세계!").unwrap(); + w.write_hwp_string("Hello 세계!"); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); @@ -201,7 +195,7 @@ mod tests { #[test] fn test_write_color_ref() { let mut w = ByteWriter::new(); - w.write_color_ref(0x00FF8040).unwrap(); + w.write_color_ref(0x00FF8040); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); @@ -211,7 +205,7 @@ mod tests { #[test] fn test_write_zeros() { let mut w = ByteWriter::new(); - w.write_zeros(5).unwrap(); + w.write_zeros(5); assert_eq!(w.into_bytes(), [0, 0, 0, 0, 0]); } @@ -219,18 +213,18 @@ mod tests { fn test_position() { let mut w = ByteWriter::new(); assert_eq!(w.position(), 0); - w.write_u16(0x1234).unwrap(); + w.write_u16(0x1234); assert_eq!(w.position(), 2); - w.write_u32(0).unwrap(); + w.write_u32(0); assert_eq!(w.position(), 6); } #[test] fn test_sequential_writes_roundtrip() { let mut w = ByteWriter::new(); - w.write_u8(42).unwrap(); - w.write_u16(1000).unwrap(); - w.write_i32(-500).unwrap(); + w.write_u8(42); + w.write_u16(1000); + w.write_i32(-500); let bytes = w.into_bytes(); let mut reader = ByteReader::new(&bytes); diff --git a/src/serializer/control.rs b/src/serializer/control.rs index 7923d2f3e..f53be303f 100644 --- a/src/serializer/control.rs +++ b/src/serializer/control.rs @@ -227,26 +227,26 @@ fn make_ctrl_record(ctrl_id: u32, level: u16, ctrl_data: &[u8]) -> Record { fn serialize_section_def(sd: &SectionDef, level: u16, records: &mut Vec) { let mut w = ByteWriter::new(); - w.write_u32(sd.flags).unwrap(); - w.write_i16(sd.column_spacing).unwrap(); - w.write_i16(sd.line_grid).unwrap(); - w.write_i16(sd.char_grid).unwrap(); - w.write_u32(sd.default_tab_spacing).unwrap(); - w.write_u16(sd.outline_numbering_id).unwrap(); - w.write_u16(sd.page_num).unwrap(); - w.write_u16(sd.picture_num).unwrap(); - w.write_u16(sd.table_num).unwrap(); - w.write_u16(sd.equation_num).unwrap(); + w.write_u32(sd.flags); + w.write_i16(sd.column_spacing); + w.write_i16(sd.line_grid); + w.write_i16(sd.char_grid); + w.write_u32(sd.default_tab_spacing); + w.write_u16(sd.outline_numbering_id); + w.write_u16(sd.page_num); + w.write_u16(sd.picture_num); + w.write_u16(sd.table_num); + w.write_u16(sd.equation_num); // [Task #1058] HWP5 spec 표 129 정합 — SectionDef payload 26 byte (위 24 + 2 Language): // UINT16 대표Language (5.0.1.5 이상) // 한컴 정답지는 26 byte payload + 8 byte zero (확장 영역) = 34 byte payload + 4 byte ctrl_id = 38 byte CTRL_HEADER. // 원본 추가 바이트 복원 (라운드트립용) if !sd.raw_ctrl_extra.is_empty() { - w.write_bytes(&sd.raw_ctrl_extra).unwrap(); + w.write_bytes(&sd.raw_ctrl_extra); } else { // HWPX 출처: 한컴 default 10 byte (Language 0 + zero 8) - w.write_u16(0).unwrap(); // 대표Language = 0 (Application 지정) - w.write_bytes(&[0u8; 8]).unwrap(); // 추가 zero padding (관찰된 한컴 정답지 패턴) + w.write_u16(0); // 대표Language = 0 (Application 지정) + w.write_bytes(&[0u8; 8]); // 추가 zero padding (관찰된 한컴 정답지 패턴) } records.push(make_ctrl_record( @@ -352,46 +352,46 @@ pub(crate) fn serialize_master_page( fn serialize_page_def(pd: &PageDef) -> Vec { let mut w = ByteWriter::new(); - w.write_u32(pd.width).unwrap(); - w.write_u32(pd.height).unwrap(); - w.write_u32(pd.margin_left).unwrap(); - w.write_u32(pd.margin_right).unwrap(); - w.write_u32(pd.margin_top).unwrap(); - w.write_u32(pd.margin_bottom).unwrap(); - w.write_u32(pd.margin_header).unwrap(); - w.write_u32(pd.margin_footer).unwrap(); - w.write_u32(pd.margin_gutter).unwrap(); - w.write_u32(pd.attr).unwrap(); + w.write_u32(pd.width); + w.write_u32(pd.height); + w.write_u32(pd.margin_left); + w.write_u32(pd.margin_right); + w.write_u32(pd.margin_top); + w.write_u32(pd.margin_bottom); + w.write_u32(pd.margin_header); + w.write_u32(pd.margin_footer); + w.write_u32(pd.margin_gutter); + w.write_u32(pd.attr); w.into_bytes() } fn serialize_footnote_shape(fs: &FootnoteShape) -> Vec { let mut w = ByteWriter::new(); - w.write_u32(fs.attr).unwrap(); - w.write_u16(fs.user_char as u16).unwrap(); - w.write_u16(fs.prefix_char as u16).unwrap(); - w.write_u16(fs.suffix_char as u16).unwrap(); - w.write_u16(fs.start_number).unwrap(); - w.write_i16(fs.separator_length).unwrap(); - w.write_i16(fs.separator_margin_top).unwrap(); - w.write_i16(fs.separator_margin_bottom).unwrap(); - w.write_i16(fs.note_spacing).unwrap(); + w.write_u32(fs.attr); + w.write_u16(fs.user_char as u16); + w.write_u16(fs.prefix_char as u16); + w.write_u16(fs.suffix_char as u16); + w.write_u16(fs.start_number); + w.write_i16(fs.separator_length); + w.write_i16(fs.separator_margin_top); + w.write_i16(fs.separator_margin_bottom); + w.write_i16(fs.note_spacing); // 미문서화 2바이트 (원본 보존) - w.write_u16(fs.raw_unknown).unwrap(); - w.write_u8(fs.separator_line_type).unwrap(); - w.write_u8(fs.separator_line_width).unwrap(); - w.write_color_ref(fs.separator_color).unwrap(); + w.write_u16(fs.raw_unknown); + w.write_u8(fs.separator_line_type); + w.write_u8(fs.separator_line_width); + w.write_color_ref(fs.separator_color); w.into_bytes() } fn serialize_page_border_fill(pbf: &PageBorderFill) -> Vec { let mut w = ByteWriter::new(); - w.write_u32(pbf.attr).unwrap(); - w.write_i16(pbf.spacing_left).unwrap(); - w.write_i16(pbf.spacing_right).unwrap(); - w.write_i16(pbf.spacing_top).unwrap(); - w.write_i16(pbf.spacing_bottom).unwrap(); - w.write_u16(pbf.border_fill_id).unwrap(); + w.write_u32(pbf.attr); + w.write_i16(pbf.spacing_left); + w.write_i16(pbf.spacing_right); + w.write_i16(pbf.spacing_top); + w.write_i16(pbf.spacing_bottom); + w.write_u16(pbf.border_fill_id); w.into_bytes() } @@ -424,26 +424,26 @@ fn serialize_column_def(cd: &ColumnDef, level: u16, records: &mut Vec) { a }; - w.write_u16(attr).unwrap(); + w.write_u16(attr); // hwplib 기준: same_width 여부에 따라 바이트 순서가 다름 if !cd.same_width && cd.column_count > 1 { // same_width=false: [attr2(2)] [col0_width(2) col0_gap(2)] ... - w.write_u16(0).unwrap(); // attr2 + w.write_u16(0); // attr2 for i in 0..cd.widths.len() { - w.write_i16(cd.widths[i]).unwrap(); + w.write_i16(cd.widths[i]); let gap = cd.gaps.get(i).copied().unwrap_or(0); - w.write_i16(gap).unwrap(); + w.write_i16(gap); } } else { // same_width=true: [gap(2)] [attr2(2)] - w.write_i16(cd.spacing).unwrap(); - w.write_u16(0).unwrap(); // attr2 + w.write_i16(cd.spacing); + w.write_u16(0); // attr2 } - w.write_u8(cd.separator_type).unwrap(); - w.write_u8(cd.separator_width).unwrap(); - w.write_color_ref(cd.separator_color).unwrap(); + w.write_u8(cd.separator_type); + w.write_u8(cd.separator_width); + w.write_color_ref(cd.separator_color); records.push(make_ctrl_record(tags::CTRL_COLUMN_DEF, level, w.as_bytes())); } @@ -502,28 +502,28 @@ fn serialize_table_record(table: &Table) -> Vec { } a }; - w.write_u32(attr).unwrap(); + w.write_u32(attr); - w.write_u16(table.row_count).unwrap(); - w.write_u16(table.col_count).unwrap(); - w.write_i16(table.cell_spacing).unwrap(); + w.write_u16(table.row_count); + w.write_u16(table.col_count); + w.write_i16(table.cell_spacing); // 안쪽 여백 - w.write_i16(table.padding.left).unwrap(); - w.write_i16(table.padding.right).unwrap(); - w.write_i16(table.padding.top).unwrap(); - w.write_i16(table.padding.bottom).unwrap(); + w.write_i16(table.padding.left); + w.write_i16(table.padding.right); + w.write_i16(table.padding.top); + w.write_i16(table.padding.bottom); // 행별 셀 수 (HWP 스펙: UINT16[NRows]) for &h in &table.row_sizes { - w.write_i16(h).unwrap(); + w.write_i16(h); } - w.write_u16(table.border_fill_id).unwrap(); + w.write_u16(table.border_fill_id); // 원본 추가 바이트 복원 (라운드트립용) if !table.raw_table_record_extra.is_empty() { - w.write_bytes(&table.raw_table_record_extra).unwrap(); + w.write_bytes(&table.raw_table_record_extra); } w.into_bytes() @@ -534,7 +534,7 @@ fn serialize_cell(cell: &Cell, level: u16, records: &mut Vec) { // LIST_HEADER 공통 (6 + 2 = 8바이트) let n_paragraphs = cell.paragraphs.len() as u16; - w.write_u16(n_paragraphs).unwrap(); + w.write_u16(n_paragraphs); // list_attr 재구성 (text_direction + vertical_align) let v_align_code: u32 = match cell.vertical_align { @@ -543,25 +543,25 @@ fn serialize_cell(cell: &Cell, level: u16, records: &mut Vec) { VerticalAlign::Bottom => 2, }; let list_attr: u32 = ((cell.text_direction as u32) << 16) | (v_align_code << 21); - w.write_u32(list_attr).unwrap(); - w.write_u16(cell.list_header_width_ref).unwrap(); + w.write_u32(list_attr); + w.write_u16(cell.list_header_width_ref); // 셀 속성 - w.write_u16(cell.col).unwrap(); - w.write_u16(cell.row).unwrap(); - w.write_u16(cell.col_span).unwrap(); - w.write_u16(cell.row_span).unwrap(); - w.write_u32(cell.width).unwrap(); - w.write_u32(cell.height).unwrap(); - w.write_i16(cell.padding.left).unwrap(); - w.write_i16(cell.padding.right).unwrap(); - w.write_i16(cell.padding.top).unwrap(); - w.write_i16(cell.padding.bottom).unwrap(); - w.write_u16(cell.border_fill_id).unwrap(); + w.write_u16(cell.col); + w.write_u16(cell.row); + w.write_u16(cell.col_span); + w.write_u16(cell.row_span); + w.write_u32(cell.width); + w.write_u32(cell.height); + w.write_i16(cell.padding.left); + w.write_i16(cell.padding.right); + w.write_i16(cell.padding.top); + w.write_i16(cell.padding.bottom); + w.write_u16(cell.border_fill_id); // 원본 추가 바이트 복원 (라운드트립용) if !cell.raw_list_extra.is_empty() { - w.write_bytes(&cell.raw_list_extra).unwrap(); + w.write_bytes(&cell.raw_list_extra); } records.push(Record { @@ -580,7 +580,7 @@ fn serialize_caption(caption: &Caption, level: u16, records: &mut Vec) { // LIST_HEADER 공통 (8바이트: n_para + list_attr + width_ref) let n_paragraphs = caption.paragraphs.len() as u16; - w.write_u16(n_paragraphs).unwrap(); + w.write_u16(n_paragraphs); // list_attr: bit 21~22 = 세로 정렬 (Left/Right 캡션용) let vert_align_bits: u32 = match caption.vert_align { CaptionVertAlign::Top => 0, @@ -588,8 +588,8 @@ fn serialize_caption(caption: &Caption, level: u16, records: &mut Vec) { CaptionVertAlign::Bottom => 2, }; let list_attr: u32 = vert_align_bits << 21; - w.write_u32(list_attr).unwrap(); - w.write_u16(0).unwrap(); // width_ref + w.write_u32(list_attr); + w.write_u16(0); // width_ref // 캡션 데이터 let dir_val: u32 = match caption.direction { @@ -602,13 +602,13 @@ fn serialize_caption(caption: &Caption, level: u16, records: &mut Vec) { if caption.include_margin { caption_attr |= 0x04; } - w.write_u32(caption_attr).unwrap(); - w.write_u32(caption.width).unwrap(); - w.write_i16(caption.spacing).unwrap(); - w.write_u32(caption.max_width).unwrap(); + w.write_u32(caption_attr); + w.write_u32(caption.width); + w.write_i16(caption.spacing); + w.write_u32(caption.max_width); // 예약 필드 8바이트 (한컴 호환성: 원본 파일은 30바이트 LIST_HEADER) - w.write_u32(0).unwrap(); - w.write_u32(0).unwrap(); + w.write_u32(0); + w.write_u32(0); records.push(Record { tag_id: tags::HWPTAG_LIST_HEADER, @@ -636,9 +636,9 @@ fn serialize_header_control(header: &Header, level: u16, records: &mut Vec) { // [Task #1050] hwplib::ForControlFootnote::ctrlHeader 정합 — size=20 형식 // number(UInt4) + before(WChar) + after(WChar) + numberShape(UInt4) + instanceId(UInt4) let mut w = ByteWriter::new(); - w.write_u32(fn_.number as u32).unwrap(); - w.write_u16(fn_.before_decoration_letter).unwrap(); + w.write_u32(fn_.number as u32); + w.write_u16(fn_.before_decoration_letter); let after = if fn_.after_decoration_letter == 0 { 0x0029 // ')' default } else { fn_.after_decoration_letter }; - w.write_u16(after).unwrap(); - w.write_u32(fn_.number_shape).unwrap(); - w.write_u32(fn_.instance_id).unwrap(); + w.write_u16(after); + w.write_u32(fn_.number_shape); + w.write_u32(fn_.instance_id); records.push(make_ctrl_record(tags::CTRL_FOOTNOTE, level, w.as_bytes())); serialize_footnote_endnote_list_header( @@ -719,16 +719,16 @@ fn serialize_footnote(fn_: &Footnote, level: u16, records: &mut Vec) { fn serialize_endnote(en: &Endnote, level: u16, records: &mut Vec) { // [Task #1050] Footnote 와 동일 구조 let mut w = ByteWriter::new(); - w.write_u32(en.number as u32).unwrap(); - w.write_u16(en.before_decoration_letter).unwrap(); + w.write_u32(en.number as u32); + w.write_u16(en.before_decoration_letter); let after = if en.after_decoration_letter == 0 { 0x0029 } else { en.after_decoration_letter }; - w.write_u16(after).unwrap(); - w.write_u32(en.number_shape).unwrap(); - w.write_u32(en.instance_id).unwrap(); + w.write_u16(after); + w.write_u32(en.number_shape); + w.write_u32(en.instance_id); records.push(make_ctrl_record(tags::CTRL_ENDNOTE, level, w.as_bytes())); serialize_footnote_endnote_list_header( @@ -749,9 +749,9 @@ fn serialize_footnote_endnote_list_header( records: &mut Vec, ) { let mut w = ByteWriter::new(); - w.write_i32(paragraphs.len() as i32).unwrap(); - w.write_u32(property).unwrap(); - w.write_bytes(&[0u8; 8]).unwrap(); // 8 byte zero padding + w.write_i32(paragraphs.len() as i32); + w.write_u32(property); + w.write_bytes(&[0u8; 8]); // 8 byte zero padding records.push(Record { tag_id: tags::HWPTAG_LIST_HEADER, level, @@ -857,23 +857,23 @@ fn serialize_page_hide(ph: &PageHide) -> Vec { fn serialize_bookmark(bm: &Bookmark) -> Vec { let mut w = ByteWriter::new(); - w.write_hwp_string(&bm.name).unwrap(); + w.write_hwp_string(&bm.name); w.into_bytes() } /// 글자 겹침 직렬화 (HWP 스펙 표 152) fn serialize_char_overlap(co: &CharOverlap) -> Vec { let mut w = ByteWriter::new(); - w.write_u16(co.chars.len() as u16).unwrap(); + w.write_u16(co.chars.len() as u16); for &ch in &co.chars { - w.write_u16(ch as u16).unwrap(); + w.write_u16(ch as u16); } - w.write_u8(co.border_type).unwrap(); - w.write_i8(co.inner_char_size).unwrap(); - w.write_u8(co.expansion).unwrap(); - w.write_u8(co.char_shape_ids.len() as u8).unwrap(); + w.write_u8(co.border_type); + w.write_i8(co.inner_char_size); + w.write_u8(co.expansion); + w.write_u8(co.char_shape_ids.len() as u8); for &id in &co.char_shape_ids { - w.write_u32(id).unwrap(); + w.write_u32(id); } w.into_bytes() } @@ -929,53 +929,53 @@ fn serialize_picture_control( fn serialize_picture_data(pic: &Picture) -> Vec { let mut w = ByteWriter::new(); - w.write_color_ref(pic.border_color).unwrap(); - w.write_i32(pic.border_width).unwrap(); - w.write_u32(0).unwrap(); // border_attr + w.write_color_ref(pic.border_color); + w.write_i32(pic.border_width); + w.write_u32(0); // border_attr for &x in &pic.border_x { - w.write_i32(x).unwrap(); + w.write_i32(x); } for &y in &pic.border_y { - w.write_i32(y).unwrap(); + w.write_i32(y); } // 자르기 정보 - w.write_i32(pic.crop.left).unwrap(); - w.write_i32(pic.crop.top).unwrap(); - w.write_i32(pic.crop.right).unwrap(); - w.write_i32(pic.crop.bottom).unwrap(); + w.write_i32(pic.crop.left); + w.write_i32(pic.crop.top); + w.write_i32(pic.crop.right); + w.write_i32(pic.crop.bottom); // 안쪽 여백 - w.write_i16(pic.padding.left).unwrap(); - w.write_i16(pic.padding.right).unwrap(); - w.write_i16(pic.padding.top).unwrap(); - w.write_i16(pic.padding.bottom).unwrap(); + w.write_i16(pic.padding.left); + w.write_i16(pic.padding.right); + w.write_i16(pic.padding.top); + w.write_i16(pic.padding.bottom); // 이미지 속성 - w.write_i8(pic.image_attr.brightness).unwrap(); - w.write_i8(pic.image_attr.contrast).unwrap(); + w.write_i8(pic.image_attr.brightness); + w.write_i8(pic.image_attr.contrast); let effect_val: u8 = match pic.image_attr.effect { ImageEffect::RealPic => 0, ImageEffect::GrayScale => 1, ImageEffect::BlackWhite => 2, ImageEffect::Pattern8x8 => 3, }; - w.write_u8(effect_val).unwrap(); - w.write_u16(pic.image_attr.bin_data_id).unwrap(); + w.write_u8(effect_val); + w.write_u16(pic.image_attr.bin_data_id); // 원본 추가 바이트 복원 (라운드트립 보존) if !pic.raw_picture_extra.is_empty() { - w.write_bytes(&pic.raw_picture_extra).unwrap(); + w.write_bytes(&pic.raw_picture_extra); } else { // border_opacity(1) + instance_id(4) + image_effect(4) = 9바이트 - w.write_u8(pic.border_opacity).unwrap(); - w.write_u32(pic.instance_id).unwrap(); - w.write_u32(0).unwrap(); // image_effect_extra - // 원본 이미지 크기(HWPUNIT) + 플래그(1): 한컴 호환 추가 9바이트 - w.write_u32(pic.crop.right as u32).unwrap(); // original width in HWPUNIT - w.write_u32(pic.crop.bottom as u32).unwrap(); // original height in HWPUNIT - w.write_u8(0).unwrap(); // flag + w.write_u8(pic.border_opacity); + w.write_u32(pic.instance_id); + w.write_u32(0); // image_effect_extra + // 원본 이미지 크기(HWPUNIT) + 플래그(1): 한컴 호환 추가 9바이트 + w.write_u32(pic.crop.right as u32); // original width in HWPUNIT + w.write_u32(pic.crop.bottom as u32); // original height in HWPUNIT + w.write_u8(0); // flag } w.into_bytes() @@ -1025,27 +1025,26 @@ fn serialize_shape_control( emit_ctrl_data(records); serialize_text_box_if_present(&line.drawing, level + 2, records); let mut w = ByteWriter::new(); - w.write_i32(line.start.x).unwrap(); - w.write_i32(line.start.y).unwrap(); - w.write_i32(line.end.x).unwrap(); - w.write_i32(line.end.y).unwrap(); + w.write_i32(line.start.x); + w.write_i32(line.start.y); + w.write_i32(line.end.x); + w.write_i32(line.end.y); if let Some(ref conn) = line.connector { // 연결선 확장 데이터 - w.write_u32(conn.link_type as u32).unwrap(); - w.write_u32(conn.start_subject_id).unwrap(); - w.write_u32(conn.start_subject_index).unwrap(); - w.write_u32(conn.end_subject_id).unwrap(); - w.write_u32(conn.end_subject_index).unwrap(); - w.write_u32(conn.control_points.len() as u32).unwrap(); + w.write_u32(conn.link_type as u32); + w.write_u32(conn.start_subject_id); + w.write_u32(conn.start_subject_index); + w.write_u32(conn.end_subject_id); + w.write_u32(conn.end_subject_index); + w.write_u32(conn.control_points.len() as u32); for cp in &conn.control_points { - w.write_i32(cp.x).unwrap(); - w.write_i32(cp.y).unwrap(); - w.write_u16(cp.point_type).unwrap(); + w.write_i32(cp.x); + w.write_i32(cp.y); + w.write_u16(cp.point_type); } - w.write_bytes(&conn.raw_trailing).unwrap(); + w.write_bytes(&conn.raw_trailing); } else { - w.write_i32(if line.started_right_or_bottom { 1 } else { 0 }) - .unwrap(); + w.write_i32(if line.started_right_or_bottom { 1 } else { 0 }); } records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_LINE, @@ -1070,10 +1069,10 @@ fn serialize_shape_control( // 글상자(텍스트) 내용 직렬화 serialize_text_box_if_present(&rect.drawing, level + 2, records); let mut w = ByteWriter::new(); - w.write_u8(rect.round_rate).unwrap(); + w.write_u8(rect.round_rate); for i in 0..4 { - w.write_i32(rect.x_coords[i]).unwrap(); - w.write_i32(rect.y_coords[i]).unwrap(); + w.write_i32(rect.x_coords[i]); + w.write_i32(rect.y_coords[i]); } records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_RECTANGLE, @@ -1101,21 +1100,21 @@ fn serialize_shape_control( emit_ctrl_data(records); serialize_text_box_if_present(&ellipse.drawing, level + 2, records); let mut w = ByteWriter::new(); - w.write_u32(ellipse.attr).unwrap(); - w.write_i32(ellipse.center.x).unwrap(); - w.write_i32(ellipse.center.y).unwrap(); - w.write_i32(ellipse.axis1.x).unwrap(); - w.write_i32(ellipse.axis1.y).unwrap(); - w.write_i32(ellipse.axis2.x).unwrap(); - w.write_i32(ellipse.axis2.y).unwrap(); - w.write_i32(ellipse.start1.x).unwrap(); - w.write_i32(ellipse.start1.y).unwrap(); - w.write_i32(ellipse.end1.x).unwrap(); - w.write_i32(ellipse.end1.y).unwrap(); - w.write_i32(ellipse.start2.x).unwrap(); - w.write_i32(ellipse.start2.y).unwrap(); - w.write_i32(ellipse.end2.x).unwrap(); - w.write_i32(ellipse.end2.y).unwrap(); + w.write_u32(ellipse.attr); + w.write_i32(ellipse.center.x); + w.write_i32(ellipse.center.y); + w.write_i32(ellipse.axis1.x); + w.write_i32(ellipse.axis1.y); + w.write_i32(ellipse.axis2.x); + w.write_i32(ellipse.axis2.y); + w.write_i32(ellipse.start1.x); + w.write_i32(ellipse.start1.y); + w.write_i32(ellipse.end1.x); + w.write_i32(ellipse.end1.y); + w.write_i32(ellipse.start2.x); + w.write_i32(ellipse.start2.y); + w.write_i32(ellipse.end2.x); + w.write_i32(ellipse.end2.y); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_ELLIPSE, level: level + 2, @@ -1142,15 +1141,15 @@ fn serialize_shape_control( emit_ctrl_data(records); serialize_text_box_if_present(&poly.drawing, level + 2, records); let mut w = ByteWriter::new(); - w.write_i32(poly.points.len() as i32).unwrap(); + w.write_i32(poly.points.len() as i32); for p in &poly.points { - w.write_i32(p.x).unwrap(); - w.write_i32(p.y).unwrap(); + w.write_i32(p.x); + w.write_i32(p.y); } if poly.raw_trailing.is_empty() { - w.write_u32(0).unwrap(); + w.write_u32(0); } else { - w.write_bytes(&poly.raw_trailing).unwrap(); + w.write_bytes(&poly.raw_trailing); } records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_POLYGON, @@ -1174,13 +1173,13 @@ fn serialize_shape_control( emit_ctrl_data(records); serialize_text_box_if_present(&arc.drawing, level + 2, records); let mut w = ByteWriter::new(); - w.write_u8(arc.arc_type).unwrap(); - w.write_i32(arc.center.x).unwrap(); - w.write_i32(arc.center.y).unwrap(); - w.write_i32(arc.axis1.x).unwrap(); - w.write_i32(arc.axis1.y).unwrap(); - w.write_i32(arc.axis2.x).unwrap(); - w.write_i32(arc.axis2.y).unwrap(); + w.write_u8(arc.arc_type); + w.write_i32(arc.center.x); + w.write_i32(arc.center.y); + w.write_i32(arc.axis1.x); + w.write_i32(arc.axis1.y); + w.write_i32(arc.axis2.x); + w.write_i32(arc.axis2.y); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_ARC, level: level + 2, @@ -1203,16 +1202,16 @@ fn serialize_shape_control( emit_ctrl_data(records); serialize_text_box_if_present(&curve.drawing, level + 2, records); let mut w = ByteWriter::new(); - w.write_i32(curve.points.len() as i32).unwrap(); + w.write_i32(curve.points.len() as i32); for p in &curve.points { - w.write_i32(p.x).unwrap(); - w.write_i32(p.y).unwrap(); + w.write_i32(p.x); + w.write_i32(p.y); } for &t in &curve.segment_types { - w.write_u8(t).unwrap(); + w.write_u8(t); } // hwplib: sr.skip(4) — 4바이트 패딩 - w.write_u32(0).unwrap(); + w.write_u32(0); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_CURVE, level: level + 2, @@ -1231,7 +1230,7 @@ fn serialize_shape_control( let mut data = serialize_shape_component(0x24636f6e, &group.shape_attr, true); // '$con' // 자식 수 (u16) let mut w = ByteWriter::new(); - w.write_u16(group.children.len() as u16).unwrap(); + w.write_u16(group.children.len() as u16); // 각 자식의 ctrl_id (u32) for child in &group.children { let child_ctrl_id = match child { @@ -1246,10 +1245,10 @@ fn serialize_shape_control( ShapeObject::Chart(c) => c.drawing.shape_attr.ctrl_id, ShapeObject::Ole(o) => o.drawing.shape_attr.ctrl_id, }; - w.write_u32(child_ctrl_id).unwrap(); + w.write_u32(child_ctrl_id); } // instance_id (한컴 호환) - w.write_u32(group.common.instance_id).unwrap(); + w.write_u32(group.common.instance_id); data.extend_from_slice(&w.into_bytes()); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT, @@ -1342,26 +1341,25 @@ fn serialize_group_child( }); serialize_text_box_if_present(&line.drawing, type_level, records); let mut w = ByteWriter::new(); - w.write_i32(line.start.x).unwrap(); - w.write_i32(line.start.y).unwrap(); - w.write_i32(line.end.x).unwrap(); - w.write_i32(line.end.y).unwrap(); + w.write_i32(line.start.x); + w.write_i32(line.start.y); + w.write_i32(line.end.x); + w.write_i32(line.end.y); if let Some(ref conn) = line.connector { - w.write_u32(conn.link_type as u32).unwrap(); - w.write_u32(conn.start_subject_id).unwrap(); - w.write_u32(conn.start_subject_index).unwrap(); - w.write_u32(conn.end_subject_id).unwrap(); - w.write_u32(conn.end_subject_index).unwrap(); - w.write_u32(conn.control_points.len() as u32).unwrap(); + w.write_u32(conn.link_type as u32); + w.write_u32(conn.start_subject_id); + w.write_u32(conn.start_subject_index); + w.write_u32(conn.end_subject_id); + w.write_u32(conn.end_subject_index); + w.write_u32(conn.control_points.len() as u32); for cp in &conn.control_points { - w.write_i32(cp.x).unwrap(); - w.write_i32(cp.y).unwrap(); - w.write_u16(cp.point_type).unwrap(); + w.write_i32(cp.x); + w.write_i32(cp.y); + w.write_u16(cp.point_type); } - w.write_bytes(&conn.raw_trailing).unwrap(); + w.write_bytes(&conn.raw_trailing); } else { - w.write_i32(if line.started_right_or_bottom { 1 } else { 0 }) - .unwrap(); + w.write_i32(if line.started_right_or_bottom { 1 } else { 0 }); } records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_LINE, @@ -1379,10 +1377,10 @@ fn serialize_group_child( }); serialize_text_box_if_present(&rect.drawing, type_level, records); let mut w = ByteWriter::new(); - w.write_u8(rect.round_rate).unwrap(); + w.write_u8(rect.round_rate); for i in 0..4 { - w.write_i32(rect.x_coords[i]).unwrap(); - w.write_i32(rect.y_coords[i]).unwrap(); + w.write_i32(rect.x_coords[i]); + w.write_i32(rect.y_coords[i]); } records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_RECTANGLE, @@ -1404,21 +1402,21 @@ fn serialize_group_child( }); serialize_text_box_if_present(&ellipse.drawing, type_level, records); let mut w = ByteWriter::new(); - w.write_u32(ellipse.attr).unwrap(); - w.write_i32(ellipse.center.x).unwrap(); - w.write_i32(ellipse.center.y).unwrap(); - w.write_i32(ellipse.axis1.x).unwrap(); - w.write_i32(ellipse.axis1.y).unwrap(); - w.write_i32(ellipse.axis2.x).unwrap(); - w.write_i32(ellipse.axis2.y).unwrap(); - w.write_i32(ellipse.start1.x).unwrap(); - w.write_i32(ellipse.start1.y).unwrap(); - w.write_i32(ellipse.end1.x).unwrap(); - w.write_i32(ellipse.end1.y).unwrap(); - w.write_i32(ellipse.start2.x).unwrap(); - w.write_i32(ellipse.start2.y).unwrap(); - w.write_i32(ellipse.end2.x).unwrap(); - w.write_i32(ellipse.end2.y).unwrap(); + w.write_u32(ellipse.attr); + w.write_i32(ellipse.center.x); + w.write_i32(ellipse.center.y); + w.write_i32(ellipse.axis1.x); + w.write_i32(ellipse.axis1.y); + w.write_i32(ellipse.axis2.x); + w.write_i32(ellipse.axis2.y); + w.write_i32(ellipse.start1.x); + w.write_i32(ellipse.start1.y); + w.write_i32(ellipse.end1.x); + w.write_i32(ellipse.end1.y); + w.write_i32(ellipse.start2.x); + w.write_i32(ellipse.start2.y); + w.write_i32(ellipse.end2.x); + w.write_i32(ellipse.end2.y); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_ELLIPSE, level: type_level, @@ -1435,13 +1433,13 @@ fn serialize_group_child( }); serialize_text_box_if_present(&arc.drawing, type_level, records); let mut w = ByteWriter::new(); - w.write_u8(arc.arc_type).unwrap(); - w.write_i32(arc.center.x).unwrap(); - w.write_i32(arc.center.y).unwrap(); - w.write_i32(arc.axis1.x).unwrap(); - w.write_i32(arc.axis1.y).unwrap(); - w.write_i32(arc.axis2.x).unwrap(); - w.write_i32(arc.axis2.y).unwrap(); + w.write_u8(arc.arc_type); + w.write_i32(arc.center.x); + w.write_i32(arc.center.y); + w.write_i32(arc.axis1.x); + w.write_i32(arc.axis1.y); + w.write_i32(arc.axis2.x); + w.write_i32(arc.axis2.y); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_ARC, level: type_level, @@ -1462,15 +1460,15 @@ fn serialize_group_child( }); serialize_text_box_if_present(&poly.drawing, type_level, records); let mut w = ByteWriter::new(); - w.write_i32(poly.points.len() as i32).unwrap(); + w.write_i32(poly.points.len() as i32); for p in &poly.points { - w.write_i32(p.x).unwrap(); - w.write_i32(p.y).unwrap(); + w.write_i32(p.x); + w.write_i32(p.y); } if poly.raw_trailing.is_empty() { - w.write_u32(0).unwrap(); + w.write_u32(0); } else { - w.write_bytes(&poly.raw_trailing).unwrap(); + w.write_bytes(&poly.raw_trailing); } records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_POLYGON, @@ -1492,15 +1490,15 @@ fn serialize_group_child( }); serialize_text_box_if_present(&curve.drawing, type_level, records); let mut w = ByteWriter::new(); - w.write_i32(curve.points.len() as i32).unwrap(); + w.write_i32(curve.points.len() as i32); for p in &curve.points { - w.write_i32(p.x).unwrap(); - w.write_i32(p.y).unwrap(); + w.write_i32(p.x); + w.write_i32(p.y); } for &t in &curve.segment_types { - w.write_u8(t).unwrap(); + w.write_u8(t); } - w.write_u32(0).unwrap(); + w.write_u32(0); records.push(Record { tag_id: tags::HWPTAG_SHAPE_COMPONENT_CURVE, level: type_level, @@ -1583,14 +1581,14 @@ fn serialize_text_box_if_present(drawing: &DrawingObjAttr, level: u16, records: // LIST_HEADER let mut w = ByteWriter::new(); // para_count: 스펙은 INT16이지만 실제 HWP 파일에서는 UINT32로 저장됨 - w.write_u32(text_box.paragraphs.len() as u32).unwrap(); - w.write_u32(text_box.list_attr).unwrap(); + w.write_u32(text_box.paragraphs.len() as u32); + w.write_u32(text_box.list_attr); // 여백 + 최대 폭 (글상자 고유 데이터) - w.write_i16(text_box.margin_left).unwrap(); - w.write_i16(text_box.margin_right).unwrap(); - w.write_i16(text_box.margin_top).unwrap(); - w.write_i16(text_box.margin_bottom).unwrap(); - w.write_u32(text_box.max_width).unwrap(); + w.write_i16(text_box.margin_left); + w.write_i16(text_box.margin_right); + w.write_i16(text_box.margin_top); + w.write_i16(text_box.margin_bottom); + w.write_u32(text_box.max_width); // 원본 추가 바이트 복원 (라운드트립 보존) // [Task #1058] hwplib::ForTextBox::listHeader 정합 — TextBox LIST_HEADER 의 // 마지막 13 byte 필드 contract: @@ -1601,10 +1599,10 @@ fn serialize_text_box_if_present(drawing: &DrawingObjAttr, level: u16, records: // 신규 paragraph (각주) 추가 시 다단계 목록 번호 "1.1.1.1.1.1" 자동 부여. // HWP 출처 IR 은 raw_list_header_extra 에 보존 → HWPX 출처 (raw 부재) 는 default 적용. if !text_box.raw_list_header_extra.is_empty() { - w.write_bytes(&text_box.raw_list_header_extra).unwrap(); + w.write_bytes(&text_box.raw_list_header_extra); } else { // HWPX 출처: 한컴 default 13 byte (zero 8 + editable 0 + fieldName flag 0) - w.write_bytes(&[0u8; 13]).unwrap(); + w.write_bytes(&[0u8; 13]); } records.push(Record { tag_id: tags::HWPTAG_LIST_HEADER, @@ -1630,24 +1628,24 @@ fn serialize_common_obj_attr(common: &CommonObjAttr) -> Vec { } else { pack_common_attr_bits(common) }; - w.write_u32(attr).unwrap(); - w.write_u32(common.vertical_offset).unwrap(); - w.write_u32(common.horizontal_offset).unwrap(); - w.write_u32(common.width).unwrap(); - w.write_u32(common.height).unwrap(); - w.write_i32(common.z_order).unwrap(); - w.write_i16(common.margin.left).unwrap(); - w.write_i16(common.margin.right).unwrap(); - w.write_i16(common.margin.top).unwrap(); - w.write_i16(common.margin.bottom).unwrap(); - w.write_u32(common.instance_id).unwrap(); + w.write_u32(attr); + w.write_u32(common.vertical_offset); + w.write_u32(common.horizontal_offset); + w.write_u32(common.width); + w.write_u32(common.height); + w.write_i32(common.z_order); + w.write_i16(common.margin.left); + w.write_i16(common.margin.right); + w.write_i16(common.margin.top); + w.write_i16(common.margin.bottom); + w.write_u32(common.instance_id); // 쪽나눔 방지 (INT32) - w.write_i32(common.prevent_page_break).unwrap(); + w.write_i32(common.prevent_page_break); // 설명문 (항상 길이 포함, 빈 문자열이면 0) - w.write_hwp_string(&common.description).unwrap(); + w.write_hwp_string(&common.description); // 원본 추가 바이트 복원 (라운드트립 보존) if !common.raw_extra.is_empty() { - w.write_bytes(&common.raw_extra).unwrap(); + w.write_bytes(&common.raw_extra); } w.into_bytes() } @@ -1677,24 +1675,24 @@ fn serialize_drawing_shape_component( write_shape_component_base(&mut w, default_ctrl_id, &drawing.shape_attr, top_level); // 테두리 선 정보 (13바이트: color 4 + width 4 + attr 4 + outline 1) - w.write_color_ref(drawing.border_line.color).unwrap(); - w.write_i32(drawing.border_line.width).unwrap(); - w.write_u32(drawing.border_line.attr).unwrap(); - w.write_u8(drawing.border_line.outline_style).unwrap(); + w.write_color_ref(drawing.border_line.color); + w.write_i32(drawing.border_line.width); + w.write_u32(drawing.border_line.attr); + w.write_u8(drawing.border_line.outline_style); // 채우기 정보 serialize_shape_fill(&mut w, &drawing.fill); // 그림자 정보 (16바이트) - w.write_u32(drawing.shadow_type).unwrap(); - w.write_color_ref(drawing.shadow_color).unwrap(); - w.write_i32(drawing.shadow_offset_x).unwrap(); - w.write_i32(drawing.shadow_offset_y).unwrap(); + w.write_u32(drawing.shadow_type); + w.write_color_ref(drawing.shadow_color); + w.write_i32(drawing.shadow_offset_x); + w.write_i32(drawing.shadow_offset_y); // 인스턴스 ID (4바이트) + 예약 (1바이트) + 그림자 투명도 (1바이트) - w.write_u32(drawing.inst_id).unwrap(); - w.write_u8(0).unwrap(); // 예약 - w.write_u8(drawing.shadow_alpha).unwrap(); + w.write_u32(drawing.inst_id); + w.write_u8(0); // 예약 + w.write_u8(drawing.shadow_alpha); w.into_bytes() } @@ -1718,20 +1716,20 @@ fn write_shape_component_base( top_level }; - w.write_u32(actual_id).unwrap(); + w.write_u32(actual_id); if is_two { - w.write_u32(actual_id).unwrap(); + w.write_u32(actual_id); } // ShapeComponentAttr - w.write_i32(attr.offset_x).unwrap(); - w.write_i32(attr.offset_y).unwrap(); - w.write_u16(attr.group_level).unwrap(); - w.write_u16(attr.local_file_version).unwrap(); - w.write_u32(attr.original_width).unwrap(); - w.write_u32(attr.original_height).unwrap(); - w.write_u32(attr.current_width).unwrap(); - w.write_u32(attr.current_height).unwrap(); + w.write_i32(attr.offset_x); + w.write_i32(attr.offset_y); + w.write_u16(attr.group_level); + w.write_u16(attr.local_file_version); + w.write_u32(attr.original_width); + w.write_u32(attr.original_height); + w.write_u32(attr.current_width); + w.write_u32(attr.current_height); // flip: 원본 전체 값 사용 (상위 비트 보존) let flip = if attr.flip != 0 { @@ -1746,59 +1744,59 @@ fn write_shape_component_base( } f }; - w.write_u32(flip).unwrap(); + w.write_u32(flip); - w.write_i16(attr.rotation_angle).unwrap(); - w.write_i32(attr.rotation_center.x).unwrap(); - w.write_i32(attr.rotation_center.y).unwrap(); + w.write_i16(attr.rotation_angle); + w.write_i32(attr.rotation_center.x); + w.write_i32(attr.rotation_center.y); // Rendering 정보 (원본이 있으면 복원, 없으면 적절한 행렬 생성) if !attr.raw_rendering.is_empty() { - w.write_bytes(&attr.raw_rendering).unwrap(); + w.write_bytes(&attr.raw_rendering); } else if has_explicit_rendering_matrix(attr) { write_parsed_rendering_matrix(w, attr); } else { let is_group_child = attr.group_level > 0; let cnt: u16 = if is_group_child { 2 } else { 1 }; - w.write_u16(cnt).unwrap(); + w.write_u16(cnt); // translation matrix [1, 0, tx, 0, 1, ty] - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(attr.offset_x as f64).unwrap(); // tx (그룹 자식: 로컬 offset) - w.write_f64(0.0).unwrap(); - w.write_f64(1.0).unwrap(); - w.write_f64(attr.offset_y as f64).unwrap(); // ty - // scale matrix = identity [1, 0, 0, 0, 1, 0] - // (스케일은 current_width/original_width 값으로 표현 — 행렬에 중복 기록하면 이중 적용됨) - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); + w.write_f64(1.0); + w.write_f64(0.0); + w.write_f64(attr.offset_x as f64); // tx (그룹 자식: 로컬 offset) + w.write_f64(0.0); + w.write_f64(1.0); + w.write_f64(attr.offset_y as f64); // ty + // scale matrix = identity [1, 0, 0, 0, 1, 0] + // (스케일은 current_width/original_width 값으로 표현 — 행렬에 중복 기록하면 이중 적용됨) + w.write_f64(1.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(1.0); + w.write_f64(0.0); // rotation matrix = identity [1, 0, 0, 0, 1, 0] - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); + w.write_f64(1.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(1.0); + w.write_f64(0.0); // 그룹 자식 (cnt=2): 두 번째 scale + rotation 세트 (identity) if is_group_child { // scale2 = identity - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); + w.write_f64(1.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(1.0); + w.write_f64(0.0); // rotation2 = identity - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(0.0).unwrap(); - w.write_f64(1.0).unwrap(); - w.write_f64(0.0).unwrap(); + w.write_f64(1.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(0.0); + w.write_f64(1.0); + w.write_f64(0.0); } } } @@ -1816,7 +1814,7 @@ fn has_explicit_rendering_matrix(attr: &ShapeComponentAttr) -> bool { fn write_matrix(w: &mut ByteWriter, matrix: [f64; 6]) { for value in matrix { - w.write_f64(value).unwrap(); + w.write_f64(value); } } @@ -1826,7 +1824,7 @@ fn write_parsed_rendering_matrix(w: &mut ByteWriter, attr: &ShapeComponentAttr) // // Store the parsed affine transform so reload reconstructs exactly: // [sx, b, tx; c, sy, ty] = [1,0,tx;0,1,ty] x I x [sx,b,0;c,sy,0] - w.write_u16(1).unwrap(); + w.write_u16(1); write_matrix(w, [1.0, 0.0, attr.render_tx, 0.0, 1.0, attr.render_ty]); write_matrix( w, @@ -1850,40 +1848,40 @@ fn serialize_shape_fill(w: &mut ByteWriter, fill: &Fill) { FillType::Image => 2, FillType::Gradient => 4, }; - w.write_u32(fill_type_val).unwrap(); + w.write_u32(fill_type_val); if fill_type_val == 0 { // 채우기 없음: 4바이트 추가 (additional_size = 0) - w.write_u32(0).unwrap(); + w.write_u32(0); return; } // bit 0: 단색 채우기 if fill_type_val & 0x01 != 0 { if let Some(ref solid) = fill.solid { - w.write_color_ref(solid.background_color).unwrap(); - w.write_color_ref(solid.pattern_color).unwrap(); - w.write_i32(solid.pattern_type).unwrap(); + w.write_color_ref(solid.background_color); + w.write_color_ref(solid.pattern_color); + w.write_i32(solid.pattern_type); } } // bit 2: 그라데이션 채우기 (parse_fill 형식: kind=u8, angle/cx/cy/blur/count=u32) if fill_type_val & 0x04 != 0 { if let Some(ref grad) = fill.gradient { - w.write_u8(grad.gradient_type as u8).unwrap(); - w.write_u32(grad.angle as u32).unwrap(); - w.write_u32(grad.center_x as u32).unwrap(); - w.write_u32(grad.center_y as u32).unwrap(); - w.write_u32(grad.blur as u32).unwrap(); - w.write_u32(grad.colors.len() as u32).unwrap(); + w.write_u8(grad.gradient_type as u8); + w.write_u32(grad.angle as u32); + w.write_u32(grad.center_x as u32); + w.write_u32(grad.center_y as u32); + w.write_u32(grad.blur as u32); + w.write_u32(grad.colors.len() as u32); // change_points: count > 2일 때만 기록 if grad.colors.len() > 2 { for &pos in &grad.positions { - w.write_i32(pos).unwrap(); + w.write_i32(pos); } } for &color in &grad.colors { - w.write_color_ref(color).unwrap(); + w.write_color_ref(color); } } } @@ -1909,32 +1907,31 @@ fn serialize_shape_fill(w: &mut ByteWriter, fill: &Fill) { ImageFillMode::RightBottom => 14, ImageFillMode::None => 15, }; - w.write_u8(mode_val).unwrap(); - w.write_i8(img.brightness).unwrap(); - w.write_i8(img.contrast).unwrap(); - w.write_u8(img.effect).unwrap(); - w.write_u16(img.bin_data_id).unwrap(); + w.write_u8(mode_val); + w.write_i8(img.brightness); + w.write_i8(img.contrast); + w.write_u8(img.effect); + w.write_u16(img.bin_data_id); } } // 추가 속성. 그라데이션은 HWP5 fill contract상 blurring center 1바이트를 가진다. if fill_type_val & 0x04 != 0 { - w.write_u32(1).unwrap(); - w.write_u8(fill.gradient.as_ref().map(|g| g.step_center).unwrap_or(0)) - .unwrap(); + w.write_u32(1); + w.write_u8(fill.gradient.as_ref().map(|g| g.step_center).unwrap_or(0)); } else { - w.write_u32(0).unwrap(); + w.write_u32(0); } // alpha 바이트 (채우기 종류별 각 1바이트) if fill_type_val & 0x01 != 0 { - w.write_u8(fill.alpha).unwrap(); + w.write_u8(fill.alpha); } if fill_type_val & 0x04 != 0 { - w.write_u8(fill.alpha).unwrap(); + w.write_u8(fill.alpha); } if fill_type_val & 0x02 != 0 { - w.write_u8(fill.alpha).unwrap(); + w.write_u8(fill.alpha); } } @@ -1945,8 +1942,8 @@ fn serialize_list_header_with_paragraphs( records: &mut Vec, ) { let mut w = ByteWriter::new(); - w.write_u16(paragraphs.len() as u16).unwrap(); - w.write_u32(0).unwrap(); // list_attr + w.write_u16(paragraphs.len() as u16); + w.write_u32(0); // list_attr records.push(Record { tag_id: tags::HWPTAG_LIST_HEADER, @@ -2003,15 +2000,15 @@ fn build_header_footer_list_header( ext_flags: u16, ) -> Vec { let mut w = ByteWriter::new(); - w.write_u16(para_count).unwrap(); - w.write_u32(list_attr).unwrap(); - w.write_u16(0).unwrap(); - w.write_u32(text_width).unwrap(); - w.write_u32(text_height).unwrap(); - w.write_u8(text_ref).unwrap(); - w.write_u8(num_ref).unwrap(); - w.write_u16(ext_flags).unwrap(); - w.write_bytes(&[0u8; 14]).unwrap(); + w.write_u16(para_count); + w.write_u32(list_attr); + w.write_u16(0); + w.write_u32(text_width); + w.write_u32(text_height); + w.write_u8(text_ref); + w.write_u8(num_ref); + w.write_u16(ext_flags); + w.write_bytes(&[0u8; 14]); w.into_bytes() } @@ -2034,21 +2031,21 @@ fn serialize_equation_control(eq: &Equation, level: u16, records: &mut Vec Vec { return raw.clone(); } let mut w = ByteWriter::new(); - w.write_u16(props.section_count).unwrap(); - w.write_u16(props.page_start_num).unwrap(); - w.write_u16(props.footnote_start_num).unwrap(); - w.write_u16(props.endnote_start_num).unwrap(); - w.write_u16(props.picture_start_num).unwrap(); - w.write_u16(props.table_start_num).unwrap(); - w.write_u16(props.equation_start_num).unwrap(); + w.write_u16(props.section_count); + w.write_u16(props.page_start_num); + w.write_u16(props.footnote_start_num); + w.write_u16(props.endnote_start_num); + w.write_u16(props.picture_start_num); + w.write_u16(props.table_start_num); + w.write_u16(props.equation_start_num); // 캐럿 위치 정보 (스펙: 전체 26바이트) - w.write_u32(props.caret_list_id).unwrap(); - w.write_u32(props.caret_para_id).unwrap(); - w.write_u32(props.caret_char_pos).unwrap(); + w.write_u32(props.caret_list_id); + w.write_u32(props.caret_para_id); + w.write_u32(props.caret_char_pos); w.into_bytes() } @@ -156,7 +156,7 @@ pub fn serialize_id_mappings(doc_info: &DocInfo) -> Vec { let mut w = ByteWriter::new(); // bin_data_count - w.write_u32(doc_info.bin_data_list.len() as u32).unwrap(); + w.write_u32(doc_info.bin_data_list.len() as u32); // font_counts (7개 언어) for lang_idx in 0..7 { @@ -165,62 +165,62 @@ pub fn serialize_id_mappings(doc_info: &DocInfo) -> Vec { } else { 0 }; - w.write_u32(count).unwrap(); + w.write_u32(count); } // border_fill_count - w.write_u32(doc_info.border_fills.len() as u32).unwrap(); + w.write_u32(doc_info.border_fills.len() as u32); // char_shape_count - w.write_u32(doc_info.char_shapes.len() as u32).unwrap(); + w.write_u32(doc_info.char_shapes.len() as u32); // tab_def_count - w.write_u32(doc_info.tab_defs.len() as u32).unwrap(); + w.write_u32(doc_info.tab_defs.len() as u32); // numbering_count - w.write_u32(doc_info.numberings.len() as u32).unwrap(); + w.write_u32(doc_info.numberings.len() as u32); // bullet_count (파싱된 bullets 배열 크기 우선, 없으면 보존값) let bullet_count = if doc_info.bullets.is_empty() { doc_info.bullet_count } else { doc_info.bullets.len() as u32 }; - w.write_u32(bullet_count).unwrap(); + w.write_u32(bullet_count); // para_shape_count - w.write_u32(doc_info.para_shapes.len() as u32).unwrap(); + w.write_u32(doc_info.para_shapes.len() as u32); // style_count - w.write_u32(doc_info.styles.len() as u32).unwrap(); + w.write_u32(doc_info.styles.len() as u32); // memo_shape_count (5.0.2.x 이후, 파싱 시 보존된 값 사용) - w.write_u32(doc_info.memo_shape_count).unwrap(); + w.write_u32(doc_info.memo_shape_count); // 최근 한컴 생성 HWP는 ID_MAPPINGS 뒤쪽 reserved count 2개까지 포함한 // 18개 u32 테이블을 쓴다. HWPX에서 DocInfo를 새로 구성할 때도 이 // 길이를 맞춰 한컴의 DocInfo contract와 동일한 형태로 직렬화한다. - w.write_u32(0).unwrap(); - w.write_u32(0).unwrap(); + w.write_u32(0); + w.write_u32(0); w.into_bytes() } pub fn serialize_bin_data(bin_data: &BinData) -> Vec { let mut w = ByteWriter::new(); - w.write_u16(bin_data.attr).unwrap(); + w.write_u16(bin_data.attr); match bin_data.data_type { BinDataType::Link => { if let Some(ref abs_path) = bin_data.abs_path { - w.write_hwp_string(abs_path).unwrap(); + w.write_hwp_string(abs_path); } else { - w.write_hwp_string("").unwrap(); + w.write_hwp_string(""); } if let Some(ref rel_path) = bin_data.rel_path { - w.write_hwp_string(rel_path).unwrap(); + w.write_hwp_string(rel_path); } else { - w.write_hwp_string("").unwrap(); + w.write_hwp_string(""); } } BinDataType::Embedding | BinDataType::Storage => { - w.write_u16(bin_data.storage_id).unwrap(); + w.write_u16(bin_data.storage_id); if let Some(ref ext) = bin_data.extension { - w.write_hwp_string(ext).unwrap(); + w.write_hwp_string(ext); } else { - w.write_hwp_string("").unwrap(); + w.write_hwp_string(""); } } } @@ -242,19 +242,19 @@ pub fn serialize_face_name(font: &Font) -> Vec { if font.default_name.is_some() { attr |= 0x20; } - w.write_u8(attr).unwrap(); + w.write_u8(attr); - w.write_hwp_string(&font.name).unwrap(); + w.write_hwp_string(&font.name); if let Some(ref alt_name) = font.alt_name { - w.write_u8(font.alt_type & 0x03).unwrap(); - w.write_hwp_string(alt_name).unwrap(); + w.write_u8(font.alt_type & 0x03); + w.write_hwp_string(alt_name); } if let Some(type_info) = font.type_info { - w.write_bytes(&type_info).unwrap(); + w.write_bytes(&type_info); } if let Some(ref default_name) = font.default_name { - w.write_hwp_string(default_name).unwrap(); + w.write_hwp_string(default_name); } w.into_bytes() @@ -306,20 +306,19 @@ fn image_fill_mode_to_u8(mode: ImageFillMode) -> u8 { pub fn serialize_border_fill(bf: &BorderFill) -> Vec { let mut w = ByteWriter::new(); - w.write_u16(bf.attr).unwrap(); + w.write_u16(bf.attr); // 4방향 테두리 (인터리브: 종류 + 굵기 + 색상) for border in &bf.borders { - w.write_u8(border_line_type_to_u8(border.line_type)) - .unwrap(); - w.write_u8(border.width).unwrap(); - w.write_color_ref(border.color).unwrap(); + w.write_u8(border_line_type_to_u8(border.line_type)); + w.write_u8(border.width); + w.write_color_ref(border.color); } // 대각선 - w.write_u8(bf.diagonal.diagonal_type).unwrap(); - w.write_u8(bf.diagonal.width).unwrap(); - w.write_color_ref(bf.diagonal.color).unwrap(); + w.write_u8(bf.diagonal.diagonal_type); + w.write_u8(bf.diagonal.width); + w.write_color_ref(bf.diagonal.color); // 채우기 serialize_fill(&mut w, &bf.fill); @@ -334,55 +333,54 @@ fn serialize_fill(w: &mut ByteWriter, fill: &crate::model::style::Fill) { FillType::Image => 2, FillType::Gradient => 4, }; - w.write_u32(fill_type_val).unwrap(); + w.write_u32(fill_type_val); match fill.fill_type { FillType::Solid => { if let Some(ref solid) = fill.solid { - w.write_color_ref(solid.background_color).unwrap(); - w.write_color_ref(solid.pattern_color).unwrap(); - w.write_i32(solid.pattern_type).unwrap(); + w.write_color_ref(solid.background_color); + w.write_color_ref(solid.pattern_color); + w.write_i32(solid.pattern_type); } // 추가 채우기 속성: size(u32) + alpha(u8) - w.write_u32(1).unwrap(); - w.write_u8(0).unwrap(); // alpha + w.write_u32(1); + w.write_u8(0); // alpha } FillType::Gradient => { if let Some(ref grad) = fill.gradient { - w.write_u8(grad.gradient_type as u8).unwrap(); - w.write_u32(grad.angle as u32).unwrap(); - w.write_u32(grad.center_x as u32).unwrap(); - w.write_u32(grad.center_y as u32).unwrap(); - w.write_u32(grad.blur as u32).unwrap(); - w.write_u32(grad.colors.len() as u32).unwrap(); + w.write_u8(grad.gradient_type as u8); + w.write_u32(grad.angle as u32); + w.write_u32(grad.center_x as u32); + w.write_u32(grad.center_y as u32); + w.write_u32(grad.blur as u32); + w.write_u32(grad.colors.len() as u32); if grad.colors.len() > 2 { for &pos in &grad.positions { - w.write_i32(pos).unwrap(); + w.write_i32(pos); } } for &color in &grad.colors { - w.write_color_ref(color).unwrap(); + w.write_color_ref(color); } } - w.write_u32(1).unwrap(); - w.write_u8(fill.gradient.as_ref().map(|g| g.step_center).unwrap_or(0)) - .unwrap(); - w.write_u8(fill.alpha).unwrap(); + w.write_u32(1); + w.write_u8(fill.gradient.as_ref().map(|g| g.step_center).unwrap_or(0)); + w.write_u8(fill.alpha); } FillType::Image => { if let Some(ref img) = fill.image { - w.write_u8(image_fill_mode_to_u8(img.fill_mode)).unwrap(); - w.write_i8(img.brightness).unwrap(); - w.write_i8(img.contrast).unwrap(); - w.write_u8(img.effect).unwrap(); - w.write_u16(img.bin_data_id).unwrap(); + w.write_u8(image_fill_mode_to_u8(img.fill_mode)); + w.write_i8(img.brightness); + w.write_i8(img.contrast); + w.write_u8(img.effect); + w.write_u16(img.bin_data_id); } // 추가 채우기 속성: size(u32) - w.write_u32(0).unwrap(); + w.write_u32(0); } FillType::None => { // 추가 채우기 속성: size(u32) = 0 - w.write_u32(0).unwrap(); + w.write_u32(0); } } } @@ -392,26 +390,26 @@ pub fn serialize_char_shape(cs: &CharShape) -> Vec { // font_ids (7 × u16) for &id in &cs.font_ids { - w.write_u16(id).unwrap(); + w.write_u16(id); } // ratios (7 × u8) for &ratio in &cs.ratios { - w.write_u8(ratio).unwrap(); + w.write_u8(ratio); } // spacings (7 × i8) for &spacing in &cs.spacings { - w.write_i8(spacing).unwrap(); + w.write_i8(spacing); } // relative_sizes (7 × u8) for &size in &cs.relative_sizes { - w.write_u8(size).unwrap(); + w.write_u8(size); } // char_offsets (7 × i8) for &offset in &cs.char_offsets { - w.write_i8(offset).unwrap(); + w.write_i8(offset); } // base_size - w.write_i32(cs.base_size).unwrap(); + w.write_i32(cs.base_size); // attr: 원본 비트를 기반으로, 모델링된 필드 반영 let mut attr = cs.attr; // bit 0: italic @@ -485,32 +483,32 @@ pub fn serialize_char_shape(cs: &CharShape) -> Vec { } else { attr &= !(1u32 << 30); } - w.write_u32(attr).unwrap(); + w.write_u32(attr); // shadow offsets (i8 × 2) - w.write_i8(cs.shadow_offset_x).unwrap(); - w.write_i8(cs.shadow_offset_y).unwrap(); + w.write_i8(cs.shadow_offset_x); + w.write_i8(cs.shadow_offset_y); // colors - w.write_color_ref(cs.text_color).unwrap(); - w.write_color_ref(cs.underline_color).unwrap(); - w.write_color_ref(cs.shade_color).unwrap(); - w.write_color_ref(cs.shadow_color).unwrap(); + w.write_color_ref(cs.text_color); + w.write_color_ref(cs.underline_color); + w.write_color_ref(cs.shade_color); + w.write_color_ref(cs.shadow_color); // 글자 테두리/배경 ID (5.0.2.1 이상) - w.write_u16(cs.border_fill_id).unwrap(); + w.write_u16(cs.border_fill_id); // 취소선 색 (5.0.3.0 이상) - w.write_color_ref(cs.strike_color).unwrap(); + w.write_color_ref(cs.strike_color); w.into_bytes() } pub fn serialize_tab_def(td: &TabDef) -> Vec { let mut w = ByteWriter::new(); - w.write_u32(td.attr).unwrap(); - w.write_u32(td.tabs.len() as u32).unwrap(); + w.write_u32(td.attr); + w.write_u32(td.tabs.len() as u32); for tab in &td.tabs { - w.write_u32(tab.position).unwrap(); - w.write_u8(tab.tab_type).unwrap(); - w.write_u8(tab.fill_type).unwrap(); - w.write_zeros(2).unwrap(); // 예약 + w.write_u32(tab.position); + w.write_u8(tab.tab_type); + w.write_u8(tab.fill_type); + w.write_zeros(2); // 예약 } w.into_bytes() } @@ -521,26 +519,26 @@ fn serialize_numbering(numbering: &Numbering) -> Vec { // 수준별(1~7) 문단 머리 정보 + 번호 형식 문자열 for level in 0..7 { let head = &numbering.heads[level]; - w.write_u32(head.attr).unwrap(); - w.write_i16(head.width_adjust).unwrap(); - w.write_i16(head.text_distance).unwrap(); - w.write_u32(head.char_shape_id).unwrap(); + w.write_u32(head.attr); + w.write_i16(head.width_adjust); + w.write_i16(head.text_distance); + w.write_u32(head.char_shape_id); // 번호 형식 문자열 let fmt_str = &numbering.level_formats[level]; let utf16: Vec = fmt_str.encode_utf16().collect(); - w.write_u16(utf16.len() as u16).unwrap(); + w.write_u16(utf16.len() as u16); for &ch in &utf16 { - w.write_u16(ch).unwrap(); + w.write_u16(ch); } } // 시작 번호 - w.write_u16(numbering.start_number).unwrap(); + w.write_u16(numbering.start_number); // 수준별 시작 번호 (5.0.2.5 이상) for level in 0..7 { - w.write_u32(numbering.level_start_numbers[level]).unwrap(); + w.write_u32(numbering.level_start_numbers[level]); } w.into_bytes() @@ -551,23 +549,23 @@ fn serialize_bullet(bullet: &Bullet) -> Vec { let mut w = ByteWriter::new(); // 문단 머리 정보 (8바이트) - w.write_u32(bullet.attr).unwrap(); - w.write_i16(bullet.width_adjust).unwrap(); - w.write_i16(bullet.text_distance).unwrap(); + w.write_u32(bullet.attr); + w.write_i16(bullet.width_adjust); + w.write_i16(bullet.text_distance); // 글머리표 문자 (WCHAR) - w.write_u16(bullet.bullet_char as u16).unwrap(); + w.write_u16(bullet.bullet_char as u16); // 이미지 글머리표 여부 (INT32) - w.write_i32(bullet.image_bullet).unwrap(); + w.write_i32(bullet.image_bullet); // 이미지 글머리 데이터 (4바이트) for &byte in &bullet.image_data { - w.write_u8(byte).unwrap(); + w.write_u8(byte); } // 체크 글머리표 문자 (WCHAR) - w.write_u16(bullet.check_bullet_char as u16).unwrap(); + w.write_u16(bullet.check_bullet_char as u16); w.into_bytes() } @@ -605,41 +603,41 @@ pub fn serialize_para_shape(ps: &ParaShape) -> Vec { // bits 25-27: para_level attr1 &= !(0x07 << 25); attr1 |= (ps.para_level as u32 & 0x07) << 25; - w.write_u32(attr1).unwrap(); - w.write_i32(ps.margin_left).unwrap(); - w.write_i32(ps.margin_right).unwrap(); - w.write_i32(ps.indent).unwrap(); - w.write_i32(ps.spacing_before).unwrap(); - w.write_i32(ps.spacing_after).unwrap(); - w.write_i32(ps.line_spacing).unwrap(); - w.write_u16(ps.tab_def_id).unwrap(); - w.write_u16(ps.numbering_id).unwrap(); - w.write_u16(ps.border_fill_id).unwrap(); + w.write_u32(attr1); + w.write_i32(ps.margin_left); + w.write_i32(ps.margin_right); + w.write_i32(ps.indent); + w.write_i32(ps.spacing_before); + w.write_i32(ps.spacing_after); + w.write_i32(ps.line_spacing); + w.write_u16(ps.tab_def_id); + w.write_u16(ps.numbering_id); + w.write_u16(ps.border_fill_id); for &spacing in &ps.border_spacing { - w.write_i16(spacing).unwrap(); + w.write_i16(spacing); } // 속성2 (5.0.1.7 이상) - w.write_u32(ps.attr2).unwrap(); + w.write_u32(ps.attr2); // 속성3 - 줄 간격 종류 확장 (5.0.2.5 이상) - w.write_u32(ps.attr3).unwrap(); + w.write_u32(ps.attr3); // 줄 간격 (5.0.2.5 이상) - w.write_u32(ps.line_spacing_v2).unwrap(); + w.write_u32(ps.line_spacing_v2); // 한컴 편집기가 현재 HWP5 저장 시 붙이는 PARA_SHAPE 말미 4바이트. // // 공개 스펙 표 43은 전체 길이를 54바이트로 적지만, 한컴이 HWPX를 HWP로 // 내보낸 정답지들은 PARA_SHAPE를 58바이트로 저장한다. 이 tail이 없으면 // 한컴 편집기가 일부 masterpage/header 글상자 내부 줄나눔 폭을 다르게 // 해석해 페이지 번호가 다음 줄로 밀리는 사례가 있다. - w.write_u32(0).unwrap(); + w.write_u32(0); w.into_bytes() } pub fn serialize_style(style: &Style) -> Vec { let mut w = ByteWriter::new(); - w.write_hwp_string(&style.local_name).unwrap(); - w.write_hwp_string(&style.english_name).unwrap(); - w.write_u8(style.style_type).unwrap(); - w.write_u8(style.next_style_id).unwrap(); + w.write_hwp_string(&style.local_name); + w.write_hwp_string(&style.english_name); + w.write_u8(style.style_type); + w.write_u8(style.next_style_id); // [Task #1058 후속] HWP5 spec 표 47 정합 — lang_id (INT16) 추가. // 누락 시 ps_id/cs_id 가 2 byte 앞당겨져 한컴이 잘못된 ParaShape 적용 → // 신규 각주 추가 시 본문 paragraph 의 ParaShape (60.0 pt 여백 + 160% 줄간격) 부여. @@ -648,13 +646,13 @@ pub fn serialize_style(style: &Style) -> Vec { } else { style.lang_id }; - w.write_i16(lang_id).unwrap(); - w.write_u16(style.para_shape_id).unwrap(); - w.write_u16(style.char_shape_id).unwrap(); + w.write_i16(lang_id); + w.write_u16(style.para_shape_id); + w.write_u16(style.char_shape_id); // [Task #1058 후속] 한컴 정답지 STYLE record 마지막 2 byte zero — 스펙 미문서화 영역. // footnote-01.hwp 의 모든 STYLE record 가 끝에 0x0000 (UINT16) 보유. // 누락 시 record size mismatch + DocInfo record 순서 shift. - w.write_u16(0).unwrap(); + w.write_u16(0); w.into_bytes() } @@ -850,8 +848,7 @@ pub fn surgical_insert_font_all_langs(raw_stream: &mut Vec, data: &[u8]) -> let end_offset = if fn_idx > 0 && fn_idx <= face_recs.len() { let rec = face_recs[fn_idx - 1]; rec.header_offset + rec.total_bytes - } else if !face_recs.is_empty() { - let last = face_recs.last().unwrap(); + } else if let Some(last) = face_recs.last() { last.header_offset + last.total_bytes } else { // FACE_NAME이 없으면 BIN_DATA 뒤 또는 ID_MAPPINGS 뒤 diff --git a/src/serializer/hwpx/canonical_defaults.rs b/src/serializer/hwpx/canonical_defaults.rs index b4054159c..c158e34a7 100644 --- a/src/serializer/hwpx/canonical_defaults.rs +++ b/src/serializer/hwpx/canonical_defaults.rs @@ -210,6 +210,7 @@ mod tests { use super::*; #[test] + #[allow(clippy::assertions_on_constants)] fn snap_to_grid_is_the_only_true_default() { assert!(PARASHAPE_SNAP_TO_GRID); assert!(!PARASHAPE_FONT_LINE_HEIGHT); diff --git a/src/serializer/hwpx/field.rs b/src/serializer/hwpx/field.rs index 4b3dec954..5e66ab90f 100644 --- a/src/serializer/hwpx/field.rs +++ b/src/serializer/hwpx/field.rs @@ -189,7 +189,7 @@ mod tests { } #[test] - fn footnote_emits_autoNum() { + fn footnote_emits_auto_num() { let xml = to_string(|w| { write_footnote_open(w, 3)?; write_footnote_close(w) diff --git a/src/serializer/hwpx/mod.rs b/src/serializer/hwpx/mod.rs index 07fb34b2e..619b5ff12 100644 --- a/src/serializer/hwpx/mod.rs +++ b/src/serializer/hwpx/mod.rs @@ -345,8 +345,7 @@ mod tests { para.char_offsets = vec![0, 9, 18]; para.char_count = 20; para.controls.push(Control::ColumnDef(ColumnDef::default())); - para.controls - .push(Control::Table(Box::new(Table::default()))); + para.controls.push(Control::Table(Box::default())); para.controls.push(Control::Equation(Box::new(Equation { common: CommonObjAttr { width: 1000, @@ -698,8 +697,7 @@ mod tests { para.text = "A".to_string(); para.char_offsets = vec![8]; para.char_count = 10; - para.controls - .push(Control::Table(Box::new(Table::default()))); + para.controls.push(Control::Table(Box::default())); section.paragraphs.push(para); doc.sections.push(section); diff --git a/src/serializer/hwpx/section.rs b/src/serializer/hwpx/section.rs index 563e138d8..0dcc5e9dd 100644 --- a/src/serializer/hwpx/section.rs +++ b/src/serializer/hwpx/section.rs @@ -69,8 +69,7 @@ pub fn write_section( }; vert_cursor = first_advance; - let mut out = EMPTY_SECTION_XML.replacen(TEXT_SLOT, &first_t, 1); - out = replace_first_linesegs(&out, &first_linesegs); + let mut out = EMPTY_SECTION_XML.to_string(); // 첫 문단 `` 태그를 IR 기반 속성으로 교체 if let Some(p) = first_para { @@ -81,13 +80,11 @@ pub fn write_section( // 템플릿에서 TEXT_SLOT 이 있던 자리 바로 앞의 패턴. let first_run_cs = first_run_char_shape_id(p); let new_run = format!(r#""#, first_run_cs); - let replacement = format!("{}{}", new_run, &first_t); - // 이미 first_t 는 out 에 들어갔으므로 그 직전의 만 변경 - let anchor = format!("{}{}", r#""#, &first_t); - if out.contains(&anchor) { - out = out.replacen(&anchor, &replacement, 1); - } + let new_run_with_slot = format!("{}{}", new_run, TEXT_SLOT); + out = out.replacen(TEMPLATE_RUN_BEFORE_TEXT, &new_run_with_slot, 1); } + out = out.replacen(TEXT_SLOT, &first_t, 1); + out = replace_first_linesegs(&out, &first_linesegs)?; // 추가 문단: `` 직전에 `` 요소를 삽입. if section.paragraphs.len() > 1 { @@ -633,20 +630,20 @@ fn push_lineseg_static(out: &mut String, textpos: u32, vertpos: u32) { )); } -fn replace_first_linesegs(xml: &str, new_inner: &str) -> String { - let open = xml - .find(LINESEG_SLOT_OPEN) - .expect("template has linesegarray"); +fn replace_first_linesegs(xml: &str, new_inner: &str) -> Result { + let open = xml.find(LINESEG_SLOT_OPEN).ok_or_else(|| { + SerializeError::XmlError("section template missing linesegarray start tag".to_string()) + })?; let inner_start = open + LINESEG_SLOT_OPEN.len(); - let close_rel = xml[inner_start..] - .find(LINESEG_SLOT_CLOSE) - .expect("template has closing linesegarray"); + let close_rel = xml[inner_start..].find(LINESEG_SLOT_CLOSE).ok_or_else(|| { + SerializeError::XmlError("section template missing linesegarray end tag".to_string()) + })?; let inner_end = inner_start + close_rel; let mut out = String::with_capacity(xml.len() + new_inner.len()); out.push_str(&xml[..inner_start]); out.push_str(new_inner); out.push_str(&xml[inner_end..]); - out + Ok(out) } // `TEMPLATE_RUN_BEFORE_TEXT` 는 패턴 인식용 상수로만 쓰이므로 명시 참조. @@ -797,6 +794,13 @@ mod tests { &xml[xml.find("", "").unwrap_err(); + assert!(matches!(err, SerializeError::XmlError(_))); + assert!(err.to_string().contains("linesegarray start tag")); + } + #[test] fn task177_multiple_linesegs_preserved_in_order() { let mut para = Paragraph::default(); diff --git a/src/wasm_api/tests.rs b/src/wasm_api/tests.rs index a011ba5d0..775b2ea85 100644 --- a/src/wasm_api/tests.rs +++ b/src/wasm_api/tests.rs @@ -418,7 +418,7 @@ fn test_insert_text_in_cell() { assert!(json.contains("\"ok\":true")); assert!(json.contains("\"charOffset\":3")); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells[0].paragraphs[0].text, "셀추가A"); } else { panic!("표 컨트롤을 찾을 수 없음"); @@ -433,7 +433,7 @@ fn test_delete_text_in_cell() { let json = result.unwrap(); assert!(json.contains("\"ok\":true")); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells[1].paragraphs[0].text, "B"); } else { panic!("표 컨트롤을 찾을 수 없음"); @@ -474,14 +474,14 @@ fn test_insert_and_delete_roundtrip_in_cell() { let result = doc.insert_text_in_cell_native(0, 0, 0, 2, 0, 2, "테스트"); assert!(result.is_ok()); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells[2].paragraphs[0].text, "셀C테스트"); } let result = doc.delete_text_in_cell_native(0, 0, 0, 2, 0, 2, 3); assert!(result.is_ok()); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells[2].paragraphs[0].text, "셀C"); } } @@ -493,7 +493,7 @@ fn test_svg_render_with_table_after_cell_edit() { doc.insert_text_in_cell_native(0, 0, 0, 3, 0, 2, "수정됨") .unwrap(); // 삽입 후 셀 텍스트 확인 - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells[3].paragraphs[0].text, "셀D수정됨"); } let svg = doc.render_page_svg_native(0); @@ -556,7 +556,7 @@ fn test_insert_table_row_below() { assert!(json.contains("\"rowCount\":3")); assert!(json.contains("\"colCount\":2")); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.row_count, 3); assert_eq!(table.cells.len(), 6); // 원래 첫 행의 셀A는 여전히 행 0 @@ -579,7 +579,7 @@ fn test_insert_table_column_right() { assert!(json.contains("\"rowCount\":2")); assert!(json.contains("\"colCount\":3")); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.col_count, 3); assert_eq!(table.cells.len(), 6); } else { @@ -596,7 +596,7 @@ fn test_merge_table_cells() { let json = result.unwrap(); assert!(json.contains("\"cellCount\":3")); // 비주 셀 1개 제거 - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells.len(), 3); // 비주 셀 제거됨 let merged = &table.cells[0]; assert_eq!(merged.col_span, 2); @@ -611,7 +611,7 @@ fn test_split_table_cell() { let mut doc = create_doc_with_table(); // 먼저 병합 doc.merge_table_cells_native(0, 0, 0, 0, 0, 0, 1).unwrap(); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells.len(), 3); } @@ -621,7 +621,7 @@ fn test_split_table_cell() { let json = result.unwrap(); assert!(json.contains("\"cellCount\":4")); - if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.get(0) { + if let Some(Control::Table(table)) = doc.document.sections[0].paragraphs[0].controls.first() { assert_eq!(table.cells.len(), 4); let cell = &table.cells[0]; assert_eq!(cell.col_span, 1); @@ -632,7 +632,7 @@ fn test_split_table_cell() { } #[test] -fn test_merge_then_control_layout_has_colSpan() { +fn test_merge_then_control_layout_has_col_span() { let mut doc = create_doc_with_table(); // 병합 전: colSpan=1 let layout_before = doc.get_page_control_layout_native(0).unwrap(); @@ -3543,7 +3543,7 @@ fn test_docinfo_roundtrip_charshape_preservation() { if let Ok(cdoc) = HwpDocument::from_bytes(&d) { eprintln!( " {} → char_shapes={} para_shapes={} border_fills={} styles={}", - cf.split('/').last().unwrap_or(cf), + cf.rsplit('/').next().unwrap_or(cf), cdoc.document.doc_info.char_shapes.len(), cdoc.document.doc_info.para_shapes.len(), cdoc.document.doc_info.border_fills.len(), @@ -4590,7 +4590,7 @@ fn test_pasted_table_structure_analysis() { if t.ctrl_header.data.len() >= 8 { let obj_attr = u32::from_le_bytes(t.ctrl_header.data[4..8].try_into().unwrap()); eprintln!(" obj_attr = 0x{:08X}", obj_attr); - let vert_offset = (obj_attr >> 0) & 0x3; + let vert_offset = obj_attr & 0x3; let horiz_offset = (obj_attr >> 2) & 0x3; let vert_rel = (obj_attr >> 4) & 0x3; let horiz_rel = (obj_attr >> 7) & 0x3; @@ -6808,7 +6808,7 @@ fn test_rp006_dangling_references() { // LIST_HEADER의 border_fill_id (셀) 및 TABLE의 border_fill_id if rec.tag_id == tags::HWPTAG_LIST_HEADER && rec.data.len() >= 34 { let bf_id = u16::from_le_bytes([rec.data[32], rec.data[33]]) as usize; - if bf_id > 0 && bf_id - 1 >= bf_count { + if bf_id > bf_count { dangling_bf.push((ri, "LIST_HEADER", bf_id)); } } @@ -6943,7 +6943,7 @@ fn test_template_comparison() { } let bytes = std::fs::read(path).unwrap(); - let mut cfb = CfbReader::open(&bytes).expect(&format!("{} CFB 열기 실패", label)); + let mut cfb = CfbReader::open(&bytes).unwrap_or_else(|_| panic!("{} CFB 열기 실패", label)); // DocInfo 분석 let doc_info_data = cfb.read_doc_info(true).expect("DocInfo 읽기 실패"); @@ -7323,8 +7323,8 @@ fn test_complex_comparison() { eprintln!("\n --- Record-by-record comparison ---"); eprintln!( - " {:<6} {:<25} {:<6} {:<8} | {:<25} {:<6} {:<8} | {}", - "Idx", "Damaged Tag", "Lvl", "Size", "Fixed Tag", "Lvl", "Size", "Differences" + " {:<6} {:<25} {:<6} {:<8} | {:<25} {:<6} {:<8} | Differences", + "Idx", "Damaged Tag", "Lvl", "Size", "Fixed Tag", "Lvl", "Size" ); eprintln!(" {}", "-".repeat(120)); @@ -9358,8 +9358,8 @@ fn test_step2_comparison() { eprintln!(" Total record data bytes: {}", total_data_bytes); eprintln!( - "\n{:<5} {:<5} {:<25} {:>8} {}", - "Idx", "Lvl", "Tag", "Size", "Details" + "\n{:<5} {:<5} {:<25} {:>8} Details", + "Idx", "Lvl", "Tag", "Size" ); eprintln!("{:-<120}", ""); @@ -9562,8 +9562,8 @@ fn test_step2_comparison() { let mut total_diffs = 0; eprintln!( - "\n{:<5} | {:<30} {:>4} {:>6} | {:<30} {:>4} {:>6} | {}", - "#", "VALID Tag", "Lvl", "Size", "DAMAGED Tag", "Lvl", "Size", "Status" + "\n{:<5} | {:<30} {:>4} {:>6} | {:<30} {:>4} {:>6} | Status", + "#", "VALID Tag", "Lvl", "Size", "DAMAGED Tag", "Lvl", "Size" ); eprintln!("{:-<130}", ""); @@ -10739,8 +10739,7 @@ fn test_empty_save_analysis() { let ch = u16::from_le_bytes([data[i], data[i + 1]]); match ch { // Extended controls take 8 WCHARs (16 bytes) - 0x0001 | 0x0002 | 0x0003 | 0x000B | 0x000C | 0x000D | 0x000E | 0x000F | 0x0004 - | 0x0005 | 0x0006 | 0x0007 | 0x0008 | 0x0009 | 0x000A => { + 0x0001..=0x000F => { let name = match ch { 0x0002 => "SEC/COL", 0x0003 => "FIELD_BEGIN", @@ -10753,7 +10752,7 @@ fn test_empty_save_analysis() { }; result.push_str(&format!("[{}@{}]", name, char_pos)); // Extended controls (2,3,11,12,13,14,15) occupy 8 WCHARs - if ch >= 1 && ch <= 9 { + if (1..=9).contains(&ch) { // These chars occupy 8 WCHARs (16 bytes) i += 16; char_pos += 8; @@ -10801,9 +10800,9 @@ fn test_empty_save_analysis() { // textStartPos(4) + lineVPos(4) + lineHPos(4) + lineHeight(4) // + textPartHeight(4) + distBaseline(4) + lineSpacing(4) + colStartPos(4) + segWidth(4) // Some versions use 32 bytes per segment - let seg_size = if data.len() % 36 == 0 { + let seg_size = if data.len().is_multiple_of(36) { 36 - } else if data.len() % 32 == 0 { + } else if data.len().is_multiple_of(32) { 32 } else { 36 @@ -11301,7 +11300,7 @@ fn test_text_insert_detailed_diff() { let mut doc = HwpDocument::from_bytes(&orig_data).unwrap(); // 텍스트 삽입 - doc.insert_text_native(0, 0, 0, "가나다라마바사아"); + doc.insert_text_native(0, 0, 0, "가나다라마바사아").unwrap(); let saved = doc.export_hwp_native().unwrap(); // 레코드 파싱 @@ -15432,8 +15431,8 @@ fn test_diag_msb_pattern_kwater() { let para_count = section.paragraphs.len(); eprintln!("\n Section {} ({} paragraphs)", si, para_count); eprintln!( - " {:>4} | {:>5} | {:>3} | {:>5} | {:>3} | {:>8} | {}", - "idx", "cc", "msb", "psid", "sid", "ctrl", "text_preview" + " {:>4} | {:>5} | {:>3} | {:>5} | {:>3} | {:>8} | text_preview", + "idx", "cc", "msb", "psid", "sid", "ctrl" ); eprintln!(" {}", "-".repeat(65)); @@ -15780,7 +15779,7 @@ fn test_textbox_render_tree_debug() { let data = std::fs::read(path).unwrap(); let mut doc = HwpDocument::from_bytes(&data).unwrap(); - doc.convert_to_editable_native(); + doc.convert_to_editable_native().unwrap(); // 문서 구조 확인: Shape 컨트롤 찾기 let mut shape_found = false; @@ -16720,7 +16719,7 @@ fn test_task81_vertical_cell_text() { .collect(); let sec_mt = doc .measured_tables - .get(0) + .first() .map(|v| v.as_slice()) .unwrap_or(&[]); let tree = engine.build_render_tree( @@ -17625,7 +17624,7 @@ fn test_task110_treatise_diag() { eprintln!("\n=== 2단 영역 편집: insert_text_native(0, 14, 0, \"Y\") ==="); let col_idx_14_before = doc .para_column_map - .get(0) + .first() .and_then(|m| m.get(14)) .copied() .unwrap_or(0); @@ -17941,7 +17940,7 @@ fn test_blank2020_enter_corruption_diagnosis() { } let bytes = std::fs::read(path).unwrap(); - let mut cfb = CfbReader::open(&bytes).expect(&format!("{} CFB 열기 실패", label)); + let mut cfb = CfbReader::open(&bytes).unwrap_or_else(|_| panic!("{} CFB 열기 실패", label)); eprintln!("\n{}", "=".repeat(80)); eprintln!(" {} ({} bytes)", label, bytes.len()); diff --git a/src/wmf/parser/constants/enums/character_set.rs b/src/wmf/parser/constants/enums/character_set.rs index dd67a5b60..a3d5f5884 100644 --- a/src/wmf/parser/constants/enums/character_set.rs +++ b/src/wmf/parser/constants/enums/character_set.rs @@ -1,4 +1,5 @@ use crate::wmf::imports::*; +use std::sync::OnceLock; /// The CharacterSet Enumeration defines the possible sets of character glyphs /// that are defined in fonts for graphics output. @@ -59,19 +60,14 @@ impl From for &'static encoding_rs::Encoding { } } -static SYMBOL_CHARSET_TABLE_INITIALIZED: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); -static mut SYMBOL_CHARSET_TABLE: BTreeMap = BTreeMap::new(); +static SYMBOL_CHARSET_TABLE: OnceLock> = OnceLock::new(); #[rustfmt::skip] pub(in crate::wmf::parser) fn symbol_charset_table( ) -> &'static BTreeMap { - if !SYMBOL_CHARSET_TABLE_INITIALIZED.load(core::sync::atomic::Ordering::Acquire) { - SYMBOL_CHARSET_TABLE_INITIALIZED.store(true, core::sync::atomic::Ordering::Release); - - unsafe { - // via: https://en.wikipedia.org/wiki/Symbol_(typeface) - SYMBOL_CHARSET_TABLE = BTreeMap::from_iter([ + SYMBOL_CHARSET_TABLE.get_or_init(|| { + // via: https://en.wikipedia.org/wiki/Symbol_(typeface) + BTreeMap::from_iter([ // 2x (0x20, ' '), (0x21, '!'), (0x22, '∀'), (0x23, '#'), (0x24, '∃'), (0x25, '%'), (0x26, '&'), (0x27, '∍'), @@ -132,52 +128,33 @@ pub(in crate::wmf::parser) fn symbol_charset_table( (0xF5, '⌡'), (0xF6, '⎞'), (0xF7, '⎟'), (0xF8, '⎠'), (0xF9, '⎤'), (0xFA, '⎥'), (0xFB, '⎦'), (0xFC, '⎫'), (0xFD, '⎬'), (0xFE, '⎭'), - ]); - } - } - - // SAFETY: Mutable access is not possible after state has been - // initialized. - #[allow(clippy::deref_addrof)] - unsafe { &*&raw const SYMBOL_CHARSET_TABLE } + ]) + }) } -static CODEPAGE_TABLE_INITIALIZED: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); -static mut CODEPAGE_TABLE: BTreeMap = BTreeMap::new(); +static CODEPAGE_TABLE: OnceLock> = OnceLock::new(); fn codepage_table() -> &'static BTreeMap { - if !CODEPAGE_TABLE_INITIALIZED.load(core::sync::atomic::Ordering::Acquire) { - CODEPAGE_TABLE_INITIALIZED.store(true, core::sync::atomic::Ordering::Release); - - unsafe { - // via: https://en.wikipedia.org/wiki/Code_page - CODEPAGE_TABLE = BTreeMap::from_iter([ - (CharacterSet::ANSI_CHARSET, 1252), - (CharacterSet::SYMBOL_CHARSET, 42), - (CharacterSet::SHIFTJIS_CHARSET, 932), - (CharacterSet::HANGUL_CHARSET, 949), - (CharacterSet::JOHAB_CHARSET, 1361), - (CharacterSet::GB2312_CHARSET, 936), - (CharacterSet::CHINESEBIG5_CHARSET, 950), - (CharacterSet::GREEK_CHARSET, 1253), - (CharacterSet::TURKISH_CHARSET, 1254), - (CharacterSet::VIETNAMESE_CHARSET, 1258), - (CharacterSet::HEBREW_CHARSET, 1255), - (CharacterSet::ARABIC_CHARSET, 1256), - (CharacterSet::BALTIC_CHARSET, 1257), - (CharacterSet::RUSSIAN_CHARSET, 1251), - (CharacterSet::THAI_CHARSET, 874), - (CharacterSet::EASTEUROPE_CHARSET, 1250), - (CharacterSet::OEM_CHARSET, 437), - ]); - } - } - - // SAFETY: Mutable access is not possible after state has been - // initialized. - #[allow(clippy::deref_addrof)] - unsafe { - &*&raw const CODEPAGE_TABLE - } + CODEPAGE_TABLE.get_or_init(|| { + // via: https://en.wikipedia.org/wiki/Code_page + BTreeMap::from_iter([ + (CharacterSet::ANSI_CHARSET, 1252), + (CharacterSet::SYMBOL_CHARSET, 42), + (CharacterSet::SHIFTJIS_CHARSET, 932), + (CharacterSet::HANGUL_CHARSET, 949), + (CharacterSet::JOHAB_CHARSET, 1361), + (CharacterSet::GB2312_CHARSET, 936), + (CharacterSet::CHINESEBIG5_CHARSET, 950), + (CharacterSet::GREEK_CHARSET, 1253), + (CharacterSet::TURKISH_CHARSET, 1254), + (CharacterSet::VIETNAMESE_CHARSET, 1258), + (CharacterSet::HEBREW_CHARSET, 1255), + (CharacterSet::ARABIC_CHARSET, 1256), + (CharacterSet::BALTIC_CHARSET, 1257), + (CharacterSet::RUSSIAN_CHARSET, 1251), + (CharacterSet::THAI_CHARSET, 874), + (CharacterSet::EASTEUROPE_CHARSET, 1250), + (CharacterSet::OEM_CHARSET, 437), + ]) + }) } diff --git a/src/wmf/parser/constants/mod.rs b/src/wmf/parser/constants/mod.rs index e73563483..6f9115773 100644 --- a/src/wmf/parser/constants/mod.rs +++ b/src/wmf/parser/constants/mod.rs @@ -8,44 +8,42 @@ pub use self::{enums::*, flags::*}; #[rustfmt::skip] macro_rules! impl_parser { ($t:ident,u8) => { - $crate::wmf::parser::constants::impl_parser!(_, $t, u8, 1, 4); + $crate::wmf::parser::constants::impl_parser!(_, $t, read_u8_from_le_bytes, 4); }; ($t:ident,u16) => { - $crate::wmf::parser::constants::impl_parser!(_, $t, u16, 2, 6); + $crate::wmf::parser::constants::impl_parser!(_, $t, read_u16_from_le_bytes, 6); }; ($t:ident,u32) => { - $crate::wmf::parser::constants::impl_parser!(_, $t, u32, 4, 10); + $crate::wmf::parser::constants::impl_parser!(_, $t, read_u32_from_le_bytes, 10); }; ($t:ident,i32) => { - $crate::wmf::parser::constants::impl_parser!(_, $t, i32, 4, 10); + $crate::wmf::parser::constants::impl_parser!(_, $t, read_i32_from_le_bytes, 10); }; - (_, $t:ident, $raw:ty, $size:expr, $digits:expr) => { - paste::paste! { - impl $t { - #[cfg_attr(feature = "tracing", ::tracing::instrument( - level = tracing::Level::TRACE, - skip_all, - err(level = tracing::Level::ERROR, Display), - ))] - pub fn parse( - buf: &mut R, - ) -> Result<(Self, usize), $crate::wmf::parser::ParseError> { - let (value, consumed_bytes) = crate::wmf::parser::[](buf)?; - let Some(v) = Self::from_repr(value) else { - return Err($crate::wmf::parser::ParseError::UnexpectedEnumValue { - cause: format!( - ::core::concat!( - "unexpected value as ", - ::core::stringify!($t), - ": {:#0", $digits, "X}", - ), - value + (_, $t:ident, $read_fn:ident, $digits:expr) => { + impl $t { + #[cfg_attr(feature = "tracing", ::tracing::instrument( + level = tracing::Level::TRACE, + skip_all, + err(level = tracing::Level::ERROR, Display), + ))] + pub fn parse( + buf: &mut R, + ) -> Result<(Self, usize), $crate::wmf::parser::ParseError> { + let (value, consumed_bytes) = crate::wmf::parser::$read_fn(buf)?; + let Some(v) = Self::from_repr(value) else { + return Err($crate::wmf::parser::ParseError::UnexpectedEnumValue { + cause: format!( + ::core::concat!( + "unexpected value as ", + ::core::stringify!($t), + ": {:#0", $digits, "X}", ), - }); - }; + value + ), + }); + }; - Ok((v, consumed_bytes)) - } + Ok((v, consumed_bytes)) } } }; diff --git a/src/wmf/parser/mod.rs b/src/wmf/parser/mod.rs index 7b86ae2e0..8c0840d55 100644 --- a/src/wmf/parser/mod.rs +++ b/src/wmf/parser/mod.rs @@ -71,22 +71,27 @@ pub fn read_variable( } macro_rules! impl_from_le_bytes { - ($(($t:ty, $n:expr)),+) => { - paste::paste!{ - $( - pub fn []( - buf: &mut R, - ) -> Result<($t, usize), ReadError> { - let (bytes, consumed_bytes) = read::(buf)?; - - Ok((<$t>::from_le_bytes(bytes), consumed_bytes)) - } - )* - } + ($(($t:ty, $n:expr, $read_fn:ident)),+) => { + $( + pub fn $read_fn( + buf: &mut R, + ) -> Result<($t, usize), ReadError> { + let (bytes, consumed_bytes) = read::(buf)?; + + Ok((<$t>::from_le_bytes(bytes), consumed_bytes)) + } + )* }; } -impl_from_le_bytes! {(i8, 1), (i16, 2), (i32, 4), (u8, 1), (u16, 2), (u32, 4) } +impl_from_le_bytes! { + (i8, 1, read_i8_from_le_bytes), + (i16, 2, read_i16_from_le_bytes), + (i32, 4, read_i32_from_le_bytes), + (u8, 1, read_u8_from_le_bytes), + (u16, 2, read_u16_from_le_bytes), + (u32, 4, read_u32_from_le_bytes) +} /// Converts the given byte slice to a UTF-8 string using the specified /// character set. diff --git a/tests/hwpx_to_hwp_adapter.rs b/tests/hwpx_to_hwp_adapter.rs index a6f68fadd..0ee8eadf9 100644 --- a/tests/hwpx_to_hwp_adapter.rs +++ b/tests/hwpx_to_hwp_adapter.rs @@ -1223,16 +1223,16 @@ fn task903_para29_shape_pair_mut<'a>( .expect("target section0 para29 필요"); let positive_shape = positive .sections - .get(0) + .first() .and_then(|section| section.paragraphs.get(29)) - .and_then(|para| para.controls.get(0)) + .and_then(|para| para.controls.first()) .and_then(|ctrl| match ctrl { Control::Shape(shape) => Some(shape.as_ref()), _ => None, }) .expect("positive section0 para29 ctrl0 shape 필요"); - let is_target_shape = matches!(target_para.controls.get(0), Some(Control::Shape(_))); + let is_target_shape = matches!(target_para.controls.first(), Some(Control::Shape(_))); assert!(is_target_shape, "target section0 para29 ctrl0 shape 필요"); (target_para, positive_shape) } @@ -1247,12 +1247,12 @@ fn task903_target_shape_mut(para: &mut Paragraph) -> &mut ShapeObject { fn task903_copy_para29_ctrl_data_record(target_para: &mut Paragraph, positive: &Document) { let positive_para = positive .sections - .get(0) + .first() .and_then(|section| section.paragraphs.get(29)) .expect("positive section0 para29 필요"); let positive_ctrl_data = positive_para .ctrl_data_records - .get(0) + .first() .cloned() .unwrap_or(None); @@ -1265,12 +1265,12 @@ fn task903_copy_para29_ctrl_data_record(target_para: &mut Paragraph, positive: & fn task903_copy_para29_shape_control(target_para: &mut Paragraph, positive: &Document) { let positive_para = positive .sections - .get(0) + .first() .and_then(|section| section.paragraphs.get(29)) .expect("positive section0 para29 필요"); target_para.controls[0] = positive_para .controls - .get(0) + .first() .expect("positive para29 ctrl0 필요") .clone(); task903_copy_para29_ctrl_data_record(target_para, positive); @@ -3839,7 +3839,7 @@ fn task903_shape_picture_extra(data: &[u8]) -> &[u8] { fn task903_decode_shape_picture_extra(data: &[u8]) -> String { let extra = task903_shape_picture_extra(data); - let border_opacity = extra.get(0).copied().unwrap_or(0); + let border_opacity = extra.first().copied().unwrap_or(0); let instance_id = task903_read_u32_at(data, 74).unwrap_or(0); let image_effect_extra = task903_read_u32_at(data, 78).unwrap_or(0); let original_width = task903_read_u32_at(data, 82).unwrap_or(0); diff --git a/tests/issue_1058_textbox_list_header.rs b/tests/issue_1058_textbox_list_header.rs index eebcf51dd..83578cf92 100644 --- a/tests/issue_1058_textbox_list_header.rs +++ b/tests/issue_1058_textbox_list_header.rs @@ -9,7 +9,7 @@ //! (src/parser/hwpx/header.rs::parse_para_shape) //! 3. **Style lang_id (INT16) + trailing 2 byte** — 각주 추가 시 왼쪽 여백 60.0pt 부작용 해소 //! (src/model/style.rs, src/parser/doc_info.rs, src/serializer/doc_info.rs, -//! src/parser/hwpx/header.rs::parse_style) +//! src/parser/hwpx/header.rs::parse_style) //! 4. **HWPX `` 파싱 추가** — 일반 문단 자동 글머리표 부작용 해소 //! (src/parser/hwpx/header.rs, parse_bullet_hwpx) //! diff --git a/tests/issue_884_charshape_diagnostic.rs b/tests/issue_884_charshape_diagnostic.rs index f93157c20..4bab16ecb 100644 --- a/tests/issue_884_charshape_diagnostic.rs +++ b/tests/issue_884_charshape_diagnostic.rs @@ -4,12 +4,12 @@ //! " 충남중부권지사장" 의 char_shape: //! - (start_pos=0, id=14 HY헤드라인M 26pt) //! - (start_pos=9, id=20 HY수평선B 16pt) -//! char_offsets = [8,9,…,16] — 선두 인라인 그림이 stream offset 0~7 을 점유. +//! char_offsets = [8,9,…,16] — 선두 인라인 그림이 stream offset 0~7 을 점유. //! //! `start_pos` 는 paragraph 텍스트의 UTF-16 stream offset 이다 (해석 A): //! - start_pos=0 → offset 0 이상 첫 가시문자 = visible[0](" ") → id=14 //! - start_pos=9 → offset 9 이상 첫 가시문자 = visible[1]("충") → id=20 -//! ⇒ "충남중부권지사장" = id=20 HY수평선B 16pt. +//! ⇒ "충남중부권지사장" = id=20 HY수평선B 16pt. //! //! 이력: #884 가 이 footer 를 "26pt HY헤드라인M" 으로 오진(HY수평선B 굵은 //! 글꼴 + 로고 옆 배치로 인한 육안 착시)하고 `start_pos` 를 visible char diff --git a/tests/svg_snapshot.rs b/tests/svg_snapshot.rs index 00f8bbc29..bd5d707c1 100644 --- a/tests/svg_snapshot.rs +++ b/tests/svg_snapshot.rs @@ -1,9 +1,9 @@ //! SVG snapshot regression tests for HWPX rendering. //! -//! Pure-Rust replacement for `tools/verify_hwpx.py`, which requires Windows -//! + Hancom Office + pyhwpx and cannot run in CI. This harness invokes -//! `rhwp::wasm_api::HwpDocument::render_page_svg_native()` directly so the -//! same SVG the CLI produces is diffed against committed golden files. +//! Pure-Rust replacement for `tools/verify_hwpx.py`, which requires Windows, +//! Hancom Office, and pyhwpx and cannot run in CI. This harness invokes +//! `rhwp::wasm_api::HwpDocument::render_page_svg_native()` directly so the same +//! SVG the CLI produces is diffed against committed golden files. //! //! # Updating goldens //! diff --git a/web/certs/README.md b/web/certs/README.md new file mode 100644 index 000000000..b6cf0a62b --- /dev/null +++ b/web/certs/README.md @@ -0,0 +1,7 @@ +# Local HTTPS certificates + +This directory is a placeholder for development-only localhost TLS certificates. + +Do not commit generated certificate or key files. `web/https_server.py` creates +`localhost-cert.pem` and `localhost-key.pem` under `~/.cache/rhwp/certs` by +default. Set `RHWP_CERT_DIR` only if you need a custom local path. diff --git a/web/certs/localhost-cert.pem b/web/certs/localhost-cert.pem deleted file mode 100644 index e9d11392d..000000000 --- a/web/certs/localhost-cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDJTCCAg2gAwIBAgIUVm/3gfVPUCElde3j67h2No+XvQ4wDQYJKoZIhvcNAQEL -BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDIxMDExMzgxOVoXDTI3MDIx -MDExMzgxOVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAq7KqUK/42NPkC35Q7nFSD5ZJLpxAW2k+ZrOR20yuk0ug -1VMYjuWcK5DyRoQl1rhYSznNKIDLRpY60M1UGOw0ZOWCjOQ+R0u3Sp0WhqhuV+Bf -GIzZo9bditYVnvWLIHSsG+81nQ0s9Q2tTtqRnYzz5CviOAV9L9+IuX6eVitg0SWO -beGyjwHeikmqutb4Tpn9B1FENIGOsGCdksFsrE+N2EvnmlWzy3O0eWvSyDnnlgQG -ZWpQCNPMqnlA/il2SH5EALIeMp2Klm57RUSaLVVSXOO2LlwMGE/LRaoOi6vDcnZH -UtlbLc5jG6EjXPgy5BgGxCLgJdPq4Tk1llmqTDwzsQIDAQABo28wbTAdBgNVHQ4E -FgQUCtJKDj/JHbAkEQKJiTIbEv3n42QwHwYDVR0jBBgwFoAUCtJKDj/JHbAkEQKJ -iTIbEv3n42QwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH -BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAFy0qXglDgUFf+cDBkZk6z5JG+ZpSEwG -ic47Fbyd/5t5bbfDDQpkcWqIuWwLK6HxqeyGHtheXPAQu/V1sLkzVMWmcdw9tw9Z -ViJxFaULoMowof6aiwhYWSXrFcAGWBD9Vd+6VEqpgd3vqZNdiyEdJAFTgONlsFbu -qVeaiE8mBUq3Oj6XUlq0eIHVN6D0fzQuSCW/JX48jUPJ96vgdlTxmBoxiSnjHTgI -RV5hCfAE1pduBGaX3S2V6QdyAIX7KwRfIUQTy57xvO7afguyAk4OomZPydmDEg1b -RT4+d0zB/s1eUshk6udDqONYGLP9uC4bhCyW8UGL7xVvpSi+XpLe9W4= ------END CERTIFICATE----- diff --git a/web/certs/localhost-key.pem b/web/certs/localhost-key.pem deleted file mode 100644 index 24b50288e..000000000 --- a/web/certs/localhost-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCrsqpQr/jY0+QL -flDucVIPlkkunEBbaT5ms5HbTK6TS6DVUxiO5ZwrkPJGhCXWuFhLOc0ogMtGljrQ -zVQY7DRk5YKM5D5HS7dKnRaGqG5X4F8YjNmj1t2K1hWe9YsgdKwb7zWdDSz1Da1O -2pGdjPPkK+I4BX0v34i5fp5WK2DRJY5t4bKPAd6KSaq61vhOmf0HUUQ0gY6wYJ2S -wWysT43YS+eaVbPLc7R5a9LIOeeWBAZlalAI08yqeUD+KXZIfkQAsh4ynYqWbntF -RJotVVJc47YuXAwYT8tFqg6Lq8NydkdS2VstzmMboSNc+DLkGAbEIuAl0+rhOTWW -WapMPDOxAgMBAAECggEAANs9SHW/WgH2VeWsghzZstKmMJNT1RKrUFhFZN0TfyyW -EB3ji3hBRvAhepECLKQ53IRq2HCmr1urlTW0iB+8mWNZAj4CF7zyDjZUmOCuo1PL -ZaJlVGZSoXSmk9ifBiRrSDzU/S7ih6Hv1OrbgjEArmysRxOzfNFrgPFbLppndBx9 -EpKDwUqzYOiuAy+DPGoIGXyC2ftkAq2LAdx74o8ljs55oPRMUgJLJmcm1Ycsd9/t -Ine7oUihMiUumEsP7x46QB5ZfiHyAJrFBObF2XeRC/8YQpC3ol7eW1aG1/iCPRFI -jf72geO8B703BfBuRSln0k979VkkEo53N7Egf31dgQKBgQDrdxr+3N6v/lm3nwUf -zfF478PU6xNaAA60v3iOL3B8WM8SrKEPnwgE9EBcQCaVLsKb6oqTl2t+vlcbjYeg -8m6extSEzeez2e6sOZ8ShiCiQiYTGEDUNWwJBcH6X7xfEaNk+yxoI32OamhFpLmn -s/2EfpfZ0NRkWVcofFkEXe56SQKBgQC6q+r9/BeE0uV7ewnm1JfBNNVkof8uBZjQ -Dq4BhXYJLlz9wCquM2xYIhVvu+5BkkfCwNnqoqEC1IRql+I4kRruDFJUnxpSvPBa -hzKab0boIvu9SIGRAitpgYVCxT3j7UsWa4t9P+bomJQoWG7CrMo04EdKP5n/MojG -q2WckoWuKQKBgQDhsyoyL75YziJrSscbpEkY1iJNXgMPmsx93qaNC9XzqyDZ0pUT -pymFriZfBrK0/rxVvWyVamgEdiiC5Zei4567HPUqU0AMehI4DYd3tw9i2KssVGwX -vRlQUK+N8eRa8EpBii0XqSU5tK88SlgW8wVH0hx2EaD0i1prGT4uXGcx2QKBgQCR -Y0LEDGq47ZUEJmEg4mVLen3VQgLAcmWBbl2CG9NB9VVB4UCcqrpwriMqAiHf+jFf -8toG0CE9Vg3IA4GS8xkZXIlnFP/ofvVoXLJB6/c657xPwUAAk8MCCnmi9oJsmi2I -Sby7kWCjuepyveWORAu9QQzlPEIRq+cxbN7YIppxeQKBgQCxiaVf/gjPKbTUsp6p -THCQbybXnA9JivLGLVdwNTTCy1w8tI4ULc6YB5a6gKPLyaUzU5at6xc1vsEHZxCK -T7sFSKJ3K2vVdzTnQma8RkRAI31VbL+UZdGJhT/PKvpal5nqHfXcVgglM+bDvHaG -p24/fN1nW9aLy9ifkXePWYJeew== ------END PRIVATE KEY----- diff --git a/web/https_server.py b/web/https_server.py index 6ec08ac88..dfaf33085 100644 --- a/web/https_server.py +++ b/web/https_server.py @@ -1,19 +1,62 @@ #!/usr/bin/env python3 """로컬 HTTPS 개발 서버 (Clipboard API text/html 읽기용)""" import http.server -import ssl import os +import shutil +import ssl +import subprocess import sys PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 7700 BIND = '0.0.0.0' -CERT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certs') +DEFAULT_CERT_DIR = os.path.join(os.path.expanduser('~'), '.cache', 'rhwp', 'certs') +CERT_DIR = os.environ.get('RHWP_CERT_DIR', DEFAULT_CERT_DIR) CERTFILE = os.path.join(CERT_DIR, 'localhost-cert.pem') KEYFILE = os.path.join(CERT_DIR, 'localhost-key.pem') + +def ensure_local_cert(): + if os.path.exists(CERTFILE) and os.path.exists(KEYFILE): + return + + openssl = shutil.which('openssl') + if openssl is None: + raise SystemExit( + '로컬 HTTPS 인증서 생성에 필요한 openssl을 설치한 뒤 다시 실행하세요.' + ) + + os.makedirs(CERT_DIR, exist_ok=True) + subprocess.run( + [ + openssl, + 'req', + '-x509', + '-newkey', + 'rsa:2048', + '-sha256', + '-days', + '825', + '-nodes', + '-keyout', + KEYFILE, + '-out', + CERTFILE, + '-subj', + '/CN=localhost', + '-addext', + 'subjectAltName=DNS:localhost,IP:127.0.0.1,IP:::1', + ], + check=True, + ) + os.chmod(KEYFILE, 0o600) + os.chmod(CERTFILE, 0o644) + + # 프로젝트 루트에서 서빙 (../pkg/ 경로 접근 가능하도록) os.chdir(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) +ensure_local_cert() + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.minimum_version = ssl.TLSVersion.TLSv1_2 context.load_cert_chain(CERTFILE, KEYFILE)