diff --git a/crates/doc/src/array.rs b/crates/doc/src/array.rs index 57d4da53..b663785e 100644 --- a/crates/doc/src/array.rs +++ b/crates/doc/src/array.rs @@ -248,10 +248,17 @@ impl ArrayParams { return false; } let Some(items) = self.items() else { return false }; + // Index the replacement entries by placement so each item is matched in O(1) instead of + // scanning the whole `replaced` vector per item, which is quadratic when both the items + // and the replacements are large (CWE-407). Last entry wins, as `.rev().find` did. + let mut last_replaced: std::collections::HashMap<(u32, u32, u32), usize> = std::collections::HashMap::with_capacity(self.replaced.len()); + for (idx, r) in self.replaced.iter().enumerate() { + last_replaced.insert((r.i, r.j, r.level), idx); + } let mut source: Option<&str> = None; for (it, e) in items.iter().zip(&ents) { let EntityKind::Insert(ins) = &e.kind else { return false }; - match self.replaced.iter_mut().rev().find(|r| (r.i, r.j, r.level) == (it.i, it.j, it.level)) { + match last_replaced.get(&(it.i, it.j, it.level)).and_then(|&idx| self.replaced.get_mut(idx)) { Some(r) => r.block.clone_from(&ins.block), None => match source { None => source = Some(&ins.block), diff --git a/crates/doc/src/dynblock.rs b/crates/doc/src/dynblock.rs index 6b501a83..6887459a 100644 --- a/crates/doc/src/dynblock.rs +++ b/crates/doc/src/dynblock.rs @@ -334,7 +334,11 @@ fn graph_members<'a>(objects: &HashMap, xdict: Handle } let graph = objects.get(&graph?).filter(|o| o.kind.eq_ignore_ascii_case("ACAD_EVALUATION_GRAPH"))?; let members = section(§ions(&graph.tags), "AcDbEvalGraph").iter().filter(|t| t.code == 360).filter_map(|t| Handle::parse_hex(&t.value)); - Some(members.take(MAX_NODES).filter_map(|h| objects.get(&h).copied()).collect()) + // Deduplicate member handles before resolving them: a graph can list the same member many + // times, and parsing each occurrence into its own owned parameter multiplies a single large + // record into gigabytes of retained allocations (CWE-400). + let mut seen = std::collections::HashSet::new(); + Some(members.take(MAX_NODES).filter(|h| seen.insert(*h)).filter_map(|h| objects.get(&h).copied()).collect()) } /// The dynamic block data of block `name` (any case; `*Model_Space` for a drawing that is a diff --git a/crates/doc/src/dynblock/apply.rs b/crates/doc/src/dynblock/apply.rs index b727c153..70ef484e 100644 --- a/crates/doc/src/dynblock/apply.rs +++ b/crates/doc/src/dynblock/apply.rs @@ -464,7 +464,15 @@ fn apply( if !(v.x.is_finite() && v.y.is_finite()) || (v.x == 0.0 && v.y == 0.0) { continue; } + // Index this action's vertex lists by handle once, and skip handles already handled in + // this action's selection, so a selection that repeats a handle does not re-scan the + // vertex list and re-clone its index vector per occurrence (CWE-400). + let vtx_by_handle: HashMap> = vertices.iter().rev().map(|(vh, i)| (*vh, i)).collect(); + let mut handled: HashSet = HashSet::new(); for h in &a.selection { + if !handled.insert(*h) { + continue; + } if let Some(node) = param_of.get(h) { // A parameter in the selection is carried along: its points in the frame. Which // points are in it is decided where the dynamic block has them (AutoCAD evaluates @@ -489,7 +497,7 @@ fn apply( continue; } let Some(c) = copy_of(h) else { continue }; - let idx = vertices.iter().find(|(vh, _)| vh == h).map(|(_, i)| i.clone()); + let idx = vtx_by_handle.get(h).map(|i| (*i).clone()); moves.entry(c).or_default().push((idx, v)); } } diff --git a/crates/doc/src/entity.rs b/crates/doc/src/entity.rs index 4d22c175..87c7e45b 100644 --- a/crates/doc/src/entity.rs +++ b/crates/doc/src/entity.rs @@ -1408,9 +1408,12 @@ impl EntityKind { EntityKind::LwPolyline(p) => { let mut g: Vec = Vec::new(); let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed }; + // Compute the segments once, not once per vertex: rebuilding the whole segment + // vector inside the loop is quadratic in the vertex count (CWE-407). + let segs = pl.segments(); for (i, v) in p.vertices.iter().enumerate() { g.push(v.p); - if let Some(s) = pl.segments().get(i) { + if let Some(s) = segs.get(i) { g.push(s.mid()); } } @@ -1468,3 +1471,21 @@ fn transform_text(t: &mut Text, m: &Mat3, s: f64, rot: f64, mirror: bool) { // MIRRTEXT = 0 (the default): mirrored text keeps reading left to right. t.rotation = cadcraft_geom::norm_angle(t.rotation + if mirror { 0.0 } else { rot }); } + +#[cfg(test)] +mod grip_tests { + use super::*; + use cadcraft_geom::PolyVertex; + + #[test] + fn polyline_grips_are_vertices_and_midpoints() { + // Grips are each vertex plus the midpoint of each segment; the segment vector is computed + // once rather than rebuilt per vertex (CWE-407, row 35). + let verts: Vec = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)].iter().map(|&(x, y)| PolyVertex::new(Vec2::new(x, y))).collect(); + let e = EntityKind::LwPolyline(LwPolyline { vertices: verts, closed: false, const_width: 0.0, elevation: 0.0, plinegen: false }); + let g = e.grips(); + // 3 vertices + 2 segment midpoints. + assert_eq!(g.len(), 5, "{g:?}"); + assert!(g.contains(&Vec2::new(0.0, 0.0)) && g.contains(&Vec2::new(5.0, 0.0))); + } +} diff --git a/crates/doc/src/extents.rs b/crates/doc/src/extents.rs index 0fbd73c8..d7c7c7a8 100644 --- a/crates/doc/src/extents.rs +++ b/crates/doc/src/extents.rs @@ -165,9 +165,21 @@ fn bounds_in<'a>(d: &'a Drawing, e: &Entity, depth: usize, blocks: &mut HashMap< if let Some(blk) = dm.block.as_ref().and_then(|n| d.block(n)) && depth < MAX_BLOCK_DEPTH { - for be in blk.entities.iter().filter(|be| in_block_visible(d, be)) { - b = b.union(&bounds_in(d, be, depth + 1, blocks)); - } + // Reuse the shared block-bounds cache (as the Insert branch does) so a dimension + // block with branching or self-referencing children costs one pass per definition + // per depth, not one per reference (which grows exponentially) (CWE-400). + let inner = match blocks.get(&(blk.name.as_str(), depth)) { + Some(cached) => *cached, + None => { + let mut inner = Bounds2::EMPTY; + for be in blk.entities.iter().filter(|be| in_block_visible(d, be)) { + inner = inner.union(&bounds_in(d, be, depth + 1, blocks)); + } + blocks.insert((blk.name.as_str(), depth), inner); + inner + } + }; + b = b.union(&inner); } b }