Merge pull request #5288 from storytold/fix/sec-dxf-import-budgets

DXF import: budget dash patterns, copied text and names, and block items looked at
This commit is contained in:
BFlat
2026-11-03 16:31:13 +07:00
committed by GitHub
2 changed files with 164 additions and 13 deletions
+66 -13
View File
@@ -23,10 +23,19 @@ const PT_PER_MM: f64 = 72.0 / 25.4;
const THINNEST: f64 = 0.25;
/// The most objects an import makes.
const MAX_NODES: usize = 2_000_000;
/// The most block visits (one per insert copy) an import makes, whatever they produce.
const MAX_VISITS: usize = 10_000_000;
/// The most block work an import does, whatever it produces: one per insert copy drawn, and one
/// per item of the block looked at for it (art on hidden layers too).
const MAX_VISITS: usize = if cfg!(test) { 1_000_000 } else { 20_000_000 };
/// The most path elements an import makes (blocks drawn as art are copied for every placement).
const MAX_ELEMENTS: usize = if cfg!(test) { 1_000_000 } else { 4_000_000 };
/// The most dash lengths an import keeps (every dashed path holds its own copy of its pattern).
const MAX_DASH_VALUES: usize = if cfg!(test) { 1_000 } else { 4_000_000 };
/// The most bytes of text, font names and symbol names an import copies into objects (a block's
/// text and name are copied for every placement).
const MAX_TEXT_BYTES: usize = if cfg!(test) { 64 << 10 } else { 64 << 20 };
/// The longest font family a text style may name; a longer one is not a font, and is set in the
/// default.
const MAX_FAMILY_BYTES: usize = 256;
/// How far from the origin art may lie (points): the canvas.
pub(crate) const MAX_EXTENT: f64 = 4.0e6;
@@ -34,6 +43,8 @@ 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_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";
/// What a block's art takes from the insert that places it: its colour, lineweight, linetype
/// and opacity where the art says "by block", and its layer for art on layer 0.
@@ -71,6 +82,9 @@ pub(crate) struct Build<'d, 'a> {
visits: usize,
/// Path elements made so far.
elements: usize,
/// Dash lengths and text bytes kept so far.
dash_values: usize,
text_bytes: usize,
warnings: Vec<String>,
}
@@ -128,6 +142,8 @@ impl<'d, 'a> Build<'d, 'a> {
nodes: 0,
visits: 0,
elements: 0,
dash_values: 0,
text_bytes: 0,
warnings: vec![],
}
}
@@ -138,6 +154,20 @@ impl<'d, 'a> Build<'d, 'a> {
}
}
/// Take `bytes` of text from [`MAX_TEXT_BYTES`]; false (with a warning) when they don't fit.
fn charge_text(&mut self, bytes: usize) -> bool {
match self.text_bytes.checked_add(bytes).filter(|t| *t <= MAX_TEXT_BYTES) {
Some(t) => {
self.text_bytes = t;
true
}
None => {
self.warn(TOO_MUCH_TEXT);
false
}
}
}
/// Import `layout`; `natural` and `ratio` are points per drawing unit in the drawing's own
/// unit and at the scale asked for (fitting computes its own).
pub fn run(mut self, layout: &LayoutDef, natural: f64, ratio: f64) -> Result<(Document, Vec<String>), String> {
@@ -484,9 +514,15 @@ 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)
{
s.dash = Some(dash);
if dots {
s.cap = LineCap::Round;
match self.dash_values.checked_add(dash.pattern.len()).filter(|n| *n <= MAX_DASH_VALUES) {
Some(n) => {
self.dash_values = n;
s.dash = Some(dash);
if dots {
s.cap = LineCap::Round;
}
}
None => self.warn(TOO_MANY_DASHES),
}
}
ap
@@ -527,10 +563,15 @@ impl<'d, 'a> Build<'d, 'a> {
if !(s > 1e-12 && s.is_finite()) {
return None;
}
let family = t.family.clone().or_else(|| self.d.styles.get(&t.style).filter(|f| !f.is_empty()).cloned());
let d = self.d;
let family = t.family.as_deref().or_else(|| d.styles.get(&t.style).map(String::as_str).filter(|f| !f.is_empty()));
let family = family.filter(|f| f.len() <= MAX_FAMILY_BYTES);
if !self.charge_text(t.text.len().saturating_add(family.map_or(0, str::len))) {
return None;
}
let default = CharStyle::default();
let style = CharStyle {
font_family: family.unwrap_or(default.font_family.clone()),
font_family: family.map_or_else(|| default.font_family.clone(), str::to_string),
size: t.height / CAP_HEIGHT * s,
h_scale: t.width * 100.0,
leading: t.leading.map(|l| l * s),
@@ -558,6 +599,13 @@ impl<'d, 'a> Build<'d, 'a> {
for cell in &ins.cells {
let node = match &symbol {
Some(name) => {
if self.nodes >= MAX_NODES {
self.warn(TOO_MANY);
break;
}
if !self.charge_text(name.len()) {
break;
}
let xf = map * *cell * self.symbol_map.inverse();
self.nodes += 1;
Some(Node::new(self.doc.alloc_id(), NodeKind::SymbolInstance { symbol: name.clone(), xf }))
@@ -581,14 +629,19 @@ 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> {
self.visits += 1;
if self.visits > MAX_VISITS {
if self.visits == MAX_VISITS + 1 {
self.warn(TOO_MANY);
}
if self.visits >= MAX_VISITS {
self.warn(TOO_MANY);
return None;
}
let items = self.block(name)?;
let items = self.block(name);
// Each placement looks at every item of the block, drawn or not (art on a hidden layer
// 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);
return None;
}
let items = items?;
if !self.enter(name, depth) {
return None;
}
+98
View File
@@ -1177,3 +1177,101 @@ fn many_array_inserts_of_a_missing_block_stay_bounded() {
assert!(total <= 2_000_000 + 3000, "total cells materialised: {total}");
assert!(conv.warnings.iter().any(|w| w.contains("array inserts")), "{:?}", conv.warnings);
}
#[test]
fn dashed_lines_past_the_dash_budget_are_drawn_solid() {
// 600 lines of a two-length linetype: 1,200 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 line = [(0, "LINE"), (6, "DASHED"), (10, "0"), (20, "0"), (11, "100"), (21, "0")];
let e: Vec<(i32, &str)> = (0..600).flat_map(|_| line).collect();
let r = open(&drawing(&[], &tables, &[], &e), &points());
let (mut dashed, mut solid, mut kept) = (0, 0, 0);
r.document.walk(|n| {
if let NodeKind::Path { .. } = n.kind {
match n.appearance.stroke().and_then(|s| s.dash.as_ref()) {
Some(d) => {
dashed += 1;
kept += d.pattern.len();
}
None => solid += 1,
}
}
});
assert_eq!(dashed + solid, 600);
assert!(dashed > 0 && solid > 0, "{dashed} dashed, {solid} solid");
assert!(kept <= 1_000, "{kept} dash lengths kept");
assert!(r.warnings.iter().any(|w| w.contains("drawn solid")), "{:?}", r.warnings);
}
/// The bytes of text in the document's text objects.
fn text_bytes(doc: &Document) -> usize {
let mut bytes = 0;
doc.walk(|n| {
if let NodeKind::Text(t) = &n.kind {
bytes += t.runs.iter().map(|r| r.text.len()).sum::<usize>();
}
});
bytes
}
#[test]
fn text_copied_into_array_cells_stops_at_the_text_budget() {
// 1,000 characters of text in an anonymous block placed 10 × 10: 100 KB of copies, past the
// 64 KB tests import with.
let long = "x".repeat(1000);
let blocks =
[(0, "BLOCK"), (2, "*U1"), (70, "1"), (10, "0"), (20, "0"), (0, "TEXT"), (10, "0"), (20, "0"), (40, "1"), (1, long.as_str()), (0, "ENDBLK")];
let insert = [(0, "INSERT"), (2, "*U1"), (10, "0"), (20, "0"), (70, "10"), (71, "10"), (44, "1"), (45, "1")];
let r = open(&drawing(&[], &[], &blocks, &insert), &points());
let bytes = text_bytes(&r.document);
assert!(bytes > 0, "some copies are kept");
assert!(bytes <= 64 << 10, "{bytes} bytes of text");
assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings);
}
#[test]
fn a_text_style_naming_an_overlong_font_family_takes_the_default() {
let family = "F".repeat(10_000);
let tables = [(0, "TABLE"), (2, "STYLE"), (0, "STYLE"), (2, "Big"), (3, "x.ttf"), (1001, "ACAD"), (1000, family.as_str()), (0, "ENDTAB")];
let e = [(0, "TEXT"), (10, "0"), (20, "0"), (40, "1"), (1, "Hi"), (7, "Big")];
let doc = open(&drawing(&[], &tables, &[], &e), &points()).document;
let mut families = vec![];
doc.walk(|n| {
if let NodeKind::Text(t) = &n.kind {
families.extend(t.runs.iter().map(|r| r.style.font_family.clone()));
}
});
assert_eq!(families, [vectorcraft_doc::CharStyle::default().font_family]);
}
#[test]
fn symbol_instances_of_a_long_block_name_stop_at_the_text_budget() {
// A 1,000-byte block name placed 10 × 10: each instance keeps its own copy of the name.
let name = "N".repeat(1000);
let blocks = [(0, "BLOCK"), (2, name.as_str()), (10, "0"), (20, "0"), (0, "LINE"), (10, "0"), (20, "0"), (11, "1"), (21, "0"), (0, "ENDBLK")];
let insert = [(0, "INSERT"), (2, name.as_str()), (10, "0"), (20, "0"), (70, "10"), (71, "10"), (44, "2"), (45, "2")];
let r = open(&drawing(&[], &[], &blocks, &insert), &points());
let mut instances = 0;
r.document.walk(|n| {
if let NodeKind::SymbolInstance { .. } = n.kind {
instances += 1;
}
});
assert!(instances > 0 && instances < 100, "{instances} instances");
assert!(r.warnings.iter().any(|w| w.contains("64 MB of text")), "{:?}", r.warnings);
}
#[test]
fn arrays_of_blocks_of_hidden_art_stop_at_the_work_budget() {
// 500 lines on a hidden layer in an anonymous block placed 50 × 50: they make nothing, but
// each placement looks at all of them (1,250,000 items, past the 1,000,000 tests import with).
let tables = [(0, "TABLE"), (2, "LAYER"), (0, "LAYER"), (2, "Off"), (70, "0"), (62, "-1"), (0, "ENDTAB")];
let mut blocks = vec![(0, "BLOCK"), (2, "*U1"), (70, "1"), (10, "0"), (20, "0")];
for _ in 0..500 {
blocks.extend([(0, "LINE"), (8, "Off"), (10, "0"), (20, "0"), (11, "1"), (21, "0")]);
}
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);
}