diff --git a/crates/io/src/dxf_ext.rs b/crates/io/src/dxf_ext.rs index 0b733f47c..7a3994f55 100644 --- a/crates/io/src/dxf_ext.rs +++ b/crates/io/src/dxf_ext.rs @@ -844,15 +844,33 @@ pub(crate) fn apply_anno(d: &mut cadcraft_doc::Drawing, p: AnnoPayload) { std::sync::Arc::make_mut(b).annotative = true; } } + // Which layout holds each paper space object, found once: `modify_entity` searches every + // layout for a handle model space lacks, which a long list of missing handles made quadratic. + let mut in_layout: std::collections::HashMap = Default::default(); + if !p.objects.is_empty() { + // First layout wins, as `modify_entity` did. + for (i, l) in d.layouts.iter().enumerate() { + for h in l.entities.handles() { + in_layout.entry(h).or_insert(i); + } + } + } for o in p.objects { let Some(h) = Handle::parse_hex(&o.handle) else { continue }; + let store = if d.model.contains(h) { + &mut d.model + } else if let Some(l) = in_layout.get(&h).and_then(|&i| d.layouts.get_mut(i)) { + &mut l.entities + } else { + continue; + }; let mut scales: Vec = Vec::new(); for s in o.scales.into_iter().filter(|s| s.is_valid() && !s.name.trim().is_empty()) { if scales.len() < cadcraft_doc::MAX_SCALES && !scales.iter().any(|x| x.name.eq_ignore_ascii_case(&s.name)) { scales.push(s); } } - let _ = d.modify_entity(h, |e| { + store.modify(h, |e| { if cadcraft_doc::can_be_annotative(&e.kind) { e.common.anno_scales = scales; } diff --git a/crates/io/src/dxf_raw.rs b/crates/io/src/dxf_raw.rs index 3eb9cb5ca..d1e0cd2c6 100644 --- a/crates/io/src/dxf_raw.rs +++ b/crates/io/src/dxf_raw.rs @@ -339,7 +339,8 @@ pub(crate) fn read(secs: &[Section], d: &mut Drawing) { } let root = objects.first().filter(|(k, _, _)| is_dictionary(k)).and_then(|(_, _, t)| head(t).handle); let root_entries = objects.first().filter(|(k, _, _)| is_dictionary(k)).map(|(_, _, t)| entries(t)).unwrap_or_default(); - let root_key = |h: &str| root_entries.iter().find(|(_, eh)| eh == h).map(|(k, _)| k.clone()); + let root_keys = root_key_index(&root_entries); + let root_key = |h: &str| root_keys.get(h).map(|k| (*k).to_string()); // An extension dictionary of an entity or record (not of another object). let is_xdict = |k: usize| { let Some((_, _, t)) = objects.get(k) else { return false }; @@ -494,6 +495,16 @@ fn extension_dictionaries( dictionaries } +/// The root dictionary's key for each entry handle (the first key when several name one handle), +/// built once: a lookup per object scanning the entries made a large dictionary quadratic. +fn root_key_index(root_entries: &[(String, String)]) -> HashMap<&str, &str> { + let mut index = HashMap::with_capacity(root_entries.len()); + for (key, handle) in root_entries { + index.entry(handle.as_str()).or_insert(key.as_str()); + } + index +} + /// Determine which objects belong to unmodelled dictionary entries or owners. fn retained_objects( objects: &[(String, usize, &[Tag])], @@ -504,7 +515,8 @@ fn retained_objects( root_entries: &[(String, String)], xdict_of: &HashMap, ) -> HashMap { - let root_key = |handle: &str| root_entries.iter().find(|(_, entry_handle)| entry_handle == handle).map(|(key, _)| key); + let root_keys = root_key_index(root_entries); + let root_key = |handle: &str| root_keys.get(handle).copied(); let is_xdict = |index: usize| { let Some((_, _, tags)) = objects.get(index) else { return false }; let header = head(tags); diff --git a/crates/io/src/dxf_write/sections.rs b/crates/io/src/dxf_write/sections.rs index 2e2d6a1e7..24c877b72 100644 --- a/crates/io/src/dxf_write/sections.rs +++ b/crates/io/src/dxf_write/sections.rs @@ -1204,33 +1204,56 @@ fn add_class_instance_counts(tags: &mut Vec, target: Version) { return; } let mut counts: HashMap = HashMap::new(); + let mut classes = 0; for t in tags.iter().filter(|t| t.code == 0) { let kind = t.str(); if kind != "CLASS" { *counts.entry(kind.to_ascii_uppercase()).or_default() += 1; + } else { + classes += 1; } } - let mut start = 0; - while start < tags.len() { - if tags.get(start).is_none_or(|t| t.code != 0 || t.str() != "CLASS") { - start += 1; + // One pass into a new list: inserting each missing count in place shifted every later tag, + // quadratic in the number of CLASS records. + // Tags move by value (no clone); the old and new buffers coexist while it runs. At most one + // group 91 is added per CLASS record, so that is all the room the new list needs beyond the old one. + let old = std::mem::take(tags); + tags.reserve(old.len() + classes); + let mut it = old.into_iter(); + let mut pending = it.next(); + while let Some(t) = pending.take() { + if t.code != 0 || t.str() != "CLASS" { + tags.push(t); + pending = it.next(); continue; } - let end = tags[start + 1..].iter().position(|t| t.code == 0).map_or(tags.len(), |i| start + 1 + i); - let name = tags[start + 1..end].iter().find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase(); + let mut record = vec![t]; + for n in it.by_ref() { + if n.code == 0 { + pending = Some(n); + break; + } + record.push(n); + } + let name = record.iter().skip(1).find(|t| t.code == 1).map(Tag::str).unwrap_or_default().to_ascii_uppercase(); let count = counts.get(&name).copied().unwrap_or(0); - let inserted = if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 91) { - tags[start + 1 + i] = Tag::i(91, count); - false - } else if let Some(i) = tags[start + 1..end].iter().position(|t| t.code == 90) { - tags.insert(start + 2 + i, Tag::i(91, count)); - true - } else { - false - }; - // Inserting the group shifts the next record's start by one. - start = end + usize::from(inserted); + // The first 91 gets the count; without one, a 91 goes in after the first 90. + let mut added = record.iter().skip(1).any(|t| t.code == 91); + let mut replaced = false; + for (i, t) in record.into_iter().enumerate() { + let code = t.code; + if i > 0 && code == 91 && !replaced { + tags.push(Tag::i(91, count)); + replaced = true; + continue; + } + tags.push(t); + if i > 0 && code == 90 && !added { + tags.push(Tag::i(91, count)); + added = true; + } + } } } @@ -1258,3 +1281,31 @@ fn mark_xrefs(tags: &mut [Tag], d: &Drawing) { } } } + +#[cfg(test)] +mod class_count_tests { + use super::*; + + fn record(kind: &str, name: &str, extra: &[Tag]) -> Vec { + let mut r = vec![Tag::s(0, kind), Tag::s(1, name), Tag::i(90, 0)]; + r.extend_from_slice(extra); + r + } + + /// The first 91 gets the count, a CLASS without one gets it after its 90, in one pass over many records. + #[test] + fn counts_are_set_and_added_in_one_pass() { + let mut tags = record("CLASS", "FOO", &[]); + tags.extend(record("CLASS", "BAR", &[Tag::i(91, 7), Tag::i(91, 8)])); + for i in 0..50_000 { + tags.extend(record("CLASS", &format!("X{i}"), &[])); + } + tags.push(Tag::s(0, "FOO")); + let started = std::time::Instant::now(); + add_class_instance_counts(&mut tags, Version::R2018); + assert!(started.elapsed() < std::time::Duration::from_secs(5)); + assert_eq!(&tags[..4], &[Tag::s(0, "CLASS"), Tag::s(1, "FOO"), Tag::i(90, 0), Tag::i(91, 1)]); + assert_eq!(&tags[4..9], &[Tag::s(0, "CLASS"), Tag::s(1, "BAR"), Tag::i(90, 0), Tag::i(91, 0), Tag::i(91, 8)]); + assert_eq!(tags.iter().filter(|t| t.code == 91).count(), 50_003); + } +} diff --git a/crates/io/src/svg_read.rs b/crates/io/src/svg_read.rs index 54ef9d846..d94a7e60b 100644 --- a/crates/io/src/svg_read.rs +++ b/crates/io/src/svg_read.rs @@ -590,7 +590,10 @@ fn hidden(e: &BytesStart) -> bool { } fn attr(e: &BytesStart, name: &[u8]) -> Option { - for a in e.attributes().flatten() { + // Stop at the first malformed attribute: skipping errors resumed the parser's duplicate check, + // which rescans the attributes before it each time, so many duplicates were quadratic. + for a in e.attributes() { + let Ok(a) = a else { return None }; if a.key.into_inner().as_bytes() == name { return a.normalized_value(XmlVersion::Explicit1_0).ok().map(|v| v.into_owned()); } diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs index 040e71927..1d21467e5 100644 --- a/crates/io/src/tests.rs +++ b/crates/io/src/tests.rs @@ -396,6 +396,16 @@ fn many_distinct_symbol_names_read_quickly() { assert_eq!(d.text_style("ST0").map(|s| s.height), Some(2.0)); } +/// Elements with many duplicate attributes are read in bounded time. +#[test] +fn svg_duplicate_attributes_are_not_rescanned() { + let dup = " x=\"1\"".repeat(20_000); + let svg = format!("{}", format!("").repeat(20)); + let started = std::time::Instant::now(); + let _ = read(svg.as_bytes(), "dup.svg"); + assert!(started.elapsed() < std::time::Duration::from_secs(10), "took {:?}", started.elapsed()); +} + #[test] fn hostile_dxf_does_not_panic() { for t in [