Place: a Word .doc style sheet that holds fewer styles than it counts is refused

Stshif.cstd counts the LPStd records of the style sheet and must be less than
0x0FFE. A sheet that ended before cstd records was read as far as it went, and
paragraphs in the missing styles were placed with the document defaults; a
cstd of 0x0FFE or more was cut to 0x0FFE. Both are now refused as a damaged
style sheet.
This commit is contained in:
dotCooCoo
2026-11-02 12:03:58 +07:00
committed by bflatastic
parent 22218e9097
commit a6db4c9112
3 changed files with 19 additions and 6 deletions
+6 -3
View File
@@ -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.
+10 -1
View File
@@ -491,6 +491,8 @@ struct Word97 {
papx: Vec<(u32, u16, Vec<u8>)>,
/// The styles by istd; None: an empty slot.
styles: Vec<Option<Word97Style>>,
/// A style count `Stshif.cstd` to write instead of the styles'.
cstd: Option<u16>,
fonts: Vec<&'static str>,
/// FibBase's flags and `nFib` (0: 0x00C1).
flags: u16,
@@ -609,7 +611,7 @@ impl Word97 {
let plc: Vec<u8> = 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::<Vec<u8>>();
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");
+3 -2
View File
@@ -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