collective

Browse / github.com/storytold/wordcraft / patch

#3406: .doc paragraph properties too big for a PAPX are read from the…

#3406: .doc paragraph properties too big for a PAPX are read from the… Refs #3406. A .doc paragraph whose properties are too big for its PAPX (the row-end mark of a table row with many cells and a long sprmTDefTable) holds only sprmPHugePapx: the offset of the real properties in the Data stream. The reader ignored it, so the mark had no table properties, the row never closed, and its cells ran into the next row’s. In HM Land Registry’s K20 form the date boxes (12 empty cells) joined the three cells after them. The sprms are now read from the Data stream; an offset or byte count outside it leaves the PAPX as it was. Two regression tests: an indent stored in the Da

Status
open
Score
0.00
Adopters
0 other operators
Author
PasqualeIannace (imported from pull request #4549, open)
Base
6a2b520fa49aa0d0595cea36589aaf16ca371cd3
Size
+59 −0 in 2 files
Review
pending
Shared
2026-10-28
Id
37b6d64d9baf98d67beb7db9

How it went

No other operator has reported using it yet.

Diff

raw · crates/docbin/src/lib.rs crates/docbin/src/tests.rs

diff --git a/crates/docbin/src/lib.rs b/crates/docbin/src/lib.rs--- a/crates/docbin/src/lib.rs+++ b/crates/docbin/src/lib.rs@@ -142,6 +142,9 @@ const C_SYMBOL: u16 = 0x6A09; /// `sprmPIlvl` / `sprmPIlfo`: the paragraph's list level and list. const P_ILVL: u16 = 0x260A; const P_ILFO: u16 = 0x460B;+/// `sprmPHugePapx`: the whole PAPX of a paragraph that is too large for its FKP page (a table row with+/// many cells) is this one sprm, whose operand is the offset of the real properties in the Data stream.+const P_HUGE_PAPX: u16 = 0x6646;  /// Errors from reading a Word 97-2003 binary file. #[derive(Debug, thiserror::Error, Clone, PartialEq)]@@ -405,6 +408,17 @@ fn story(base: u32, len: u32, (a, b): (u32, u32)) -> Option<(u32, u32)> {     Some((base.checked_add(a)?, base.checked_add(b)?)) } +/// The sprms a `sprmPHugePapx` in `papx` points to in the Data stream: a `PrcData`, a two-byte byte+/// count and then the grpprl. `None` when `papx` has none, or when it points outside the stream.+fn huge_papx<'d>(data: &'d [u8], papx: &[u8]) -> Option<&'d [u8]> {+    let prl = sprm::iter(papx).find(|p| p.op == P_HUGE_PAPX)?;+    let [a, b, c, d, ..] = *prl.operand else { return None };+    let at = u32::from_le_bytes([a, b, c, d]) as usize;+    let body = at.checked_add(2)?;+    let cb = u16::from_le_bytes(data.get(at..body)?.try_into().ok()?) as usize;+    data.get(body..body.checked_add(cb)?)+}+ /// Everything the walker needs besides its CP range. struct WalkCtx<'a> {     word: &'a [u8],@@ -526,6 +540,7 @@ fn walk<'a>(ctx: &WalkCtx<'a>, cp_start: u32, cp_end: u32, pics: &mut media::Pic                 // querying at the mark's own FC finds that paragraph's properties.                 let fc_mark = ctx.pieces.fc_of_cp(cp).unwrap_or(0);                 let (istd, papx) = ctx.papx_bins.papx(ctx.word, fc_mark);+                let papx = huge_papx(ctx.data, papx).unwrap_or(papx);                 if let Some(st) = ctx.raw_styles.get(istd as usize).filter(|s| !s.name.is_empty()) {                     pb.props.style = Some(fmt::style_id(&st.name));                 }diff --git a/crates/docbin/src/tests.rs b/crates/docbin/src/tests.rs--- a/crates/docbin/src/tests.rs+++ b/crates/docbin/src/tests.rs@@ -1026,6 +1026,50 @@ fn table_assembled_from_marks() {     assert_eq!(last.text, "tail"); } +/// A paragraph whose properties don't fit a PAPX holds only `sprmPHugePapx`: the offset of the real+/// properties in the Data stream, as a byte count and then the sprms.+#[test]+fn huge_papx_reads_the_paragraph_properties_from_the_data_stream() {+    let text = "indented\r";+    let real = grpprl(&[(0x845E, &720u16.to_le_bytes())]);+    let mut data = vec![0u8; 8]; // something before them, so the offset isn't zero+    let at = data.len() as u32;+    data.extend_from_slice(&(real.len() as u16).to_le_bytes());+    data.extend_from_slice(&real);+    let spec = StructSpec {+        text,+        papx_entries: vec![(0, 0, grpprl(&[(0x6646, &at.to_le_bytes())]))],+        data_stream: data,+        ..structured_spec(text, text.chars().count() as u32)+    };+    let doc = read(&struct_doc(&spec)).expect("opens");+    let p = doc.body.first().and_then(|b| b.as_para()).expect("paragraph");+    assert_eq!(p.props.indent_left, Some(36.0), "720 twips from the Data stream");+}++/// A table row whose end mark is a huge PAPX (its `sprmTDefTable` is in the Data stream) still ends+/// the row, with the cell widths its edges give, rather than leaving its cells to the next row.+#[test]+fn a_table_row_end_in_a_huge_papx_still_ends_the_row() {+    let text = "a\u{7}b\u{7}tail\r";+    let in_tbl = grpprl(&[(0x2416, &[0x01])]);+    let real = [grpprl(&[(0x2416, &[0x01]), (0x2417, &[0x01])]), t_def_edges(&[0, 2000, 4000])].concat();+    let mut data = Vec::new();+    data.extend_from_slice(&(real.len() as u16).to_le_bytes());+    data.extend_from_slice(&real);+    let spec = StructSpec {+        text,+        papx_entries: vec![(0, 0, in_tbl), (2, 0, grpprl(&[(0x6646, &0u32.to_le_bytes())])), (4, 0, Vec::new())],+        data_stream: data,+        ..structured_spec(text, text.chars().count() as u32)+    };+    let doc = read(&struct_doc(&spec)).expect("opens");+    let tbl = doc.body.iter().find_map(|b| b.as_table()).expect("table");+    assert_eq!(tbl.rows.len(), 1);+    assert_eq!(tbl.rows[0].cells.len(), 2);+    assert_eq!(tbl.grid, vec![100.0, 100.0]);+}+ #[test] fn table_merges_borders_shading() {     // Two rows: the first "a\x07 b\x07 c\x07" with cells 0-1 horizontally merged; the second