mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 17:40:14 -06:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user