diff --git a/ROADMAP.md b/ROADMAP.md index 1b28d2b960..f4f1c4d6a1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -234,6 +234,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm - 2026-11-03: Security: Paste Special with picture files on the clipboard refuses anything that isn't a regular file (a pipe or device reports no length and can produce endless data), and `cargo xtask stats` no longer follows directory symlinks. Readiness unchanged. - 2026-11-03: Size keeps the side it isn't asked to change (#5469): typing only the width of a straight line keeps its height of 0 instead of making it 1 pt; the 1–4000 pt limits apply only to the sides a `picture.size` call changes. Readiness unchanged. - 2026-11-03: Find and Replace: a keyboard apostrophe also finds typographic single quotes, so smart-quoted typing stays searchable (#5388). Regex, wildcards, literal searches and numeric character codes remain exact. Original UTF-8 match ranges and replacement spelling are preserved; readiness unchanged. +- 2026-11-03: Security: XLSX import finds each sheet's worksheet part through an index of the workbook's relationships, built once, instead of searching every relationship for every sheet. Readiness unchanged. - 2026-11-03: Security: pasting text boxes charges each copied story for its formatting too (run, mark, paragraph and section properties), not only its text and objects, so a story with huge style or font names can't copy past the paste's 32 MiB text box budget. Readiness unchanged. - 2026-11-03: Security: `.doc` import keeps one width, shading, alignment and merge override per table cell, the last one winning as before, instead of a copy every time a paragraph's properties repeat the sprm, so a shared PAPX can't pile up copies on every paragraph mark. Readiness unchanged. - 2026-11-03: Security: `.doc` import attaches each section to the paragraph that ends it through an index of paragraph ends, instead of scanning every paragraph for every section, so a file with a thousand sections that end nowhere can't multiply the work over its paragraphs. Readiness unchanged. diff --git a/crates/formats/src/sheet.rs b/crates/formats/src/sheet.rs index 53c7fca84f..beeaa1f142 100644 --- a/crates/formats/src/sheet.rs +++ b/crates/formats/src/sheet.rs @@ -427,6 +427,27 @@ mod tests { const RELS: &str = r#""#; + #[test] + fn workbook_sheets_find_their_relationships_by_index() { + // Every sheet searched every relationship; 4,096 sheets over 20,000 relationships. + let sheets: String = (0..MAX_SHEETS).map(|i| format!(r#""#)).collect(); + let wb = format!( + r#"{sheets}"# + ); + let mut rels = String::from(r#""#); + // A chart sheet sharing a worksheet's id doesn't hide it. + rels.push_str(r#""#); + for i in 0..20_000 { + rels.push_str(&format!( + r#""# + )); + } + rels.push_str(""); + let names = sheet_names(&zip(&[("_rels/.rels", RELS), ("xl/workbook.xml", &wb), ("xl/_rels/workbook.xml.rels", &rels)])).unwrap(); + assert_eq!(names.len(), MAX_SHEETS); + assert_eq!(names.first().map(String::as_str), Some("S0")); + } + fn xlsx(sheet1: &str) -> Vec { let wb = r#""#; let wb_rels = r#""#; diff --git a/crates/formats/src/sheet/xlsx.rs b/crates/formats/src/sheet/xlsx.rs index dffa9be7dd..cca2472119 100644 --- a/crates/formats/src/sheet/xlsx.rs +++ b/crates/formats/src/sheet/xlsx.rs @@ -83,14 +83,13 @@ fn workbook(pkg: &mut Package<'_>) -> Result { } } }); - let sheets = listed - .into_iter() - .filter_map(|(name, id)| { - // Worksheets only: chart and dialog sheets hold no cells. - let r = rels.iter().find(|r| r.id == id && r.kind.ends_with("/worksheet"))?; - Some(SheetRef { name, path: r.target.clone() }) - }) - .collect(); + // Worksheets only: chart and dialog sheets hold no cells. Indexed by id once (the first + // relationship with an id wins), not searched for every sheet. + let mut worksheets: HashMap<&str, &str> = HashMap::new(); + for r in rels.iter().filter(|r| r.kind.ends_with("/worksheet")) { + worksheets.entry(r.id.as_str()).or_insert(r.target.as_str()); + } + let sheets = listed.into_iter().filter_map(|(name, id)| Some(SheetRef { name, path: worksheets.get(id.as_str())?.to_string() })).collect(); let part = |suffix: &str| rels.iter().find(|r| r.kind.ends_with(suffix)).map(|r| r.target.clone()); Ok(Workbook { sheets, date1904, shared: part("/sharedStrings"), styles: part("/styles") }) }