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