//! Python wrapper for Brave's adblocking library, which is written in Rust. #![deny( future_incompatible, nonstandard_style, rust_2018_idioms, missing_copy_implementations, trivial_casts, trivial_numeric_casts, unsafe_code, unused_qualifications, deprecated )] use adblock::blocker::BlockerResult as RustBlockerResult; use adblock::cosmetic_filter_cache::UrlSpecificResources as RustUrlSpecificResources; use adblock::engine::Engine as RustEngine; use adblock::lists::FilterSet as RustFilterSet; use adblock::lists::{FilterFormat, ParseOptions, RuleTypes}; use adblock::request::Request; use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3::types::PyBytes; use pyo3::PyErr; use adblock::resources::{MimeType, PermissionMask, Resource, ResourceType}; use std::collections::HashSet; use std::error::Error; use std::fmt::{self, Display}; use std::fs; use std::io::{Read, Write}; /// Brave's adblocking library in Python! #[pymodule] fn adblock_py(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add("AdblockException", m.py().get_type_bound::())?; m.add("BlockerException", m.py().get_type_bound::())?; m.add("SerializationError", m.py().get_type_bound::())?; m.add( "DeserializationError", m.py().get_type_bound::(), )?; m.add( "OptimizedFilterExistence", m.py().get_type_bound::(), )?; m.add( "BadFilterAddUnsupported", m.py().get_type_bound::(), )?; m.add("FilterExists", m.py().get_type_bound::())?; m.add( "AddResourceException", m.py().get_type_bound::(), )?; m.add( "InvalidBase64ContentError", m.py().get_type_bound::(), )?; m.add( "InvalidUtf8ContentError", m.py().get_type_bound::(), )?; Ok(()) } /// The result of an ad-blocking check. #[pyclass] pub struct BlockerResult { #[pyo3(get)] pub matched: bool, /// Important is used to signal that a rule with the `important` option /// matched. An `important` match means that exceptions should not apply /// and no further checking is neccesary--the request should be blocked /// (empty body or cancelled). /// /// Brave Browser keeps seperate instances of Blocker for default lists /// and regional ones, so `important` here is used to correct behaviour /// between them: checking should stop instead of moving to the next /// instance iff an `important` rule matched. #[pyo3(get)] pub important: bool, /// Iff the blocker matches a rule which has the `redirect` option, as per /// [uBlock Origin's redirect syntax][1], the `redirect` is not `None`. /// The `redirect` field contains the body of the redirect to be injected. /// /// [1]: https://github.com/gorhill/uBlock/wiki/Static-filter-syntax#redirect #[pyo3(get)] pub redirect: Option, /// `removeparam` may remove URL parameters. If the original request URL was /// modified at all, the new version will be here. #[pyo3(get)] pub rewritten_url: Option, /// Exception is not `None` when the blocker matched on an exception rule. /// Effectively this means that there was a match, but the request should /// not be blocked. It is a non-empty string if the blocker was initialized /// from a list of rules with debugging enabled, otherwise the original /// string representation is discarded to reduce memory use. #[pyo3(get)] pub exception: Option, /// Filter--similarly to exception--includes the string representation of /// the rule when there is a match and debugging is enabled. Otherwise, on /// a match, it is not `None`. #[pyo3(get)] pub filter: Option, } impl From for BlockerResult { fn from(br: RustBlockerResult) -> Self { Self { matched: br.matched, important: br.important, redirect: br.redirect, rewritten_url: br.rewritten_url, exception: br.exception, filter: br.filter, } } } #[pymethods] impl BlockerResult { fn __repr__(&self) -> PyResult { Ok(format!( "BlockerResult(matched={}, important={}, redirect={}, rewritten_url={}, exception={}, filter={})", self.matched.diy_python_repr(), self.important.diy_python_repr(), self.redirect.diy_python_repr(), self.rewritten_url.diy_python_repr(), self.exception.diy_python_repr(), self.filter.diy_python_repr(), )) } } #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum BlockerError { SerializationError, DeserializationError, OptimizedFilterExistence, BadFilterAddUnsupported, FilterExists, } impl Error for BlockerError { fn source(&self) -> Option<&(dyn Error + 'static)> { None } } impl Display for BlockerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { Self::SerializationError => "Serialization error", Self::DeserializationError => "Deserialization error", Self::OptimizedFilterExistence => "Optimized filter exists", Self::BadFilterAddUnsupported => "Bad filter add unsupported", Self::FilterExists => "Filter exists", } ) } } create_exception!(adblock, AdblockException, PyException); create_exception!(adblock, BlockerException, AdblockException); create_exception!(adblock, AddResourceException, AdblockException); create_exception!(adblock, InvalidBase64ContentError, AddResourceException); create_exception!(adblock, InvalidUtf8ContentError, AddResourceException); create_exception!(adblock, SerializationError, BlockerException); create_exception!(adblock, DeserializationError, BlockerException); create_exception!(adblock, OptimizedFilterExistence, BlockerException); create_exception!(adblock, BadFilterAddUnsupported, BlockerException); create_exception!(adblock, FilterExists, BlockerException); impl From for PyErr { fn from(err: BlockerError) -> Self { let msg = format!("{:?}", err); match err { BlockerError::SerializationError => Self::new::(msg), BlockerError::DeserializationError => Self::new::(msg), BlockerError::OptimizedFilterExistence => Self::new::(msg), BlockerError::BadFilterAddUnsupported => Self::new::(msg), BlockerError::FilterExists => Self::new::(msg), } } } fn filter_format_from_string(filter_format: &str) -> PyResult { match filter_format { "standard" => Ok(FilterFormat::Standard), "hosts" => Ok(FilterFormat::Hosts), _ => Err(PyErr::new::( "Invalid FilterFormat value", )), } } fn rule_types_from_string(rule_types: &str) -> PyResult { match rule_types { "all" => Ok(RuleTypes::All), "networkonly" => Ok(RuleTypes::NetworkOnly), "cosmeticonly" => Ok(RuleTypes::CosmeticOnly), _ => Err(PyErr::new::("Invalid RuleTypes value")), } } /// Manages a set of rules to be added to an Engine. /// /// To be able to efficiently handle special options like $badfilter, and to /// allow optimizations, all rules must be available when the Engine is first /// created. FilterSet allows assembling a compound list from multiple /// different sources before compiling the rules into an Engine. #[pyclass] #[derive(Clone)] pub struct FilterSet { filter_set: RustFilterSet, debug: bool, } #[pymethods] impl FilterSet { /// Creates a new `FilterSet`. The `debug` argument specifies whether or /// not to save information about the original raw filter rules alongside /// the more compact internal representation. If enabled, this information /// will be passed to the corresponding Engine. #[new] #[pyo3(signature = (debug = false))] pub fn new(debug: bool) -> Self { Self { filter_set: RustFilterSet::new(debug), debug, } } /// Adds the contents of an entire filter list to this FilterSet. Filters /// that cannot be parsed successfully are ignored. /// /// The format is a string containing either "standard" (ABP/uBO-style) /// or "hosts". #[pyo3(signature = (filter_list, format = "standard", rule_types = "all"))] pub fn add_filter_list( &mut self, filter_list: &str, format: &str, rule_types: &str, ) -> PyResult<()> { let filter_format = filter_format_from_string(format)?; let rule_types = rule_types_from_string(rule_types)?; self.filter_set.add_filter_list( filter_list, ParseOptions { format: filter_format, rule_types, permissions: PermissionMask::default(), }, ); Ok(()) } /// Adds a collection of filter rules to this FilterSet. Filters that /// cannot be parsed successfully are ignored. /// /// The format is a string containing either "standard" (ABP/uBO-style) /// or "hosts". #[pyo3(signature = (filters, format = "standard", rule_types = "all"))] pub fn add_filters( &mut self, filters: Vec, format: &str, rule_types: &str, ) -> PyResult<()> { let filter_format = filter_format_from_string(format)?; let rule_types = rule_types_from_string(rule_types)?; self.filter_set.add_filters( &filters, ParseOptions { format: filter_format, rule_types, permissions: PermissionMask::default(), }, ); Ok(()) } fn __repr__(&self) -> PyResult { Ok(format!("FilterSet(debug={})", self.debug.diy_python_repr())) } } /// Contains cosmetic filter information intended to be injected into a /// particular hostname. #[pyclass] pub struct UrlSpecificResources { /// A set of any CSS selector on the page that should be hidden, i.e. /// styled as `{ display: none !important; }`. #[pyo3(get)] pub hide_selectors: HashSet, /// Set of JSON-encoded procedural filters or filters with an action. #[pyo3(get)] pub procedural_actions: HashSet, /// A set of any class or id CSS selectors that should not have generic /// rules applied. // In practice, these should be passed to `class_id_stylesheet` and not // used otherwise. #[pyo3(get)] pub exceptions: HashSet, /// Javascript code for any scriptlets that should be injected into the /// page. #[pyo3(get)] pub injected_script: String, /// `generichide` is set to `True` if there is a corresponding /// `$generichide` exception network filter. If so, the page should not /// query for additional generic rules using hidden_class_id_selectors. #[pyo3(get)] pub generichide: bool, } impl From for UrlSpecificResources { fn from(r: RustUrlSpecificResources) -> Self { Self { hide_selectors: r.hide_selectors, procedural_actions: r.procedural_actions, exceptions: r.exceptions, injected_script: r.injected_script, generichide: r.generichide, } } } #[pymethods] impl UrlSpecificResources { fn __repr__(&self) -> PyResult { Ok(format!( "UrlSpecificResources<{} hide selectors, {} procedural actions, {} exceptions, injected_javascript={}, generichide={}>", self.hide_selectors.len(), self.procedural_actions.len(), self.exceptions.len(), self.injected_script.diy_python_repr(), self.generichide.diy_python_repr(), )) } } /// The main object featured in this library. This object holds the adblocker's /// state, and can be queried to see if a given request should be blocked or /// not. /// /// # Request types /// A few of `Engine`'s methods have a field specifying a "resource type", /// valid examples are: /// * `beacon` /// * `csp_report` /// * `document` /// * `font` /// * `media` /// * `object` /// * `script` /// * `stylesheet` /// * and et cetera... /// See the [Mozilla Web Documentation][1] for more info. /// /// [1]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/ResourceType #[pyclass] pub struct Engine { engine: RustEngine, optimize: bool, } #[pymethods] impl Engine { /// Create a new adblocking engine #[new] #[pyo3(signature = (filter_set, optimize = true))] pub fn new(filter_set: FilterSet, optimize: bool) -> Self { let engine = RustEngine::from_filter_set(filter_set.filter_set, optimize); Self { engine, optimize } } /// Check if the given `url`—pointing to a resource of type `request_type`— /// is blocked, assuming the request is made from the given `source_url`. /// Returns an object of type `BlockerResult`. /// /// # Arguments /// * `url` - The URL of the request to check /// * `source_url` - The URL from where the request is made /// * `request_type` - The resource type that the request points to pub fn check_network_urls( &self, url: &str, source_url: &str, request_type: &str, ) -> BlockerResult { match Request::new(url, source_url, request_type) { Ok(request) => { let blocker_result = self.engine.check_network_request(&request); blocker_result.into() } Err(_) => BlockerResult { matched: false, important: false, redirect: None, rewritten_url: None, exception: None, filter: None, } } } /// Check if a request should be blocked based on the given parameters. /// /// # Arguments /// * `url` - The URL of the request to check /// * `hostname` - The given `url`'s hostname /// * `source_hostname` - The hostname of the source URL. /// * `request_type` - The resource type that the request points to /// * `third_party_request` - Is the given request to a third-party? Here, /// `None` can be given and the engine will figure it out based on the /// `hostname` and `source_hostname`. #[pyo3(signature = (url, hostname, source_hostname, request_type, third_party_request = None))] pub fn check_network_urls_with_hostnames( &self, url: &str, hostname: &str, source_hostname: &str, request_type: &str, third_party_request: Option, ) -> BlockerResult { let third_party = third_party_request.unwrap_or_else(|| { hostname != source_hostname }); let request = Request::preparsed(url, hostname, source_hostname, request_type, third_party); let blocker_result = self.engine.check_network_request(&request); blocker_result.into() } /// Check if a request should be blocked based on the given parameters. /// /// # Arguments /// * `url` - The URL of the request to check /// * `hostname` - The given `url`'s hostname /// * `source_hostname` - The hostname of the source URL. /// * `request_type` - The resource type that the request points to /// * `third_party_request` - Is the given request to a third-party? Here, /// `None` can be given and the engine will figure it out based on the /// `hostname` and `source_hostname`. /// * `previously_matched_rule` - Return a match as long as there are no /// exceptions /// * `force_check_exceptions` - Check exceptions even if no other rule matches #[pyo3(signature = (url, hostname, source_hostname, request_type, third_party_request = None, previously_matched_rule = false, force_check_exceptions = false))] #[allow(clippy::too_many_arguments)] pub fn check_network_urls_with_hostnames_subset( &self, url: &str, hostname: &str, source_hostname: &str, request_type: &str, third_party_request: Option, previously_matched_rule: bool, force_check_exceptions: bool, ) -> BlockerResult { let third_party = third_party_request.unwrap_or_else(|| { hostname != source_hostname }); let request = Request::preparsed(url, hostname, source_hostname, request_type, third_party); let blocker_result = self.engine.check_network_request_subset( &request, previously_matched_rule, force_check_exceptions, ); blocker_result.into() } /// Sets this engine's resources to additionally include `resource`. /// /// # Arguments /// * `name`: Represents the primary name of the resource, often a filename /// * `content_type`: How to interpret the resource data within `content`. /// Use `"template"` if wanting to specify a template resource type. /// * `content`: The resource data, encoded using standard base64 configuration /// * `aliases`: List of aliases for the resource #[pyo3(signature = (name, content_type, content, aliases = None))] pub fn add_resource( &mut self, name: &str, content_type: &str, content: &str, aliases: Option>, ) -> PyResult<()> { let resource = Resource { name: name.to_string(), aliases: aliases.unwrap_or_default(), kind: match content_type { "template" => ResourceType::Template, _ => ResourceType::Mime(MimeType::from(std::borrow::Cow::from( content_type.to_string(), ))), }, content: content.to_string(), dependencies: Vec::new(), permission: PermissionMask::default(), }; self.engine.use_resources(std::iter::once(resource)); Ok(()) } /// Serialize this blocking engine to bytes. They can then be deserialized /// using `deserialize()` to get the same engine again. pub fn serialize<'p>(&self, py: Python<'p>) -> PyResult> { let bytes = self.engine.serialize(); let py_bytes = PyBytes::new_bound(py, &bytes); Ok(py_bytes) } /// Serialize this blocking engine to a file. The file can then be /// deserialized using `deserialize_from_file()` to get the same engine /// again. pub fn serialize_to_file(&self, file: &str) -> PyResult<()> { let data = self.engine.serialize(); let mut fd = fs::OpenOptions::new() .create(true) .truncate(true) .write(true) .open(file)?; fd.write_all(&data)?; Ok(()) } /// Deserialize a blocking engine from bytes produced with `serialize()`. pub fn deserialize(&mut self, serialized: &[u8]) -> PyResult<()> { self.engine.deserialize(serialized).map_err(|_| { DeserializationError::new_err("Failed to deserialize engine data") }) } /// Deserialize a blocking engine from file produced with /// `serialize_to_file()`. pub fn deserialize_from_file(&mut self, file: &str) -> PyResult<()> { let mut fd = fs::File::open(file)?; let mut data: Vec = Vec::new(); fd.read_to_end(&mut data)?; self.deserialize(&data) } /// Checks if the given filter exists in the blocking engine. /// Note: This method has been removed in the latest version of adblock-rust. /// It will always return false. pub fn filter_exists(&self, _filter: &str) -> bool { // This method no longer exists in the adblock-rust API false } /// Sets this engine's tags to be _only_ the ones provided in tags. /// /// Tags can be used to cheaply enable or disable network rules with a /// corresponding $tag option. pub fn use_tags(&mut self, tags: Vec) { let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect(); self.engine.use_tags(&tag_refs); } /// Sets this engine's tags to additionally include the ones provided in /// tags. /// /// Tags can be used to cheaply enable or disable network rules with a /// corresponding $tag option. pub fn enable_tags(&mut self, tags: Vec) { let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect(); self.engine.enable_tags(&tag_refs); } /// Sets this engine's tags to no longer include the ones provided in /// tags. /// /// Tags can be used to cheaply enable or disable network rules with a /// corresponding $tag option. pub fn disable_tags(&mut self, tags: Vec) { let tag_refs: Vec<&str> = tags.iter().map(|s| s.as_str()).collect(); self.engine.disable_tags(&tag_refs); } /// Checks if a given tag exists in this engine. /// /// Tags can be used to cheaply enable or disable network rules with a /// corresponding $tag option. pub fn tag_exists(&self, tag: &str) -> bool { self.engine.tag_exists(tag) } /// Returns a set of cosmetic filter resources required for a particular /// url. Once this has been called, all CSS ids and classes on a /// page should be passed to hidden_class_id_selectors to obtain any /// stylesheets consisting of generic rules. pub fn url_cosmetic_resources(&self, url: &str) -> UrlSpecificResources { self.engine.url_cosmetic_resources(url).into() } /// If any of the provided CSS classes or ids could cause a certain generic /// CSS hide rule (i.e. `{ display: none !important; }`) to be required, this /// method will return a list of CSS selectors corresponding to rules /// referencing those classes or ids, provided that the corresponding rules /// are not excepted. /// /// Exceptions should be passed directly from UrlSpecificResources. pub fn hidden_class_id_selectors( &self, classes: Vec, ids: Vec, exceptions: HashSet, ) -> PyResult> { Ok(self .engine .hidden_class_id_selectors(&classes, &ids, &exceptions)) } fn __repr__(&self) -> PyResult { Ok(format!( "Engine", self.optimize.diy_python_repr() )) } } /// PyO3 doesn't offer the ability to get the Python representation of a Rust /// object, so we make our own trait. trait DiyPythonRepr { fn diy_python_repr(&self) -> String; } impl DiyPythonRepr for Option where T: DiyPythonRepr, { fn diy_python_repr(&self) -> String { match self { None => "None".to_owned(), Some(x) => x.diy_python_repr(), } } } impl DiyPythonRepr for String { fn diy_python_repr(&self) -> String { let mut res = format!("{:?}", self); // This is safe to do since we know that `res` will always be of // length >= 2. res.replace_range(0..1, "'"); res.replace_range(res.len() - 1..res.len(), "'"); res } } impl DiyPythonRepr for bool { fn diy_python_repr(&self) -> String { if *self { "True".to_owned() } else { "False".to_owned() } } }