Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
c5acea4
fix(studio): allow STUDIO_BASE override and remove VitePWA
jays0606 May 26, 2026
7fd7bbf
feat(studio): expose window.__wasm in production for parent-frame bri…
jays0606 May 26, 2026
0c540c9
feat(studio): SW self-heal + optional agent-bridge.js script tag
jays0606 May 26, 2026
1f34f74
feat(studio): ship a reference postMessage RPC agent-bridge
jays0606 May 26, 2026
d64961b
fix(studio): agent-bridge — DOM-level validation modal suppressor
jays0606 May 26, 2026
55d32d9
feat(studio): agent.getFullText — full-doc/section/table flat text dump
jays0606 May 27, 2026
b678907
typeset: trust HWPX lineseg cache when available
jays0606 May 28, 2026
554e741
typeset: dedupe wrap-zone linesegs sharing vertpos
jays0606 May 28, 2026
b617006
table_layout: overlay nested tables don't displace flow
jays0606 May 28, 2026
49959ee
height_measurer: extend outer-height shrink to non-TAC photo grids (p…
jays0606 May 28, 2026
358a7d3
rendering: hwp_used metric counts non-paragraph items in flow
jays0606 May 28, 2026
7a048ae
typeset+layout: trust-cache paragraphs skip sb/sa for both pagination…
jays0606 May 28, 2026
6241421
Revert "typeset+layout: trust-cache paragraphs skip sb/sa for both pa…
jays0606 May 28, 2026
5870b03
Revert "Revert "typeset+layout: trust-cache paragraphs skip sb/sa for…
jays0606 May 28, 2026
fe5820f
rendering: per-region hwp_used subtracts first_vpos for page-relative…
jays0606 May 28, 2026
eb61f60
pagination: respect_vpos_reset = true by default
jays0606 May 28, 2026
5f89db6
Revert "pagination: respect_vpos_reset = true by default"
jays0606 May 28, 2026
2266672
Revert "Revert "Revert "typeset+layout: trust-cache paragraphs skip s…
jays0606 May 28, 2026
fbce304
Revert "Revert "pagination: respect_vpos_reset = true by default""
jays0606 May 28, 2026
45f9bfa
Revert "Revert "Revert "Revert "typeset+layout: trust-cache paragraph…
jays0606 May 28, 2026
2cb7e5f
typeset: 2-line widow control (paragraph kept together when last line…
jays0606 May 28, 2026
6b8672c
rendering: hwp_used metric guards against internal vpos-reset in para…
jays0606 May 28, 2026
89a15cc
rendering: hwp_used metric handles floating-anchor + paragraph hybrid…
jays0606 May 28, 2026
c3074ad
rendering: hwp_used metric detects backward-jump cross-paragraph resets
jays0606 May 28, 2026
9613bbf
rendering: hwp_used metric tracks region max_vpos for mid-region resets
jays0606 May 28, 2026
ce5efa4
rendering: hwp_used metric — don't seed max_vpos with floating anchors
jays0606 May 28, 2026
118a71d
typeset: skip safety margin for last-pi before forced page break
jays0606 May 28, 2026
9c2be2e
test(typeset): give test_typeset_line_split fixtures monotonically-in…
jays0606 May 28, 2026
ca5245c
test(layout): update test_521 / test_624 expected coords after metric…
jays0606 May 28, 2026
16d407b
test(fidelity): add gifted_application + k_star_p14 regression gates
jays0606 May 28, 2026
61642b3
layout: trust cached linesegs for cell paragraph heights (Bucket A)
jays0606 May 28, 2026
fc8e2ec
test(fidelity): add internship_plan p4 vpos-reset regression test
jays0606 May 28, 2026
dd152dc
pagination: add hwpx_cross_para_reset_breaks opts flag (no-op)
jays0606 May 28, 2026
44f1d2e
pagination: cross-paragraph vpos-reset break detector (Bucket C, defa…
jays0606 May 29, 2026
b77fe6a
pagination(Bucket C): document verified default-off rationale
jays0606 May 29, 2026
b14213d
typeset: absorb terminal-empty-paragraph that overflows by ≤ blank-li…
jays0606 May 29, 2026
68cf051
test(fidelity): add small_04 trailing-blank-page regression gate
jays0606 May 29, 2026
62aecfe
height_measurer: non-TAC table shrink requires picture content
jays0606 May 29, 2026
71ab63e
typeset: side-by-side TopAndBottom para-floats share vpos (overlap → …
jays0606 May 29, 2026
958f51a
typeset+layout: sub-pixel orphan guards stop phantom pages (form_25 3…
jays0606 May 29, 2026
828aeb4
layout: include line_spacing in tac-shape paragraph advance (doc 09 c…
jays0606 May 29, 2026
8ae933e
layout: don't clobber shared para_start_y for later tac controls
jays0606 May 29, 2026
b43e62f
pagination: keep short subhead with following block-table on page bou…
jays0606 May 29, 2026
48cdc26
svg: port supScript/subScript handling from html.rs draw_text
jays0606 May 29, 2026
0b659da
layout: overlay nested tables anchor to cell content top (fixes foote…
jays0606 May 30, 2026
b8baef8
ci: anti-fingerprint renderer overfit gate
jays0606 May 30, 2026
7068154
glyph: Wingdings 0x6D maps to ○ white circle, not ● black
jays0606 May 30, 2026
ddc0fa2
layout: picture display size honors hp:sz frame, not the larger sourc…
jays0606 May 30, 2026
4c51170
layout: skip y-advance for whitespace wrap-zone dup-vpos lines (doc 0…
jays0606 May 30, 2026
6efad32
typeset: last-row orphan guard absorbs near-filling rows too (doc 07 …
jays0606 May 30, 2026
c3975cf
pagenum: honor SectionDef.page_num start offset in footer fields (doc…
jays0606 May 30, 2026
86ed18e
pagenum: render TOTAL_PAGE autoNum as document total, not current pag…
jays0606 May 30, 2026
35460ae
table: vertical-center cell counts nested-table height (doc 13 form b…
jays0606 May 30, 2026
192cce4
table: split nested-table cell top-anchors (doc 13 ~170px gap above s…
jays0606 May 30, 2026
edf60c5
table: measure_table counts nested-table cell height (doc 13 서식3 un-c…
jays0606 May 30, 2026
b64f660
parser/hwpx: honor landscape="NARROWLY" page orientation (swap W/H)
jays0606 May 30, 2026
9ecee28
layout: render header/footer text beside a non-TAC float picture
jays0606 May 30, 2026
44a2e0f
typeset+layout: trust-cache keeps paraPr spaceAfter (wild form_* tigh…
jays0606 May 31, 2026
34c5ab2
harness: objective drift metric + anti-hardcode gates (ralph scaffold…
May 31, 2026
945b3d6
harness: overnight status + morning visual-review handoff
May 31, 2026
cb43772
table: clamp whole-row picture-cell height to stored cell.height (mee…
jays0606 Jun 1, 2026
8ca24c1
typeset: suppress near-empty vpos-reset break on spillover orphans (h…
jays0606 Jun 1, 2026
48f310a
typeset: sub-line last-row absorb keeps mid-size final row (form_07 1…
jays0606 Jun 1, 2026
6607b88
harness: sync ralph queue to session state (5 done, tail classified c…
jays0606 Jun 1, 2026
44e8b2c
typeset: tac-host flush ignores post-vpos-reset line (form_22 38→37)
jays0606 Jun 1, 2026
6063881
typeset: widen last-row orphan rounding tolerance 3px→4px (doc13 20→19)
jays0606 Jun 1, 2026
46f6569
harness: queue notes — form_22 + doc13 done, form_07 two-pass diagnos…
jays0606 Jun 1, 2026
24aa6f8
harness: form_07 diagnosis corrected — measurement-context anomaly, n…
jays0606 Jun 1, 2026
ceb2484
work: overnight handoff prompt (5 remaining docs, per-doc next steps)
jays0606 Jun 1, 2026
c837874
harness: resolve form_03 oracle (lineseg-less, Hancom-unavailable) + …
jays0606 Jun 1, 2026
f1961d4
work: morning status 2026-06-03 — form_03 resolved, form_07+large_01 …
jays0606 Jun 1, 2026
ce92550
work: large_01 supervised attempt outcome — height-growth divergence,…
jays0606 Jun 1, 2026
7513754
typeset+height_cursor: restore cache-reserved spacing_before for trus…
jays0606 Jun 2, 2026
1fef305
layout: stack TopAndBottom para-floats sharing one anchor (wc28 chart…
jays0606 Jun 2, 2026
dc7ee3b
shape_layout: apply auto-list numbering to textbox paragraphs (wc03 1…
jays0606 Jun 2, 2026
c8fa11e
paragraph_layout: gap after Outline/Number auto-list prefix (wb04/wb1…
jays0606 Jun 2, 2026
9540e8f
hwpx: parse picture captions (wc32/wc48/wc52 출처/그림 captions render once)
jays0606 Jun 2, 2026
7e3a238
typeset+layout: emit tac-inline shape sharing a paragraph with a tabl…
jays0606 Jun 3, 2026
451c6a8
layout: clamp in-cell non-TAC picture into the cell box (wc24/wc25/wc…
jays0606 Jun 3, 2026
b0f54fc
typeset+layout: full-width SQUARE float reserves full height (wc47 lo…
jays0606 Jun 3, 2026
96b1e37
typeset+layout: stack+split terminal full-width SQUARE float gallerie…
jays0606 Jun 3, 2026
fb1ee10
typeset+layout: extend terminal-gallery stack+split to TopAndBottom f…
jays0606 Jun 3, 2026
834c83f
typeset+layout: TAC picture-grid pagination + per-row render (wc29 4→…
jays0606 Jun 3, 2026
fd54a6f
layout: map TAC picture-grid rows onto non-text lines (recovers dropp…
jays0606 Jun 3, 2026
a1e7753
typeset: Square-table beside-flow — absorb beside-paras + aggregate w…
jays0606 Jun 3, 2026
f0b229f
typeset: single large non-tac float page-break-before (wc37 dropped s…
jays0606 Jun 3, 2026
38cb590
typeset+layout: sa double-count zero when cache vpos already encodes …
jays0606 Jun 3, 2026
6c27619
typeset: TAC single-table flush sub-line tolerance (wc20 4→3)
jays0606 Jun 3, 2026
dca34b0
typeset: narrow Square float advances flow to next anchor vpos, not f…
jays0606 Jun 3, 2026
dcebfab
layout: tac TopAndBottom block table → font-based host line height (w…
jays0606 Jun 3, 2026
d859272
Reserve scanned-photo pages before following flow
jays0606 Jun 5, 2026
d40fa1a
Keep large rowspan table splits row-wise
jays0606 Jun 5, 2026
1717f52
Clip continuation-table cells in exports
jays0606 Jun 5, 2026
36a9e2c
Fit RowBreak rowspan tables by clipping declared slack
jays0606 Jun 5, 2026
741513a
Keep TopAndBottom table bridge spacers from stale VPOS snaps
jays0606 Jun 5, 2026
6e98ed5
Trust visual review by rasterizing SVGs at page aspect
jays0606 Jun 5, 2026
25d42bf
Fit repeated photo tables without clipping cells
jays0606 Jun 5, 2026
1afa3dc
Stabilize structural RHWP fidelity gates
jays0606 Jun 8, 2026
2fea750
Keep cover body title bands off cover pages
jays0606 Jun 8, 2026
6c0916b
Keep overflowing form tails before explicit form pages
jays0606 Jun 8, 2026
8460f1d
Stabilize repeated minutes table pagination
jays0606 Jun 8, 2026
ef824b0
Record rejected photo-form drift probes
jays0606 Jun 8, 2026
e5aacde
Preserve photo-form section heading alignment
jays0606 Jun 8, 2026
8e02022
Preserve blank explicit-break page before TAC groups
jays0606 Jun 8, 2026
b43726e
Keep contact tables before explicit section titles
jays0606 Jun 8, 2026
b398658
Preserve two-spacer contact pages before section titles
jays0606 Jun 8, 2026
4c9f82d
Record rejected title-table contact probe
jays0606 Jun 8, 2026
b17a0b1
Refocus fidelity goal on same-count visual drift
jays0606 Jun 8, 2026
29e7146
Keep late procedure headings with flowchart tables
jays0606 Jun 8, 2026
7d2ee8c
fonts: bundle 한컴고딕/HY울릉도/함초롬/HY계열(SDK UnlicenseFontList) + EB Garamon…
jays0606 Jun 10, 2026
5116621
engine: June fidelity work — accepted patches + probe state as measur…
jays0606 Jun 10, 2026
5687ab8
harness: review galleries, font resource audit, probes, gate baseline…
jays0606 Jun 10, 2026
4c902e1
docs: fidelity status boards + production gate audit + START_HERE 202…
jays0606 Jun 10, 2026
9a1eb3e
fonts: bundle 한컴 윤고딕 230-760 (HWP-bundled Yoon set) — 한컴 사용 허락 확보 202…
jays0606 Jun 10, 2026
51064d7
Preserve edited HWP export text
jays0606 Jun 15, 2026
8861708
feat(renderer): trust lineseg cache + cell/font fidelity; studio care…
jays0606 Jun 18, 2026
d4185e8
Add batch paragraph insert bridge RPC
jays0606 Jun 24, 2026
c495cd0
feat(studio): fillEmptyTableCells batch bridge RPC
jays0606 Jul 2, 2026
e7c0da5
fix(agent-bridge): nested-table guardrail — setCellText refusal, nest…
jays0606 Jul 2, 2026
1ff10f8
fix(core): empty ctrl_data_records trap + nested-table structural ByP…
jays0606 Jul 2, 2026
dd08c29
test: corpus-wide fuzz example for the ctrl_data_records insert trap
jays0606 Jul 2, 2026
c24a32e
fix(edit): reflow cell paragraph after *_by_path in-cell edits
jays0606 Jul 3, 2026
f67b39a
fix(serialize): guard against u32-wrapped-negative cell heights
jays0606 Jul 3, 2026
8913176
fix(serialize): HWPX group-shape children + MEMO field content surviv…
jays0606 Jul 3, 2026
72f4e74
fix(serialize): HWPX picture captions survive edit re-serialization
jays0606 Jul 4, 2026
7fb7d0e
fix(reflow): on-demand lineseg reflow keeps trusted cache when wrap i…
jays0606 Jul 4, 2026
48a0809
fix(serialize): preserve raw header.xml on pure-append doc_info changes
jays0606 Jul 4, 2026
9e9e737
fix(serialize): pageNum/newNum/autoNum/pageHiding survive paragraph r…
jays0606 Jul 4, 2026
b2cc5d9
fix(reflow): exclude tab-bearing paragraphs from R3 textRun-reflow rule
jays0606 Jul 4, 2026
ebed25e
fix(serialize): compose(글자겹침) + form objects survive paragraph re-ser…
jays0606 Jul 4, 2026
6764dd3
fix(serialize): HWPX masterpage files survive edit re-serialization
jays0606 Jul 4, 2026
05398c8
fix(reflow+parse): R3 metric-noise discriminator + entity decode for …
jays0606 Jul 4, 2026
c36fde0
fix(serialize): control-only runs keep Hancom's trailing empty <hp:t/>
jays0606 Jul 4, 2026
a31a985
chore(probe): compose host-paragraph IR comparator (corpus/52 목차 resi…
jays0606 Jul 4, 2026
22d64ca
fix(serialize): images survive native-Hancom parse of edited exports
jays0606 Jul 4, 2026
69e93be
fix(render): Top-cell per-paragraph vpos anchor ignores reflowed vpos…
jays0606 Jul 4, 2026
c8810a3
fix(studio): auto-grow table row on cell-edit overflow (agent-bridge)
jays0606 Jul 6, 2026
e7672f8
fix(render): 1x1-wrapper nested tables are editable (hit_test cellPath)
jays0606 Jul 6, 2026
1a37f7d
feat(agent-bridge): surface wrong-cell audit info from setCellText
jays0606 Jul 6, 2026
0009a56
feat(studio): v0.7.13-12 CDN 핫픽스 소스 반영 — FactChat 저장 메뉴·통합 undo/redo
heptacode Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ jobs:
- name: Format check
run: cargo fmt --all -- --check

- name: Renderer overfit guard
run: python3 scripts/check_renderer_overfit.py

- name: Build
run: cargo build --verbose

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/render-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
- 'src/renderer/**'
- 'src/document_core/queries/rendering.rs'
- 'src/wasm_api.rs'
- 'scripts/check_renderer_overfit.py'
- 'scripts/renderer_overfit_baseline.txt'
- 'rhwp-studio/**'
- '.github/workflows/render-diff.yml'
workflow_dispatch:
Expand Down Expand Up @@ -93,6 +95,9 @@ jobs:
node --check e2e/canvas-render-diff.test.mjs
node --check e2e/run-render-diff.mjs

- name: Renderer overfit guard
run: python3 scripts/check_renderer_overfit.py

- name: Run canvas visual diff
working-directory: rhwp-studio
env:
Expand Down
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ path = "src/main.rs"
name = "font-metric-gen"
path = "src/tools/font_metric_gen.rs"

# === [Mindlogic — fidelity corpus integration tests] ===
# Lives under tests/integration/ (Cargo's default discovery picks up
# only top-level tests/*.rs; nested files need an explicit entry).
[[test]]
name = "test_fidelity_corpus"
path = "tests/integration/test_fidelity_corpus.rs"

[dependencies]
wasm-bindgen = "0.2"
cfb = "0.14"
Expand Down
168 changes: 168 additions & 0 deletions examples/diag_empty_para_trap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Diagnose the insertFootnote/insertEquation 'unreachable' trap on form_19's
// empty body paragraph by emulating the agent-bridge call sequence natively.
use rhwp::model::control::Control;
use rhwp::wasm_api::HwpDocument;
use std::panic::{catch_unwind, AssertUnwindSafe};

fn load() -> HwpDocument {
let bytes = std::fs::read("samples/form_19_empty_para.hwpx").expect("read fixture");
HwpDocument::from_bytes(&bytes).expect("parse")
}

fn step(name: &str, f: impl FnOnce()) {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(()) => println!(" ok {name}"),
Err(e) => {
let msg = e
.downcast_ref::<String>()
.cloned()
.or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_else(|| "<non-string panic>".into());
println!(" PANIC {name}: {msg}");
}
}
}

fn main() {
let doc = load();
{
let document = doc.document();
let section = &document.sections[0];
println!("paragraphs: {}", section.paragraphs.len());
for (pi, para) in section.paragraphs.iter().enumerate().take(12) {
let has_table = para.controls.iter().any(|c| matches!(c, Control::Table(_)));
let kinds: Vec<&str> = para
.controls
.iter()
.map(|c| match c {
Control::Table(_) => "Table",
Control::Shape(_) => "Shape",
Control::Footnote(_) => "Footnote",
Control::Bookmark(_) => "Bookmark",
Control::AutoNumber(_) => "AutoNumber",
_ => "Other",
})
.collect();
println!(
" p{pi}: table={has_table} text={:?} chars(text)={} char_count={} offsets={} controls={:?} linesegs={}",
para.text.chars().take(30).collect::<String>(),
para.text.chars().count(),
para.char_count,
para.char_offsets.len(),
kinds,
para.line_segs.len(),
);
}
}

// The probe's P_TEXT actually resolves to an empty-text TABLE-HOSTING
// paragraph: both form_19 paragraphs host tables, the probe splits p0 to
// manufacture a text para at p1, and P_TEXT = s0:p2 = the shifted
// original p1 (table host, text="", char_offsets=[]).
let p_text = 1usize; // pristine-doc equivalent: the second table para
println!("P_TEXT equivalent = s0:p{p_text} (table host, empty text)");

println!("--- footnote sequence (bridge emulation, offset 0, with inner text) ---");
let mut d = load();
step("beginBatch", || {
d.begin_batch().unwrap();
});
let mut ctrl_idx = 0usize;
step("insertFootnote(sec=0, para=P_TEXT, offset=0)", || {
let r = d.insert_footnote_native(0, p_text, 0).expect("insert");
println!(" -> {r}");
ctrl_idx = r
.split("\"controlIdx\":")
.nth(1)
.and_then(|s| s.split([',', '}']).next())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
});
step("insertTextInFootnote(..., inner=0, off=0)", || {
let r = d
.insert_text_in_footnote_native(0, p_text, ctrl_idx, 0, 0, "프로브 각주")
.expect("insert text in footnote");
println!(" -> {r}");
});
step("endBatch", || {
d.end_batch().unwrap();
});
step("render all pages", || {
for pg in 0..d.page_count() {
d.render_page_svg(pg).unwrap();
}
});
step("snapshot save+restore", || {
let id = d.save_snapshot();
d.restore_snapshot(id).unwrap();
});

println!("--- equation sequence (offset 0) ---");
let mut d2 = load();
step("beginBatch", || {
d2.begin_batch().unwrap();
});
step("insertEquation(sec=0, para=P_TEXT, offset=0)", || {
let r = d2
.insert_equation_native(0, p_text, 0, "a^2+b^2=c^2", 1200, 0)
.expect("insert equation");
println!(" -> {r}");
});
step("endBatch", || {
d2.end_batch().unwrap();
});
step("render all pages", || {
for pg in 0..d2.page_count() {
d2.render_page_svg(pg).unwrap();
}
});
step("snapshot save+restore", || {
let id = d2.save_snapshot();
d2.restore_snapshot(id).unwrap();
});

println!("--- exact probe emulation: split p0, text at p1, footnote at p2 off 0 ---");
let mut d3 = load();
step("splitParagraph(p0 at end) + insertText(p1)", || {
let len = {
let para = &d3.document().sections[0].paragraphs[0];
para.text.chars().count() as u32
};
d3.split_paragraph(0, 0, len).unwrap();
d3.insert_text(0, 1, 0, "프로브 텍스트 문단").unwrap();
});
step("beginBatch", || {
d3.begin_batch().unwrap();
});
let mut ctrl_idx3 = 0usize;
step("insertFootnote(sec=0, para=2, offset=0)", || {
let r = d3.insert_footnote_native(0, 2, 0).expect("insert");
println!(" -> {r}");
ctrl_idx3 = r
.split("\"controlIdx\":")
.nth(1)
.and_then(|s| s.split([',', '}']).next())
.and_then(|s| s.parse().ok())
.unwrap_or(0);
});
step("insertTextInFootnote(..., inner=0, off=0)", || {
let r = d3
.insert_text_in_footnote_native(0, 2, ctrl_idx3, 0, 0, "프로브 각주")
.expect("insert text in footnote");
println!(" -> {r}");
});
step("endBatch", || {
d3.end_batch().unwrap();
});
step("render all pages", || {
for pg in 0..d3.page_count() {
d3.render_page_svg(pg).unwrap();
}
});
step("snapshot save+restore", || {
let id = d3.save_snapshot();
d3.restore_snapshot(id).unwrap();
});

println!("done");
}
76 changes: 76 additions & 0 deletions examples/edit_textloss_scan.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
//! Scan: for each input doc, compare <hp:t> char counts between the no-edit
//! export (raw pass-through) and an edited export (forced re-serialize).
//! Any deficit beyond the inserted text = content destroyed by edit-path
//! re-serialization.
//!
//! cargo run --release --example edit_textloss_scan -- <doc.hwpx> [...]

use std::env;
use std::fs;

use rhwp::document_core::DocumentCore;

fn t_chars(bytes: &[u8]) -> usize {
let cursor = std::io::Cursor::new(bytes);
let mut zip = zip::ZipArchive::new(cursor).expect("zip");
let mut total = 0usize;
let names: Vec<String> = (0..zip.len())
.map(|i| zip.by_index(i).unwrap().name().to_string())
.filter(|n| n.starts_with("Contents/section") && n.ends_with(".xml"))
.collect();
for n in names {
use std::io::Read;
let mut xml = String::new();
zip.by_name(&n).unwrap().read_to_string(&mut xml).unwrap();
let mut rest = xml.as_str();
while let Some(i) = rest.find("<hp:t>") {
rest = &rest[i + 6..];
if let Some(j) = rest.find("</hp:t>") {
total += rest[..j].chars().count();
rest = &rest[j..];
} else {
break;
}
}
}
total
}

fn main() {
const INS: &str = "probe ";
for path in env::args().skip(1) {
let data = match fs::read(&path) {
Ok(d) => d,
Err(e) => { println!("{path}: READ ERR {e}"); continue; }
};
let core = match DocumentCore::from_bytes(&data) {
Ok(c) => c,
Err(e) => { println!("{path}: PARSE ERR {e:?}"); continue; }
};
let dump = env::var("DUMP_DIR").ok();
let stem = std::path::Path::new(&path)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "doc".into());
let base_bytes = match core.export_hwpx_native() { Ok(b) => b, Err(e) => { println!("{path}: EXPORT ERR {e:?}"); continue; } };
if let Some(ref d) = dump { let _ = fs::write(format!("{d}/{stem}.base.hwpx"), &base_bytes); }
let base = t_chars(&base_bytes);

let mut core = DocumentCore::from_bytes(&data).expect("parse2");
if core.insert_text_native(0, 0, 0, INS).is_err() {
println!("{path}: EDIT ERR");
continue;
}
match core.export_hwpx_native() {
Ok(b) => {
if let Some(ref d) = dump { let _ = fs::write(format!("{d}/{stem}.edited.hwpx"), &b); }
let after = t_chars(&b);
let expected = base + INS.chars().count();
let delta = after as i64 - expected as i64;
let flag = if delta < 0 { "LOSS" } else { "ok" };
println!("{path}: base={base} after={after} delta_vs_expected={delta} {flag}");
}
Err(e) => println!("{path}: EXPORT2 ERR {e:?}"),
}
}
}
79 changes: 79 additions & 0 deletions examples/fuzz_empty_para_inserts.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Corpus-wide regression fuzz for the ctrl_data_records trap family:
// for every .hwpx in samples/corpus_tmp/, insert a footnote and an equation
// at offset 0 of each of the first paragraphs (covers p0 sectiondef hosts,
// table hosts, and plain text paras), then render every page and round-trip
// a snapshot. Any panic = the 'unreachable' WASM trap shape. Exits non-zero
// if anything fails.
use rhwp::wasm_api::HwpDocument;
use std::panic::{catch_unwind, AssertUnwindSafe};

fn main() {
let dir = "samples/corpus_tmp";
let mut files: Vec<_> = std::fs::read_dir(dir)
.expect("read corpus dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().map(|x| x == "hwpx").unwrap_or(false))
.collect();
files.sort();
assert!(!files.is_empty(), "no corpus files in {dir}");

let mut failures = 0usize;
let mut checks = 0usize;
for path in &files {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) => {
println!("FAIL read {name}: {e}");
failures += 1;
continue;
}
};
let para_count = match catch_unwind(AssertUnwindSafe(|| {
HwpDocument::from_bytes(&bytes)
.map(|d| d.document().sections[0].paragraphs.len())
})) {
Ok(Ok(n)) => n,
Ok(Err(e)) => {
println!("SKIP parse {name}: {e:?}");
continue;
}
Err(_) => {
println!("FAIL parse-panic {name}");
failures += 1;
continue;
}
};

for para in 0..para_count.min(4) {
for op in ["footnote", "equation"] {
checks += 1;
let bytes = bytes.clone();
let ok = catch_unwind(AssertUnwindSafe(|| {
let mut doc = HwpDocument::from_bytes(&bytes).unwrap();
let res = match op {
"footnote" => doc.insert_footnote_native(0, para, 0),
_ => doc.insert_equation_native(0, para, 0, "a^2+b^2=c^2", 1200, 0),
};
res.expect("insert");
for pg in 0..doc.page_count() {
doc.render_page_svg(pg).expect("render");
}
let id = doc.save_snapshot();
doc.restore_snapshot(id).expect("restore");
}))
.is_ok();
if !ok {
println!("FAIL {op} {name} p{para}");
failures += 1;
}
}
}
println!("ok {name} ({} paras probed)", para_count.min(4));
}
println!("---\n{checks} insert checks over {} docs, {failures} failures", files.len());
if failures > 0 {
std::process::exit(1);
}
}
Loading
Loading