Merge pull request #5397 from storytold/fix/sec-followup-5288

DXF import: charge every dash copy and symbol name, name the block-work limit
This commit is contained in:
BFlat
2026-11-03 23:13:35 +07:00
committed by GitHub
2 changed files with 87 additions and 8 deletions
+13 -6
View File
@@ -43,6 +43,7 @@ const NESTED: &str = "blocks nested more than 8 deep, or inside themselves, are
const XREFS: &str = "blocks referencing other drawings (external references) are left out";
const TOO_MANY: &str = "the drawing has more than 2,000,000 objects: the rest are left out";
const TOO_MANY_POINTS: &str = "the drawing has more than 4,000,000 path points: the rest are left out";
const TOO_MUCH_BLOCK_WORK: &str = "the drawing's blocks take too much work to place: the rest are left out";
const TOO_MANY_DASHES: &str = "the drawing has more dashed lines than can be kept: the rest are drawn solid";
const TOO_MUCH_TEXT: &str = "the drawing has more than 64 MB of text and block names: the rest are left out";
@@ -499,6 +500,9 @@ impl<'d, 'a> Build<'d, 'a> {
super::curve::bounds(&bp)?;
let paint = Paint::solid(rgb_color(self.rgb(p, inh)));
let scale = map.determinant().abs().sqrt();
let path = PathData::from_bezpath(&bp);
// A compound path's loops each keep a copy of its appearance, beside its own.
let copies = if path.subpaths.len() > 1 { path.subpaths.len() + 1 } else { 1 };
let appearance = if fill {
Appearance::basic(paint, Paint::None, 0.0)
} else {
@@ -514,7 +518,7 @@ impl<'d, 'a> Build<'d, 'a> {
if let Some(AppearanceItem::Stroke(s)) = ap.items.last_mut()
&& let Some((dash, dots)) = self.dash(&self.linetype(p, inh), p, scale)
{
match self.dash_values.checked_add(dash.pattern.len()).filter(|n| *n <= MAX_DASH_VALUES) {
match dash.pattern.len().checked_mul(copies).and_then(|v| self.dash_values.checked_add(v)).filter(|n| *n <= MAX_DASH_VALUES) {
Some(n) => {
self.dash_values = n;
s.dash = Some(dash);
@@ -528,7 +532,6 @@ impl<'d, 'a> Build<'d, 'a> {
ap
};
let rule = if fill { FillRule::EvenOdd } else { FillRule::NonZero };
let path = PathData::from_bezpath(&bp);
let kind = |path: PathData| NodeKind::Path { path, rule, live: None, clipping: false, guide: false };
let mut n = if path.subpaths.len() > 1 {
// Several loops (a hatch with islands) paint as one compound path. Each loop is an
@@ -630,7 +633,7 @@ impl<'d, 'a> Build<'d, 'a> {
/// The art of block `name` drawn by `map`, as one object.
fn art(&mut self, name: &str, map: Affine, inh: &Inherit, depth: u32) -> Option<Node> {
if self.visits >= MAX_VISITS {
self.warn(TOO_MANY);
self.warn(TOO_MUCH_BLOCK_WORK);
return None;
}
let items = self.block(name);
@@ -638,7 +641,7 @@ impl<'d, 'a> Build<'d, 'a> {
// makes nothing, so the node budget doesn't bound it).
self.visits = self.visits.saturating_add(1 + items.as_ref().map_or(0, |i| i.len()));
if self.visits > MAX_VISITS {
self.warn(TOO_MANY);
self.warn(TOO_MUCH_BLOCK_WORK);
return None;
}
let items = items?;
@@ -671,12 +674,16 @@ impl<'d, 'a> Build<'d, 'a> {
let base = self.d.blocks.get(name).map_or(Point::ZERO, |b| b.base);
let map = self.symbol_map * Affine::translate(-base.to_vec2());
let art = self.art(name, map, &key.1, depth);
let made = art.map(|art| {
let made = art.and_then(|art| {
let label = self.d.blocks.get(name).map_or(name, |b| b.name.as_str());
// The symbol keeps its name: text, as its instances' copies of it are.
if !self.charge_text(label.len()) {
return None;
}
let symbol = unique(label, &mut self.next_suffix, &self.symbol_names);
self.symbol_names.insert(symbol.clone());
self.doc.symbols.push(Symbol { dynamic: false, name: symbol.clone(), art: Arc::new(art) });
symbol
Some(symbol)
});
self.symbols.insert(key, made.clone());
made
+74 -2
View File
@@ -814,7 +814,7 @@ fn nested_array_inserts_of_nothing_stop_at_the_work_budget() {
blocks.extend(block("*C", None));
let top = [(0, "INSERT"), (2, "*A"), (10, "0"), (20, "0"), (70, "300"), (44, "1")];
let r = open(&drawing(&[], &[], &blocks, &top), &points());
assert!(r.warnings.iter().any(|w| w.contains("more than")), "{:?}", r.warnings);
assert!(r.warnings.iter().any(|w| w.contains("too much work")), "{:?}", r.warnings);
}
#[test]
@@ -1273,5 +1273,77 @@ fn arrays_of_blocks_of_hidden_art_stop_at_the_work_budget() {
blocks.push((0, "ENDBLK"));
let insert = [(0, "INSERT"), (2, "*U1"), (10, "0"), (20, "0"), (70, "50"), (71, "50"), (44, "1"), (45, "1")];
let r = open(&drawing(&[], &tables, &blocks, &insert), &points());
assert!(r.warnings.iter().any(|w| w.contains("more than")), "{:?}", r.warnings);
assert!(r.warnings.iter().any(|w| w.contains("too much work")), "{:?}", r.warnings);
}
#[test]
fn dashed_compound_paths_charge_every_copy_of_their_pattern() {
// 100 pattern hatches of 9 loops in a two-length linetype: each keeps its dashes on the
// compound path and on each loop, 2,000 dash lengths, past the 1,000 tests import with.
let tables = [(0, "TABLE"), (2, "LTYPE"), (0, "LTYPE"), (2, "DASHED"), (73, "2"), (40, "15"), (49, "10"), (49, "-5"), (0, "ENDTAB")];
let corners = ["0", "10", "20", "30", "40", "50", "60", "70", "80", "90"];
let mut hatch = vec![(0, "HATCH"), (6, "DASHED"), (10, "0"), (20, "0"), (30, "0"), (2, "ANSI31"), (70, "0"), (71, "0"), (91, "9")];
for i in 0..9 {
let (a, b) = (corners[i], corners[i + 1]);
hatch.extend([
(92, "2"),
(72, "0"),
(73, "1"),
(93, "4"),
(10, a),
(20, "0"),
(10, b),
(20, "0"),
(10, b),
(20, "5"),
(10, a),
(20, "5"),
(97, "0"),
]);
}
hatch.extend([(75, "0"), (76, "1"), (98, "0")]);
let e: Vec<(i32, &str)> = (0..100).flat_map(|_| hatch.iter().copied()).collect();
let r = open(&drawing(&[], &tables, &[], &e), &points());
let (mut compounds, mut kept) = (0, 0);
r.document.walk(|n| {
compounds += usize::from(matches!(n.kind, NodeKind::Compound { .. }));
kept += n.appearance.stroke().and_then(|s| s.dash.as_ref()).map_or(0, |d| d.pattern.len());
});
assert_eq!(compounds, 100, "{:?}", r.warnings);
assert!(kept > 0 && kept <= 1_000, "{kept} dash lengths kept");
assert!(r.warnings.iter().any(|w| w.contains("drawn solid")), "{:?}", r.warnings);
}
#[test]
fn symbols_of_a_long_block_name_stop_at_the_text_budget() {
// A 1,000-byte block name inserted in 100 colours its art takes: a symbol per colour, each
// keeping its own copy of the name, past the 64 KB tests import with.
let name = "N".repeat(1000);
let blocks = [
(0, "BLOCK"),
(2, name.as_str()),
(10, "0"),
(20, "0"),
(0, "LINE"),
(8, "0"),
(62, "0"),
(10, "0"),
(20, "0"),
(11, "1"),
(21, "0"),
(0, "ENDBLK"),
];
let colours: Vec<String> = (1..=100).map(|c| c.to_string()).collect();
let inserts: Vec<(i32, &str)> =
colours.iter().flat_map(|c| [(0, "INSERT"), (2, name.as_str()), (62, c.as_str()), (10, "0"), (20, "0")]).collect();
let r = open(&drawing(&[], &[], &blocks, &inserts), &points());
let mut bytes: usize = r.document.symbols.iter().map(|s| s.name.len()).sum();
r.document.walk(|n| {
if let NodeKind::SymbolInstance { symbol, .. } = &n.kind {
bytes += symbol.len();
}
});
assert!(!r.document.symbols.is_empty());
assert!(bytes <= 64 << 10, "{bytes} bytes of symbol names");
assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings);
}