diff --git a/crates/engine/src/cmd/place/doc.rs b/crates/engine/src/cmd/place/doc.rs index c1d40a34d..8e9ff7c04 100644 --- a/crates/engine/src/cmd/place/doc.rs +++ b/crates/engine/src/cmd/place/doc.rs @@ -70,7 +70,7 @@ const MAX_STREAM: u64 = 64 << 20; const MAX_PIECES: usize = 100_000; /// The most 512-byte pages of character or of paragraph properties read. const MAX_PAGES: usize = 16_384; -/// The most styles read from the style sheet (`Stshif.cstd` is below 0x0FFE). +/// The style count `Stshif.cstd` must be below. const MAX_STYLES: usize = 0x0FFE; /// The most fonts read from the font table (SttbfFfn's `cData` is at most 0x7FF0). const MAX_FONTS: usize = 0x7FF0; @@ -505,14 +505,17 @@ impl Sheet { let broken = || damaged(cmd, "its style sheet is damaged"); let size = usize::from(u16_at(stsh, 0).ok_or_else(broken)?); let stshi = bytes_at(stsh, 2, size).ok_or_else(broken)?; - let count = usize::from(u16_at(stshi, 0).ok_or_else(broken)?).min(MAX_STYLES); + let count = usize::from(u16_at(stshi, 0).ok_or_else(broken)?); + if count >= MAX_STYLES { + return Err(broken()); + } // cbSTDBaseInFile: StdfBase, and StdfPost2000 when it is 18. let base_size = usize::from(u16_at(stshi, 2).ok_or_else(broken)?).max(10); let font = u16_at(stshi, 12); let mut at = 2 + size; let mut styles = Vec::with_capacity(count); for _ in 0..count { - let Some(len) = u16_at(stsh, at) else { break }; + let len = u16_at(stsh, at).ok_or_else(broken)?; let std = bytes_at(stsh, at + 2, usize::from(len)).ok_or_else(broken)?; styles.push(if std.is_empty() { None } else { Some(style(std, base_size).ok_or_else(broken)?) }); // LPStd records start on even offsets. diff --git a/crates/engine/src/tests/textimport.rs b/crates/engine/src/tests/textimport.rs index 78b7c8977..f2aaa6b38 100644 --- a/crates/engine/src/tests/textimport.rs +++ b/crates/engine/src/tests/textimport.rs @@ -491,6 +491,8 @@ struct Word97 { papx: Vec<(u32, u16, Vec)>, /// The styles by istd; None: an empty slot. styles: Vec>, + /// A style count `Stshif.cstd` to write instead of the styles'. + cstd: Option, fonts: Vec<&'static str>, /// FibBase's flags and `nFib` (0: 0x00C1). flags: u16, @@ -609,7 +611,7 @@ impl Word97 { let plc: Vec = cps.iter().flat_map(|c| c.to_le_bytes()).chain(pcds).collect(); let clx = self.clx.clone().unwrap_or_else(|| [vec![0x02], (plc.len() as u32).to_le_bytes().to_vec(), plc].concat()); let words = |v: &[u16]| v.iter().flat_map(|w| w.to_le_bytes()).collect::>(); - let mut stsh = words(&[18, self.styles.len() as u16, 10, 1, 0, 15, 0, 0, 0, 0]); + let mut stsh = words(&[18, self.cstd.unwrap_or(self.styles.len() as u16), 10, 1, 0, 15, 0, 0, 0, 0]); for (istd, s) in self.styles.iter().enumerate() { let Some((stk, base, papx, chpx)) = s else { stsh.extend([0, 0]); @@ -879,6 +881,13 @@ fn malformed_doc_fails_cleanly() { let styles = vec![Some((1, 1, vec![], vec![])), Some((1, 0, vec![], vec![]))]; let e = place(&mut s, "a.doc", &Word97 { styles, ..words() }.build()).unwrap_err(); assert!(e.contains("style sheet is damaged"), "{e}"); + // A style sheet with fewer styles than its count, and one with 0x0FFE styles (`cstd` "MUST be + // less than 0x0FFE", Stshif). + let styles = vec![Some((1, 0x0FFF, vec![], vec![]))]; + let e = place(&mut s, "a.doc", &Word97 { styles, cstd: Some(2), ..words() }.build()).unwrap_err(); + assert!(e.contains("style sheet is damaged"), "{e}"); + let e = place(&mut s, "a.doc", &Word97 { styles: vec![None; 0x0FFE], ..words() }.build()).unwrap_err(); + assert!(e.contains("style sheet is damaged"), "{e}"); let r = place(&mut s, "a.doc", &words().build()).unwrap(); assert_eq!(placed(&s, &r).0, "words"); assert_eq!(s.doc().unwrap().history.undo.len(), 5, "failed places leave no undo step"); diff --git a/docs/mcp.md b/docs/mcp.md index 9bcc36553..c25d6240c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3901,8 +3901,9 @@ tracked changes, tables, and the other stories the document has); a field with n warning. Documents saved by Word 6.0 or Word 95, encrypted or password-protected documents and documents last saved with fast save are refused, and so are a stream larger than 64 MiB, a document with more than 4 MiB of text, 100,000 pieces, 16,384 pages of properties or 100,000 paragraphs or runs, one whose 16-bit text holds half of a surrogate pair, one -whose structures lie outside their streams, one whose property pages are out of order, listed twice or overlap, and one -whose styles are based on each other in a loop. +whose structures lie outside their streams, one whose property pages are out of order, listed twice or overlap, one whose +style sheet counts 4,094 styles or more or holds fewer styles than it counts, and one whose styles are based on each +other in a loop. `file.place` answers `format: "doc"`. ## PDF presets