From 89972882978f47df2fbe9adce4492b1994c183c4 Mon Sep 17 00:00:00 2001 From: Yonghye Kwon Date: Sun, 20 Sep 2026 23:59:20 +0900 Subject: [PATCH 1/2] Fix CDATA decoding in worksheet text Signed-off-by: Yonghye Kwon --- src/helper/utils.rs | 29 ++++++++++++++--- tests/cdata_text.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 tests/cdata_text.rs diff --git a/src/helper/utils.rs b/src/helper/utils.rs index 77f39927..fa49ffbb 100644 --- a/src/helper/utils.rs +++ b/src/helper/utils.rs @@ -15,14 +15,35 @@ macro_rules! from_err { /// quick-xml reports references as separate `GeneralRef` events, so a text /// event alone would read `R&D` as `R` and `D`. Readers take the raw /// element content with `Reader::read_text_into` and unescape it here. -/// Malformed input keeps the raw text instead of panicking. +/// CDATA content stays literal; malformed spans keep their raw text. pub(crate) fn unescape_xml_text(e: &quick_xml::events::BytesText<'_>) -> String { let decoded = match e.decode() { Ok(decoded) => decoded, Err(_) => String::from_utf8_lossy(e), }; - match quick_xml::escape::unescape(&decoded) { - Ok(unescaped) => unescaped.into_owned(), - Err(_) => decoded.into_owned(), + let mut remaining = decoded.as_ref(); + let mut output = String::with_capacity(remaining.len()); + while let Some(start) = remaining.find("") else { + return decoded.into_owned(); + }; + output.push_str(&content[..end]); + remaining = &content[end + 3..]; + } + output.push_str(&quick_xml::escape::unescape(remaining).unwrap_or(remaining.into())); + output +} + +#[cfg(test)] +mod tests { + #[test] + fn malformed_text_keeps_the_existing_tolerant_behavior() { + for text in ["R&broken", " Vec { + let mut book = umya_spreadsheet::new_file(); + let sheet = book.get_sheet_mut(&0).unwrap(); + sheet.get_cell_mut("A1").set_value("CDATA regression"); + sheet + .get_header_footer_mut() + .get_odd_header_mut() + .set_value("HEADER_PLACEHOLDER"); + sheet + .get_header_footer_mut() + .get_odd_footer_mut() + .set_value("FOOTER_PLACEHOLDER"); + 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/worksheets/sheet1.xml" { + bytes = String::from_utf8(bytes) + .unwrap() + .replace("HEADER_PLACEHOLDER", content) + .replace("FOOTER_PLACEHOLDER", content) + .into_bytes(); + } + output + .start_file(part.name(), SimpleFileOptions::default()) + .unwrap(); + output.write_all(&bytes).unwrap(); + } + output.finish().unwrap().into_inner() +} + +#[test] +fn header_footer_text_preserves_cdata_and_decodes_only_outside_entities() { + for (xml, expected) in [ + ( + "&CResearch & Development", + "&CResearch & Development", + ), + ( + "", + "&C&BResearch & Development&B", + ), + ( + "&LBefore ]]> A", + "&LBefore & literal A&Rafter", + ), + ( + "&C", + "&COneTwo", + ), + ("<![CDATA[ordinary]]>", ""), + ] { + let bytes = workbook_with_header_text(xml); + let book = reader::xlsx::read_reader(Cursor::new(bytes), true).unwrap(); + let header_footer = book.get_sheet(&0).unwrap().get_header_footer(); + assert_eq!( + header_footer.get_odd_header().get_value(), + expected, + "{xml}" + ); + assert_eq!( + header_footer.get_odd_footer().get_value(), + expected, + "{xml}" + ); + } +} From 10efd229e65cd4aa2c2a40e431c1eb38d71a5ff7 Mon Sep 17 00:00:00 2001 From: Yonghye Kwon Date: Mon, 21 Sep 2026 00:01:21 +0900 Subject: [PATCH 2/2] ci: run checks for slash-named branches Signed-off-by: Yonghye Kwon --- .github/workflows/rust.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7ca3c030..e10aaf88 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,9 +2,9 @@ name: umya-spreadsheet on: push: - branches: ["*"] + branches: ["**"] pull_request: - branches: ["*"] + branches: ["**"] env: CARGO_TERM_COLOR: always