diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 5b858b5f78..095d0ec146 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -648,6 +648,8 @@ proptest! { survive(&svg, || vectorcraft_svg::import(&svg).ok())?; } + /// The linked `` gets an id from the importer, so attribute selectors in `sheet` reach the + /// pass that copies style sheet matches into the elements' styles. #[test] fn svg_hostile_css_never_panics( sheet in r#"[a-z#.>*\[\]=:;{}@ !"'/,-]{0,160}"#, @@ -655,7 +657,7 @@ proptest! { body in prop::collection::vec(arb_svg_element(1), 0..3), ) { let svg = format!( - "abc{}", + "abc{}", body.concat() ); survive(&svg, || vectorcraft_svg::import(&svg).ok())?; diff --git a/crates/svg/src/import/prepass.rs b/crates/svg/src/import/prepass.rs index f02f436cdb..3f52c4e35f 100644 --- a/crates/svg/src/import/prepass.rs +++ b/crates/svg/src/import/prepass.rs @@ -268,7 +268,7 @@ pub(super) fn prepass<'s>( let text_edits = Edits { list: edits.list.clone(), styles: edits.styles.clone(), ..Default::default() }; found.text_source = Some(text_edits.apply(svg).into_owned()); } - id_styles::preserve_matches(svg, xml, &mut edits)?; + id_styles::preserve_matches(svg, xml, &mut edits, &mut found.warnings)?; } Ok((edits.apply(svg), found)) } diff --git a/crates/svg/src/import/prepass/id_styles.rs b/crates/svg/src/import/prepass/id_styles.rs index 7804dfa4da..217c56ab9d 100644 --- a/crates/svg/src/import/prepass/id_styles.rs +++ b/crates/svg/src/import/prepass/id_styles.rs @@ -35,11 +35,17 @@ fn append(out: &mut String, declaration: &simplecss::Declaration<'_>, remaining: Ok(()) } -pub(super) fn preserve_matches(svg: &str, xml: &roxmltree::Document, edits: &mut Edits) -> Result<(), SvgError> { - preserve_with_limit(svg, xml, edits, usize::try_from(crate::MAX_SVGZ).unwrap_or(usize::MAX)) +pub(super) fn preserve_matches(svg: &str, xml: &roxmltree::Document, edits: &mut Edits, warnings: &mut Vec) -> Result<(), SvgError> { + preserve_with_limit(svg, xml, edits, warnings, usize::try_from(crate::MAX_SVGZ).unwrap_or(usize::MAX)) } -fn preserve_with_limit(svg: &str, xml: &roxmltree::Document, edits: &mut Edits, mut remaining: usize) -> Result<(), SvgError> { +fn preserve_with_limit( + svg: &str, + xml: &roxmltree::Document, + edits: &mut Edits, + warnings: &mut Vec, + mut remaining: usize, +) -> Result<(), SvgError> { let styles: Vec<_> = xml.descendants().filter(|n| n.has_tag_name("style") && n.attribute("type").is_none_or(|t| t == "text/css")).collect(); let texts: Vec = styles.iter().map(|n| n.children().filter(XNode::is_text).filter_map(|n| n.text()).collect()).collect(); let mut sheet = simplecss::StyleSheet::new(); @@ -47,8 +53,10 @@ fn preserve_with_limit(svg: &str, xml: &roxmltree::Document, edits: &mut Edits, sheet.parse_more(text); } let element_count = xml.descendants().filter(XNode::is_element).count(); + // Too much matching: the file opens without this pass (nothing is edited yet), as it did before it. if element_count.checked_mul(sheet.rules.len()).is_none_or(|work| work > 1_000_000) { - return Err(SvgError::TooBig("SVG stylesheet matching exceeds the import work limit".into())); + warnings.push("the style sheet is too large to match against every element: attribute selectors may not apply to linked objects".into()); + return Ok(()); } for node in xml.descendants().filter(XNode::is_element) { let mut declarations = String::new(); @@ -86,8 +94,8 @@ mod tests { fn expanded_styles_are_bounded() { let svg = r#""#; let xml = roxmltree::Document::parse(svg).unwrap(); - assert!(matches!(preserve_with_limit(svg, &xml, &mut Edits::default(), 1), Err(SvgError::TooBig(_)))); - assert!(preserve_with_limit(svg, &xml, &mut Edits::default(), 64).is_ok()); + assert!(matches!(preserve_with_limit(svg, &xml, &mut Edits::default(), &mut vec![], 1), Err(SvgError::TooBig(_)))); + assert!(preserve_with_limit(svg, &xml, &mut Edits::default(), &mut vec![], 64).is_ok()); } #[test] @@ -96,7 +104,10 @@ mod tests { let elements = "".repeat(1000); let svg = format!(r#"{elements}"#); let xml = roxmltree::Document::parse(&svg).unwrap(); - assert!(matches!(preserve_matches(&svg, &xml, &mut Edits::default()), Err(SvgError::TooBig(_)))); + let (mut edits, mut warnings) = (Edits::default(), vec![]); + assert!(preserve_matches(&svg, &xml, &mut edits, &mut warnings).is_ok(), "the file still opens"); + assert!(edits.list.is_empty() && edits.styles.is_empty(), "nothing is edited"); + assert_eq!(warnings.len(), 1, "{warnings:?}"); } #[test] @@ -104,7 +115,7 @@ mod tests { let svg = r#"x"#; let xml = roxmltree::Document::parse(svg).unwrap(); let mut edits = Edits::default(); - preserve_matches(svg, &xml, &mut edits).unwrap(); + preserve_matches(svg, &xml, &mut edits, &mut vec![]).unwrap(); let output = edits.apply(svg); let xml = roxmltree::Document::parse(&output).unwrap(); let text = xml.descendants().find(|n| n.has_tag_name("text")).unwrap();