diff --git a/paclib/BUILD b/paclib/BUILD index 1aba5052..fa16dc25 100644 --- a/paclib/BUILD +++ b/paclib/BUILD @@ -5,6 +5,7 @@ rust_library( name = "paclib", srcs = [ "src/dns.rs", + "src/domain.rs", "src/engine.rs", "src/evaluator.rs", "src/lib.rs", diff --git a/paclib/src/domain.rs b/paclib/src/domain.rs new file mode 100644 index 00000000..596be6f8 --- /dev/null +++ b/paclib/src/domain.rs @@ -0,0 +1,164 @@ +use boa_engine::{class::Class, object::builtins::JsArray, JsError, JsNativeError}; +use boa_gc::{Finalize, Trace}; + +type Map = std::collections::HashMap; + +#[derive(Debug, Default, Trace, Finalize)] +pub(crate) struct Table { + root: Map, +} + +#[derive(Debug, Default, Trace, Finalize)] +struct Entry { + root: Map, +} + +impl Table { + pub(crate) fn contains>(&self, domain: A) -> bool { + let domain = Domain(domain.as_ref()); + let parts = domain.parts(); + let mut root = &self.root; + for p in parts { + if let Some(next) = root.get(p) { + root = &next.root; + if next.is_leaf() { + return true; + } + } else { + break; + } + } + false + } +} + +impl Entry { + fn is_leaf(&self) -> bool { + self.root.is_empty() + } +} + +impl FromIterator for Table +where + A: AsRef, +{ + fn from_iter>(iter: T) -> Self { + let mut result = Self { + root: Default::default(), + }; + for domain in iter { + let domain = Domain(domain.as_ref()); + let parts = domain.parts(); + let mut root = &mut result.root; + for p in parts { + root = &mut root.entry(p.to_owned()).or_default().root; + } + } + result + } +} + +struct Domain<'a>(&'a str); + +impl<'a> Domain<'a> { + fn parts(&self) -> impl Iterator { + self.0 + .trim_matches(|c: char| c.is_ascii_whitespace() || c == '.') + .split('.') + .rev() + } +} + +impl Class for Table { + const NAME: &'static str = "DomainTable"; + + fn constructor( + _this: &boa_engine::JsValue, + args: &[boa_engine::JsValue], + context: &mut boa_engine::Context, + ) -> boa_engine::JsResult { + let Some(domains) = args.get(0) else { + return Err(JsNativeError::typ() + .with_message("first argument is missing") + .into()); + }; + let domains = domains + .as_object() + .ok_or_else::(|| { + JsNativeError::typ() + .with_message("first argument must be an array") + .into() + }) + .and_then(|o| JsArray::from_object(o.clone()))?; + + // TODO: avoid the temporary vector by using an Iterator directly, however it looks like, + // that JsArray does not expose an iterator interface in the Rust world. + let len = domains.length(context)?; + let mut domains_vec = Vec::::with_capacity(len as usize); + for k in 0..len { + let domain = domains.at(k as i64, context)?; + let domain = domain + .as_string() + .ok_or_else::(|| { + JsNativeError::typ() + .with_message("domain {k} is not a string") + .into() + }) + .map(|s| s.to_std_string_escaped())?; + domains_vec.push(domain); + } + let table = domains_vec.into_iter().collect::(); + Ok(table) + } + + fn init(class: &mut boa_engine::class::ClassBuilder) -> boa_engine::JsResult<()> { + class.method( + "contains", + 1, + boa_engine::NativeFunction::from_fn_ptr(|this, args, _ctx| { + let Some(object) = this.as_object() else { + return Err(JsNativeError::typ() + .with_message("'this' is not an object'") + .into()); + }; + let Some(table) = object.downcast_ref::
() else { + return Err(JsNativeError::typ() + .with_message("'this' is not a DomainTable'") + .into()); + }; + let Some(domain) = args.get(0) else { + return Err(JsNativeError::typ() + .with_message("first argument is missing") + .into()); + }; + let Some(domain) = domain.as_string() else { + return Err(JsNativeError::typ() + .with_message("first argument must be a string") + .into()); + }; + let domain = domain.to_std_string_escaped(); + return Ok(table.contains(domain).into()); + }), + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_domain_table() { + let table = vec!["example.org", "example.net", "test.org"] + .iter() + .collect::
(); + assert_eq!(table.root.len(), 2); + assert_eq!(table.contains("example.org"), true); + assert_eq!(table.contains("www.example.org"), true); + assert_eq!(table.contains("example.info"), false); + assert_eq!(table.contains("net"), false); + assert_eq!(table.contains("org"), false); + } +} diff --git a/paclib/src/engine.rs b/paclib/src/engine.rs index c166ca22..48cd25c2 100644 --- a/paclib/src/engine.rs +++ b/paclib/src/engine.rs @@ -5,7 +5,7 @@ use std::time::Instant; use tracing::{field::debug, instrument}; use crate::dns::DnsCache; -use crate::Proxies; +use crate::{domain, Proxies}; use crate::{FindProxyError, PacScriptError}; const PAC_UTILS: &str = include_str!("pac_utils.js"); @@ -19,6 +19,7 @@ impl<'a> Engine<'a> { let mut js = Context::default(); js.register_global_class::().unwrap(); + js.register_global_class::().unwrap(); js.register_global_builtin_callable("alert", 1, NativeFunction::from_fn_ptr(alert)) .expect("register_global_property"); js.register_global_builtin_callable( diff --git a/paclib/src/lib.rs b/paclib/src/lib.rs index d539740b..97ae9ec2 100644 --- a/paclib/src/lib.rs +++ b/paclib/src/lib.rs @@ -1,4 +1,5 @@ pub mod dns; +pub mod domain; pub mod engine; pub mod evaluator; pub mod proxy;