XLS: a FORMAT record shorter than its character count keeps the characters it holds (fixes #731)

This commit is contained in:
Marco Arnulfo 2026-10-12 18:41:45 +02:00
parent fc45b7f6d8
commit 3d0ed97d71
2 changed files with 47 additions and 3 deletions

View File

@ -476,3 +476,39 @@ fn sheets_share_one_record_budget() {
let err = read_xls(&compound("Workbook", &dup)).unwrap_err().to_string();
assert!(err.contains("same substream"), "{err}");
}
#[test]
fn a_format_shorter_than_its_count_keeps_its_characters() {
// A FORMAT whose cch is one more than the characters in the record (seen in real files):
// Excel opens them, the record's characters are the format code.
let short = rec(0x041E, &cat(&[&165u16.to_le_bytes(), &6u16.to_le_bytes(), &[0], b"0.000"]));
let xf = |ifmt: u16| rec(0x00E0, &cat(&[&u16s(&[0, ifmt, 0]), &[0; 14]]));
let globals = |pos: u32| {
cat(&[
&bof(0x0005),
&short,
&rec(0x041E, &cat(&[&164u16.to_le_bytes(), &xlstr("yyyy-mm-dd")])),
&xf(0),
&xf(164),
&xf(165),
&rec(0x0085, &cat(&[&pos.to_le_bytes(), &[0, 0, 6, 0], b"Sheet1"])),
&eof(),
])
};
let sheet = cat(&[
&bof(0x0010),
&rec(0x0203, &cat(&[&cell(0, 0, 0), &7.0f64.to_le_bytes()])),
&rec(0x0203, &cat(&[&cell(1, 0, 1), &0.0f64.to_le_bytes()])),
&rec(0x0203, &cat(&[&cell(2, 0, 2), &1.25f64.to_le_bytes()])),
&eof(),
]);
let stream = cat(&[&globals(globals(0).len() as u32), &sheet]);
let (wb, report) = read_xls(&compound("Workbook", &stream)).unwrap();
let sh = &wb.sheets[0];
let fmt = |a1: &str| wb.styles.get(sh.style_id(CellRef::parse(a1).unwrap())).num_fmt.as_str().to_string();
assert_eq!(v(&wb, 0, "A1"), Value::Number(7.0));
assert_eq!(v(&wb, 0, "A3"), Value::Number(1.25));
assert_eq!(fmt("A2"), "yyyy-mm-dd");
assert_eq!(fmt("A3"), "0.000");
assert!(report.warnings.iter().any(|w| w.contains("declared length")), "{:?}", report.warnings);
}

View File

@ -143,7 +143,7 @@ pub(crate) fn read_xls_with(bytes: &[u8], mut budget: Budget) -> Result<(Workboo
}
};
let mut report = ReadReport::default();
let g = globals(&stream, &mut budget)?;
let g = globals(&stream, &mut budget, &mut report)?;
let mut wb = Workbook::new();
wb.sheets.clear();
@ -205,7 +205,7 @@ fn bof(rec: &Record<'_>) -> Result<(u16, u16), IoError> {
Ok((c.u16()?, c.u16()?))
}
fn globals(stream: &[u8], budget: &mut Budget) -> Result<Globals, IoError> {
fn globals(stream: &[u8], budget: &mut Budget, report: &mut ReadReport) -> Result<Globals, IoError> {
let mut recs = Records::new(stream, 0);
let first = recs.next_record()?.ok_or_else(|| invalid("empty Workbook stream"))?;
let (vers, dt) = bof(&first)?;
@ -249,7 +249,15 @@ fn globals(stream: &[u8], budget: &mut Budget) -> Result<Globals, IoError> {
0x0022 => g.date1904 = c.u16()? == 1,
0x041E => {
let ifmt = c.u16()?;
let code = c.string()?;
let cch = usize::from(c.u16()?);
let high = c.u8()? & 1 != 0;
// Some writers count one character more than the record holds; Excel opens
// those files, so keep the characters that are there.
let fit = c.remaining() / if high { 2 } else { 1 };
if cch > fit {
report.warn("Some number formats were shorter than their declared length; the characters present were kept.");
}
let code = c.chars(cch.min(fit), high)?;
budget.text(code.len())?;
g.formats.insert(ifmt, code);
}