mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Bump quick-xml to 0.41 (RUSTSEC-2026-0194, RUSTSEC-2026-0195)
The .docx/.xlsx text import iterates attributes with quick-xml's default duplicate check, so one crafted start tag with N attributes cost O(N^2) (RUSTSEC-2026-0194, fixed in quick-xml 0.41). Bumping also clears RUSTSEC-2026-0195 (NsReader namespace allocation), which this workspace does not reach (no NsReader). unescape_value() is deprecated in 0.41 (clippy -D warnings); normalized_value(Implicit1_0) is what the deprecated method now does. Regression test: an element with 100,000 attributes parses in < 5 s. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
e862e0a613
commit
5a5e97b0a6
Generated
+4
-13
@@ -983,7 +983,7 @@ dependencies = [
|
||||
"hayro-syntax",
|
||||
"kurbo",
|
||||
"log",
|
||||
"quick-xml 0.38.4",
|
||||
"quick-xml",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1046,7 +1046,7 @@ dependencies = [
|
||||
"designcraft-compose",
|
||||
"designcraft-doc",
|
||||
"designcraft-geom",
|
||||
"quick-xml 0.38.4",
|
||||
"quick-xml",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
@@ -1120,7 +1120,7 @@ name = "designcraft-textimport"
|
||||
version = "0.7.1"
|
||||
dependencies = [
|
||||
"designcraft-doc",
|
||||
"quick-xml 0.38.4",
|
||||
"quick-xml",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -3505,15 +3505,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"
|
||||
@@ -4989,7 +4980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml 0.41.0",
|
||||
"quick-xml",
|
||||
"quote",
|
||||
]
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ flo_curves = "0.8"
|
||||
qrcodegen = "1.8"
|
||||
krilla = { version = "0.8", features = ["pdf"] }
|
||||
hayro = "0.7"
|
||||
quick-xml = "0.38"
|
||||
quick-xml = "0.41"
|
||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||
|
||||
# ui
|
||||
|
||||
@@ -283,7 +283,10 @@ fn start_el(e: &quick_xml::events::BytesStart) -> Result<El, String> {
|
||||
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)
|
||||
|
||||
@@ -67,7 +67,9 @@ pub(crate) fn parse(xml: &str) -> Result<El, ImportError> {
|
||||
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![],
|
||||
};
|
||||
@@ -79,7 +81,9 @@ pub(crate) fn parse(xml: &str) -> Result<El, ImportError> {
|
||||
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![],
|
||||
};
|
||||
@@ -510,4 +514,18 @@ mod tests {
|
||||
i.story.check().unwrap();
|
||||
assert_eq!(i.story.text, "Kept text. MOVED HERE. Plain INSERTED\nEnd.");
|
||||
}
|
||||
|
||||
#[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!("<w:document><w:body><w:p{attrs}/></w:body></w:document>");
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user