svg: allocate a CSS block only for accepted selectors; charge class tokens (#2140)

This commit is contained in:
Barzabel
2026-10-22 21:24:33 +03:00
parent ee13586814
commit 210af24abf
2 changed files with 41 additions and 4 deletions
+15 -4
View File
@@ -483,13 +483,14 @@ fn parse_css(css: &str, sheet: &mut StyleSheet) -> Result<(), Error> {
if sel.starts_with('@') {
continue;
}
let decls = parse_decls(body);
let mut decls = parse_decls(body);
sheet.decls = sheet.decls.saturating_add(decls.len());
if sheet.decls > MAX_CSS_DECLARATIONS {
return Err(Error::Xml(format!("its <style> sheets hold more than {MAX_CSS_DECLARATIONS} declarations")));
}
let block = sheet.blocks.len();
sheet.blocks.push(decls);
// Stored with the rule's first accepted selector: a rule of unsupported selectors only
// (`a b{}`, repeated) adds no block.
let mut block = None;
for one in sel.split(',') {
let one = one.trim();
// Only simple selectors (no combinators / pseudo-classes / attributes).
@@ -499,6 +500,11 @@ fn parse_css(css: &str, sheet: &mut StyleSheet) -> Result<(), Error> {
if sheet.rules.len() >= MAX_CSS_SELECTORS {
return Err(Error::Xml(format!("its <style> sheets hold more than {MAX_CSS_SELECTORS} selectors")));
}
let block = *block.get_or_insert_with(|| {
let i = sheet.blocks.len();
sheet.blocks.push(std::mem::take(&mut decls));
i
});
let mut tag = None;
let mut classes = vec![];
let mut id = None;
@@ -611,8 +617,13 @@ impl<'a, 'i> Parser<'a, 'i> {
if css.rules.is_empty() {
return;
}
// Splitting and sorting the class list is work too: charge its length first.
let class = n.attribute("class").unwrap_or_default();
if !self.charge_css(class.len()) {
return;
}
let tag = n.tag_name().name();
let mut classes: Vec<&str> = n.attribute("class").map(|c| c.split_whitespace().collect()).unwrap_or_default();
let mut classes: Vec<&str> = class.split_whitespace().collect();
classes.sort_unstable();
classes.dedup();
let id = n.attribute("id");
+26
View File
@@ -334,6 +334,32 @@ fn a_comma_list_stores_its_declarations_once() {
assert_eq!(sheet.decls, 50);
}
#[test]
fn rules_of_unsupported_selectors_store_no_blocks() {
let mut sheet = StyleSheet::default();
parse_css(&"a b{}".repeat(100_000), &mut sheet).unwrap();
parse_css("a b{fill:red} a>b, rect{fill:blue} c:hover, d{fill:red}", &mut sheet).unwrap();
assert_eq!(sheet.rules.len(), 2);
assert_eq!(sheet.blocks.len(), 2);
assert!(sheet.rules.iter().all(|r| sheet.blocks.get(r.block).is_some_and(|b| b.len() == 1)));
}
#[test]
fn huge_class_lists_count_against_the_matching_budget() {
// One rule, but a 1 MB class attribute re-split on each of 100 `<use>` copies (#2140).
let class = "a ".repeat(500_000);
let uses: String = (0..100).map(|i| format!(r##"<use href="#r" x="{i}"/>"##)).collect();
let svg = |uses: &str| {
format!(
r##"<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><style>.z{{fill:red}}</style><defs><rect id="r" width="1" height="1" class="{class}"/></defs>{uses}</svg>"##
)
};
let e = parse(svg(&uses).as_bytes()).unwrap_err();
assert!(e.to_string().contains("<style> rules"), "{e}");
let few: String = uses.split_inclusive("/>").take(10).collect();
assert!(parse(svg(&few).as_bytes()).is_ok());
}
#[test]
fn matching_work_past_the_budget_errors_without_doing_it() {
// Within both sheet caps, yet one rectangle would apply 10 000 x 5 001 declarations.