mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 11:56:50 -06:00
* 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>
66 lines
3.1 KiB
Rust
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);
|
|
}
|