Expose renderer-neutral SVG preflight

This commit is contained in:
Dylan M. Taylor committed 2026-11-13 00:13:31 -05:00
1 parent dc20b58f01
commit 3afade5163
3 files changed
+109 -17

No files matched your search

+1
View File
@@ -20,6 +20,7 @@
pub mod color;
pub mod guard;
pub mod paths;
pub mod preflight;
pub mod sniff;
pub mod svgz;
#[cfg(feature = "tree")]
+91
View File
@@ -0,0 +1,91 @@
//! Decode and validate SVG/SVGZ text before a caller hands it to a renderer.
//!
//! This boundary has no dependency on an SVG renderer. It decodes the file's text encoding and
//! bounded gzip container, then checks entity expansion, attribute work, XML structure, node
//! count and nesting depth.
use std::borrow::Cow;
/// Limits for decoding and validating one SVG document.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Limits {
/// Bytes of source text, also the largest unpacked SVGZ.
pub bytes: usize,
/// Deepest XML element nesting (the root alone is 1).
pub depth: usize,
/// Total bytes entities may expand to.
pub entity_bytes: usize,
/// Refuse a doctype entity containing markup.
pub refuse_entity_markup: bool,
/// Attributes on one element.
pub attributes: usize,
/// Sum of squared attribute counts across the document.
pub attribute_work: usize,
/// Maximum XML nodes after entity expansion.
pub xml_nodes: u32,
}
impl Default for Limits {
fn default() -> Self {
Limits {
bytes: 64 << 20,
depth: 256,
entity_bytes: crate::guard::MAX_ENTITY_EXPANSION,
refuse_entity_markup: false,
attributes: crate::guard::MAX_ATTRIBUTES,
attribute_work: crate::guard::MAX_ATTRIBUTE_WORK,
xml_nodes: 4_000_000,
}
}
}
/// Why an SVG's text or XML could not be read within `Limits`.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Error {
/// The bytes are too large, corrupt SVGZ, nested SVGZ, or invalid text encoding.
#[error(transparent)]
Text(#[from] crate::svgz::Error),
/// Entity references expand past the configured limit.
#[error("the SVG's entities expand to too much text")]
Entities,
/// A doctype entity contains markup and is refused by the caller's policy.
#[error("the SVG's document type declares an entity holding markup, which isn't supported")]
EntityMarkup,
/// XML element nesting exceeds the configured limit.
#[error("the SVG's elements are nested more than {0} levels deep")]
TooDeep(usize),
/// Attribute count or total attribute work exceeds the configured limit.
#[error("an element of the SVG has more than {0} attributes, or its elements have too many in all")]
TooWide(usize),
/// The XML is malformed or exceeds the node limit.
#[error("invalid SVG XML: {0}")]
Xml(String),
}
/// Decodes SVG or SVGZ text and checks the XML against `limits`.
pub fn text<'a>(bytes: &'a [u8], limits: &Limits) -> Result<Cow<'a, str>, Error> {
let text = crate::svgz::text(bytes, limits.bytes)?;
parse_text(&text, limits)?;
Ok(text)
}
/// Parses already-decoded SVG text and checks it against `limits`.
pub fn parse_text<'a>(svg: &'a str, limits: &Limits) -> Result<roxmltree::Document<'a>, Error> {
if svg.len() > limits.bytes {
return Err(Error::Text(crate::svgz::Error::TooLarge(limits.bytes)));
}
if limits.refuse_entity_markup && crate::guard::entity_holds_markup(svg) {
return Err(Error::EntityMarkup);
}
if crate::guard::entities_expand_past(svg, limits.entity_bytes) {
return Err(Error::Entities);
}
if crate::guard::too_wide(svg, limits.attributes, limits.attribute_work) {
return Err(Error::TooWide(limits.attributes));
}
if crate::guard::deeper_than(svg, limits.depth) {
return Err(Error::TooDeep(limits.depth));
}
let options = roxmltree::ParsingOptions { allow_dtd: true, nodes_limit: limits.xml_nodes, ..Default::default() };
roxmltree::Document::parse_with_options(svg, options).map_err(|e| Error::Xml(e.to_string()))
}
+17 -17
View File
@@ -125,23 +125,23 @@ pub fn parse_text(svg: &str, limits: &Limits, options: &usvg::Options<'_>) -> Re
/// Every check on SVG text, cheapest first; the XML it parses to on the way.
fn checked<'s>(svg: &'s str, limits: &Limits) -> Result<roxmltree::Document<'s>, Error> {
if svg.len() > limits.bytes {
return Err(Error::Text(svgz::Error::TooLarge(limits.bytes)));
}
if limits.refuse_entity_markup && guard::entity_holds_markup(svg) {
return Err(Error::EntityMarkup);
}
if guard::entities_expand_past(svg, limits.entity_bytes) {
return Err(Error::Entities);
}
if guard::too_wide(svg, limits.attributes, limits.attribute_work) {
return Err(Error::TooWide(limits.attributes));
}
if guard::deeper_than(svg, limits.depth) {
return Err(Error::TooDeep(limits.depth));
}
let options = roxmltree::ParsingOptions { allow_dtd: true, nodes_limit: limits.xml_nodes, ..Default::default() };
let xml = roxmltree::Document::parse_with_options(svg, options).map_err(|e| Error::Xml(e.to_string()))?;
let preflight_limits = crate::preflight::Limits {
bytes: limits.bytes,
depth: limits.depth,
entity_bytes: limits.entity_bytes,
refuse_entity_markup: limits.refuse_entity_markup,
attributes: limits.attributes,
attribute_work: limits.attribute_work,
xml_nodes: limits.xml_nodes,
};
let xml = crate::preflight::parse_text(svg, &preflight_limits).map_err(|e| match e {
crate::preflight::Error::Text(e) => Error::Text(e),
crate::preflight::Error::Entities => Error::Entities,
crate::preflight::Error::EntityMarkup => Error::EntityMarkup,
crate::preflight::Error::TooDeep(depth) => Error::TooDeep(depth),
crate::preflight::Error::TooWide(attributes) => Error::TooWide(attributes),
crate::preflight::Error::Xml(msg) => Error::Xml(msg),
})?;
if height(&xml, limits.depth)?.is_none() {
return Err(Error::TooDeep(limits.depth));
}