diff --git a/Cargo.lock b/Cargo.lock index 6b405ed6..aa11ac71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -994,7 +994,7 @@ dependencies = [ "hayro-syntax", "kurbo", "log", - "quick-xml 0.38.4", + "quick-xml", "regex", "serde", "serde_json", @@ -1058,7 +1058,7 @@ dependencies = [ "designcraft-compose", "designcraft-doc", "designcraft-geom", - "quick-xml 0.38.4", + "quick-xml", "thiserror 2.0.21", "zip", ] @@ -1132,7 +1132,7 @@ name = "designcraft-textimport" version = "0.8.0" dependencies = [ "designcraft-doc", - "quick-xml 0.38.4", + "quick-xml", "zip", ] @@ -3514,15 +3514,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.38.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -5009,7 +5000,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml 0.41.0", + "quick-xml", "quote", ] diff --git a/Cargo.toml b/Cargo.toml index efd3b617..4e972632 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ flo_curves = "0.8" # QR codes (Object › Generate QR Code); MIT. qrcodegen = "1.8" hayro = "0.7" -quick-xml = "0.38" +quick-xml = "0.41" zip = { version = "3", default-features = false, features = ["deflate"] } # ui diff --git a/crates/idml/src/xml.rs b/crates/idml/src/xml.rs index cb09249a..97aab239 100644 --- a/crates/idml/src/xml.rs +++ b/crates/idml/src/xml.rs @@ -283,7 +283,10 @@ fn start_el(e: &quick_xml::events::BytesStart) -> Result { for a in e.attributes().with_checks(false) { let a = a.map_err(|e| e.to_string())?; let k = String::from_utf8_lossy(a.key.as_ref()).to_string(); - let v = a.unescape_value().map(|v| v.to_string()).unwrap_or_else(|_| String::from_utf8_lossy(&a.value).to_string()); + let v = a + .normalized_value(quick_xml::XmlVersion::Implicit1_0) + .map(|v| v.to_string()) + .unwrap_or_else(|_| String::from_utf8_lossy(&a.value).to_string()); el.attrs.push((k, v)); } Ok(el) diff --git a/crates/textimport/src/docx.rs b/crates/textimport/src/docx.rs index a51e9fe0..32797919 100644 --- a/crates/textimport/src/docx.rs +++ b/crates/textimport/src/docx.rs @@ -68,7 +68,9 @@ pub(crate) fn parse(xml: &str) -> Result { attrs: e .attributes() .flatten() - .map(|a| (local(a.key.as_ref()), a.unescape_value().map(|v| v.to_string()).unwrap_or_default())) + .map(|a| { + (local(a.key.as_ref()), a.normalized_value(quick_xml::XmlVersion::Implicit1_0).map(|v| v.to_string()).unwrap_or_default()) + }) .collect(), kids: vec![], }; @@ -80,7 +82,9 @@ pub(crate) fn parse(xml: &str) -> Result { attrs: e .attributes() .flatten() - .map(|a| (local(a.key.as_ref()), a.unescape_value().map(|v| v.to_string()).unwrap_or_default())) + .map(|a| { + (local(a.key.as_ref()), a.normalized_value(quick_xml::XmlVersion::Implicit1_0).map(|v| v.to_string()).unwrap_or_default()) + }) .collect(), kids: vec![], }; @@ -792,4 +796,18 @@ mod tests { assert_eq!(i.story.paras[0].para.list_type, None); assert_eq!(i.warnings.len(), 1); } + + #[test] + fn an_element_with_many_attributes_parses_in_linear_time() { + // RUSTSEC-2026-0194: before quick-xml 0.41 the duplicate-attribute check compared each name with every + // earlier one on the element, so a crafted .docx/.xlsx with one huge tag hung the import (O(N²)). + let attrs: String = (0..100_000).map(|i| format!(" w:a{i}=\"{i}\"")).collect(); + let xml = format!(""); + let started = std::time::Instant::now(); + let doc = parse(&xml).unwrap(); + assert!(started.elapsed() < std::time::Duration::from_secs(5), "parse took {:?}", started.elapsed()); + let p = doc.child("body").and_then(|b| b.child("p")).unwrap(); + assert_eq!(p.attr("a0"), Some("0")); + assert_eq!(p.attr("a99999"), Some("99999")); + } }