Files
cadcraft/crates/io/tests/block_expansion.rs
Valentin KleinandClaude Opus 5.5 575e690cc5 Bound block expansion in drawing and extents: nested, self-referencing or arrayed blocks no longer hang (fixes #255) (#267)
* Bound block expansion in drawing and extents (fixes #255)

MAX_BLOCK_DEPTH bounds only the depth of nested block references, so a
small DXF with a self-referencing block, blocks that each insert the next
several times, or one large MINSERT expanded to billions of copies and
hung the app (drawing, PNG/SVG/PDF export, extents, saving).

- render: a per-display-list budget (MAX_BLOCK_EXPANSION, 2 million
  block copies and entities drawn inside blocks); past it the remaining
  block contents are not drawn.
- doc: entity_bounds computes each block's content bounds once per call
  and reuses them for every further reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Move Builder::expand below the drawing primitives

Pure code motion so this branch merges cleanly with fix/272-aci7-only-flips
(#272), which changes the Builder::polyline signature next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:33:37 -04:00

66 lines
3.1 KiB
Rust

//! Nested, self-referencing and arrayed block references in a hostile DXF must not hang drawing
//! or extents (issue #255): the expansion is bounded, not only its depth.
use cadcraft_doc::{Drawing, EntityKind, Space};
use cadcraft_render::{MAX_BLOCK_EXPANSION, Options, build};
fn read(text: &str) -> Drawing {
cadcraft_io::read(text.as_bytes(), "x.dxf").unwrap()
}
/// Block `A` holds a line and `refs` references to itself; model space holds one `A`.
fn self_referencing(refs: usize) -> String {
let mut s = String::from("0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\nA\n0\nLINE\n10\n0\n20\n0\n11\n1\n21\n1\n");
for k in 1..=refs {
s += &format!("0\nINSERT\n2\nA\n10\n{k}\n20\n0\n");
}
s + "0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nINSERT\n2\nA\n10\n0\n20\n0\n0\nENDSEC\n0\nEOF\n"
}
/// Blocks `F0`..`F14`, each holding four references to the next; `F15` holds a line (drawn
/// 4^15 times: its references sit exactly at `MAX_BLOCK_DEPTH`).
fn fan_out() -> String {
let mut s = String::from("0\nSECTION\n2\nBLOCKS\n");
for k in 0..15 {
s += &format!("0\nBLOCK\n2\nF{k}\n");
for x in 0..4 {
s += &format!("0\nINSERT\n2\nF{}\n10\n{x}\n20\n0\n", k + 1);
}
s += "0\nENDBLK\n";
}
s + "0\nBLOCK\n2\nF15\n0\nLINE\n10\n0\n20\n0\n11\n1\n21\n1\n0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nINSERT\n2\nF0\n10\n0\n20\n0\n0\nENDSEC\n0\nEOF\n"
}
/// One MINSERT of a one-line block with `cols` x `rows` copies 2 units apart.
fn minsert(cols: u32, rows: u32) -> String {
format!(
"0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\nA\n0\nLINE\n10\n0\n20\n0\n11\n1\n21\n1\n0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nINSERT\n2\nA\n10\n0\n20\n0\n70\n{cols}\n71\n{rows}\n44\n2\n45\n2\n0\nENDSEC\n0\nEOF\n"
)
}
#[test]
fn block_expansion_bombs_are_bounded() {
for text in [self_referencing(4), self_referencing(12), fan_out(), minsert(10_000, 10_000)] {
let d = read(&text);
let ext = d.extents(&Space::Model);
assert!(!ext.is_empty() && ext.min.is_finite() && ext.max.is_finite(), "{ext:?}");
let list = build(&d, &Space::Model, &Options::default());
assert!(!list.prims.is_empty() && list.prims.len() <= MAX_BLOCK_EXPANSION, "{}", list.prims.len());
// Saving computes the extents again ($EXTMIN/$EXTMAX).
assert!(cadcraft_io::write_dxf(&d).contains("$EXTMAX"));
}
}
#[test]
fn ordinary_nested_and_arrayed_blocks_are_drawn_whole() {
// 3 x 2 copies of a one-line block: six lines, extents spanning all copies.
let d = read(&minsert(3, 2));
assert_eq!(build(&d, &Space::Model, &Options::default()).prims.len(), 6);
let ext = d.extents(&Space::Model);
assert_eq!((ext.min.x, ext.min.y, ext.max.x, ext.max.y), (0.0, 0.0, 5.0, 3.0));
// A self-referencing block draws its line at every level down to MAX_BLOCK_DEPTH.
let d = read(&self_referencing(1));
assert!(matches!(d.model.iter().next().map(|e| &e.kind), Some(EntityKind::Insert(_))));
assert_eq!(build(&d, &Space::Model, &Options::default()).prims.len(), cadcraft_doc::MAX_BLOCK_DEPTH);
}