diff --git a/ROADMAP.md b/ROADMAP.md index 372a5ad928..9ee2b706d0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -259,6 +259,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm - 2026-11-03: Security: XLSX and ODS import (mail merge recipient lists) keep each cell's character count as text is added, instead of counting the whole cell again for every piece of text, so a full cell followed by many small text events no longer costs its length each time. Readiness unchanged. - 2026-11-03: Security: exported hyperlinks (ODT, LaTeX, HTML, Markdown, RTF) holding a control character or noncharacter that the writer drops are written as plain text, so `file\u{85}:///…` can't pass the link check and come out as a `file:` link. Readiness unchanged. - 2026-11-03: Security: pictures are measured, and checked for transparency for tight text wrapping, from a PNG's chunk headers, instead of through the decoder, whose header read inflates an embedded ICC profile of up to 512 MiB for every picture it only measures. Readiness unchanged. +- 2026-11-03: Security: saving an embedded object (a native document's kept markup included) collects the relationship ids its markup uses in a set and checks them against the writable ones in one pass, instead of scanning every id collected so far for each attribute. Readiness unchanged. - 2026-11-03: Security: the renderer leaves out a drawing path reaching more than a million device pixels (64 times the largest canvas) or of no finite size, which the rasteriser would otherwise flatten into points by the billion before clipping it. Readiness unchanged. - 2026-11-03: Language packs: language-packs-2 is published and WordCraft pins its index: 75 languages, adding Danish, German (Germany, Austria, Switzerland), Esperanto, Estonian, Persian, Croatian, Hungarian, Norwegian (Bokmål, Nynorsk), Dutch (#4145, #2505) and Swedish (Sweden, Finland), which need craft-proof's flag-based compounding (storytold/craft-libs#497, pinned). English (Australia) is checked against SCOWL's en-AU list as well as en-GB's (storytold/craft-libs#496). Mongolian and Thai stay hidden. Readiness unchanged. - 2026-11-03: Language lists (CP4.3): a language installed from a Language pack (Bulgarian, say) can now be chosen. Set Proofing Language lists every built-in language and every installed or available pack's, each once and sorted by name; Review › Language and the status bar's menu add the installed packs' languages to the short list, with one way of working out the check mark, by tag. A pack's language is stored as a tag that finds the pack again (`sr-Latn-RS` is proofed by the Latin `sr-latn` pack, not the Cyrillic one; Valencian is Word's `ca-ES-valencia`). Every pack in language-packs-2 now has an English name (Belarusian used to show its own name, Беларуская). Readiness unchanged. diff --git a/crates/docx/src/write/embed.rs b/crates/docx/src/write/embed.rs index 19154a8790..d8f40a209e 100644 --- a/crates/docx/src/write/embed.rs +++ b/crates/docx/src/write/embed.rs @@ -353,11 +353,11 @@ impl super::Writer<'_> { }; self.embeds.markup_bytes = left; let mut root = xml::parse(src.xml.as_bytes()).ok()?; - let mut used = Vec::new(); + let mut used = HashSet::new(); rel_attr_values(&root, &mut used, 0); // Every relationship the markup uses must be writable, or the object would be broken. - let writable = |id: &String| src.refs.iter().any(|r| r.id == *id && (r.external || self.embeds.available(&r.target))); - if !used.iter().all(writable) { + let writable: HashSet<&str> = src.refs.iter().filter(|r| r.external || self.embeds.available(&r.target)).map(|r| r.id.as_str()).collect(); + if !used.iter().all(|id| writable.contains(id.as_str())) { log::warn!("docx: an embedded object's parts are missing; it is left out"); return None; } @@ -638,13 +638,15 @@ fn set_attr(e: &mut El, k: &str, v: &str) { } } -fn rel_attr_values(e: &El, out: &mut Vec, depth: usize) { +/// The distinct relationship ids `e` uses: a set, so a large tree (a native document's markup +/// never went through the reader's id limit) costs no scan per attribute. +fn rel_attr_values(e: &El, out: &mut HashSet, depth: usize) { if depth > MAX_DEPTH { return; } for (k, v) in &e.attrs { if xml::is_rel_attr(&e.name, k) && !out.contains(v) { - out.push(v.clone()); + out.insert(v.clone()); } } for c in e.els() { @@ -852,6 +854,24 @@ fn valid_part_name(name: &str) -> String { mod tests { use super::*; + #[test] + fn relationship_ids_are_collected_once_each() { + // A native document's embedded markup skips the reader's id limit: 40,000 distinct ids + // (each named twice) were a scan of every id collected so far, per attribute. + let mut xml = String::from( + r#""#, + ); + for i in 0..40_000 { + xml.push_str(&format!(r#""#)); + } + xml.push_str(""); + let root = xml::parse(xml.as_bytes()).unwrap(); + let mut used = HashSet::new(); + rel_attr_values(&root, &mut used, 0); + assert_eq!(used.len(), 40_000); + assert!(used.contains("rId39999")); + } + #[test] fn charts_cut_to_the_limits_on_save_say_so_once() { // A model deserialised as is can hold more than a chart part keeps; the save left the rest