mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 04:01:39 +00:00
XLSX import: index worksheet relationships by id (#5424)
* fix(formats): index XLSX worksheet relationships by id * style: rustfmt --------- Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
This commit is contained in:
1 parent
b7c2fe8dff
commit
cd81d5da49
3 files changed
+29
-8
No files matched your search
@@ -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.
|
||||
|
||||
@@ -427,6 +427,27 @@ mod tests {
|
||||
|
||||
const RELS: &str = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#;
|
||||
|
||||
#[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#"<sheet name="S{i}" sheetId="{i}" r:id="rId{i}"/>"#)).collect();
|
||||
let wb = format!(
|
||||
r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets>{sheets}</sheets></workbook>"#
|
||||
);
|
||||
let mut rels = String::from(r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">"#);
|
||||
// A chart sheet sharing a worksheet's id doesn't hide it.
|
||||
rels.push_str(r#"<Relationship Id="rId0" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" Target="chartsheets/c.xml"/>"#);
|
||||
for i in 0..20_000 {
|
||||
rels.push_str(&format!(
|
||||
r#"<Relationship Id="rId{i}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet{i}.xml"/>"#
|
||||
));
|
||||
}
|
||||
rels.push_str("</Relationships>");
|
||||
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<u8> {
|
||||
let wb = r#"<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><workbookPr/><sheets><sheet name="People" sheetId="1" r:id="rId1"/><sheet name="Chart" sheetId="2" r:id="rId9"/><sheet name="Notes" sheetId="3" r:id="rId2"/></sheets></workbook>"#;
|
||||
let wb_rels = r#"<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="/xl/worksheets/sheet2.xml"/><Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" Target="chartsheets/sheet1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/><Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>"#;
|
||||
|
||||
@@ -83,14 +83,13 @@ fn workbook(pkg: &mut Package<'_>) -> Result<Workbook, String> {
|
||||
}
|
||||
}
|
||||
});
|
||||
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") })
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user