Merge pull request #3676 from odgrim/fix/3562-svg-nested-image-depth

SVG import: images in images share the depth cap, SVGZ images one unpacking cap (#3562, #3563)
This commit is contained in:
BFlat
2026-10-23 01:42:22 +07:00
committed by GitHub
7 changed files with 319 additions and 69 deletions
+24 -3
View File
@@ -3353,19 +3353,40 @@ fn deeply_nested_svg_is_refused_or_merged() {
"<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());
// SVG images in SVG images, read on the same stack: each too deep, of a type that is read
// as SVG too (`text/plain`), or each within the cap but not all together (#3562). What is
// past the cap is left out.
let images = [nested_images(3, 1000), nested_images(12, most - 4)];
for (i, file) in images.iter().enumerate() {
let (_, warnings) = open_deep("images.svg", file.as_bytes()).expect("opens");
assert!(warnings.iter().any(|w| w.contains("SVG images were left out")), "{i}: {warnings:?}");
}
// 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())});
for svg in [nested_svg(40), nested_svg(2 * most)].iter().chain(&images) {
let p = json!({"name": "deep.svg", "dataBase64": vectorcraft_format::base64_encode(svg.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("clipboard.importSvg", &json!({"svg": svg})));
let _ = no_panic(s.execute("edit.paste", &json!({})));
assert!(!s.doc().unwrap().doc.too_deep(None));
}
});
}
/// An SVG showing a chain of `levels` SVG images from `text/plain` data URLs, each `n` groups
/// deep around the next.
fn nested_images(levels: usize, n: usize) -> String {
let image = |svg: &str| {
format!("<image href=\"data:text/plain;base64,{}\" width=\"9\" height=\"9\"/>", vectorcraft_format::base64_encode(svg.as_bytes()))
};
let mut svg = nested_svg(0);
for _ in 0..levels {
svg = format!("<svg {SVG_NS} width=\"9\" height=\"9\">{}{}{}</svg>", "<g>".repeat(n), image(&svg), "</g>".repeat(n));
}
format!("<svg {SVG_NS} width=\"9\" height=\"9\">{}</svg>", image(&svg))
}
/// 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]")
+6 -3
View File
@@ -47,9 +47,9 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
return Err(too_deep());
}
let xml = parse(svg)?;
if depth::too_deep(&xml) {
let Some(height) = depth::height(&xml, depth::MAX_ELEMENT_DEPTH) else {
return Err(too_deep());
}
};
let (svg, found) = prepass(svg, &scan.markers, &xml, opts);
// The prepass's edits are in the text: the XML is read again only when it made any.
let xml = match &svg {
@@ -66,7 +66,7 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
let opt = usvg::Options {
dpi: units.dpi as f32,
font_size: DEFAULT_FONT_SIZE as f32,
image_href_resolver: files.resolver(),
image_href_resolver: files.resolver(height),
..usvg::Options::default()
};
usvg::Tree::from_str(&src, &opt).map_err(|e| SvgError::Parse(e.to_string()))?
@@ -100,6 +100,9 @@ pub(crate) fn import(svg: &str, opts: &ImportOptions) -> Result<(Document, Vec<S
if im.files.nested_text() {
im.warn("text inside an SVG image isn't imported".into());
}
for note in im.files.left_out() {
im.warn(note);
}
// usvg wraps everything in an id-less group carrying the viewBox transform when needed.
let mut top = tree.root();
+35 -11
View File
@@ -7,6 +7,10 @@
//! 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.
//!
//! An SVG image (`<image>` of SVG data or an SVG file) is read while the SVG showing it is, on
//! the same stack: the depths of the SVGs around it are added to its own, and an image past the
//! cap is left out (`files`).
use std::borrow::Cow;
use std::collections::HashMap;
@@ -88,10 +92,21 @@ pub(crate) fn xml_too_deep(svg: &str) -> bool {
prescan(svg).too_deep
}
/// The one pass: 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. Past the cap the pass stops, and the markers are the whole text.
/// Do the elements of the XML text `svg` nest deeper than `most`? See [`prescan_with`].
pub(super) fn xml_deeper_than(svg: &str, most: usize) -> bool {
prescan_with(svg, most).too_deep
}
/// [`prescan_with`] against [`MAX_ELEMENT_DEPTH`].
pub(crate) fn prescan(svg: &str) -> Prescan<'_> {
prescan_with(svg, MAX_ELEMENT_DEPTH)
}
/// The one pass, with elements allowed to nest `most` deep: 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. Past the cap the pass stops, and
/// the markers are the whole text.
fn prescan_with(svg: &str, most: usize) -> Prescan<'_> {
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());
@@ -158,7 +173,7 @@ pub(crate) fn prescan(svg: &str) -> Prescan<'_> {
depth.0 += usize::from(!empty);
end
};
if elements.1 + 10 * entities.1 > MAX_ELEMENT_DEPTH {
if elements.1 + 10 * entities.1 > most {
return Prescan { too_deep: true, markers: Cow::Borrowed(svg) };
}
}
@@ -178,9 +193,9 @@ pub(crate) fn prescan(svg: &str) -> Prescan<'_> {
Prescan { too_deep: false, markers }
}
/// Does anything in `xml` nest deeper than [`MAX_ELEMENT_DEPTH`], references followed? Without
/// recursion.
pub(super) fn too_deep(xml: &roxmltree::Document) -> bool {
/// How deep the elements of `xml` nest, references followed (the root alone is 1); `None` past
/// `most`. Without recursion.
pub(super) fn height(xml: &roxmltree::Document, most: usize) -> Option<usize> {
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);
@@ -210,14 +225,14 @@ pub(super) fn too_deep(xml: &roxmltree::Document) -> bool {
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;
if height > most {
return None;
}
heights.insert(n.id(), height);
}
}
}
false
heights.get(&xml.root_element().id()).copied()
}
#[cfg(test)]
@@ -225,7 +240,7 @@ mod tests {
use super::*;
fn deep(svg: &str) -> bool {
too_deep(&roxmltree::Document::parse(svg).unwrap())
height(&roxmltree::Document::parse(svg).unwrap(), MAX_ELEMENT_DEPTH).is_none()
}
fn svg(body: &str) -> String {
@@ -246,6 +261,15 @@ mod tests {
assert!(xml_too_deep(&svg(&groups(200_000))), "any depth is measured");
}
#[test]
fn what_is_left_of_the_cap_is_measured_too() {
let ten = svg(&groups(8));
assert!(!xml_deeper_than(&ten, 10) && xml_deeper_than(&ten, 9));
let xml = roxmltree::Document::parse(&ten).unwrap();
assert_eq!((height(&xml, 10), height(&xml, 9)), (Some(10), None));
assert_eq!(height(&roxmltree::Document::parse("<svg/>").unwrap(), 1), Some(1));
}
#[test]
fn the_text_is_measured_as_xml_reads_it() {
let many = r#"<g a='>' b="<g>"><rect/></g>"#.repeat(500);
+176 -43
View File
@@ -7,13 +7,13 @@
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use usvg::{ImageHrefResolver, ImageKind, roxmltree};
use vectorcraft_doc::LinkInfo;
use super::{Edits, href};
use crate::ImportOptions;
use super::{Edits, depth, href};
use crate::{ImportOptions, MAX_ELEMENT_DEPTH, MAX_SVGZ, SvgError};
/// The files an SVG's images link to.
#[derive(Default)]
@@ -24,13 +24,28 @@ pub(in crate::import) struct Files {
links: HashMap<usize, Linked>,
/// An SVG image had text (which isn't imported).
text: AtomicBool,
/// How deep the SVGs around the SVG image being read nest, added up: usvg reads an SVG image
/// while it reads the SVG showing it, on the same stack, so one cap ([`MAX_ELEMENT_DEPTH`])
/// holds for them all.
depth: AtomicUsize,
/// The bytes the SVGZ images (data and files) unpacked to so far, whether or not they
/// decoded: together they unpack to at most what one SVGZ file may ([`MAX_SVGZ`]), however
/// they nest.
unpacked: AtomicU64,
/// An SVG image was left out for nesting too deep in the SVGs around it.
too_deep: AtomicBool,
/// An SVGZ image was left out for unpacking past what the SVGZ images may.
too_big: AtomicBool,
}
/// How usvg holds the bytes of a raster of one format (`ImageKind::PNG`…).
type RasterKind = fn(Arc<Vec<u8>>) -> ImageKind;
enum Source {
/// A raster file of the kind usvg reads it as.
Raster(fn(Arc<Vec<u8>>) -> ImageKind, Arc<Vec<u8>>),
/// An SVG file, parsed by usvg as an image.
Svg(Arc<Vec<u8>>),
Raster(RasterKind, Arc<Vec<u8>>),
/// An SVG file's text (unpacked when it is SVGZ), parsed by usvg as an image.
Svg(String),
}
/// A linked raster image (or the placeholder of a missing one).
@@ -150,24 +165,18 @@ impl Files {
/// What usvg gets for a file read (`bytes`, linked by `link`): a raster usvg reads (others are
/// stored as PNG) or an SVG; `None` for anything else.
fn source(&mut self, bytes: Vec<u8>, link: LinkInfo) -> Option<Source> {
use image::ImageFormat as F;
let Ok(format) = image::guess_format(&bytes) else {
let svg = crate::text_of(&bytes).is_ok_and(|t| t.contains("<svg"));
return svg.then(|| Source::Svg(Arc::new(bytes)));
// Unpacked once, here, within what the SVGZ images may unpack to.
let text = crate::text_within(&bytes, &self.unpacked).ok()?;
return text.contains("<svg").then(|| Source::Svg(text.into_owned()));
};
let kind = match format {
F::Png => ImageKind::PNG,
F::Jpeg => ImageKind::JPEG,
F::Gif => ImageKind::GIF,
F::WebP => ImageKind::WEBP,
_ => {
let img = image::load_from_memory_with_format(&bytes, format).ok()?;
let mut png = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png), F::Png).ok()?;
let png = Arc::new(png);
self.links.insert(key(&png), Linked { link, missing: false });
return Some(Source::Raster(ImageKind::PNG, png));
}
let Some(kind) = raster(format) else {
let img = image::load_from_memory_with_format(&bytes, format).ok()?;
let mut png = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).ok()?;
let png = Arc::new(png);
self.links.insert(key(&png), Linked { link, missing: false });
return Some(Source::Raster(ImageKind::PNG, png));
};
let bytes = Arc::new(bytes);
self.links.insert(key(&bytes), Linked { link, missing: false });
@@ -192,41 +201,103 @@ impl Files {
self.text.load(Ordering::Relaxed)
}
fn note_text(&self, svg: &[u8]) {
if svg.windows(5).any(|w| w == b"<text") {
/// The warnings of the SVG images left out.
pub(in crate::import) fn left_out(&self) -> Vec<String> {
let mut notes = vec![];
if self.too_deep.load(Ordering::Relaxed) {
notes.push(format!("SVG images were left out: with the SVGs around them, their elements nest more than {MAX_ELEMENT_DEPTH} deep"));
}
if self.too_big.load(Ordering::Relaxed) {
notes.push(format!("SVGZ images were left out: together they unpack to more than {} MB", MAX_SVGZ >> 20));
}
notes
}
fn note_text(&self, svg: &str) {
if svg.contains("<text") {
self.text.store(true, Ordering::Relaxed);
}
}
/// usvg's image resolver: data URLs as usvg reads them, other hrefs only from the files read.
pub(in crate::import) fn resolver(&self) -> ImageHrefResolver<'_> {
let data = ImageHrefResolver::default_data_resolver();
/// usvg's image resolver, for an SVG whose elements nest `height` deep: data URLs as usvg
/// reads them, other hrefs only from the files read. SVG images are read here
/// ([`Files::svg_data`], [`Files::svg`]), never by usvg's own resolver, which caps neither how
/// deep they nest nor what they unpack to.
pub(in crate::import) fn resolver(&self, height: usize) -> ImageHrefResolver<'_> {
self.depth.store(height, Ordering::Relaxed);
ImageHrefResolver {
// By type as usvg reads them; one without a type (`text/plain`) by its bytes: a
// raster usvg reads, else SVG.
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)
let kind = match mime {
"image/jpg" | "image/jpeg" => ImageKind::JPEG,
"image/png" => ImageKind::PNG,
"image/gif" => ImageKind::GIF,
"image/webp" => ImageKind::WEBP,
"image/svg+xml" => return self.svg_data(&bytes, opts),
"text/plain" => match image::guess_format(&bytes).ok().and_then(raster) {
Some(kind) => kind,
None => return self.svg_data(&bytes, opts),
},
_ => return None,
};
Some(kind(bytes))
}),
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)
}
Source::Svg(text) => self.svg(text, opts),
}),
}
}
/// The SVG image of data `bytes` (SVG or SVGZ), as [`Files::svg`] reads it. Unpacked here,
/// within what the SVGZ images have left, so usvg unpacks nothing itself; past it, `None`,
/// noted.
fn svg_data(&self, bytes: &[u8], opts: &usvg::Options) -> Option<ImageKind> {
match crate::text_within(bytes, &self.unpacked) {
Ok(text) => self.svg(&text, opts),
Err(e) => leave_out(&self.too_big, matches!(e, SvgError::TooBig(_))),
}
}
/// The SVG image `text` as usvg reads it with `opts`. `None`, the image left out as one usvg
/// can't read, when it isn't SVG or when its elements nest deeper than the SVGs around it
/// leave: usvg reads it while it reads them, on the same stack.
fn svg(&self, text: &str, opts: &usvg::Options) -> Option<ImageKind> {
// Measured as the SVG holding it is (`import`): the text before the XML reader.
let used = self.depth.load(Ordering::Relaxed);
let left = MAX_ELEMENT_DEPTH.saturating_sub(used);
if depth::xml_deeper_than(text, left) {
return leave_out(&self.too_deep, true);
}
let height = depth::height(&super::parse(text).ok()?, left);
let Some(height) = height else { return leave_out(&self.too_deep, true) };
self.note_text(text);
self.depth.store(used + height, Ordering::Relaxed);
let tree = usvg::Tree::from_data_nested(text.as_bytes(), opts);
self.depth.store(used, Ordering::Relaxed);
tree.ok().map(ImageKind::SVG)
}
}
/// 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))
/// How usvg holds a raster of `format`, for the formats it reads.
fn raster(format: image::ImageFormat) -> Option<RasterKind> {
use image::ImageFormat as F;
match format {
F::Png => Some(ImageKind::PNG),
F::Jpeg => Some(ImageKind::JPEG),
F::Gif => Some(ImageKind::GIF),
F::WebP => Some(ImageKind::WEBP),
_ => None,
}
}
/// No image: left out, noted in `why` when `noted`.
fn leave_out(why: &AtomicBool, noted: bool) -> Option<ImageKind> {
if noted {
why.store(true, Ordering::Relaxed);
}
None
}
#[cfg(test)]
@@ -305,6 +376,68 @@ mod tests {
assert_eq!(percent_decoded("%E2%82%AC"), "€");
}
/// SVGZ images unpack, all together, to at most what one SVGZ file may: past it, an image is
/// left out and noted, where usvg unpacked it without a cap (#3563). Text that isn't packed
/// isn't counted, and a broken SVGZ image is left out as one usvg can't read.
#[test]
fn svgz_images_share_the_unpacking_cap() {
let inner = r#"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><rect width="1" height="1"/></svg>"#;
let (gz, plain) = (Arc::new(crate::compress(inner)), Arc::new(inner.as_bytes().to_vec()));
let files = Files::default();
let resolver = files.resolver(2);
let opts = usvg::Options::default();
let read = |mime: &str, bytes: &Arc<Vec<u8>>| matches!((resolver.resolve_data)(mime, bytes.clone(), &opts), Some(ImageKind::SVG(_)));
assert!(read("image/svg+xml", &gz) && read("text/plain", &gz) && read("image/svg+xml", &plain));
assert_eq!(files.unpacked.load(Ordering::Relaxed), 2 * inner.len() as u64);
assert!(!read("image/svg+xml", &Arc::new(vec![0x1f, 0x8b, 8, 0, 1, 2])));
assert!(files.left_out().is_empty(), "broken, not too big");
// What they unpack to is counted, not the text it decodes to (UTF-16 is twice as long),
// and counted when it doesn't decode.
let counted = |bytes: &[u8], svg: bool| {
let before = files.unpacked.load(Ordering::Relaxed);
assert_eq!(read("image/svg+xml", &Arc::new(crate::compress_bytes(bytes))), svg);
files.unpacked.load(Ordering::Relaxed) - before
};
let utf16: Vec<u8> = [0xff, 0xfe].into_iter().chain(inner.encode_utf16().flat_map(u16::to_le_bytes)).collect();
assert_eq!(counted(&utf16, true), utf16.len() as u64);
assert_eq!(counted(&[0xff; 100], false), 100);
files.unpacked.store(MAX_SVGZ - inner.len() as u64 + 1, Ordering::Relaxed);
assert!(!read("image/svg+xml", &gz) && !read("text/plain", &gz));
assert!(read("image/svg+xml", &plain), "not packed");
assert_eq!(files.left_out(), [format!("SVGZ images were left out: together they unpack to more than {} MB", MAX_SVGZ >> 20)]);
}
/// A linked SVGZ file is unpacked once, when it is read, within the same cap.
#[test]
fn a_linked_svgz_file_unpacks_once() {
let inner = r#"<svg xmlns="http://www.w3.org/2000/svg" width="2" height="2"><rect width="1" height="1"/></svg>"#;
let mut files = Files::default();
let source = files.source(crate::compress(inner), LinkInfo::new("a.svgz")).unwrap();
files.sources.insert("a.svgz".into(), source);
assert_eq!(files.unpacked.load(Ordering::Relaxed), inner.len() as u64);
let resolver = files.resolver(2);
assert!(matches!((resolver.resolve_string)("a.svgz", &usvg::Options::default()), Some(ImageKind::SVG(_))));
assert_eq!(files.unpacked.load(Ordering::Relaxed), inner.len() as u64);
drop(resolver);
files.unpacked.store(MAX_SVGZ, Ordering::Relaxed);
assert!(files.source(crate::compress(inner), LinkInfo::new("b.svgz")).is_none(), "past the cap");
}
/// An SVG image gets what the SVGs around it leave of the depth cap.
#[test]
fn svg_images_get_what_is_left_of_the_depth_cap() {
let svg = |n: usize| format!(r#"<svg xmlns="http://www.w3.org/2000/svg">{}{}</svg>"#, "<g>".repeat(n), "</g>".repeat(n));
let read = |around: usize, n: usize| {
let files = Files::default();
let resolver = files.resolver(around);
let kind = (resolver.resolve_data)("image/svg+xml", Arc::new(svg(n).into_bytes()), &usvg::Options::default());
(matches!(kind, Some(ImageKind::SVG(_))), files.left_out().len())
};
assert_eq!(read(1, MAX_ELEMENT_DEPTH - 2), (true, 0));
assert_eq!(read(2, MAX_ELEMENT_DEPTH - 2), (false, 1));
assert_eq!(read(MAX_ELEMENT_DEPTH, 0), (false, 1));
}
#[test]
fn the_placeholder_is_a_png() {
let png = marker().unwrap();
+18 -7
View File
@@ -328,6 +328,9 @@ pub enum SvgError {
/// Its elements nest (references followed) deeper than the reader goes.
#[error("the SVG's elements are nested more than {0} deep")]
TooDeep(usize),
/// SVGZ data unpacks to more than the reader takes (a guard against decompression bombs).
#[error("SVG parse error: {0}")]
TooBig(String),
}
/// Export a document as an SVG string (linked images are not returned: see [`export_full`]).
@@ -435,25 +438,33 @@ pub fn compress_bytes(svg: &[u8]) -> Vec<u8> {
}
/// Most bytes an SVGZ file may unpack to (a guard against decompression bombs).
const MAX_SVGZ: u64 = 512 << 20;
pub(crate) const MAX_SVGZ: u64 = 512 << 20;
/// SVG text from SVG or SVGZ bytes: UTF-8, UTF-16 (after a byte order mark) or, when the XML
/// declaration says so, ISO 8859-1.
pub fn text_of(bytes: &[u8]) -> Result<std::borrow::Cow<'_, str>, SvgError> {
text_within(bytes, &std::sync::atomic::AtomicU64::new(0))
}
/// [`text_of`], SVGZ bytes unpacking within what `unpacked`, the bytes SVGZ data unpacked to so
/// far, leaves of [`MAX_SVGZ`]. What they unpack to is added to it, whether or not it decodes.
pub(crate) fn text_within<'a>(bytes: &'a [u8], unpacked: &std::sync::atomic::AtomicU64) -> Result<std::borrow::Cow<'a, str>, SvgError> {
use std::io::Read as _;
use std::sync::atomic::Ordering;
if !is_svgz(bytes) {
return match decode(bytes)? {
Decoded::Utf8(skip) => std::str::from_utf8(bytes.get(skip..).unwrap_or_default()).map(Into::into).map_err(|_| not_utf8()),
Decoded::Text(t) => Ok(t.into()),
};
}
let most = MAX_SVGZ.saturating_sub(unpacked.load(Ordering::Relaxed));
let mut raw = Vec::new();
flate2::read::GzDecoder::new(bytes)
.take(MAX_SVGZ + 1)
.read_to_end(&mut raw)
.map_err(|e| SvgError::Parse(format!("not a valid SVGZ file: {e}")))?;
if raw.len() as u64 > MAX_SVGZ {
return Err(SvgError::Parse(format!("the SVGZ file unpacks to more than {} MB", MAX_SVGZ >> 20)));
let read = flate2::read::GzDecoder::new(bytes).take(most.saturating_add(1)).read_to_end(&mut raw);
// Read before a failure is kept: it was unpacked too.
unpacked.fetch_add(raw.len() as u64, Ordering::Relaxed);
read.map_err(|e| SvgError::Parse(format!("not a valid SVGZ file: {e}")))?;
if raw.len() as u64 > most {
return Err(SvgError::TooBig(format!("the SVGZ file unpacks to more than {} MB", most >> 20)));
}
match decode(&raw)? {
Decoded::Utf8(skip) => {
+53 -1
View File
@@ -126,7 +126,7 @@ fn svg_images_import_as_vector_art() {
assert!((boxes[1].x0 - 100.0).abs() < 1e-6 && (boxes[1].height() - 20.0).abs() < 1e-6, "{boxes:?}");
}
/// usvg unpacks gzipped SVG images itself (its `svgz` feature, which the workspace keeps).
/// Gzipped SVG images are unpacked before usvg reads them, within the cap of an SVGZ file.
#[test]
fn svgz_images_import_as_vector_art() {
let dir = folder("svgzimage");
@@ -147,6 +147,58 @@ fn svgz_images_import_as_vector_art() {
assert!((boxes[1].x0 - 100.0).abs() < 1e-6 && (boxes[1].height() - 20.0).abs() < 1e-6, "{boxes:?}");
}
const SVG_NS: &str = r#"xmlns="http://www.w3.org/2000/svg""#;
/// An image showing `svg`, from a base64 data URL of type `mime` (none when empty).
fn image_of(mime: &str, svg: &str) -> String {
format!(r#"<image href="data:{mime};base64,{}" width="20" height="10"/>"#, vectorcraft_svg::base64_encode(svg.as_bytes()))
}
/// An SVG showing a chain of `levels` SVG images from data URLs of type `mime`: each holds a
/// rectangle in `n` groups in one another, and the next image beside it.
fn nested_images(levels: usize, n: usize, mime: &str) -> String {
let mut inner = String::new();
for _ in 0..levels {
let (open, close) = ("<g>".repeat(n), "</g>".repeat(n));
let svg = format!(r#"<svg {SVG_NS} width="20" height="10">{open}<rect width="10" height="10"/>{inner}{close}</svg>"#);
inner = image_of(mime, &svg);
}
format!(r#"<svg {SVG_NS} width="20" height="10">{inner}</svg>"#)
}
fn rects(d: &Document) -> usize {
art(d).iter().filter(|n| n.path_data().is_some()).count()
}
/// usvg reads an SVG image while it reads the SVG showing it, on the same stack, so images in
/// images share one depth cap, whatever type their data URL has (usvg reads SVG from
/// `image/svg+xml`, `text/plain` and untyped data): what is past it is left out, with a warning,
/// where it overflowed the stack, an abort (#3562).
#[test]
fn svg_images_in_svg_images_share_the_depth_cap() {
let most = vectorcraft_svg::MAX_ELEMENT_DEPTH;
let left_out = |w: &[String]| w.iter().any(|w| w.contains("SVG images were left out"));
for mime in ["image/svg+xml", "text/plain", ""] {
// Within the cap together: all of it.
let (d, w) = import_with_report(&nested_images(3, 20, mime)).unwrap();
assert_eq!(rects(&d), 3, "{mime}");
assert!(!left_out(&w), "{mime}: {w:?}");
// Each image past the cap; each within it, but not together (the file, the image, then
// the groups and the rectangle in each).
for (levels, n, kept) in [(3, 1000, 0), (12, most - 4, 1)] {
let (d, w) = import_with_report(&nested_images(levels, n, mime)).unwrap();
assert_eq!(rects(&d), kept, "{mime}: {levels} × {n}");
assert!(left_out(&w), "{mime}: {levels} × {n}: {w:?}");
}
}
// Shallow XML, a deep tree: a chain of `<use>`, measured as in the file itself.
let links: String = (0..3000).map(|i| format!("<g id='u{i}'><use href='#u{}'/></g>", i + 1)).collect();
let chain = format!(r##"<svg {SVG_NS} width="9" height="9"><defs>{links}<rect id="u3000" width="1" height="1"/></defs><use href="#u0"/></svg>"##);
let (d, w) = import_with_report(&svg(&image_of("image/svg+xml", &chain))).unwrap();
assert_eq!(rects(&d), 0);
assert!(left_out(&w), "{w:?}");
}
#[test]
fn nested_clips_intersect() {
let body = r##"<defs><clipPath id="inner"><rect x="60" y="0" width="200" height="200"/></clipPath>
+7 -1
View File
File diff suppressed because one or more lines are too long