Importers: nesting is capped, so a deeply nested file can't overflow the stack

This commit is contained in:
bflatastic committed 2026-10-14 07:37:52 +07:00
1 parent 8ab574b7de
commit c10da25ce4
36 files changed
+1481 -38

No files matched your search

+1
View File
@@ -214,6 +214,7 @@ pub fn import(bytes: &[u8], o: &ImportOptions) -> Result<Imported, String> {
let (mut document, mut warnings) = build::Build::new(&d, o).run(layout, natural, ratio)?;
document.units = if o.fit { unit } else { o.unit.unwrap_or(unit) };
warnings.extend(damaged);
document.limit_depth_noting(&mut warnings);
Ok(Imported { document, warnings })
}
+4 -2
View File
@@ -24,8 +24,10 @@ pub struct Clip {
}
/// Deepest nesting of clips, and of groups, an importer records ([`nest`] goes one level deeper
/// for each): deeper ones are left out.
pub const MAX_NEST: usize = 128;
/// for each): deeper ones are left out. As deep as a document's objects may nest
/// ([`crate::MAX_DEPTH`]) under a layer, around the art; clips in groups that deep together are
/// merged by [`Document::limit_depth`].
pub const MAX_NEST: usize = crate::MAX_DEPTH - 2;
/// The warning for clips left out for nesting deeper than [`MAX_NEST`].
pub fn deep_clips() -> String {
+588
View File
@@ -0,0 +1,588 @@
//! How deep objects may nest.
//!
//! The tree is walked by recursion everywhere (drawing, bounds, hit tests, saving, exporting), so
//! a tree of unbounded depth overflows the stack: an abort no guard catches. No document deeper
//! than [`MAX_DEPTH`] is ever kept, nor one whose symbols and patterns draw deeper than
//! [`MAX_DRAWN_DEPTH`] (a symbol holding an instance of itself, or a pattern painting its own
//! tile, draws without end): importers and the native reader merge deeper objects into the group
//! around them and leave such instances and paints out
//! ([`Document::limit_depth`], with [`merged_note`] and [`symbols_note`] as their warnings), and
//! the engine refuses an edit that would nest deeper ([`Document::too_deep`]).
use std::collections::HashMap;
use std::sync::Arc;
use vectorcraft_color::Paint;
use crate::{Document, Node, NodeKind};
/// The deepest an object may be: a top-level layer is 1 deep, what it holds 2, and so on (the art
/// of an opacity mask is one deeper than the object it masks). A symbol's or pattern's art counts
/// from 1 again.
///
/// A native file writes three levels of JSON for each level of objects, and its reader stops at
/// 128 levels: 32 leaves the deepest object 29 of them for its own data, so every document that
/// can be made can be saved and opened again.
pub const MAX_DEPTH: usize = 32;
/// The deepest an object may be drawn: as [`MAX_DEPTH`], the art of a symbol counted under its
/// instance and a pattern's tile under the object it paints (and what those use, under them).
/// Symbols a few objects deep nest in one another dozens deep within it.
pub const MAX_DRAWN_DEPTH: usize = 4 * MAX_DEPTH;
/// The warning of a file whose objects were nested deeper than [`MAX_DEPTH`].
pub fn merged_note() -> String {
format!("objects nested more than {MAX_DEPTH} deep were merged into the group around them")
}
/// The warning of a file with symbols or patterns that use themselves or are drawn deeper than
/// [`MAX_DRAWN_DEPTH`].
pub fn symbols_note() -> String {
format!(
"symbol instances and pattern paints inside their own symbol or pattern, or drawn more than {MAX_DRAWN_DEPTH} objects deep, were left out"
)
}
/// Run `f` where recursion has room: on a thread of its own with a 16 MB stack (the main thread
/// has 1 MB on Windows), or here where there are no threads (the web) or none can be started. For
/// readers whose recursion is capped but heavy; a panic in `f` goes on to the caller.
pub fn with_deep_stack<T: Send>(f: impl Fn() -> T + Sync) -> T {
#[cfg(not(target_arch = "wasm32"))]
{
let threaded = std::thread::scope(|s| {
let thread = std::thread::Builder::new().stack_size(16 << 20).spawn_scoped(s, &f).ok()?;
Some(thread.join().unwrap_or_else(|panic| std::panic::resume_unwind(panic)))
});
if let Some(done) = threaded {
return done;
}
}
f()
}
/// Is anything in `now` (each `depth` deep) deeper than [`MAX_DEPTH`], or `over` (given its
/// depth)? Without recursion: the trees may be of any depth. With `before` (the same list before
/// an edit, within the caps), only what the edit changed is looked at: a subtree still shared with
/// it, in the same place, is as it was.
fn deeper(now: &[Arc<Node>], before: Option<&[Arc<Node>]>, depth: usize, over: &dyn Fn(&Node, usize) -> bool) -> bool {
type Pending<'a> = Vec<(&'a Node, Option<&'a Node>, usize)>;
fn push<'a>(stack: &mut Pending<'a>, now: &'a [Arc<Node>], before: Option<&'a [Arc<Node>]>, depth: usize) {
for (i, n) in now.iter().enumerate() {
let was = before.and_then(|b| b.get(i));
if !was.is_some_and(|w| Arc::ptr_eq(w, n)) {
stack.push((n, was.map(|w| &**w), depth));
}
}
}
let mut stack = vec![];
push(&mut stack, now, before, depth);
while let Some((n, was, d)) = stack.pop() {
if d > MAX_DEPTH || over(n, d) {
return true;
}
if let Some(ch) = n.children() {
push(&mut stack, ch, was.and_then(Node::children).map(Vec::as_slice), d + 1);
}
if let Some(m) = &n.extras().mask {
let was = was.and_then(|w| w.extras().mask.as_ref()).map(|m| std::slice::from_ref(&m.art));
push(&mut stack, std::slice::from_ref(&m.art), was, d + 1);
}
}
false
}
/// [`deeper`] for the nesting alone.
fn nested_deeper(now: &[Arc<Node>], depth: usize) -> bool {
deeper(now, None, depth, &|_, _| false)
}
/// Merge what is deeper than [`MAX_DEPTH`] in `nodes` (each `depth` deep) into the group around
/// it: a container at the cap is replaced by what it holds (a clipping group's clipping path and
/// an opacity mask too deep are left out); what it held stays hidden or locked as it was. Returns
/// whether anything changed. Recurses no deeper than the cap.
fn limit(nodes: &mut Vec<Arc<Node>>, depth: usize) -> bool {
let mut changed = false;
let mut i = 0;
while let Some(n) = nodes.get_mut(i) {
if !nested_deeper(std::slice::from_ref(n), depth) {
i += 1;
continue;
}
changed = true;
let n = Arc::make_mut(n);
if n.extras().mask.as_ref().is_some_and(|m| nested_deeper(std::slice::from_ref(&m.art), depth + 1)) {
n.extras_mut().mask = None;
}
if depth < MAX_DEPTH {
if let Some(ch) = n.children_mut() {
limit(ch, depth + 1);
}
i += 1;
continue;
}
let clip = matches!(n.kind, NodeKind::Group { clip: true, .. } | NodeKind::Layer { clip: true, .. });
let (visible, locked) = (n.visible, n.locked);
let mut held = n.children_mut().map(std::mem::take).unwrap_or_default();
if clip && !held.is_empty() {
held.remove(0);
}
for c in &mut held {
if (!visible && c.visible) || (locked && !c.locked) {
let c = Arc::make_mut(c);
c.visible &= visible;
c.locked |= locked;
}
}
// What it held takes its place, and is looked at in its turn.
nodes.splice(i..=i, held);
}
changed
}
/// A symbol (`false`) or a pattern (`true`) of the document, by name: what art draws by name, an
/// instance its symbol's art and a pattern paint its tile's.
type Def<'a> = (bool, &'a str);
/// How deep each symbol's and pattern's art is drawn, the art of the symbols and patterns it uses
/// included (drawn under the object that uses them): `None` for one that uses itself, in its art
/// or through what that uses, or is drawn deeper than [`MAX_DRAWN_DEPTH`].
type Heights<'a> = HashMap<Def<'a>, Option<usize>>;
/// What `n` itself draws by name: the symbol it is an instance of, the patterns that paint it.
fn uses(n: &Node) -> impl Iterator<Item = Def<'_>> {
let symbol = match &n.kind {
NodeKind::SymbolInstance { symbol, .. } => Some((false, symbol.as_str())),
_ => None,
};
let patterns = crate::swatches::node_paints(n).filter_map(|p| match p {
Paint::Pattern { pattern, .. } => Some((true, pattern.as_str())),
_ => None,
});
symbol.into_iter().chain(patterns)
}
/// [`Heights`] of the document's symbols and patterns. Without recursion: they may use one
/// another in a chain of any length. Their own art is within [`MAX_DEPTH`].
fn heights(doc: &Document) -> Heights<'_> {
// Each one's own depth, and what its art uses with the deepest use of each.
type Own<'a> = (usize, Vec<(Def<'a>, usize)>);
let arts = (doc.symbols.iter().map(|s| ((false, s.name.as_str()), std::slice::from_ref(&s.art))))
.chain(doc.patterns.iter().map(|p| ((true, p.name.as_str()), &p.art[..])));
let mut own: HashMap<Def, Own> = HashMap::new();
for (def, art) in arts {
let (mut deepest, mut used) = (0, HashMap::<Def, usize>::new());
let mut stack: Vec<(&Node, usize)> = art.iter().map(|n| (&**n, 1)).collect();
while let Some((n, d)) = stack.pop() {
deepest = deepest.max(d);
for u in uses(n) {
let at = used.entry(u).or_default();
*at = (*at).max(d);
}
stack.extend(n.children().into_iter().flatten().chain(n.extras().mask.as_ref().map(|m| &m.art)).map(|c| (&**c, d + 1)));
}
// (The first of two of one name is the one drawn.)
own.entry(def).or_insert((deepest, used.into_iter().collect()));
}
// Depth first over what uses what. One met again while it is being worked out (`None` in its
// entry) uses itself.
let mut found: HashMap<Def, Option<Option<usize>>> = HashMap::new();
let starts: Vec<Def> = own.keys().copied().collect();
for start in starts {
if found.contains_key(&start) {
continue;
}
// (which, the next of its uses to look at, the deepest found so far: `None` once it can't be drawn)
let mut path: Vec<(Def, usize, Option<usize>)> = vec![(start, 0, own.get(&start).map(|o| o.0))];
found.insert(start, None);
while let Some(&(def, next, deepest)) = path.last() {
let used = own.get(&def).map_or(&[][..], |o| &o.1);
let Some(&(other, at)) = used.get(next) else {
// Everything it uses is worked out.
let deepest = deepest.filter(|d| *d <= MAX_DRAWN_DEPTH);
found.insert(def, Some(deepest));
path.pop();
if let Some((user, next, held)) = path.last_mut() {
let at = own.get(user).and_then(|o| o.1.get(next.wrapping_sub(1))).map_or(1, |u| u.1);
*held = held.zip(deepest).map(|(h, d)| h.max(at + d));
}
continue;
};
let known = found.get(&other).copied();
if known.is_none() && own.contains_key(&other) {
found.insert(other, None);
}
let Some((_, next, deepest)) = path.last_mut() else { break };
*next += 1;
match known {
// Not in the document: it draws nothing.
None if !own.contains_key(&other) => {}
None => path.push((other, 0, own.get(&other).map(|o| o.0))),
Some(Some(Some(d))) => *deepest = deepest.map(|h| h.max(at + d)),
// It uses itself (it is on the way down here), or can't be drawn.
Some(None | Some(None)) => *deepest = None,
}
}
}
found.into_iter().map(|(def, h)| (def, h.flatten())).collect()
}
/// Can't `used`, by an object `depth` deep, be drawn there?
fn undrawable(heights: &Heights, used: Def, depth: usize) -> bool {
heights.get(&used).is_some_and(|h| h.is_none_or(|h| depth + h > MAX_DRAWN_DEPTH))
}
/// Take out of `nodes` (each `depth` deep) the uses `bad` picks (given the depth of the object):
/// an instance goes, a pattern paint becomes no paint. Returns whether anything changed. For trees
/// within [`MAX_DEPTH`].
fn strip(nodes: &mut Vec<Arc<Node>>, depth: usize, bad: &dyn Fn(Def, usize) -> bool) -> bool {
let any = |n: &Node, d: usize| uses(n).any(|u| bad(u, d));
let before = nodes.len();
nodes.retain(|n| !matches!(&n.kind, NodeKind::SymbolInstance { symbol, .. } if bad((false, symbol.as_str()), depth)));
let mut changed = nodes.len() != before;
for n in nodes.iter_mut() {
if !deeper(std::slice::from_ref(n), None, depth, &any) {
continue;
}
changed = true;
let n = Arc::make_mut(n);
if uses(n).any(|u| bad(u, depth)) {
for p in crate::swatches::node_paints_mut(n) {
if matches!(p, Paint::Pattern { pattern, .. } if bad((true, pattern.as_str()), depth)) {
*p = Paint::None;
}
}
}
// An opacity mask whose art is such an instance goes with it.
let mut mask = n.extras().mask.as_ref().map(|m| vec![m.art.clone()]);
if let Some(art) = &mut mask
&& strip(art, depth + 1, bad)
{
match art.pop() {
Some(art) => {
if let Some(m) = n.extras_mut().mask.as_mut() {
m.art = art;
}
}
None => n.extras_mut().mask = None,
}
}
if let Some(ch) = n.children_mut() {
strip(ch, depth + 1, bad);
}
}
changed
}
impl Document {
/// Is anything nested deeper than [`MAX_DEPTH`], or drawn deeper than [`MAX_DRAWN_DEPTH`]
/// (a symbol or pattern that uses itself is)? With `before` (this document before an edit,
/// itself within the caps), only what the edit changed is looked at.
pub fn too_deep(&self, before: Option<&Document>) -> bool {
fn symbol(d: &Document, i: usize) -> Option<&[Arc<Node>]> {
d.symbols.get(i).map(|s| std::slice::from_ref(&s.art))
}
fn pattern(d: &Document, i: usize) -> Option<&[Arc<Node>]> {
d.patterns.get(i).map(|p| &p.art[..])
}
let nested = |_: &Node, _: usize| false;
if (0..self.symbols.len()).any(|i| symbol(self, i).is_some_and(|art| deeper(art, before.and_then(|b| symbol(b, i)), 1, &nested)))
|| (0..self.patterns.len()).any(|i| pattern(self, i).is_some_and(|art| deeper(art, before.and_then(|b| pattern(b, i)), 1, &nested)))
{
return true;
}
if self.symbols.is_empty() && self.patterns.is_empty() {
return deeper(&self.layers, before.map(|b| &b.layers[..]), 1, &nested);
}
let heights = heights(self);
if heights.values().any(Option::is_none) {
return true;
}
// Symbols and patterns draw as deep as before when none changed; else every use of them
// is looked at again.
let same = |x: &[Arc<Node>], y: &[Arc<Node>]| x.len() == y.len() && x.iter().zip(y).all(|(x, y)| Arc::ptr_eq(x, y));
let before = before.filter(|b| {
b.symbols.len() == self.symbols.len()
&& b.patterns.len() == self.patterns.len()
&& b.symbols.iter().zip(&self.symbols).all(|(x, y)| x.name == y.name && Arc::ptr_eq(&x.art, &y.art))
&& b.patterns.iter().zip(&self.patterns).all(|(x, y)| x.name == y.name && same(&x.art, &y.art))
});
let over = |n: &Node, d: usize| uses(n).any(|u| undrawable(&heights, u, d));
deeper(&self.layers, before.map(|b| &b.layers[..]), 1, &over)
}
/// [`Self::limit_depth`] → whether objects were merged, and whether uses of symbols and
/// patterns were left out.
fn limit_depths(&mut self) -> (bool, bool) {
let mut merged = limit(&mut self.layers, 1);
for s in &mut self.symbols {
// The art itself is 1 deep, so it stays the one object it is.
let mut art = vec![s.art.clone()];
if limit(&mut art, 1)
&& let Some(one) = art.pop()
{
s.art = one;
merged = true;
}
}
for p in &mut self.patterns {
merged |= limit(&mut p.art, 1);
}
if self.symbols.is_empty() && self.patterns.is_empty() {
return (merged, false);
}
// The symbols and patterns that can't be drawn lose what their art uses: each is then as
// deep as its own art, and what uses it can be drawn.
let (symbols, patterns): (Vec<usize>, Vec<usize>) = {
let heights = heights(self);
let bad = |def: Def| heights.get(&def).is_some_and(Option::is_none);
(
(0..self.symbols.len()).filter(|i| self.symbols.get(*i).is_some_and(|s| bad((false, s.name.as_str())))).collect(),
(0..self.patterns.len()).filter(|i| self.patterns.get(*i).is_some_and(|p| bad((true, p.name.as_str())))).collect(),
)
};
let mut left_out = !(symbols.is_empty() && patterns.is_empty());
let all = |_: Def, _: usize| true;
for i in symbols {
let Some(s) = self.symbols.get_mut(i) else { continue };
let mut art = vec![s.art.clone()];
strip(&mut art, 1, &all);
// (An instance alone was the whole art: an empty group stands in.)
s.art = art.pop().unwrap_or_else(|| Arc::new(Node::group(s.art.id, vec![])));
}
for i in patterns {
if let Some(p) = self.patterns.get_mut(i) {
strip(&mut p.art, 1, &all);
}
}
// Then the uses drawn too deep where they are.
let mut layers = self.layers.clone();
left_out |= {
let heights = heights(self);
strip(&mut layers, 1, &|used, depth| undrawable(&heights, used, depth))
};
self.layers = layers;
(merged, left_out)
}
/// Bring the document within the caps: merge the objects nested deeper than [`MAX_DEPTH`]
/// into the group around them, and leave out the uses of symbols and patterns that can't be
/// drawn (inside their own symbol or pattern, or deeper than [`MAX_DRAWN_DEPTH`]). What an
/// importer does to what it read. Returns whether anything changed.
pub fn limit_depth(&mut self) -> bool {
let (merged, left_out) = self.limit_depths();
merged || left_out
}
/// [`Self::limit_depth`], adding [`merged_note`] and [`symbols_note`] to an importer's
/// `warnings` for what it did.
pub fn limit_depth_noting(&mut self, warnings: &mut Vec<String>) {
let (merged, left_out) = self.limit_depths();
warnings.extend(merged.then(merged_note));
warnings.extend(left_out.then(symbols_note));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{NodeId, OpacityMask};
/// One path in `levels` groups, each holding a second path beside the next group.
fn nested(levels: usize) -> Arc<Node> {
let mut id = 0;
let mut next = || {
id += 1;
NodeId(id)
};
let path = |id| Arc::new(Node::path(id, Default::default(), Default::default()));
let mut inner = path(next());
for _ in 0..levels {
inner = Arc::new(Node::group(next(), vec![inner, path(next())]));
}
inner
}
fn doc(art: Arc<Node>) -> Document {
let mut d = Document::new(100.0, 100.0);
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
ch.push(art);
}
d
}
#[test]
fn a_document_at_the_cap_is_left_alone() {
// The layer, the groups and the path in them: MAX_DEPTH deep.
let mut d = doc(nested(MAX_DEPTH - 2));
let before = d.clone();
assert!(!d.too_deep(None));
assert!(!d.limit_depth());
assert_eq!(d, before);
}
#[test]
fn deeper_objects_are_merged_into_the_group_around_them() {
for levels in [MAX_DEPTH - 1, MAX_DEPTH + 5, 100_000] {
let mut d = doc(nested(levels));
assert!(d.too_deep(None), "{levels}");
assert!(d.limit_depth(), "{levels}");
assert!(!d.too_deep(None), "{levels}");
// Every path is kept, under the layer and the groups down to the cap.
assert_eq!(d.node_count(), 1 + (MAX_DEPTH - 2) + (levels + 1), "{levels}");
assert!(!d.limit_depth(), "nothing more to merge");
}
}
#[test]
fn a_clipping_path_and_a_mask_too_deep_are_left_out_and_hidden_art_stays_hidden() {
let mut inner = Node::new(NodeId(9000), NodeKind::Group { children: vec![nested(0), nested(0)], clip: true });
inner.visible = false;
let mut art = Arc::new(inner);
for i in 0..MAX_DEPTH {
art = Arc::new(Node::group(NodeId(9001 + i as u64), vec![art]));
}
let mut masked = Node::group(NodeId(8000), vec![]);
masked.extras_mut().mask = Some(Box::new(OpacityMask::new((*nested(MAX_DEPTH)).clone(), true)));
let mut d = doc(art);
if let Some(ch) = d.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
ch.push(Arc::new(masked));
}
assert!(d.limit_depth());
assert!(!d.too_deep(None));
let mut leaves = vec![];
d.walk(|n| {
if n.children().is_none() {
leaves.push(n.visible);
}
});
assert_eq!(leaves, [false], "the clipping path is gone, what it clipped is still hidden");
assert!(d.node(NodeId(8000)).is_some_and(|n| n.extras().mask.is_none()));
}
#[test]
fn an_edit_is_checked_by_what_it_changed() {
let before = doc(nested(MAX_DEPTH - 2));
let mut wrapped = before.clone();
if let Some(ch) = wrapped.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
let all = std::mem::take(ch);
ch.push(Arc::new(Node::group(NodeId(7000), all)));
}
assert!(wrapped.too_deep(Some(&before)), "the same art, one deeper");
let mut beside = before.clone();
if let Some(ch) = beside.layers.first_mut().and_then(|l| Arc::make_mut(l).children_mut()) {
ch.push(nested(3));
}
assert!(!beside.too_deep(Some(&before)));
assert!(!before.too_deep(Some(&before)));
}
#[test]
fn symbols_and_patterns_are_limited_too() {
let mut d = Document::new(10.0, 10.0);
d.symbols.push(crate::Symbol { name: "s".into(), art: nested(MAX_DEPTH + 3) });
assert!(d.too_deep(None));
assert!(d.limit_depth());
assert!(!d.too_deep(None));
assert!(d.symbols.first().is_some_and(|s| s.art.children().is_some()), "still one object");
}
fn instance(id: u64, of: &str) -> Arc<Node> {
Arc::new(Node::new(NodeId(id), NodeKind::SymbolInstance { symbol: of.into(), xf: Default::default() }))
}
/// `count` symbols, each `levels` groups around an instance of the next; the last one's of `last`.
fn symbols(count: usize, levels: usize, last: &str) -> Document {
let mut d = doc(instance(1, "s0"));
for i in 0..count {
let mut art = instance(2, &if i + 1 < count { format!("s{}", i + 1) } else { last.to_string() });
for _ in 0..levels {
art = Arc::new(Node::group(NodeId(3), vec![art]));
}
d.symbols.push(crate::Symbol { name: format!("s{i}"), art });
}
d
}
fn instances(d: &Document) -> usize {
let mut n = 0;
let mut count = |x: &Node| n += usize::from(matches!(x.kind, NodeKind::SymbolInstance { .. }));
d.walk(&mut count);
d.symbols.iter().for_each(|s| s.art.walk(&mut count));
n
}
#[test]
fn symbols_in_symbols_are_drawn_only_so_deep() {
// Three objects deep each (two groups and the instance), under the instance on the layer.
let fits = MAX_DRAWN_DEPTH / 3 - 1;
let mut d = symbols(fits, 2, "none");
assert!(!d.too_deep(None) && !d.limit_depth());
let mut d = symbols(fits + 2, 2, "none");
assert!(d.too_deep(None));
let before = instances(&d);
assert!(d.limit_depth() && !d.too_deep(None));
assert!(instances(&d) < before && instances(&d) > 0, "only the instances drawn too deep go");
// A chain of any length is measured, not walked by recursion.
let mut d = symbols(100_000, 0, "none");
assert!(d.too_deep(None) && d.limit_depth() && !d.too_deep(None));
}
#[test]
fn a_symbol_holding_itself_loses_the_instance() {
for (count, last) in [(1, "s0"), (3, "s0"), (3, "s1")] {
let mut d = symbols(count, 1, last);
assert!(d.too_deep(None), "{count} {last}");
let mut warnings = vec![];
d.limit_depth_noting(&mut warnings);
assert_eq!(warnings, [symbols_note()]);
assert!(!d.too_deep(None), "{count} {last}");
assert!(d.symbols.iter().all(|s| s.art.children().is_some()), "the art is still a group");
}
// An edit that makes one hold itself is seen, whatever else stayed as it was.
let before = symbols(2, 1, "none");
let mut after = before.clone();
after.symbols[1].art = Arc::new(Node::group(NodeId(9), vec![instance(8, "s0")]));
assert!(!before.too_deep(None) && after.too_deep(Some(&before)));
}
/// A path painted with the pattern named `with`.
fn painted(id: u64, with: &str) -> Arc<Node> {
let fill = Paint::Pattern { pattern: with.into(), xf: Default::default() };
Arc::new(Node::path(NodeId(id), Default::default(), crate::Appearance::basic(fill, Paint::None, 0.0)))
}
fn pattern_fills(d: &Document) -> usize {
let mut n = 0;
d.visit_paints(&mut |p| n += usize::from(matches!(p, Paint::Pattern { .. })));
n
}
#[test]
fn a_pattern_painting_its_own_tile_loses_the_paint() {
// One that paints its own tile; two that paint each other's; one in a symbol it holds.
let itself = |d: &mut Document| d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "p")]));
let each_other = |d: &mut Document| {
d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "q")]));
d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "p")]));
};
let through_a_symbol = |d: &mut Document| {
d.patterns.push(crate::PatternDef::new("p", vec![instance(2, "s")]));
d.symbols.push(crate::Symbol { name: "s".into(), art: painted(3, "p") });
};
for make in [&itself as &dyn Fn(&mut Document), &each_other, &through_a_symbol] {
let mut d = doc(painted(1, "p"));
make(&mut d);
assert!(d.too_deep(None));
let mut warnings = vec![];
d.limit_depth_noting(&mut warnings);
assert_eq!(warnings, [symbols_note()]);
assert!(!d.too_deep(None));
assert_eq!(pattern_fills(&d), 1, "the object on the layer keeps its pattern");
}
// A pattern whose tile is painted with another is fine.
let mut d = doc(painted(1, "p"));
d.patterns.push(crate::PatternDef::new("p", vec![painted(2, "q")]));
d.patterns.push(crate::PatternDef::new("q", vec![painted(3, "none")]));
assert!(!d.too_deep(None) && !d.limit_depth());
}
}
+2
View File
@@ -11,6 +11,7 @@ pub mod blend;
pub mod clipnest;
pub mod cmyk;
pub mod corners;
pub mod depth;
pub mod graph;
pub mod hit;
mod id_index;
@@ -79,6 +80,7 @@ pub use appearance::{
};
pub use assets::ExportAsset;
pub use corners::LiveCorners;
pub use depth::MAX_DEPTH;
pub use graph::{
GraphDesign, GraphKind, GraphSpec, LabelStyles, MAX_GRAPH_CATEGORIES, MAX_GRAPH_SERIES, PiePosition, PieSort, SeriesPaint, TickLength,
ValueAxisSide,
+2 -2
View File
@@ -224,7 +224,7 @@ fn item_paint_mut(it: &mut AppearanceItem) -> &mut Paint {
}
/// The paints a node holds itself (not its children): appearance items, then text runs.
fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
pub(crate) fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
let runs = match &n.kind {
NodeKind::Text(t) => t.runs.as_slice(),
_ => &[],
@@ -233,7 +233,7 @@ fn node_paints(n: &Node) -> impl Iterator<Item = &Paint> {
}
/// Mutable [`node_paints`], in the same order.
fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
pub(crate) fn node_paints_mut(n: &mut Node) -> impl Iterator<Item = &mut Paint> {
let runs = match &mut n.kind {
NodeKind::Text(t) => t.runs.as_mut_slice(),
_ => &mut [],
+1
View File
@@ -32,6 +32,7 @@ pub(super) fn import(name: &str, bytes: &[u8]) -> Result<Imported> {
Ok(d) => {
let mut b = Builder { doc: Document::new(0.0, 0.0), warnings: Vec::new(), k: 72.0 / d.dpi, layer: 0 };
b.document(&d)?;
b.doc.limit_depth_noting(&mut b.warnings);
let mut warnings = d.warnings.iter().map(|w| format!("Affinity: not imported or approximated: {w}.")).collect::<Vec<_>>();
warnings.extend(b.warnings.iter().map(|w| format!("Affinity: {w}.")));
Ok(Imported { doc: b.doc, warnings, preview_only: false })
+2 -1
View File
@@ -139,7 +139,8 @@ fn restore_or_import(
/// (with a warning for each that can't be used).
pub(crate) fn native_file(bytes: &[u8]) -> Result<(Document, vectorcraft_format::FileInfo, Vec<String>)> {
let f = vectorcraft_format::load_file(bytes).map_err(err)?;
let warnings = super::native::install_profiles(f.profiles);
let mut warnings = super::native::install_profiles(f.profiles);
warnings.extend(f.depth_notes);
Ok((f.doc, f.info, warnings))
}
+12 -1
View File
@@ -583,6 +583,10 @@ pub(crate) fn is_guide_path(d: &Document, id: NodeId) -> bool {
d.node(id).is_some_and(|n| matches!(n.kind, NodeKind::Path { guide: true, .. }))
}
/// The error of an edit that would nest objects deeper than [`vectorcraft_doc::MAX_DEPTH`].
pub const TOO_DEEP: &str = "objects can't be nested more than 32 deep";
const _: () = assert!(vectorcraft_doc::MAX_DEPTH == 32, "TOO_DEEP (and its catalog rows) names the cap");
/// Cheap sanity check after an edit: artboards and the objects just touched (the selection) must
/// have finite, in-range geometry, so saved files always reload and renderers never see NaN/∞.
fn doc_sane(d: &Document, sel: &Selection) -> bool {
@@ -1245,6 +1249,9 @@ impl Session {
}
/// Add a document and make it active.
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
// Every edit is checked against the document before it, so the first one is within the cap
// (importers and the native reader have seen to theirs already, with a warning).
doc.limit_depth();
Self::refresh_text_bounds(&mut doc);
// Guides from files saved before guides had layers go on a layer.
doc.adopt_guides();
@@ -1262,6 +1269,7 @@ impl Session {
if index >= self.docs.len() {
return false;
}
doc.limit_depth();
Self::refresh_text_bounds(&mut doc);
doc.adopt_guides();
if self.active == Some(index) {
@@ -1476,7 +1484,10 @@ impl Session {
if !st.doc.variables.bindings.is_empty() {
Arc::make_mut(&mut st.doc).prune_variable_bindings();
}
if doc_sane(&st.doc, &st.selection) {
if st.doc.too_deep(Some(&before)) {
// Deeper trees overflow the stack of what walks them.
Err(EngineError::Other(TOO_DEEP.into()))
} else if doc_sane(&st.doc, &st.selection) {
Ok(v)
} else {
Err(EngineError::Other("result would exceed the canvas (coordinates out of range)".into()))
+342
View File
@@ -2440,3 +2440,345 @@ proptest! {
checked("mutated script", json!({"dataBase64": vectorcraft_format::base64_encode(&bytes), "name": "sample.jsx"}))?;
}
}
// ---------- deep nesting ----------
//
// The tree is walked by recursion, so a file nested deeply enough overflowed the stack: an abort,
// which nothing catches. Every reader caps what it reads, and no document is deeper than
// `MAX_DEPTH`. These run on a thread with the stack of a Windows main thread (1 MB), where an
// overflow ends the test run.
use vectorcraft_doc::MAX_DEPTH;
/// `f` on a thread with a 1 MB stack.
fn on_a_small_stack(f: impl FnOnce() + Send + 'static) {
std::thread::Builder::new().stack_size(1 << 20).spawn(f).unwrap().join().unwrap();
}
/// Open `bytes` as the file `name`: an error, or a document within the cap that renders and
/// exports. Returns the document and the import's warnings.
fn open_deep(name: &str, bytes: &[u8]) -> Option<(Document, Vec<String>)> {
let loaded = vectorcraft_engine::cmd::fileio::load(name, bytes).ok()?;
assert!(!loaded.doc.too_deep(None), "{name}: deeper than the cap");
let doc = loaded.doc.clone();
survive(name, move || Some(doc)).unwrap();
Some((loaded.doc, loaded.warnings))
}
fn merged(warnings: &[String]) -> bool {
warnings.iter().any(|w| w.contains("nested more than"))
}
fn paths_in(doc: &Document) -> usize {
let mut paths = 0;
doc.walk(|n| paths += usize::from(n.path_data().is_some()));
paths
}
const SVG_NS: &str = "xmlns=\"http://www.w3.org/2000/svg\"";
/// `n` groups in one another, each named (so it is kept) and with a rectangle beside the next.
fn nested_svg(n: usize) -> String {
let open: String = (0..n).map(|i| format!("<g id=\"g{i}\"><rect width=\"5\" height=\"5\"/>")).collect();
format!("<svg {SVG_NS} width=\"100\" height=\"100\">{open}<rect width=\"10\" height=\"10\"/>{}</svg>", "</g>".repeat(n))
}
#[test]
fn deeply_nested_svg_is_refused_or_merged() {
on_a_small_stack(|| {
// Within what the reader takes: opened, the groups past the cap merged into the last.
let (doc, warnings) = open_deep("deep.svg", nested_svg(40).as_bytes()).expect("40 groups open");
assert!(merged(&warnings), "{warnings:?}");
assert_eq!(paths_in(&doc), 41, "no art is lost");
let (_, warnings) = open_deep("shallow.svg", nested_svg(20).as_bytes()).expect("20 groups open");
assert!(!merged(&warnings), "{warnings:?}");
// As deep as the reader goes (on a stack of its own): opened and merged the same.
let most = vectorcraft_svg::MAX_ELEMENT_DEPTH;
let (doc, warnings) = open_deep("deepest.svg", nested_svg(most - 2).as_bytes()).expect("opens at the reader's cap");
assert!(merged(&warnings), "{warnings:?}");
assert_eq!(paths_in(&doc), most - 1, "no art is lost");
// Past it: an error that says why, whatever the depth.
for n in [most, 2 * most, 200_000] {
let e = vectorcraft_engine::cmd::fileio::load("deep.svg", nested_svg(n).as_bytes()).err().expect("refused").to_string();
assert!(e.contains(&format!("nested more than {most} deep")), "{n}: {e}");
}
// Unnamed groups of one object aren't kept, but usvg walks them all the same.
let plain = format!(
"<svg {SVG_NS} width=\"9\" height=\"9\">{}<rect width=\"1\" height=\"1\"/>{}</svg>",
"<g>".repeat(2 * most),
"</g>".repeat(2 * most)
);
assert!(open_deep("plain.svg", plain.as_bytes()).is_none());
// A chain of `<use>`: shallow XML, a deep tree.
let links: String = (0..3000).map(|i| format!("<g id=\"u{i}\"><use href=\"#u{}\"/></g>", i + 1)).collect();
let chain = format!(
"<svg {SVG_NS} width=\"9\" height=\"9\"><defs>{links}<rect id=\"u3000\" width=\"1\" height=\"1\"/></defs><use href=\"#u0\"/></svg>"
);
assert!(open_deep("chain.svg", chain.as_bytes()).is_none());
// Placed and pasted, as opened.
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 300, "height": 200})).unwrap();
for n in [40, 2 * most] {
let p = json!({"name": "deep.svg", "dataBase64": vectorcraft_format::base64_encode(nested_svg(n).as_bytes())});
let _ = no_panic(s.execute("file.place", &p));
let _ = no_panic(s.execute("clipboard.importSvg", &json!({"svg": nested_svg(n)})));
let _ = no_panic(s.execute("edit.paste", &json!({})));
assert!(!s.doc().unwrap().doc.too_deep(None));
}
});
}
/// A page whose content is `content`, with `resources`.
fn deep_pdf(content: &str, resources: &str) -> Vec<u8> {
handmade_pdf(content, resources, "[0 0 100 100]")
}
/// An EPS whose program is `body`.
fn deep_eps(body: &str) -> Vec<u8> {
format!("%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n%%EndComments\n{body}\nshowpage\n%%EOF\n").into_bytes()
}
/// A test for each file: it opens as an error or as a document within the cap, on a small stack.
macro_rules! deep_files {
($($test:ident: $name:literal, $bytes:expr;)*) => {$(
#[test]
fn $test() {
on_a_small_stack(|| {
open_deep($name, &$bytes);
});
}
)*};
}
deep_files! {
// PDF: clips inside clips (saved states never restored); arrays in arrays as an operand and as
// an object; dictionaries in dictionaries as a resource and inline; marked content never ended.
pdf_clips_in_clips: "clips.pdf", deep_pdf(&format!("{} 0 0 5 5 re f", "q 0 0 50 50 re W n ".repeat(20_000)), "<< >>");
pdf_arrays_in_an_operand: "arrays.pdf", deep_pdf(&format!("BT {}{} TJ ET 0 0 5 5 re f", "[".repeat(100_000), "]".repeat(100_000)), "<< >>");
pdf_arrays_in_an_object: "boxes.pdf", handmade_pdf("0 0 5 5 re f", "<< >>", &format!("{}0 0 100 100{}", "[".repeat(100_000), "]".repeat(100_000)));
pdf_dictionaries_in_resources: "dicts.pdf", deep_pdf("0 0 5 5 re f", &format!("{}{}", "<< /A ".repeat(50_000), ">> ".repeat(50_000)));
pdf_dictionaries_in_content: "inline.pdf", deep_pdf(&format!("/P {}{} DP 0 0 5 5 re f", "<< /A ".repeat(50_000), ">> ".repeat(50_000)), "<< >>");
pdf_marked_content_never_ended: "marked.pdf", deep_pdf(&format!("{} 0 0 5 5 re f", "/Span << /MCID 0 >> BDC ".repeat(50_000)), "<< >>");
// PostScript: clips inside clips; procedures in procedures; arrays in arrays; a procedure that
// calls itself; dictionaries never ended.
eps_clips_in_clips: "clips.eps", deep_eps(&format!("{} 0 0 5 5 rectfill", "gsave 0 0 50 50 rectclip ".repeat(20_000)));
eps_procedures_in_procedures: "procs.eps", deep_eps(&format!("{}{} 0 0 5 5 rectfill", "{".repeat(100_000), "}".repeat(100_000)));
eps_arrays_in_arrays: "arrays.eps", deep_eps(&format!("{}{} pop 0 0 5 5 rectfill", "[".repeat(100_000), "]".repeat(100_000)));
eps_a_procedure_calling_itself: "runaway.eps", deep_eps("/f { 0 0 5 5 rectfill f } def f");
eps_dictionaries_never_ended: "dicts.eps", deep_eps(&format!("{} 0 0 5 5 rectfill", "1 dict begin ".repeat(100_000)));
}
#[test]
fn deeply_nested_dxf_blocks_are_read_within_the_cap() {
on_a_small_stack(|| {
// Block `B0` inserts `B1`, which inserts `B2`, and so on; the last one holds a line.
let pair = |c: i32, v: &str| (c, v.to_string());
let mut pairs = vec![pair(0, "SECTION"), pair(2, "BLOCKS")];
for i in 0..2_000 {
pairs.extend([pair(0, "BLOCK"), pair(2, &format!("B{i}")), pair(10, "0"), pair(20, "0")]);
if i + 1 < 2_000 {
pairs.extend([pair(0, "INSERT"), pair(2, &format!("B{}", i + 1)), pair(10, "1"), pair(20, "1")]);
} else {
pairs.extend([pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "5"), pair(21, "5")]);
}
pairs.push(pair(0, "ENDBLK"));
}
pairs.extend([pair(0, "ENDSEC"), pair(0, "SECTION"), pair(2, "ENTITIES"), pair(0, "INSERT"), pair(2, "B0"), pair(10, "0"), pair(20, "0")]);
pairs.extend([pair(0, "LINE"), pair(10, "0"), pair(20, "0"), pair(11, "9"), pair(21, "9"), pair(0, "ENDSEC"), pair(0, "EOF")]);
open_deep("blocks.dxf", dxf_text(&pairs).as_bytes()).expect("opens");
});
}
/// A layer holding `levels` groups in one another around a rectangle, each with a second
/// rectangle beside the next: `levels + 2` deep.
fn nested_document(levels: usize) -> Document {
use vectorcraft_doc::Node;
use vectorcraft_testkit::fixtures::DocBuilder;
let mut b = DocBuilder::new(100.0, 100.0);
let rect =
|b: &mut DocBuilder, x: f64| b.rect_node(vectorcraft_geom::Rect::new(x, 0.0, x + 10.0, 10.0), vectorcraft_color::Color::rgb8(200, 0, 0));
let mut inner = rect(&mut b, 0.0);
for _ in 0..levels {
let beside = rect(&mut b, 20.0);
inner = Node::group(b.alloc(), vec![std::sync::Arc::new(inner), std::sync::Arc::new(beside)]);
inner.opacity = 0.9;
}
b.add(inner);
b.build()
}
#[test]
fn a_document_at_the_cap_saves_opens_and_exports() {
on_a_small_stack(|| {
let doc = nested_document(MAX_DEPTH - 2);
assert!(!doc.too_deep(None));
let back = vectorcraft_format::load_file(&vectorcraft_format::save_file(&doc)).expect("what can be made opens again");
assert!(back.depth_notes.is_empty());
assert_eq!(back.doc.layers, doc.layers);
survive("at the cap", || Some(doc.clone())).unwrap();
// Every format that keeps the nesting reads its own file back within the cap.
let mut s = vectorcraft_engine::Session::new();
s.add_document(doc, None);
for format in ["svg", "pdf", "eps", "ai"] {
let r = s.execute("document.export", &json!({"format": format})).unwrap();
let bytes = vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap();
open_deep(&format!("cap.{format}"), &bytes).unwrap_or_else(|| panic!("{format} opens"));
}
for cmd in ["select.all", "document.inspect", "document.info", "edit.copy", "edit.paste", "object.ungroup", "edit.undo"] {
no_panic(s.execute(cmd, &json!({}))).unwrap_or_else(|e| panic!("{cmd}: {e}"));
}
});
}
#[test]
fn a_native_file_nested_too_deep_is_merged_or_refused() {
on_a_small_stack(|| {
// Saved before the cap by a version that could still open it: opened, the deepest merged.
let old = vectorcraft_format::save_file(&nested_document(MAX_DEPTH + 2));
let (doc, warnings) = open_deep("old.vectorcraft", &old).expect("opens");
assert!(merged(&warnings), "{warnings:?}");
assert_eq!(paths_in(&doc), MAX_DEPTH + 3, "no art is lost");
// Deeper than any version could open, and a hostile file: an error that says why. Written
// by hand: a tree this deep can't be built and saved.
let text = String::from_utf8(vectorcraft_format::save(&nested_document(0), false)).unwrap();
let (open, close) = ("{\"id\":900000,\"kind\":{\"type\":\"group\",\"children\":[", "]}}");
let at = text.find("\"children\":[").expect("the layer's children") + "\"children\":[".len();
for levels in [100, 50_000] {
let deep = format!("{}{}{},{}", &text[..at], open.repeat(levels), close.repeat(levels), &text[at..]);
let e = vectorcraft_engine::cmd::fileio::load("deep.vectorcraft", deep.as_bytes()).err().expect("refused").to_string();
assert!(e.contains("nested too deep"), "{levels}: {e}");
}
});
}
#[test]
fn commands_cannot_nest_deeper_than_the_cap() {
on_a_small_stack(|| {
let mut s = vectorcraft_engine::Session::new();
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
s.execute("select.all", &json!({})).unwrap();
let mut grouped = 0;
for _ in 0..10_000 {
match no_panic(s.execute("object.group", &json!({}))) {
Ok(_) => grouped += 1,
Err(e) => assert!(e.to_string().contains(vectorcraft_engine::TOO_DEEP), "after {grouped} groups: {e}"),
}
}
// The layer, the groups and the rectangle.
assert_eq!(grouped, MAX_DEPTH - 2);
assert!(!s.doc().unwrap().doc.too_deep(None));
// Other ways down: a clipping mask, a copy pasted, a symbol made of it.
for (cmd, p) in [
("object.clippingMask.make", json!({})),
("edit.copy", json!({})),
("edit.pasteInFront", json!({})),
("symbol.new", json!({"name": "deep"})),
] {
let _ = no_panic(s.execute(cmd, &p));
assert!(!s.doc().unwrap().doc.too_deep(None), "{cmd}");
}
survive("grouped to the cap", || Some((*s.doc().unwrap().doc).clone())).unwrap();
});
}
/// A document whose one object is an instance of the first of `count` symbols: each symbol's art
/// is `levels` groups around an instance of the next symbol, the last one of `last`.
fn symbols_document(count: usize, levels: usize, last: &str) -> Document {
use vectorcraft_doc::{Node, NodeKind, Symbol};
let instance =
|doc: &mut Document, of: String| Node::new(doc.alloc_id(), NodeKind::SymbolInstance { symbol: of, xf: vectorcraft_geom::Affine::IDENTITY });
let mut doc = nested_document(1);
for i in 0..count {
let mut art = instance(&mut doc, if i + 1 < count { format!("s{}", i + 1) } else { last.to_string() });
for _ in 0..levels {
art = Node::group(doc.alloc_id(), vec![std::sync::Arc::new(art)]);
}
doc.symbols.push(Symbol { name: format!("s{i}"), art: std::sync::Arc::new(art) });
}
let first = instance(&mut doc, "s0".into());
let layer = doc.layers[0].id;
doc.insert(Some(layer), usize::MAX, first).unwrap();
doc
}
/// `doc` saved (as an older version, or a hostile file, could have) and opened again: within the
/// caps (`kept`: as it was), drawn, exported and edited.
fn symbols_survive(what: &str, doc: Document, kept: bool) {
let (back, warnings) = open_deep("symbols.vectorcraft", &vectorcraft_format::save_file(&doc)).expect("opens");
assert_eq!(!warnings.iter().any(|w| w.contains("symbol instances")), kept, "{what}: {warnings:?}");
let mut s = vectorcraft_engine::Session::new();
s.add_document(back, None);
for cmd in [
"select.all",
"document.inspect",
"document.info",
"object.expand",
"symbol.breakLink",
"object.flattenTransparency",
"edit.copy",
"edit.paste",
] {
let _ = no_panic(s.execute(cmd, &json!({})));
assert!(!s.doc().unwrap().doc.too_deep(None), "{what}: {cmd}");
}
for format in ["svg", "pdf", "eps", "png"] {
let _ = no_panic(s.execute("document.export", &json!({"format": format})));
}
}
#[test]
fn a_symbol_holding_itself_opens_without_the_instance() {
on_a_small_stack(|| {
symbols_survive("a symbol holding itself", symbols_document(1, 1, "s0"), false);
symbols_survive("symbols holding one another", symbols_document(3, 2, "s0"), false);
});
}
#[test]
fn symbols_in_symbols_are_drawn_only_so_deep() {
on_a_small_stack(|| {
// As deep as symbols draw: each one's art as deep as art goes.
symbols_survive("symbols at the cap", symbols_document(3, MAX_DEPTH - 2, "none"), true);
symbols_survive("symbols in symbols", symbols_document(40, MAX_DEPTH - 1, "none"), false);
// A symbol can't be made to hold itself, or one that holds it, by editing either.
let mut s = vectorcraft_engine::Session::new();
s.add_document(symbols_document(2, 1, "none"), None);
s.execute("select.all", &json!({})).unwrap();
for (cmd, p) in [
("symbol.new", json!({"name": "s1"})),
("symbol.update", json!({"name": "s1"})),
("symbol.update", json!({"name": "s0"})),
("symbol.replace", json!({"name": "s1"})),
] {
let _ = no_panic(s.execute(cmd, &p));
assert!(!s.doc().unwrap().doc.too_deep(None), "{cmd} {p}");
}
});
}
#[test]
fn a_pattern_painting_its_own_tile_opens_without_the_paint() {
on_a_small_stack(|| {
use vectorcraft_color::Paint;
use vectorcraft_doc::{Appearance, PatternDef};
let painted = |doc: &mut Document, with: &str| {
let fill = Paint::Pattern { pattern: with.into(), xf: vectorcraft_geom::Affine::IDENTITY };
let square = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 20.0, 20.0));
vectorcraft_doc::Node::path(doc.alloc_id(), square, Appearance::basic(fill, Paint::None, 0.0))
};
// One pattern painting its own tile, and two painting each other's.
for names in [["p", "p"], ["p", "q"]] {
let mut doc = nested_document(1);
let (first, second) = (painted(&mut doc, names[1]), painted(&mut doc, names[0]));
doc.patterns.push(PatternDef::new(names[0], vec![std::sync::Arc::new(first)]));
if names[0] != names[1] {
doc.patterns.push(PatternDef::new(names[1], vec![std::sync::Arc::new(second)]));
}
let on_layer = painted(&mut doc, "p");
let layer = doc.layers[0].id;
doc.insert(Some(layer), usize::MAX, on_layer).unwrap();
symbols_survive("a pattern painting its own tile", doc, false);
}
});
}
+1 -2
View File
@@ -4,7 +4,6 @@
//! Every operation counts against a budget and procedures nest only so deep, so a looping or
//! recursing program ends with [`PsError::Limit`] instead of hanging.
use std::cell::RefCell;
use std::rc::Rc;
use vectorcraft_geom::Affine;
@@ -86,7 +85,7 @@ struct SaveMark {
}
fn new_dict() -> DictRef {
Rc::new(RefCell::new(Dict::new()))
DictRef::new(Dict::new())
}
/// An encoding vector: 256 `/.notdef` names (type is read as Latin-1 whatever the encoding).
+3 -1
View File
@@ -268,5 +268,7 @@ fn finish_with(out: Out, layers: Vec<Arc<Node>>) -> Imported {
}
}
doc.layers = layers;
Imported { document: doc, warnings: out.warnings, preview: false }
let mut warnings = out.warnings;
doc.limit_depth_noting(&mut warnings);
Imported { document: doc, warnings, preview: false }
}
+1
View File
@@ -993,6 +993,7 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr
}
None => unmark(&mut doc.layers, true, &mut vec![]),
}
doc.limit_depth_noting(&mut notes);
Ok((doc, notes))
}
+81 -5
View File
@@ -12,7 +12,73 @@ use super::graphics::GState;
pub(crate) type Dict = HashMap<Key, Obj>;
/// A shared, mutable dictionary.
pub(crate) type DictRef = Rc<RefCell<Dict>>;
#[derive(Clone, Debug)]
pub(crate) struct DictRef(Rc<RefCell<Dict>>);
impl DictRef {
pub fn new(d: Dict) -> Self {
Self(Rc::new(RefCell::new(d)))
}
}
impl std::ops::Deref for DictRef {
type Target = Rc<RefCell<Dict>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Drop for DictRef {
fn drop(&mut self) {
if Rc::strong_count(&self.0) == 1
&& let Ok(mut d) = self.0.try_borrow_mut()
{
drop_held(d.drain().map(|(_, v)| v));
}
}
}
thread_local! {
/// What the composite objects being dropped held, waiting for their turn (while any is).
static HELD: RefCell<Option<Vec<Obj>>> = const { RefCell::new(None) };
}
/// Drop what a composite object held, now that nothing else refers to it, without recursion: a
/// program nests arrays and dictionaries as deep as it likes (`[ exch ]` in a loop), and dropping
/// them one inside the other overflows the stack. What `held` holds in its turn waits in [`HELD`]
/// for the outermost drop, which works through it.
fn drop_held(held: impl Iterator<Item = Obj>) {
let holds = |o: &Obj| matches!(o, Obj::Array { .. } | Obj::Dict(_) | Obj::File { .. } | Obj::GState(_));
// (Without the list, when the thread is ending: dropped as they come.)
let outermost = HELD.try_with(|h| {
let Ok(mut h) = h.try_borrow_mut() else { return false };
let outermost = h.is_none();
h.get_or_insert_with(Vec::new).extend(held.filter(holds));
outermost
});
if !matches!(outermost, Ok(true)) {
return;
}
// Each drop here puts what it held on the list instead of dropping it.
while let Ok(Some(next)) = HELD.try_with(|h| h.try_borrow_mut().ok().and_then(|mut h| h.as_mut().and_then(Vec::pop))) {
drop(next);
}
let _ = HELD.try_with(|h| h.try_borrow_mut().map(|mut h| *h = None)); // Nothing to do when the thread is ending.
}
/// What [`Shared`] values may be: bytes, or objects (which may hold more of themselves).
pub(crate) trait Held: Sized {
/// Drop the values of storage nothing refers to any more.
fn release(_values: Vec<Self>) {}
}
impl Held for u8 {}
impl Held for Obj {
fn release(values: Vec<Self>) {
drop_held(values.into_iter());
}
}
/// A dictionary key.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -72,7 +138,7 @@ impl Obj {
}
pub fn dict(d: Dict) -> Self {
Self::Dict(Rc::new(RefCell::new(d)))
Self::Dict(DictRef::new(d))
}
/// The number this is (integers and reals).
@@ -135,13 +201,23 @@ impl Obj {
/// The values of a string or an array: a run of storage that other strings or arrays may share
/// (`getinterval` makes a new object over part of the same values, as PostScript does).
#[derive(Clone, Debug)]
pub(crate) struct Shared<T> {
pub(crate) struct Shared<T: Held> {
buf: Rc<RefCell<Vec<T>>>,
at: usize,
len: usize,
}
impl<T: Clone> Shared<T> {
impl<T: Held> Drop for Shared<T> {
fn drop(&mut self) {
if Rc::strong_count(&self.buf) == 1
&& let Ok(mut values) = self.buf.try_borrow_mut()
{
T::release(std::mem::take(&mut *values));
}
}
}
impl<T: Clone + Held> Shared<T> {
pub fn new(v: Vec<T>) -> Self {
Self { len: v.len(), buf: Rc::new(RefCell::new(v)), at: 0 }
}
@@ -198,7 +274,7 @@ impl<T: Clone> Shared<T> {
}
/// Hashes what [`Shared::same`] compares: the storage and the run of it.
impl<T> Hash for Shared<T> {
impl<T: Held> Hash for Shared<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
Rc::as_ptr(&self.buf).hash(state);
(self.at, self.len).hash(state);
+2 -2
View File
@@ -389,7 +389,7 @@ impl Interp<'_> {
d.insert(Key::name("FontMatrix"), matrix_obj(Affine::scale(0.001)));
d.insert(Key::name("FontBBox"), Obj::array(vec![Obj::Int(0), Obj::Int(-250), Obj::Int(1000), Obj::Int(1000)]));
d.insert(Key::name("Encoding"), Obj::array(vec![Obj::name(".notdef"); 256]));
let font = Rc::new(std::cell::RefCell::new(d));
let font = DictRef::new(d);
self.fonts.borrow_mut().insert(key, Obj::Dict(font.clone()));
Ok(font)
}
@@ -489,7 +489,7 @@ fn transformed(font: &DictRef, m: Affine) -> DictRef {
let mut d = font.borrow().clone();
let fm = d.get(&Key::name("FontMatrix")).and_then(|o| o.items().and_then(|i| matrix_of(&i.borrow()))).unwrap_or(Affine::scale(0.001));
d.insert(Key::name("FontMatrix"), matrix_obj(m * fm));
Rc::new(std::cell::RefCell::new(d))
DictRef::new(d)
}
/// The character a glyph name stands for (`A`, `space`, `uni20AC`), else a bullet.
+11 -2
View File
@@ -482,6 +482,9 @@ pub fn load(bytes: &[u8]) -> Result<Document, FormatError> {
#[derive(Debug)]
pub struct NativeFile {
pub doc: Document,
/// What was done to bring the document within the nesting caps
/// ([`Document::limit_depth_noting`]: a file saved before them, or a damaged one).
pub depth_notes: Vec<String>,
pub info: FileInfo,
/// ICC files by profile name (saved with Embed ICC Profiles; undecodable ones are left out).
pub profiles: BTreeMap<String, Vec<u8>>,
@@ -511,7 +514,11 @@ pub fn load_info(bytes: &[u8]) -> Result<(Document, FileInfo), FormatError> {
/// [`load`], with everything else the file carries.
pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
let file = unpack::Unpacked::new(bytes)?;
let f: File = serde_json::from_slice(file.json()).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?;
// The JSON reader stops at 128 levels (a document's objects use three each).
let f: File = serde_json::from_slice(file.json()).map_err(|e| {
let why = if e.to_string().contains("recursion limit") { format!("its objects are nested too deep ({e})") } else { e.to_string() };
FormatError::NotVectorcraft(why)
})?;
if f.format != EXTENSION && f.format != LEGACY_EXTENSION {
return Err(FormatError::NotVectorcraft(format!("format is `{}`", f.format)));
}
@@ -545,8 +552,10 @@ pub fn load_file(bytes: &[u8]) -> Result<NativeFile, FormatError> {
// Untrusted: capped, named and pointing at objects the file has.
doc.tidy_saved_selections();
doc.tidy_graph_designs();
let mut depth_notes = vec![];
doc.limit_depth_noting(&mut depth_notes);
let profiles = f.profiles.into_iter().filter_map(|(name, icc)| Some((name, bytes_of(&icc)?))).collect();
Ok(NativeFile { doc, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
Ok(NativeFile { doc, depth_notes, info: FileInfo { version: f.version, legacy: f.format == LEGACY_EXTENSION }, profiles })
}
/// The members of a native file besides its document (which is skipped, not decoded).
+3 -1
View File
@@ -102,7 +102,9 @@ pub struct Imported {
/// Read an EMF or WMF file (with or without a placeable header) into a document whose one
/// artboard is the picture's frame.
pub fn import(bytes: &[u8]) -> Result<Imported, String> {
import::import(bytes)
let mut i = import::import(bytes)?;
i.document.limit_depth_noting(&mut i.warnings);
Ok(i)
}
#[cfg(test)]
+3
View File
@@ -149,6 +149,7 @@ pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it,
pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportReport, PdfError> {
let original = crate::pages::open(bytes, opts.password.as_deref())?;
let picked = crate::pages::picked(opts, original.pages().len())?;
picked.iter().filter_map(|&n| original.pages().get(n)).try_for_each(crate::pages::content_too_deep)?;
let mut ocgs = Ocgs::read(&original);
let route = opts.layers && !ocgs.list.is_empty();
// Content that is off imports as hidden layers: an update of the file turns every group on
@@ -166,6 +167,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
let all_on = turned_on.is_some();
let pdf = turned_on.unwrap_or(original);
let pages = pdf.pages();
picked.iter().filter_map(|&n| pages.get(n)).try_for_each(crate::pages::content_too_deep)?;
let sink: Arc<Mutex<Vec<String>>> = Arc::default();
let sink2 = sink.clone();
@@ -417,6 +419,7 @@ pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result<ImportRe
}
}
}
doc.limit_depth_noting(&mut warnings);
Ok(ImportReport { document: doc, warnings, native: crate::editing::editing_in(&pdf), only_text })
}
+70
View File
@@ -33,12 +33,76 @@ pub fn is_postscript(bytes: &[u8]) -> bool {
bytes.starts_with(b"%!PS") || bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6])
}
/// The deepest a file's arrays and dictionaries may nest. The PDF reader (hayro) reads them by
/// recursion, without a cap of its own: a file nested deeply enough overflows the stack, which is
/// an abort, not an error. Far beyond what a PDF writer makes.
const MAX_OBJECT_NESTING: usize = 256;
fn too_deep() -> PdfError {
PdfError::Parse(format!("its arrays and dictionaries are nested more than {MAX_OBJECT_NESTING} deep"))
}
/// Do the arrays and dictionaries in the text of `pdf` nest deeper than [`MAX_OBJECT_NESTING`]?
/// Read as tokens (strings and comments hold none), an object at a time. The data of a stream
/// with a filter is skipped: it isn't PDF text until decoded, which this doesn't do, so nesting
/// inside compressed streams is not seen here.
fn nests_too_deep(pdf: &[u8]) -> bool {
let after =
|from: usize, what: &[u8]| pdf.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
let word = |at: usize, w: &[u8]| pdf.get(at..).is_some_and(|rest| rest.starts_with(w));
let (mut depth, mut filtered) = (0usize, false);
let mut i = 0;
while let Some(&c) = pdf.get(i) {
i += 1;
match c {
b'%' => i += pdf.get(i..).and_then(|rest| rest.iter().position(|c| matches!(c, b'\n' | b'\r'))).unwrap_or(pdf.len()),
b'(' => {
// A string: parentheses nest in it, a backslash takes the next byte.
let mut open = 1usize;
while open > 0 {
match pdf.get(i) {
None => return false,
Some(b'\\') => i += 1,
Some(b'(') => open += 1,
Some(b')') => open -= 1,
Some(_) => {}
}
i += 1;
}
}
b'[' => depth += 1,
b']' => depth = depth.saturating_sub(1),
b'<' if pdf.get(i) == Some(&b'<') => (depth, i) = (depth + 1, i + 1),
b'>' if pdf.get(i) == Some(&b'>') => (depth, i) = (depth.saturating_sub(1), i + 1),
// A string in hexadecimal.
b'<' => i = after(i, b">").unwrap_or(pdf.len()),
b'/' if word(i - 1, b"/Filter") => filtered = true,
b'e' if word(i - 1, b"endobj") => (depth, filtered) = (0, false),
b'e' if word(i - 1, b"endstream") => i += 8,
b's' if word(i - 1, b"stream") => {
depth = 0;
if std::mem::take(&mut filtered) {
i = after(i, b"endstream").unwrap_or(pdf.len());
}
}
_ => {}
}
if depth > MAX_OBJECT_NESTING {
return true;
}
}
false
}
/// Read a PDF, decrypting it with `password` (none: the empty user password). The permissions
/// password opens a file too.
pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError> {
if is_postscript(bytes) {
return Err(PdfError::PostScript);
}
if nests_too_deep(bytes) {
return Err(too_deep());
}
let password = password.unwrap_or_default();
let read = |pw: &str| Pdf::new_with_password(bytes.to_vec(), pw);
read(password)
@@ -57,6 +121,12 @@ pub(crate) fn open(bytes: &[u8], password: Option<&str>) -> Result<Pdf, PdfError
})
}
/// The error of a page whose content, decoded, nests arrays and dictionaries too deep (see
/// [`MAX_OBJECT_NESTING`]): its operands are read the same way as the file's objects.
pub(crate) fn content_too_deep(page: &Page<'_>) -> Result<(), PdfError> {
if page.page_stream().is_some_and(nests_too_deep) { Err(too_deep()) } else { Ok(()) }
}
/// The pages and their boxes. An encrypted PDF needs its `password`
/// ([`PdfError::NeedsPassword`] / [`PdfError::WrongPassword`]).
pub fn info(bytes: &[u8], password: Option<&str>) -> Result<PdfInfo, PdfError> {
+34 -14
View File
@@ -18,10 +18,13 @@ use crate::{ImportOptions, SvgError, fnv1a};
use css::XNode;
use text::TextSlots;
pub use depth::MAX_ELEMENT_DEPTH;
pub(crate) use depth::xml_too_deep;
pub(crate) use fx::BLEND;
mod css;
mod css_transform;
mod depth;
mod files;
mod fx;
mod spread;
@@ -29,11 +32,30 @@ mod symbol_units;
mod text;
mod vector_effect;
/// The XML of `svg`.
fn parse(svg: &str) -> Result<roxmltree::Document<'_>, SvgError> {
roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() })
.map_err(|e| SvgError::Parse(e.to_string()))
}
pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<String>), SvgError> {
let (svg, found) = prepass(svg, opts);
// Before anything reads the tree: the XML reader, usvg, and what is made of its tree, all by
// recursion.
let too_deep = || SvgError::TooDeep(depth::MAX_ELEMENT_DEPTH);
if depth::xml_too_deep(svg) {
return Err(too_deep());
}
let xml = parse(svg)?;
if depth::too_deep(&xml) {
return Err(too_deep());
}
let (svg, found) = prepass(svg, &xml, opts);
// The prepass's edits are in the text: the XML is read again only when it made any.
let xml = match &svg {
Cow::Borrowed(_) => xml,
Cow::Owned(edited) => parse(edited)?,
};
let svg = svg.as_ref();
let xml = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() })
.map_err(|e| SvgError::Parse(e.to_string()))?;
let units = RootUnits::of(xml.root_element());
let root_name = root_layer_name(xml.root_element());
let mut warnings = found.warnings;
@@ -165,6 +187,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
promote_sublayers(Arc::make_mut(l), &im.sublayer_groups);
}
}
im.doc.limit_depth_noting(&mut im.warnings);
Ok((im.doc, im.warnings))
}
@@ -554,7 +577,7 @@ impl Edits {
/// * linked files are read ([`files`]);
/// * percentage lengths in a `<symbol>` with a `viewBox` become numbers ([`symbol_units`]);
/// * what style sheet rules with a `:not()` set goes in `style` attributes ([`not_rules`]).
fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
fn prepass<'s>(svg: &'s str, xml: &roxmltree::Document, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
let mut found = Found::default();
// (`:` for prefixed elements such as `<svg:image>`.)
let css_transform = has_css_transform(svg);
@@ -565,9 +588,6 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
{
return (svg.into(), found);
}
let Ok(xml) = roxmltree::Document::parse_with_options(svg, roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() }) else {
return (svg.into(), found);
};
let mut edits = Edits::default();
let elements = || xml.descendants().filter(|n| n.is_element());
for a in elements().filter(|n| n.tag_name().name() == "a") {
@@ -592,24 +612,24 @@ fn prepass<'s>(svg: &'s str, opts: &ImportOptions) -> (Cow<'s, str>, Found) {
}
let (display, non_scaling) = (svg.contains("display"), svg.contains(vector_effect::NON_SCALING));
if display || non_scaling || not {
let css = css::Styles::new(&xml);
let css = css::Styles::new(xml);
// First: an undisplayed object shown for usvg is shown after these.
if not && css.has_not_rules() {
not_rules(svg, &xml, &css, &mut edits);
not_rules(svg, xml, &css, &mut edits);
}
if display {
hidden_objects(svg, &xml, &css, &mut edits, &mut found.hidden);
hidden_objects(svg, xml, &css, &mut edits, &mut found.hidden);
}
if non_scaling {
vector_effect::find(svg, &xml, &css, &mut edits, &mut found.non_scaling, &mut found.warnings);
vector_effect::find(svg, xml, &css, &mut edits, &mut found.non_scaling, &mut found.warnings);
}
}
found.files = files::Files::read(svg, &xml, opts, &mut edits, &mut found.warnings);
found.files = files::Files::read(svg, xml, opts, &mut edits, &mut found.warnings);
if !symbols.is_empty() && svg.contains('%') {
edits.list.extend(symbol_units::rewrites(&xml));
edits.list.extend(symbol_units::rewrites(xml));
}
if css_transform {
edits.list.extend(css_transform::rewrites(svg, &xml));
edits.list.extend(css_transform::rewrites(svg, xml));
}
(edits.apply(svg), found)
}
+7 -2
View File
@@ -17,7 +17,7 @@ pub(super) type XNode<'a, 'i> = roxmltree::Node<'a, 'i>;
/// `name: value` declarations of a block or `style` attribute, with their `!important` flag. The
/// `font` shorthand becomes its longhands, as usvg reads it for the rest of the drawing (#1557).
fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
pub(super) fn parse_decls(s: &str) -> Vec<(String, String, bool)> {
s.split(';')
.filter_map(|d| {
let (k, v) = d.split_once(':')?;
@@ -371,8 +371,13 @@ impl Styles {
Self { rules, index, cache: RefCell::default() }
}
/// Do the document's style sheets have any rules?
pub(super) fn has_rules(&self) -> bool {
!self.rules.is_empty()
}
/// Properties set on `n` by rules and its `style` attribute, after the cascade.
fn declared(&self, n: XNode) -> Rc<HashMap<String, String>> {
pub(super) fn declared(&self, n: XNode) -> Rc<HashMap<String, String>> {
if let Some(m) = self.cache.borrow().get(&n.id()) {
return m.clone();
}
+264
View File
@@ -0,0 +1,264 @@
//! How deep an SVG's elements nest, found in the XML before usvg runs: usvg converts the tree by
//! recursion, a few stack frames an element, so a file nested deeply enough overflows the stack
//! (an abort, not an error). Such a file is refused: its nesting can't be merged before usvg has
//! read it, and usvg is what can't read it.
//!
//! What counts is the tree usvg walks, not the XML alone: a `<use>` holds a copy of what it
//! refers to, and a clip path, mask, pattern, marker or filter is converted where it is used
//! (`clip-path: url(#a)` on an element of a clip path makes a chain as deep as there are clip
//! paths). So each element's depth is the deepest of what it holds and what it refers to.
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use usvg::roxmltree;
use super::css::{Styles, XNode, parse_decls};
/// The deepest an SVG's elements may nest, references followed. Far beyond what drawing apps
/// write, and deeper than a document's objects may nest ([`vectorcraft_doc::MAX_DEPTH`]: wrappers
/// that hold one object aren't kept, and what is still deeper is merged into the group around
/// it). usvg uses about 15 kB of stack for each level: the importer runs on a thread with 16 MB
/// ([`vectorcraft_doc::depth::with_deep_stack`]), except on the web, where there is the one
/// 1 MB stack.
pub const MAX_ELEMENT_DEPTH: usize = if cfg!(target_arch = "wasm32") { 48 } else { 256 };
/// Properties whose value an element's descendants use too: the paint server they name is
/// converted at each descendant, under everything between.
const INHERITED: [&str; 6] = ["fill", "stroke", "marker", "marker-start", "marker-mid", "marker-end"];
/// The ids of the `url(#id)` references in a property value.
fn urls(value: &str) -> impl Iterator<Item = &str> {
value.split("url(").skip(1).filter_map(|rest| {
let id = rest.trim_start().trim_start_matches(['"', '\'']).strip_prefix('#')?;
id.split([')', '"', '\'']).next().map(str::trim).filter(|id| !id.is_empty())
})
}
/// The elements `n` refers to, each with whether `n`'s descendants use it too.
fn references<'a, 'i>(n: XNode<'a, 'i>, ids: &HashMap<&str, XNode<'a, 'i>>, css: &Styles, out: &mut Vec<(XNode<'a, 'i>, bool)>) {
let add = |out: &mut Vec<(XNode<'a, 'i>, bool)>, name: &str, value: &str| {
let inherited = INHERITED.contains(&name);
out.extend(urls(value).filter_map(|id| ids.get(id)).map(|t| (*t, inherited)));
};
for a in n.attributes() {
match a.name() {
"href" => out.extend(a.value().trim().strip_prefix('#').and_then(|id| ids.get(id)).map(|t| (*t, false))),
// Read with the style sheets' rules below.
"style" if css.has_rules() => {}
"style" => parse_decls(a.value()).iter().for_each(|(k, v, _)| add(out, k, v)),
name => add(out, name, a.value()),
}
}
if css.has_rules() {
css.declared(n).iter().for_each(|(k, v)| add(out, k, v));
}
}
/// Do the elements of the XML text `svg` nest deeper than [`MAX_ELEMENT_DEPTH`]? Read from the
/// text, before any XML reader: roxmltree (ours and usvg's) reads elements by recursion too. Tags
/// are counted as the text has them; markup in the values of a document type's entities counts
/// ten times (how deep entities may refer to one another), on top of the deepest element.
pub(crate) fn xml_too_deep(svg: &str) -> bool {
let b = svg.as_bytes();
let find =
|from: usize, what: &[u8]| b.get(from..).and_then(|rest| rest.windows(what.len()).position(|w| w == what)).map(|at| from + at + what.len());
// The end of the tag starting at `from` (past its `>`), and whether it closes itself.
let tag_end = |from: usize| {
let mut quote = None;
for (i, c) in b.iter().enumerate().skip(from) {
match (quote, c) {
(None, b'"' | b'\'') => quote = Some(*c),
(Some(q), c) if q == *c => quote = None,
(None, b'>') => return (i + 1, b.get(i.wrapping_sub(1)) == Some(&b'/')),
_ => {}
}
}
(b.len(), false)
};
// (now, deepest) of the elements, and of the markup inside the document type.
let (mut elements, mut entities) = ((0usize, 0usize), (0usize, 0usize));
let mut doctype_end = 0;
let mut i = 0;
while let Some(at) = b.get(i..).and_then(|rest| rest.iter().position(|c| *c == b'<')).map(|at| i + at) {
let rest = b.get(at..).unwrap_or_default();
let depth = if at < doctype_end { &mut entities } else { &mut elements };
i = if rest.starts_with(b"<!--") {
find(at, b"-->").unwrap_or(b.len())
} else if rest.starts_with(b"<![CDATA[") {
find(at, b"]]>").unwrap_or(b.len())
} else if rest.starts_with(b"<?") {
find(at, b"?>").unwrap_or(b.len())
} else if rest.starts_with(b"<!DOCTYPE") {
// Up to the `>` that ends it: after its internal subset (`[` … `]`) when it has one.
let plain = tag_end(at).0;
let subset = b.get(at..plain).is_some_and(|head| head.contains(&b'['));
doctype_end = if subset { find(at, b"]>").unwrap_or(b.len()) } else { plain };
at + 2
} else if rest.starts_with(b"<!") {
// A declaration of the document type: the markup in its value is counted as it comes.
at + 2
} else if rest.starts_with(b"</") {
depth.0 = depth.0.saturating_sub(1);
tag_end(at).0
} else if at < doctype_end {
// In an entity's value, itself in quotes: up to the tag's `>`.
let end = find(at, b">").unwrap_or(b.len());
depth.1 = depth.1.max(depth.0 + 1);
depth.0 += usize::from(b.get(end.wrapping_sub(2)) != Some(&b'/'));
end
} else {
let (end, empty) = tag_end(at);
depth.1 = depth.1.max(depth.0 + 1);
depth.0 += usize::from(!empty);
end
};
if elements.1 + 10 * entities.1 > MAX_ELEMENT_DEPTH {
return true;
}
}
false
}
/// Does anything in `xml` nest deeper than [`MAX_ELEMENT_DEPTH`], references followed? Without
/// recursion.
pub(super) fn too_deep(xml: &roxmltree::Document) -> bool {
let elements = || xml.descendants().filter(|n| n.is_element());
let ids: HashMap<&str, XNode> = elements().filter_map(|n| Some((n.attribute("id")?, n))).collect();
let css = Styles::new(xml);
// How deep each element's own tree goes, itself included; 0 while it is being worked out: a
// reference back to it then is a loop, which usvg leaves out.
let mut heights: HashMap<roxmltree::NodeId, usize> = HashMap::new();
enum Step<'a, 'i> {
Enter(XNode<'a, 'i>),
Leave(XNode<'a, 'i>, Vec<(XNode<'a, 'i>, bool)>),
}
let mut stack = vec![Step::Enter(xml.root_element())];
while let Some(step) = stack.pop() {
match step {
Step::Enter(n) => {
match heights.entry(n.id()) {
Entry::Occupied(_) => continue,
Entry::Vacant(v) => v.insert(0),
};
let mut refs = vec![];
references(n, &ids, &css, &mut refs);
let targets: Vec<XNode> = refs.iter().map(|(t, _)| *t).collect();
stack.push(Step::Leave(n, refs));
stack.extend(n.children().filter(|c| c.is_element()).chain(targets).map(Step::Enter));
}
Step::Leave(n, refs) => {
let of = |n: XNode| heights.get(&n.id()).copied().unwrap_or(0);
let held = n.children().filter(|c| c.is_element()).map(of).max().unwrap_or(0);
let used = |inherited: bool| refs.iter().filter(|(_, i)| *i == inherited).map(|(t, _)| of(*t)).max().unwrap_or(0);
let height = 1 + held.max(used(false)) + used(true);
if height > MAX_ELEMENT_DEPTH {
return true;
}
heights.insert(n.id(), height);
}
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
fn deep(svg: &str) -> bool {
too_deep(&roxmltree::Document::parse(svg).unwrap())
}
fn svg(body: &str) -> String {
format!(r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10" height="10">{body}</svg>"#)
}
/// `n` groups in one another around a rectangle.
fn groups(n: usize) -> String {
format!("{}<rect width='1' height='1'/>{}", "<g>".repeat(n), "</g>".repeat(n))
}
#[test]
fn elements_nest_up_to_the_cap() {
// The root, the groups and the rectangle.
let (at, past) = (svg(&groups(MAX_ELEMENT_DEPTH - 2)), svg(&groups(MAX_ELEMENT_DEPTH - 1)));
assert!(!deep(&at) && !xml_too_deep(&at));
assert!(deep(&past) && xml_too_deep(&past));
assert!(xml_too_deep(&svg(&groups(200_000))), "any depth is measured");
}
#[test]
fn the_text_is_measured_as_xml_reads_it() {
let many = r#"<g a='>' b="<g>"><rect/></g>"#.repeat(500);
assert!(!xml_too_deep(&svg(&many)), "side by side, with `>` and tags in attribute values");
let hidden = format!("<!-- {0} --><![CDATA[{0}]]><?pi {0}?>", "<g>".repeat(500));
assert!(!xml_too_deep(&svg(&hidden)), "comments, character data and instructions hold no elements");
assert!(xml_too_deep(&format!("<svg>{}", "<g>".repeat(MAX_ELEMENT_DEPTH))), "never closed");
assert!(!xml_too_deep(&format!("{}<svg/>", "</g>".repeat(2 * MAX_ELEMENT_DEPTH))), "ends without beginnings");
// Entities: namespaces as drawing apps declare them, and markup that would be read ten deep.
let names = r#"<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ <!ENTITY ns_svg "http://www.w3.org/2000/svg"> ]>"#;
assert!(!xml_too_deep(&format!("{names}{}", svg(&groups(30)))));
let tenth = MAX_ELEMENT_DEPTH / 10 + 1;
let bomb = format!("<!DOCTYPE svg [ <!ENTITY a \"{}&a;{}\"> ]>", "<g>".repeat(tenth), "</g>".repeat(tenth));
assert!(xml_too_deep(&format!("{bomb}<svg>&a;</svg>")));
assert!(!xml_too_deep(""));
}
#[test]
fn a_chain_of_uses_is_as_deep_as_what_it_copies() {
let chain = |n: usize| {
let defs: String = (0..n).map(|i| format!("<g id='g{i}'><use xlink:href='#g{}'/></g>", i + 1)).collect();
svg(&format!("<defs>{defs}<rect id='g{n}' width='1' height='1'/></defs><use href='#g0'/>"))
};
assert!(!deep(&chain(10)));
assert!(deep(&chain(MAX_ELEMENT_DEPTH / 2)), "two elements a link");
assert!(deep(&chain(5000)));
}
#[test]
fn chains_of_clips_masks_and_patterns_count_by_attribute_style_and_style_sheet() {
let by_attribute = |n: usize| -> String {
(0..n).map(|i| format!("<clipPath id='c{i}' clip-path='url(#c{})'><rect width='1' height='1'/></clipPath>", i + 1)).collect()
};
let by_style = |n: usize| -> String {
(0..n).map(|i| format!("<mask id='c{i}' style='mask: url( \"#c{}\" )'><rect width='1' height='1'/></mask>", i + 1)).collect()
};
let by_sheet = |n: usize| -> String {
let rules: String = (0..n).map(|i| format!(".p{i} {{ fill: url(#c{}) }}", i + 1)).collect();
let patterns: String = (0..n)
.map(|i| format!("<pattern id='c{i}' width='1' height='1'><g class='p{i}'><rect width='1' height='1'/></g></pattern>"))
.collect();
format!("<style>{rules}</style>{patterns}")
};
for chain in [&by_attribute as &dyn Fn(usize) -> String, &by_style, &by_sheet] {
let file =
|n| svg(&format!("<defs>{}</defs><rect width='5' height='5' fill='url(#c0)' clip-path='url(#c0)' mask='url(#c0)'/>", chain(n)));
assert!(!deep(&file(5)));
assert!(deep(&file(MAX_ELEMENT_DEPTH)));
}
}
#[test]
fn many_paint_servers_used_side_by_side_are_not_a_chain() {
// As drawing apps write gradients: a class each, used by objects that aren't inside one another.
let rules: String = (0..300).map(|i| format!(".c{i}{{fill:url(#g{i})}}")).collect();
let defs: String = (0..300).map(|i| format!("<linearGradient id='g{i}'><stop class='c{i}' offset='0'/></linearGradient>")).collect();
let art: String = (0..300).map(|i| format!("<g class='c{i}'><rect class='c{i}' width='1' height='1'/></g>")).collect();
assert!(!deep(&svg(&format!("<style>{rules}</style><defs>{defs}</defs>{art}"))));
}
#[test]
fn a_loop_of_references_is_not_deep() {
assert!(!deep(&svg("<g id='a'><use href='#b'/></g><g id='b'><use href='#a'/></g><clipPath id='c' clip-path='url(#c)'/>")));
}
#[test]
fn an_inherited_paint_is_converted_under_everything_between() {
// Neither the pattern nor the art is too deep; the pattern under the art's last object is.
let half = MAX_ELEMENT_DEPTH / 2;
let pattern = format!("<pattern id='p' width='1' height='1'>{}</pattern>", groups(half));
assert!(!deep(&svg(&format!("<defs>{pattern}</defs><g fill='url(#p)'>{}</g>", groups(half / 2)))));
assert!(deep(&svg(&format!("<defs>{pattern}</defs><g fill='url(#p)'>{}</g>", groups(half)))));
}
}
+10
View File
@@ -203,12 +203,16 @@ impl Files {
ImageHrefResolver {
resolve_data: Box::new(move |mime, bytes, opts| {
if mime == "image/svg+xml" {
if nested_too_deep(&bytes) {
return None;
}
self.note_text(&bytes);
}
data(mime, bytes, opts)
}),
resolve_string: Box::new(move |h, opts| match self.sources.get(h).or_else(|| self.sources.get(h.trim()))? {
Source::Raster(kind, bytes) => Some(kind(bytes.clone())),
Source::Svg(bytes) if nested_too_deep(bytes) => None,
Source::Svg(bytes) => {
self.note_text(bytes);
usvg::Tree::from_data_nested(bytes, opts).ok().map(ImageKind::SVG)
@@ -218,6 +222,12 @@ impl Files {
}
}
/// Is the SVG image `bytes` nested too deep for usvg to read (as the SVG holding it would be)?
/// It is then left out, as an image usvg can't read.
fn nested_too_deep(bytes: &[u8]) -> bool {
crate::text_of(bytes).is_ok_and(|text| super::xml_too_deep(&text))
}
#[cfg(test)]
mod tests {
use super::*;
+11 -3
View File
@@ -312,6 +312,9 @@ pub struct LinkedImage {
pub enum SvgError {
#[error("SVG parse error: {0}")]
Parse(String),
/// Its elements nest (references followed) deeper than the reader goes.
#[error("the SVG's elements are nested more than {0} deep")]
TooDeep(usize),
}
/// Export a document as an SVG string (linked images are not returned: see [`export_full`]).
@@ -347,7 +350,8 @@ pub struct Editing {
/// The native document an SVG carries, if any, and whether the SVG was edited since.
pub fn editing(svg: &str) -> Option<Editing> {
if !svg.contains(EDITING_NS) {
// (An SVG nested too deep for the XML reader carries none: it isn't one of ours.)
if !svg.contains(EDITING_NS) || import::xml_too_deep(svg) {
return None;
}
let opts = usvg::roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
@@ -500,14 +504,18 @@ pub fn import_with_report(svg: &str) -> Result<(Document, Vec<String>), SvgError
import_with(svg, &ImportOptions::default())
}
pub use import::MAX_ELEMENT_DEPTH;
/// Import an SVG document reading the files it links to as `opts` says, also returning warnings.
pub fn import_with(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<String>), SvgError> {
import::import(svg, opts)
// usvg converts the tree by recursion, in large stack frames: the nesting it is given is
// capped (`import::depth`), and it gets room for that cap whatever thread asks.
vectorcraft_doc::depth::with_deep_stack(|| import::import(svg, opts))
}
/// Reads a linked file: its bytes and the link to it (absolute path, size, time and hash), or
/// `None` when there is no readable file at the (absolute) path.
pub type ReadFile<'a> = &'a dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)>;
pub type ReadFile<'a> = &'a (dyn Fn(&str) -> Option<(Vec<u8>, vectorcraft_doc::LinkInfo)> + Sync);
/// How [`import_with`] reads the files an SVG's `<image>` elements link to.
///
+2
View File
@@ -4099,6 +4099,8 @@ imageTrace Outline Obrys
@msg recovery copy `{file}`: {e} kopie pro obnovení `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi rozlišení musí být mezi 1 a 2400 ppi
@msg result would exceed the canvas (coordinates out of range) výsledek by přesáhl plátno (souřadnice mimo rozsah)
@msg objects can't be nested more than 32 deep objekty nelze vnořit hlouběji než do 32 úrovní
@msg the SVG's elements are nested more than {0} deep prvky SVG jsou vnořeny hlouběji než do {0} úrovní
@msg scale must be a number from {_1} to {_2}, not {_3} měřítko musí být číslo od {_1} do {_2}, ne {_3}
@msg scale must be a number from {_1} to {_2}, not {s} měřítko musí být číslo od {_1} do {_2}, ne {s}
@msg select (or target) objects whose appearance isn't basic vyberte (nebo zacilte) objekty, jejichž vzhled není základní
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3705,6 +3705,8 @@ theme Appearance Erscheinungsbild
@msg recovery copy `{file}`: {e} Wiederherstellungskopie `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi Auflösungen müssen zwischen 1 und 2400 ppi liegen
@msg result would exceed the canvas (coordinates out of range) das Ergebnis würde die Arbeitsfläche überschreiten (Koordinaten außerhalb des Bereichs)
@msg objects can't be nested more than 32 deep Objekte können nicht tiefer als 32 Ebenen verschachtelt werden
@msg the SVG's elements are nested more than {0} deep die Elemente der SVG-Datei sind tiefer als {0} Ebenen verschachtelt
@msg scale must be a number from {_1} to {_2}, not {_3} scale muss eine Zahl von {_1} bis {_2} sein, nicht {_3}
@msg scale must be a number from {_1} to {_2}, not {s} scale muss eine Zahl von {_1} bis {_2} sein, nicht {s}
@msg select (or target) objects whose appearance isn't basic wählen Sie Objekte aus (oder als Ziel), deren Aussehen nicht einfach ist
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3804,6 +3804,8 @@ axis Both Ambos
@msg recovery copy `{file}`: {e} copia de recuperación `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi las resoluciones deben estar entre 1 y 2400 ppi
@msg result would exceed the canvas (coordinates out of range) el resultado excedería el lienzo (coordenadas fuera de rango)
@msg objects can't be nested more than 32 deep los objetos no pueden anidarse a más de 32 niveles
@msg the SVG's elements are nested more than {0} deep los elementos del SVG están anidados a más de {0} niveles
@msg scale must be a number from {_1} to {_2}, not {_3} la escala debe ser un número de {_1} a {_2}, no {_3}
@msg scale must be a number from {_1} to {_2}, not {s} la escala debe ser un número de {_1} a {_2}, no {s}
@msg select (or target) objects whose appearance isn't basic selecciona (o apunta a) objetos cuya apariencia no sea básica
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3801,6 +3801,8 @@ axis Both Les deux
@msg recovery copy `{file}`: {e} copie de récupération `{file}` : {e}
@msg resolutions must be between 1 and 2400 ppi les résolutions doivent être comprises entre 1 et 2400 ppp
@msg result would exceed the canvas (coordinates out of range) le résultat dépasserait la zone de travail (coordonnées hors limites)
@msg objects can't be nested more than 32 deep les objets ne peuvent pas être imbriqués sur plus de 32 niveaux
@msg the SVG's elements are nested more than {0} deep les éléments du SVG sont imbriqués sur plus de {0} niveaux
@msg scale must be a number from {_1} to {_2}, not {_3} l’échelle doit être un nombre compris entre {_1} et {_2}, pas {_3}
@msg scale must be a number from {_1} to {_2}, not {s} l’échelle doit être un nombre compris entre {_1} et {_2}, pas {s}
@msg select (or target) objects whose appearance isn't basic sélectionnez (ou ciblez) des objets dont l’aspect n’est pas basique
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -4441,6 +4441,8 @@ imageTrace Outline Körvonal
@msg resolutions must be between 1 and 2400 ppi a felbontás 1 és 2400 ppi között lehet
@msg Restored {_1} recovered document(s): save them to keep them Helyreállított dokumentumok: {_1}; mentsd őket, ha meg akarod tartani
@msg result would exceed the canvas (coordinates out of range) az eredmény túllépné a vásznat (a koordináták tartományon kívül esnek)
@msg objects can't be nested more than 32 deep az objektumok legfeljebb 32 szint mélyen ágyazhatók egymásba
@msg the SVG's elements are nested more than {0} deep az SVG elemei több mint {0} szint mélyen vannak egymásba ágyazva
@msg Save for Web: the image is empty Mentés webre: a kép üres
@msg Save writes {_1}, not {_2}: use document.export for other formats a Mentés ezt írja: {_1}, ezt nem: {_2}; más formátumhoz használd a document.export parancsot
@msg saved panel layout is invalid; reset the workspace first a mentett panelelrendezés érvénytelen; előbb állítsd vissza a munkaterületet
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3804,6 +3804,8 @@ axis Both Entrambi
@msg recovery copy `{file}`: {e} copia di recupero `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi le risoluzioni devono essere comprese tra 1 e 2400 ppi
@msg result would exceed the canvas (coordinates out of range) il risultato supererebbe l'area di disegno (coordinate fuori intervallo)
@msg objects can't be nested more than 32 deep gli oggetti non possono essere nidificati oltre 32 livelli
@msg the SVG's elements are nested more than {0} deep gli elementi dell'SVG sono nidificati oltre {0} livelli
@msg scale must be a number from {_1} to {_2}, not {_3} la scala deve essere un numero da {_1} a {_2}, non {_3}
@msg scale must be a number from {_1} to {_2}, not {s} la scala deve essere un numero da {_1} a {_2}, non {s}
@msg select (or target) objects whose appearance isn't basic seleziona (o imposta come destinazione) oggetti con un aspetto non di base
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3730,6 +3730,8 @@ theme Appearance 外観
@msg no interaction in progress 進行中の操作がありません
@msg no such object {0} オブジェクト {0} が見つかりません
@msg result would exceed the canvas (coordinates out of range) 結果がカンバスの範囲を超えます (座標が範囲外です)
@msg objects can't be nested more than 32 deep オブジェクトは 32 階層より深く入れ子にできません
@msg the SVG's elements are nested more than {0} deep SVG の要素が {0} 階層より深く入れ子になっています
@msg unknown command `{0}` `{0}` は不明なコマンドです
@msg `{what}` closes nothing `{what}` に対応する開始部分がありません
@msg `{what}` closes something it didn't open `{what}` が対応しない開始部分を閉じています
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3708,6 +3708,8 @@ theme Appearance 외관
@msg no interaction in progress 진행 중인 상호작용이 없습니다
@msg no such object {0} 개체 {0}이(가) 없습니다
@msg result would exceed the canvas (coordinates out of range) 결과가 캔버스를 벗어납니다(좌표 범위 초과)
@msg objects can't be nested more than 32 deep 개체는 32단계보다 깊게 중첩할 수 없습니다
@msg the SVG's elements are nested more than {0} deep SVG 요소가 {0}단계보다 깊게 중첩되어 있습니다
@msg unknown command `{0}` 알 수 없는 명령 `{0}`
@msg `{what}` closes nothing `{what}`이(가) 닫는 항목이 없습니다
@msg `{what}` closes something it didn't open `{what}`이(가) 열지 않은 항목을 닫습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3965,6 +3965,8 @@ imageTrace Outline Kontur
@msg recovery copy `{file}`: {e} kopia odzyskiwania `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi rozdzielczości muszą mieścić się w zakresie od 1 do 2400 ppi
@msg result would exceed the canvas (coordinates out of range) wynik wykroczyłby poza płótno (współrzędne poza zakresem)
@msg objects can't be nested more than 32 deep obiektów nie można zagnieżdżać głębiej niż na 32 poziomy
@msg the SVG's elements are nested more than {0} deep elementy SVG są zagnieżdżone głębiej niż na {0} poziomów
@msg scale must be a number from {_1} to {_2}, not {_3} skala musi być liczbą od {_1} do {_2}, nie {_3}
@msg scale must be a number from {_1} to {_2}, not {s} skala musi być liczbą od {_1} do {_2}, nie {s}
@msg select (or target) objects whose appearance isn't basic zaznacz (lub wskaż) obiekty, których wygląd nie jest podstawowy
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3449,6 +3449,8 @@ axis Both Ambos
@msg recovery copy `{file}`: {e} cópia de recuperação `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi as resoluções devem estar entre 1 e 2400 ppi
@msg result would exceed the canvas (coordinates out of range) o resultado excederia a tela (coordenadas fora do intervalo)
@msg objects can't be nested more than 32 deep os objetos não podem ser aninhados em mais de 32 níveis
@msg the SVG's elements are nested more than {0} deep os elementos do SVG estão aninhados em mais de {0} níveis
@msg scale must be a number from {_1} to {_2}, not {_3} a escala deve ser um número de {_1} a {_2}, não {_3}
@msg scale must be a number from {_1} to {_2}, not {s} a escala deve ser um número de {_1} a {_2}, não {s}
@msg select (or target) objects whose appearance isn't basic selecione (ou direcione) objetos cuja aparência não seja básica
Can't render this file because it contains an unexpected character in line 6 and column 47.
+2
View File
@@ -3800,6 +3800,8 @@ axis Both Оба
@msg recovery copy `{file}`: {e} копия восстановления `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi разрешения должны быть от 1 до 2400 ppi
@msg result would exceed the canvas (coordinates out of range) результат превысил бы холст (координаты вне диапазона)
@msg objects can't be nested more than 32 deep объекты нельзя вкладывать глубже 32 уровней
@msg the SVG's elements are nested more than {0} deep элементы SVG вложены глубже {0} уровней
@msg scale must be a number from {_1} to {_2}, not {_3} масштаб должен быть числом от {_1} до {_2}, а не {_3}
@msg scale must be a number from {_1} to {_2}, not {s} масштаб должен быть числом от {_1} до {_2}, а не {s}
@msg select (or target) objects whose appearance isn't basic выделите (или укажите как объект) объекты, оформление которых не является основным
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -3788,6 +3788,8 @@ axis Both Обидві
@msg recovery copy `{file}`: {e} копія відновлення `{file}`: {e}
@msg resolutions must be between 1 and 2400 ppi дозволи мають бути від 1 до 2400 ppi
@msg result would exceed the canvas (coordinates out of range) результат перевищив би полотно (координати поза діапазоном)
@msg objects can't be nested more than 32 deep об’єкти не можна вкладати глибше ніж на 32 рівні
@msg the SVG's elements are nested more than {0} deep елементи SVG вкладено глибше ніж на {0} рівнів
@msg scale must be a number from {_1} to {_2}, not {_3} масштаб має бути числом від {_1} до {_2}, а не {_3}
@msg scale must be a number from {_1} to {_2}, not {s} масштаб має бути числом від {_1} до {_2}, а не {s}
@msg select (or target) objects whose appearance isn't basic виділіть (або вкажіть як об’єкт) об’єкти, оформлення яких не є основним
Can't render this file because it contains an unexpected character in line 713 and column 40.
+2
View File
@@ -4071,6 +4071,8 @@ theme Appearance Giao diện
@msg resolutions must be between 1 and 2400 ppi độ phân giải phải nằm trong khoảng 1 đến 2400 ppi
@msg Restored {_1} recovered document(s): save them to keep them Đã phục hồi {_1} tài liệu: hãy lưu để giữ lại
@msg result would exceed the canvas (coordinates out of range) kết quả sẽ vượt quá vùng vẽ (tọa độ ngoài phạm vi)
@msg objects can't be nested more than 32 deep không thể lồng các đối tượng sâu quá 32 cấp
@msg the SVG's elements are nested more than {0} deep các phần tử SVG lồng nhau sâu quá {0} cấp
@msg Save for Web: the image is empty Lưu cho Web: ảnh trống
@msg Save writes {_1}, not {_2}: use document.export for other formats Lưu ghi {_1}, không phải {_2}: dùng document.export cho các định dạng khác
@msg saved panel layout is invalid; reset the workspace first bố cục bảng đã lưu không hợp lệ; hãy đặt lại không gian làm việc trước
Can't render this file because it contains an unexpected character in line 3642 and column 10.