diff --git a/src/reader/xlsx/workbook_rels.rs b/src/reader/xlsx/workbook_rels.rs index 53185210..815bdb24 100644 --- a/src/reader/xlsx/workbook_rels.rs +++ b/src/reader/xlsx/workbook_rels.rs @@ -1,8 +1,12 @@ use std::io; use quick_xml::{ - Reader, + NsReader, events::Event, + name::{ + Namespace, + ResolveResult, + }, }; use super::{ @@ -16,6 +20,7 @@ use crate::{ helper::const_str::{ PIVOT_CACHE_DEF_NS, PKG_WORKBOOK_RELS, + REL_NS, }, structs::Workbook, }; @@ -25,7 +30,7 @@ pub(crate) fn read( wb: &mut Workbook, ) -> Result, XlsxError> { let r = io::BufReader::new(super::driver::zip_by_name(arv, PKG_WORKBOOK_RELS)?); - let mut reader = Reader::from_reader(r); + let mut reader = NsReader::from_reader(r); reader.config_mut().trim_text(true); let mut result: Vec<(String, String, String)> = Vec::new(); @@ -33,7 +38,10 @@ pub(crate) fn read( xml_read_loop!( reader, Event::Empty(ref e) => { - if e.name().into_inner() == b"Relationship" { + let (namespace, local_name) = reader.resolver().resolve_element(e.name()); + if namespace == ResolveResult::Bound(Namespace(REL_NS.as_bytes())) + && local_name.as_ref() == b"Relationship" + { let id_value = get_attribute(e, b"Id").unwrap(); let type_value = get_attribute(e, b"Type").unwrap(); let target_value = get_attribute(e, b"Target").unwrap(); @@ -52,3 +60,74 @@ pub(crate) fn read( Ok(result) } + +#[cfg(test)] +mod tests { + use std::io::{ + Cursor, + Write, + }; + + use rstest::rstest; + use zip::{ + ZipArchive, + ZipWriter, + write::SimpleFileOptions, + }; + + use super::*; + + fn relationships(xml: &str) -> Vec<(String, String, String)> { + let mut zip = ZipWriter::new(Cursor::new(Vec::new())); + zip.start_file(PKG_WORKBOOK_RELS, SimpleFileOptions::default()) + .unwrap(); + zip.write_all(xml.as_bytes()).unwrap(); + let mut archive = ZipArchive::new(zip.finish().unwrap()).unwrap(); + read(&mut archive, &mut Workbook::default()).unwrap() + } + + #[rstest] + #[case("")] + #[case("ns0:")] + #[case("pkg:")] + fn relationship_namespace_accepts_equivalent_spellings(#[case] prefix: &str) { + let declaration = if prefix.is_empty() { + "xmlns".to_string() + } else { + format!("xmlns:{}", prefix.trim_end_matches(':')) + }; + let xml = format!( + "<{prefix}Relationships {declaration}=\"{REL_NS}\"><{prefix}Relationship Id=\"rId1\" \ + Type=\"worksheet\" Target=\"/xl/worksheets/sheet1.xml\"/>" + ); + assert_eq!( + relationships(&xml), + vec![( + "rId1".to_string(), + "worksheet".to_string(), + "worksheets/sheet1.xml".to_string(), + )] + ); + } + + #[rstest] + #[case(r#""#)] + #[case(r#""#)] + #[case(r#""#)] + #[case("")] + fn unrelated_relationship_names_are_not_interpreted(#[case] child: &str) { + let xml = format!(r#"{child}"#); + assert!(relationships(&xml).is_empty()); + } + + #[test] + fn child_namespace_declarations_are_resolved_in_scope() { + let xml = format!( + "" + ); + assert_eq!(relationships(&xml).len(), 1); + } +} diff --git a/tests/workbook_relationship_namespaces.rs b/tests/workbook_relationship_namespaces.rs new file mode 100644 index 00000000..4e0e09ec --- /dev/null +++ b/tests/workbook_relationship_namespaces.rs @@ -0,0 +1,77 @@ +//! Workbook relationships must be resolved by namespace, not prefix spelling. +use std::io::{ + Cursor, + Read, + Write, +}; + +use rstest::rstest; +use umya_spreadsheet::{ + Border, + reader, + writer, +}; +use zip::{ + ZipArchive, + ZipWriter, + write::SimpleFileOptions, +}; + +fn workbook_with_relationship_prefix(prefix: &str) -> Vec { + let mut book = umya_spreadsheet::new_file(); + let sheet = book.sheet_mut(0).unwrap(); + sheet.cell_mut("A1").set_value("Namespace regression"); + sheet.cell_mut("B2").set_value_number(42); + sheet.style_mut("A1").font_mut().set_bold(true); + sheet + .style_mut("A1") + .borders_mut() + .bottom_mut() + .set_border_style(Border::BORDER_THIN); + + let mut original = Vec::new(); + writer::xlsx::write_writer(&book, &mut original).unwrap(); + let mut source = ZipArchive::new(Cursor::new(original)).unwrap(); + let mut output = ZipWriter::new(Cursor::new(Vec::new())); + for index in 0..source.len() { + let mut part = source.by_index(index).unwrap(); + let mut bytes = Vec::new(); + part.read_to_end(&mut bytes).unwrap(); + if part.name() == "xl/_rels/workbook.xml.rels" && !prefix.is_empty() { + let xml = String::from_utf8(bytes).unwrap(); + bytes = xml + .replace("", &format!("")) + .replace("xmlns=", &format!("xmlns:{prefix}=")) + .into_bytes(); + } + output + .start_file(part.name(), SimpleFileOptions::default()) + .unwrap(); + output.write_all(&bytes).unwrap(); + } + output.finish().unwrap().into_inner() +} + +#[rstest] +#[case("")] +#[case("ns0")] +#[case("pkg")] +fn workbook_relationship_prefix_preserves_cells_and_styles(#[case] prefix: &str) { + let bytes = workbook_with_relationship_prefix(prefix); + let book = reader::xlsx::read_reader(Cursor::new(bytes), true).unwrap(); + for book in [book.clone(), { + let mut output = Vec::new(); + writer::xlsx::write_writer(&book, &mut output).unwrap(); + reader::xlsx::read_reader(Cursor::new(output), true).unwrap() + }] { + let sheet = book.sheet(0).unwrap(); + assert_eq!(sheet.value("A1"), "Namespace regression"); + assert_eq!(sheet.value("B2"), "42"); + assert!(sheet.style("A1").font().unwrap().bold()); + assert_eq!( + sheet.style("A1").borders().unwrap().bottom().border_style(), + Border::BORDER_THIN + ); + } +}