Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions src/reader/xlsx/workbook_rels.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
use std::io;

use quick_xml::{
Reader,
NsReader,
events::Event,
name::{
Namespace,
ResolveResult,
},
};

use super::{
Expand All @@ -16,6 +20,7 @@ use crate::{
helper::const_str::{
PIVOT_CACHE_DEF_NS,
PKG_WORKBOOK_RELS,
REL_NS,
},
structs::Workbook,
};
Expand All @@ -25,15 +30,18 @@ pub(crate) fn read<R: io::Read + io::Seek>(
wb: &mut Workbook,
) -> Result<Vec<(String, String, String)>, 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();

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();
Expand All @@ -52,3 +60,74 @@ pub(crate) fn read<R: io::Read + io::Seek>(

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\"/></{prefix}Relationships>"
);
assert_eq!(
relationships(&xml),
vec![(
"rId1".to_string(),
"worksheet".to_string(),
"worksheets/sheet1.xml".to_string(),
)]
);
}

#[rstest]
#[case(r#"<Relationship xmlns="urn:unrelated"/>"#)]
#[case(r#"<other:Relationship xmlns:other="urn:unrelated"/>"#)]
#[case(r#"<Relationship xmlns=""/>"#)]
#[case("<Other/>")]
fn unrelated_relationship_names_are_not_interpreted(#[case] child: &str) {
let xml = format!(r#"<Relationships xmlns="{REL_NS}">{child}</Relationships>"#);
assert!(relationships(&xml).is_empty());
}

#[test]
fn child_namespace_declarations_are_resolved_in_scope() {
let xml = format!(
"<Relationships xmlns=\"{REL_NS}\" \
xmlns:p=\"urn:unrelated\"><p:Relationship/><p:Relationship xmlns:p=\"{REL_NS}\" \
Id=\"rId1\" Type=\"worksheet\" \
Target=\"worksheets/sheet1.xml\"/><p:Relationship/></Relationships>"
);
assert_eq!(relationships(&xml).len(), 1);
}
}
77 changes: 77 additions & 0 deletions tests/workbook_relationship_namespaces.rs
Original file line number Diff line number Diff line change
@@ -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<u8> {
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("<Relationship", &format!("<{prefix}:Relationship"))
.replace("</Relationships>", &format!("</{prefix}:Relationships>"))
.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
);
}
}
Loading