//! Compiled regexes can take up large amounts of memory. To reduce the overall memory footprint of //! the [`crate::Engine`], infrequently used regexes can be discarded. The [`RegexManager`] is //! responsible for managing the storage of regexes used by filters.
use regex::{
bytes::Regex as BytesRegex, bytes::RegexBuilder as BytesRegexBuilder,
bytes::RegexSet as BytesRegexSet, bytes::RegexSetBuilder as BytesRegexSetBuilder, Regex,
};
use std::collections::HashMap; use std::fmt; use std::time::Duration;
#[cfg(test)] #[cfg(not(target_arch = "wasm32"))] use mock_instant::thread_local::Instant; #[cfg(not(test))] #[cfg(not(target_arch = "wasm32"))] use std::time::Instant;
/// `*const NetworkFilter` could technically leak across threads through `RegexDebugEntry::id`, but /// it's disguised as a unique identifier and not intended to be dereferenced. unsafeimpl Send for RegexManager {}
/// Reports [`RegexManager`] metrics that may be useful for creating an optimized /// [`RegexManagerDiscardPolicy`]. #[cfg(feature = "debug-info")] pubstruct RegexDebugInfo { /// Information about each regex contained in the [`RegexManager`]. pub regex_data: Vec<RegexDebugEntry>, /// Total count of compiled regexes. pub compiled_regex_count: usize,
}
/// Describes metrics about a single regex from the [`RegexManager`]. #[cfg(feature = "debug-info")] pubstruct RegexDebugEntry { /// Id for this particular regex, which is constant and unique for its lifetime. /// /// Note that there are no guarantees about a particular id's constancy or uniqueness beyond /// the lifetime of a corresponding regex. pub id: u64, /// A string representation of this regex, if available. It may be `None` if the regex has been /// cleaned up to conserve memory. pub regex: Option<String>, /// When this regex was last used. pub last_used: Instant, /// How many times this regex has been used. pub usage_count: usize,
}
/// Used for customization of regex discarding behavior in the [`RegexManager`]. pubstruct RegexManagerDiscardPolicy { /// The [`RegexManager`] will check for and cleanup unused filters on this interval. pub cleanup_interval: Duration, /// The [`RegexManager`] will discard a regex if it hasn't been used for this much time. pub discard_unused_time: Duration,
}
type RandomState = std::hash::BuildHasherDefault<seahash::SeaHasher>;
/// A manager that creates and stores all regular expressions used by filters. /// Rarely used entries are discarded to save memory. /// /// The [`RegexManager`] is not thread safe, so any access to it must be synchronized externally. pubstruct RegexManager {
map: HashMap<u64, RegexEntry, RandomState>,
compiled_regex_count: usize,
now: Instant, #[cfg_attr(target_arch = "wasm32", allow(unused))]
last_cleanup: Instant,
discard_policy: RegexManagerDiscardPolicy,
}
/// Compiles a filter pattern to a regex. This is only performed *lazily* for /// filters containing at least a * or ^ symbol. Because Regexes are expansive, /// we try to convert some patterns to plain filters. #[allow(clippy::trivial_regex)] pub(crate) fn compile_regex<'a, I>(
filters: I,
is_right_anchor: bool,
is_left_anchor: bool,
is_complete_regex: bool,
) -> CompiledRegex where
I: Iterator<Item = &'a str> + ExactSizeIterator,
{ use once_cell::sync::Lazy; // Escape special regex characters: |.$+?{}()[]\ static SPECIAL_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([\|\.\$\+\?\{\}\(\)\[\]])").unwrap()); // * can match anything static WILDCARD_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\*").unwrap()); // ^ can match any separator or the end of the pattern static ANCHOR_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"\^(.)").unwrap()); // ^ can match any separator or the end of the pattern static ANCHOR_RE_EOL: Lazy<Regex> = Lazy::new(|| Regex::new(r"\^$").unwrap());
letmut escaped_patterns = Vec::with_capacity(filters.len()); for filter_str in filters { // If any filter is empty, the entire set matches anything if filter_str.is_empty() { return CompiledRegex::MatchAll;
} if is_complete_regex { // unescape unrecognised escaping sequences, otherwise a normal regex let unescaped = filter_str[1..filter_str.len() - 1]
.replace("\\/", "/")
.replace("\\:", ":");
escaped_patterns.push(unescaped);
} else { let repl = SPECIAL_RE.replace_all(filter_str, "\\$1"); let repl = WILDCARD_RE.replace_all(&repl, ".*"); // in adblock rules, '^' is a separator. // The separator character is anything but a letter, a digit, or one of the following: _ - . % let repl = ANCHOR_RE.replace_all(&repl, "(?:[^\\w\\d\\._%-])$1"); let repl = ANCHOR_RE_EOL.replace_all(&repl, "(?:[^\\w\\d\\._%-]|$)");
// Should match start or end of url let left_anchor = if is_left_anchor { "^" } else { "" }; let right_anchor = if is_right_anchor { "$" } else { "" }; let filter = format!("{left_anchor}{repl}{right_anchor}");
impl RegexManager { /// Check whether or not a regex network filter matches a certain URL pattern, using the /// [`RegexManager`]'s managed regex storage. pubfn matches<'a, FiltersIter>(
&mutself,
mask: NetworkFilterMask,
filters: FiltersIter,
key: u64,
pattern: &str,
) -> bool where
FiltersIter: Iterator<Item = &'a str> + ExactSizeIterator,
{ if !mask.is_regex() && !mask.is_complete_regex() { returntrue;
} use std::collections::hash_map::Entry; matchself.map.entry(key) {
Entry::Occupied(mut e) => { let v = e.get_mut();
v.usage_count += 1;
v.last_used = self.now; if v.regex.is_none() { // A discarded entry, recreate it:
v.regex = Some(make_regexp(mask, filters)); self.compiled_regex_count += 1;
}
v.regex.as_ref().unwrap().is_match(pattern)
}
Entry::Vacant(e) => { self.compiled_regex_count += 1; let new_entry = RegexEntry {
regex: Some(make_regexp(mask, filters)),
last_used: self.now,
usage_count: 1,
};
e.insert(new_entry)
.regex
.as_ref()
.unwrap()
.is_match(pattern)
}
}
}
/// The [`RegexManager`] is just a struct and doesn't manage any worker threads, so this method /// must be called periodically to ensure that it can track usage patterns of regexes over /// time. This method will handle periodically discarding filters if necessary. #[cfg(not(target_arch = "wasm32"))] pubfn update_time(&mutself) { self.now = Instant::now(); if !self.discard_policy.cleanup_interval.is_zero()
&& self.now - self.last_cleanup >= self.discard_policy.cleanup_interval
{ self.last_cleanup = self.now; self.cleanup();
}
}
#[cfg(not(target_arch = "wasm32"))] pub(crate) fn cleanup(&mutself) { let now = self.now; for v inself.map.values_mut() { if now - v.last_used >= self.discard_policy.discard_unused_time { // Discard the regex to save memory.
v.regex = None;
}
}
}
/// Customize the discard behavior of this [`RegexManager`]. pubfn set_discard_policy(&mutself, new_discard_policy: RegexManagerDiscardPolicy) { self.discard_policy = new_discard_policy;
}
/// Discard one regex, identified by its id from a [`RegexDebugEntry`]. #[cfg(feature = "debug-info")] pubfn discard_regex(&mutself, regex_id: u64) { self.map
.iter_mut()
.filter(|(k, _)| { **k } == regex_id)
.for_each(|(_, v)| {
v.regex = None;
});
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.