From ecd184510c32dc8dbac4996eaefb107268553543 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 31 Oct 2026 19:07:17 -0700 Subject: [PATCH] Place: Word .doc styles are resolved once and damaged property tables are refused Each style's formatting is now worked out once, on top of the style it is based on, and each property run's formatting once, so a document with many hidden paragraphs or long style chains no longer resolves the chains again for every paragraph and run. Character styles are applied as a change to the paragraph's formatting. A bin table whose file positions do not rise, a property page listed twice, pages whose runs overlap, and styles based on each other in a loop are refused as damaged, as MS-DOC requires (PlcBteChpx, ChpxFkp, StdfBase). Property runs refer to their page instead of copying it. --- crates/engine/src/cmd/place/doc.rs | 324 ++++++++++++++++++++------ crates/engine/src/tests/textimport.rs | 66 +++++- docs/mcp.md | 5 +- 3 files changed, 314 insertions(+), 81 deletions(-) diff --git a/crates/engine/src/cmd/place/doc.rs b/crates/engine/src/cmd/place/doc.rs index 41f269c50..f46634b07 100644 --- a/crates/engine/src/cmd/place/doc.rs +++ b/crates/engine/src/cmd/place/doc.rs @@ -39,7 +39,8 @@ //! other stories, tracked changes and tables. A stream larger than [`MAX_STREAM`] bytes, a main //! text longer than [`text::MAX_TEXT`] characters, more than [`MAX_PIECES`] pieces or //! [`MAX_PAGES`] property pages, and placed type of more than [`MAX_PARAGRAPHS`] paragraphs or -//! [`MAX_RUNS`] runs are refused, and so is a structure that lies outside its stream. +//! [`MAX_RUNS`] runs are refused, and so are a structure that lies outside its stream, property +//! pages whose runs are listed twice or overlap, and styles based on each other in a loop. use std::io::{Cursor, Read}; @@ -71,8 +72,6 @@ const MAX_PIECES: usize = 100_000; const MAX_PAGES: usize = 16_384; /// The most styles read from the style sheet (`Stshif.cstd` is below 0x0FFE). const MAX_STYLES: usize = 0x0FFE; -/// The most styles one style's chain of `istdBase` takes in. -const MAX_BASED_ON: usize = 32; /// The most fonts read from the font table (SttbfFfn's `cData` is at most 0x7FF0). const MAX_FONTS: usize = 0x7FF0; /// The most bytes of a font name kept. @@ -359,17 +358,21 @@ enum Fkp { } /// The file positions `start..end` and the properties a property page gives them. -struct Run { +struct Run<'a> { start: u32, end: u32, /// The paragraph style (Papx only). istd: u16, - grpprl: Vec, + /// The properties, in their property page. + grpprl: &'a [u8], } /// The runs of the property pages the bin table `plc` (PlcBteChpx or PlcBtePapx) points to, in -/// the WordDocument stream `wd`, ordered by file position. -fn runs(wd: &[u8], plc: &[u8], kind: Fkp, cmd: &str) -> Result> { +/// the WordDocument stream `wd`, ordered by file position. The bin table's file positions rise +/// ("MUST be sorted in ascending order", and a PlcBteChpx "MUST NOT contain duplicate stream +/// offsets"), and so do the runs of its pages one after another: a page listed twice, or pages +/// whose runs overlap, are damaged. +fn runs<'a>(wd: &'a [u8], plc: &[u8], kind: Fkp, cmd: &str) -> Result>> { let broken = || damaged(cmd, "its character or paragraph properties are damaged"); if plc.len() < 4 || !(plc.len() - 4).is_multiple_of(8) { return Err(broken()); @@ -378,19 +381,28 @@ fn runs(wd: &[u8], plc: &[u8], kind: Fkp, cmd: &str) -> Result> { if n > MAX_PAGES { return Err(bad(cmd, format!("the Word document has more than {MAX_PAGES} pages of properties, too many to place"))); } - let mut out = vec![]; + let fcs = (0..=n).map(|i| u32_at(plc, 4 * i)).collect::>>().ok_or_else(broken)?; + if fcs.windows(2).any(|w| w.first() >= w.get(1)) { + return Err(broken()); + } + let mut out: Vec> = vec![]; for i in 0..n { // PnFkpChpx and PnFkpPapx: the page number in bits 0 to 21. let pn = u32_at(plc, 4 * (n + 1) + 4 * i).ok_or_else(broken)? & 0x003F_FFFF; let page = bytes_at(wd, pn as usize * 512, 512).ok_or_else(broken)?; + let first = out.len(); page_runs(page, kind, &mut out).ok_or_else(broken)?; + // A page's own runs rise (rgfc); its first starts at or after the end of the page before. + let before = first.checked_sub(1).and_then(|k| out.get(k)).map(|r| r.end); + if before.zip(out.get(first)).is_some_and(|(end, r)| r.start < end) { + return Err(broken()); + } } - out.sort_by_key(|r| r.start); Ok(out) } /// The runs of one 512-byte property page; None when it is damaged. -fn page_runs(page: &[u8], kind: Fkp, out: &mut Vec) -> Option<()> { +fn page_runs<'a>(page: &'a [u8], kind: Fkp, out: &mut Vec>) -> Option<()> { let count = usize::from(*page.get(511)?); let most = if kind == Fkp::Chpx { 0x65 } else { 0x1D }; if count == 0 || count > most { @@ -402,10 +414,10 @@ fn page_runs(page: &[u8], kind: Fkp, out: &mut Vec) -> Option<()> { let entry = 4 * (count + 1) + if kind == Fkp::Chpx { i } else { 13 * i }; let offset = usize::from(*page.get(entry)?) * 2; let (istd, grpprl) = match (offset, kind) { - (0, _) => (0, vec![]), + (0, _) => (0, &[][..]), (_, Fkp::Chpx) => { let cb = usize::from(*page.get(offset)?); - (0, bytes_at(page, offset + 1, cb).filter(|_| offset + 1 + cb <= 511)?.to_vec()) + (0, bytes_at(page, offset + 1, cb).filter(|_| offset + 1 + cb <= 511)?) } (_, Fkp::Papx) => { // PapxInFkp: 2 × cb − 1 bytes, or (cb = 0) 2 × cb′ bytes after cb′. @@ -414,9 +426,11 @@ fn page_runs(page: &[u8], kind: Fkp, out: &mut Vec) -> Option<()> { cb => (offset + 1, 2 * cb - 1), }; let grp = bytes_at(page, at, len).filter(|_| at + len <= 511 && len >= 2)?; - (u16_at(grp, 0)?, grp.get(2..)?.to_vec()) + (u16_at(grp, 0)?, grp.get(2..)?) } }; + // rgfc "MUST be sorted in ascending order and MUST NOT contain duplicates": each run ends + // where the next starts. if end <= start { return None; } @@ -473,10 +487,14 @@ struct Style { chpx: Vec, } -/// The style sheet: its styles by istd and its default font. +/// The style sheet: each style's formatting with that of the styles it is based on, by istd, and +/// its default font. #[derive(Default)] struct Sheet { - styles: Vec>, + /// Paragraph styles: their paragraph formatting and their text's. + paragraph: Vec>, + /// Character styles: what they do to the formatting of the text they are applied to. + character: Vec>, /// `Stshif.ftcAsci`. font: Option, } @@ -500,23 +518,74 @@ impl Sheet { // LPStd records start on even offsets. at += 2 + usize::from(len) + usize::from(len) % 2; } - Ok(Self { styles, font }) + let root = (Para::default(), Chars { font, ..Chars::default() }); + let paragraph = resolve(&styles, 1, root, |(mut para, chars), s| { + para.apply(&s.papx); + (para, chars.with(&CharsChange::of(&s.chpx))) + }) + .ok_or_else(broken)?; + let character = resolve(&styles, 2, CharsChange::default(), |change, s| change.then(&CharsChange::of(&s.chpx))).ok_or_else(broken)?; + Ok(Self { paragraph, character, font }) } - /// Style `istd` of kind `stk` and the styles it is based on, the first of the chain first. - fn chain(&self, istd: u16, stk: u8) -> Vec<&Style> { - let mut seen: Vec = vec![]; - let mut chain = vec![]; - let mut next = istd; - while next != NO_STYLE && !seen.contains(&next) && seen.len() < MAX_BASED_ON { - let Some(s) = self.styles.get(usize::from(next)).and_then(Option::as_ref).filter(|s| s.stk == stk) else { break }; - seen.push(next); - chain.push(s); - next = s.base; - } - chain.reverse(); - chain + /// Paragraph style `istd`'s paragraph formatting and its text's; the document defaults when + /// it is no paragraph style. + fn paragraph_style(&self, istd: u16) -> (Para, Chars) { + self.paragraph.get(usize::from(istd)).copied().flatten().unwrap_or((Para::default(), Chars { font: self.font, ..Chars::default() })) } + + /// What character style `istd` does to text; nothing when it is no character style. + fn character_style(&self, istd: u16) -> CharsChange { + self.character.get(usize::from(istd)).copied().flatten().unwrap_or_default() + } +} + +/// The formatting of each style of `styles` of kind `stk` with that of the styles it is based on: +/// `root` with `own` applied for each style of its chain of `istdBase`, the first of the chain +/// first. Each style's formatting is worked out once, on that of the style it is based on. A chain +/// ends at a style of another kind or none. None when `istdBase` makes a loop (which it "MUST +/// NOT", StdfBase). +fn resolve(styles: &[Option