diff --git a/src/filters/cosmetic.rs b/src/filters/cosmetic.rs index 058ab6db..f8634d03 100644 --- a/src/filters/cosmetic.rs +++ b/src/filters/cosmetic.rs @@ -429,6 +429,7 @@ impl CosmeticFilter { }; let mut translate_abp_syntax = false; + let mut adguard_css_injection = false; // Consume filter options embedded in the `##` marker: let mut between_sharps = &line[after_sharp_index..second_sharp_index]; @@ -445,10 +446,15 @@ impl CosmeticFilter { // `#%#` / `#@%#` return Err(CosmeticFilterError::UnsupportedSyntax); } - if between_sharps.starts_with('$') { - // AdGuard `:style` syntax - not supported for now - // `#$?#` for CSS rules, `#@$?#` — for exceptions - return Err(CosmeticFilterError::UnsupportedSyntax); + if let Some(rest) = between_sharps.strip_prefix('$') { + // AdGuard CSS injection: `#$#selector { ... }` (and the extended + // `#$?#` / exception `#@$#` forms). Only the style/remove form is + // handled here — it always carries a `{ ... }` body, which is + // parsed by `parse_after_sharp_nonscript` below. ABP snippet + // injection also uses `#$#` but has no braces; those produce no + // action and are rejected after the selector parse. + adguard_css_injection = true; + between_sharps = rest; } if between_sharps.starts_with('?') { // ABP/ADG extended CSS syntax: @@ -507,6 +513,12 @@ impl CosmeticFilter { (validated_selector, action) }; + if adguard_css_injection && action.is_none() { + // `#$#` without a `{ ... }` block is ABP snippet injection, which + // is not supported. + return Err(CosmeticFilterError::UnsupportedSyntax); + } + if (not_entities.is_some() || not_hostnames.is_some()) && mask.contains(CosmeticFilterMask::UNHIDE) { diff --git a/tests/unit/filters/cosmetic.rs b/tests/unit/filters/cosmetic.rs index 6061b37a..9eb21430 100644 --- a/tests/unit/filters/cosmetic.rs +++ b/tests/unit/filters/cosmetic.rs @@ -959,6 +959,62 @@ mod parse_tests { assert!(parse_cf("example.com##.ad { background: url(https://evil.com) }").is_err()); } + #[test] + fn adguard_css_injection() { + check_parse_result( + "businesshemden.com#$#.mnd-cookie-modal { display: none !important; }", + CosmeticFilterBreakdown { + hostnames: sort_hash_domains(vec!["businesshemden.com"]), + selector: SelectorType::PlainCss(".mnd-cookie-modal".to_string()), + action: Some(CosmeticFilterAction::Style( + "display: none !important;".into(), + )), + ..Default::default() + }, + ); + check_parse_result( + "makeuseof.com#$#body div.fc-consent-root { display: none !important; }", + CosmeticFilterBreakdown { + hostnames: sort_hash_domains(vec!["makeuseof.com"]), + selector: SelectorType::PlainCss("body div.fc-consent-root".to_string()), + action: Some(CosmeticFilterAction::Style( + "display: none !important;".into(), + )), + ..Default::default() + }, + ); + // `#$#` also accepts the `remove:` directive. + check_parse_result( + "example.com#$#.ad { remove: true; }", + CosmeticFilterBreakdown { + hostnames: sort_hash_domains(vec!["example.com"]), + selector: SelectorType::PlainCss(".ad".to_string()), + action: Some(CosmeticFilterAction::Remove), + ..Default::default() + }, + ); + // ABP snippet injection also uses `#$#` but has no `{ ... }` body — it must + // remain unsupported rather than being misparsed as a plain selector. + assert!(parse_cf("example.com#$#abort-on-property-read alert").is_err()); + // Generic (hostname-less) injection is rejected like any other action. + assert!(parse_cf("#$#.ad { display: none !important; }").is_err()); + } + + #[test] + #[cfg(feature = "css-validation")] + fn adguard_css_injection_extended() { + // `#$?#` is the extended (procedural) AdGuard CSS-injection form. + let rule = + parse_cf("example.com#$?#div:has(>div>span.ad) { display: none !important; }").unwrap(); + assert_eq!( + rule.action, + Some(CosmeticFilterAction::Style( + "display: none !important;".into() + )) + ); + assert_eq!(rule.plain_css_selector(), Some("div:has(>div>span.ad)")); + } + #[test] #[cfg(feature = "css-validation")] fn abp_style_injection_extended() { diff --git a/tests/unit/lists.rs b/tests/unit/lists.rs index b67a26d2..32d22fcf 100644 --- a/tests/unit/lists.rs +++ b/tests/unit/lists.rs @@ -427,25 +427,17 @@ mod tests { )); } { + // AdGuard `#$#` CSS injection is now parsed as a style action. let input = "nczas.com#$#.adsbygoogle { position: absolute!important; left: -3000px!important; }"; let result = parse_filter(input, true, Default::default()); - assert!(matches!( - result, - Err(FilterParseError::Cosmetic( - CosmeticFilterError::UnsupportedSyntax - )) - )); + assert!(matches!(result, Ok(ParsedFilter::Cosmetic(..)))); } { + // AdGuard `#@$#` is the exception form of CSS injection. let input = "kurnik.pl#@$#.adsbygoogle { height: 1px !important; width: 1px !important; }"; let result = parse_filter(input, true, Default::default()); - assert!(matches!( - result, - Err(FilterParseError::Cosmetic( - CosmeticFilterError::UnsupportedSyntax - )) - )); + assert!(matches!(result, Ok(ParsedFilter::Cosmetic(..)))); } } }