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();