musealpha / src-tauri /src /adblock /engine.rs
asdf98's picture
Upload 112 files
3d7d9b5 verified
use adblock::{Engine, FilterSet};
use adblock::lists::ParseOptions;
use adblock::request::Request;
use std::collections::HashSet;
use std::sync::RwLock;
use std::sync::atomic::{AtomicU64, Ordering};
use serde::Serialize;
/// Thread-safe wrapper around adblock Engine
pub struct AdBlockState {
pub engine: RwLock<Engine>,
pub stats: AdBlockStats,
pub allowlist: RwLock<HashSet<String>>,
pub rule_count: AtomicU64,
}
#[derive(Default)]
pub struct AdBlockStats {
pub blocked_requests: AtomicU64,
pub blocked_cosmetic: AtomicU64,
pub https_upgrades: AtomicU64,
}
#[derive(Debug, Clone, Serialize)]
pub struct ShieldReport {
pub blocked_requests: u64,
pub blocked_cosmetic: u64,
pub https_upgrades: u64,
pub engine_rules: usize,
pub allowlisted_domains: usize,
}
impl AdBlockState {
pub fn new() -> Self {
let (engine, count) = build_engine_from_bundled();
Self {
engine: RwLock::new(engine),
stats: AdBlockStats::default(),
allowlist: RwLock::new(HashSet::new()),
rule_count: AtomicU64::new(count as u64),
}
}
pub fn should_block(&self, url: &str, source_url: &str, request_type: &str) -> bool {
if let Some(domain) = extract_domain(source_url) {
if let Ok(allowlist) = self.allowlist.read() {
if allowlist.contains(&domain) {
return false;
}
}
}
let engine = match self.engine.read() {
Ok(e) => e,
Err(_) => return false,
};
match Request::new(url, source_url, request_type) {
Ok(req) => {
let result = engine.check_network_request(&req);
if result.matched && result.exception.is_none() {
self.stats.blocked_requests.fetch_add(1, Ordering::Relaxed);
true
} else {
false
}
}
Err(_) => false,
}
}
pub fn get_cosmetic_css(&self, url: &str) -> String {
let engine = match self.engine.read() {
Ok(e) => e,
Err(_) => return String::new(),
};
let resources = engine.url_cosmetic_resources(url);
if resources.hide_selectors.is_empty() {
return String::new();
}
let selectors: Vec<&String> = resources.hide_selectors.iter().collect();
let css = selectors.iter()
.map(|s| format!("{s}{{display:none!important}}"))
.collect::<Vec<_>>()
.join("\n");
self.stats.blocked_cosmetic.fetch_add(selectors.len() as u64, Ordering::Relaxed);
css
}
/// Get injected scriptlets for a URL (json-prune, set-constant, etc.)
/// Used for YouTube/Twitch/video ad blocking
pub fn get_injected_script(&self, url: &str) -> String {
let engine = match self.engine.read() {
Ok(e) => e,
Err(_) => return String::new(),
};
let resources = engine.url_cosmetic_resources(url);
resources.injected_script
}
pub fn report(&self) -> ShieldReport {
let engine_rules = self.rule_count.load(Ordering::Relaxed) as usize;
let allowlisted = self.allowlist.read().map(|a| a.len()).unwrap_or(0);
ShieldReport {
blocked_requests: self.stats.blocked_requests.load(Ordering::Relaxed),
blocked_cosmetic: self.stats.blocked_cosmetic.load(Ordering::Relaxed),
https_upgrades: self.stats.https_upgrades.load(Ordering::Relaxed),
engine_rules,
allowlisted_domains: allowlisted,
}
}
pub fn reload_engine(&self, new_engine: Engine, rule_count: usize) {
if let Ok(mut engine) = self.engine.write() {
*engine = new_engine;
}
self.rule_count.store(rule_count as u64, Ordering::Relaxed);
}
}
impl Default for AdBlockState {
fn default() -> Self {
Self::new()
}
}
pub fn build_engine_from_bundled() -> (Engine, usize) {
let mut filter_set = FilterSet::new(false);
let lists: &[&str] = &[
include_str!("../../resources/filters/easylist_mini.txt"),
include_str!("../../resources/filters/easyprivacy_mini.txt"),
include_str!("../../resources/filters/annoyances_mini.txt"),
];
let mut count = 0usize;
for list in lists {
let _meta = filter_set.add_filter_list(list, ParseOptions::default());
count += list.lines().filter(|l| !l.starts_with('!') && !l.trim().is_empty()).count();
}
(Engine::from_filter_set(filter_set, true), count)
}
fn extract_domain(url: &str) -> Option<String> {
url.split("//")
.nth(1)
.and_then(|s| s.split('/').next())
.map(|s| s.split(':').next().unwrap_or(s).to_lowercase())
.map(|s| s.trim_start_matches("www.").to_string())
}