diff --git a/crates/xlsx/src/package.rs b/crates/xlsx/src/package.rs index 1c133d0ad..e06619e27 100644 --- a/crates/xlsx/src/package.rs +++ b/crates/xlsx/src/package.rs @@ -188,9 +188,15 @@ impl<'a> Package<'a> { self.binary_strings = Some(strings.len()); return Ok(strings); } - let Some(rel) = rels.first("sharedStrings").filter(|r| !r.external) else { return Ok(Vec::new()) }; - let Some(bytes) = self.opc.read(&rel.target)? else { - self.warn(format!("missing part {}", rel.target)); + let rel = rels.first("sharedStrings").filter(|r| !r.external); + // Some older spreadsheet writers omit the workbook relationship but still emit the + // conventional shared-string part. The pre-OPC reader accepted that layout, so retain + // compatibility by checking the standard part name only when the relationship is absent. + let target = rel.map_or("xl/sharedStrings.xml", |r| r.target.as_str()); + let Some(bytes) = self.opc.read(target)? else { + if rel.is_some() { + self.warn(format!("missing part {target}")); + } return Ok(Vec::new()); }; let mut items = Vec::new(); diff --git a/crates/xlsx/tests/xlsx.rs b/crates/xlsx/tests/xlsx.rs index c8355fd8c..ef932026e 100644 --- a/crates/xlsx/tests/xlsx.rs +++ b/crates/xlsx/tests/xlsx.rs @@ -168,6 +168,41 @@ fn a_missing_shared_strings_part_is_a_warning() { assert!(wb.warnings.iter().any(|w| w.contains("missing part xl/sharedStrings.xml")), "{:?}", wb.warnings); } +#[test] +fn shared_strings_at_the_conventional_path_work_without_a_relationship() { + const REL_NS: &str = "http://schemas.openxmlformats.org/package/2006/relationships"; + const DOC_REL: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"; + const MAIN: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"; + let root_rels = + format!(r#""#); + let workbook = format!(r#""#); + let workbook_rels = format!( + r#""# + ); + let worksheet = format!(r#"0"#); + let strings = format!(r#"Tea"#); + let bytes = zip(&[ + ("_rels/.rels", root_rels.as_bytes()), + ("xl/workbook.xml", workbook.as_bytes()), + ("xl/_rels/workbook.xml.rels", workbook_rels.as_bytes()), + ("xl/worksheets/sheet1.xml", worksheet.as_bytes()), + ("xl/sharedStrings.xml", strings.as_bytes()), + ]); + + let mut package = Package::open(&bytes, &Limits::default()).unwrap(); + let strings = package.shared_strings().unwrap(); + assert_eq!(strings.iter().map(|item| item.text.as_ref()).collect::>(), ["Tea"]); + let workbook = package.workbook().unwrap(); + let mut values = Vec::new(); + package + .sheet(&workbook.sheets[0], |row| { + values.extend(row.cells.into_iter().map(|cell| cell.value.resolve(&strings, false))); + Ok(()) + }) + .unwrap(); + assert_eq!(values, [Value::text("Tea")]); +} + #[test] fn limits_are_errors() { let rows: String = (1..=2000).map(|r| format!(r#"{r}"#)).collect();