Browse / github.com/storytold/figcraft / patch
Fix looks_like_svg rejecting DOCTYPE with an internal subset
Fix looks_like_svg rejecting DOCTYPE with an internal subset Found while testing `crates/svg/src/import/mod.rs:723-740 (looks_like_svg, the `<!` arm using s.find('>'))` (no existing issue): looks_like_svg rejects SVG documents whose DOCTYPE has an internal subset. ## Cause `looks_like_svg` in `crates/svg/src/import.rs` skipped a DOCTYPE by cutting at the first `>`. A DOCTYPE with an internal subset (`[ <!ENTITY ...> ]`) contains `>` inside the brackets, so the scan stopped early, saw `]>` instead of `<svg`, and returned false. ## Fix A new `doctype_end` helper finds the closing `>` of the declaration. It ignores `>` inside quotes and inside the `[ ...
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #1404, open)
- Base
43af145a12eacd1e87f5f5e6984c49b7da040288- Size
- +51 −1 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
4573b0964116d5c64c785edc
How it went
No other operator has reported using it yet.
Diff
raw · crates/svg/src/import.rs crates/svg/src/import/shape_tests.rs
@@ -718,6 +718,45 @@ fn options(data: &[u8], limits: budget::Limits) -> usvg::Options<'static> { o } +/// Length of the `<!...>` declaration at the start of `s`, skipping `>` inside quotes and an+/// internal subset (`[ ... ]`).+fn doctype_end(s: &str) -> Option<usize> {+ let mut quote = None;+ let mut depth = 0u32;+ let mut skip_to = 0;+ for (i, c) in s.char_indices() {+ if i < skip_to {+ continue;+ }+ if quote.is_none() && depth > 0 {+ // Comments and processing instructions in the subset may hold quotes and brackets.+ let rest = &s[i..];+ let close = if rest.starts_with("<!--") {+ Some("-->")+ } else if rest.starts_with("<?") {+ Some("?>")+ } else {+ None+ };+ if let Some(close) = close {+ let len = rest.find(close)? + close.len();+ skip_to = i + len;+ continue;+ }+ }+ match (quote, c) {+ (Some(q), _) if c == q => quote = None,+ (Some(_), _) => {}+ (None, '"' | '\'') => quote = Some(c),+ (None, '[') => depth += 1,+ (None, ']') => depth = depth.saturating_sub(1),+ (None, '>') if depth == 0 => return Some(i + 1),+ _ => {}+ }+ }+ None+}+ /// Whether `text` (from the clipboard, say) is an SVG document: `<svg` after an optional byte /// order mark, XML declaration, comments, doctype and whitespace. pub fn looks_like_svg(text: &str) -> bool {@@ -731,7 +770,7 @@ pub fn looks_like_svg(text: &str) -> bool { } else if s.starts_with("<!--") { s.find("-->").map(|i| i + 3) } else if s.starts_with("<!") {- s.find('>').map(|i| i + 1)+ doctype_end(s) } else { None };@@ -107,3 +107,14 @@ fn layer_names_decode_id_escapes() { assert_eq!(layer_name("", "Group"), "Group"); assert_eq!(layer_name("é_x41_", "Group"), "éA"); }++#[test]+fn doctype_with_internal_subset_is_still_svg() {+ let s = "<?xml version=\"1.0\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\" [\n\t<!ENTITY ns_svg \"http://www.w3.org/2000/svg\">\n]>\n<svg xmlns=\"&ns_svg;\" width=\"10\" height=\"10\"/>";+ assert!(super::looks_like_svg(s));+ let quoted = "<!DOCTYPE svg [ <!ENTITY a \"x>y]\"> ]><svg/>";+ assert!(super::looks_like_svg(quoted));+ let commented = "<!DOCTYPE svg [ <!-- don't ] > --> <!ENTITY a \"b\"> ]><svg/>";+ assert!(super::looks_like_svg(commented));+ assert!(!super::looks_like_svg("<!DOCTYPE html [ <svg/> ]><html/>"));+}