rpz

Response policy zone (RPZ) file generator.
git clone https://git.philomathiclife.com/repos/rpz
Log | Files | Refs | README

commit 1966f57cd8c5687006856ddfb6089c6befee0a0d
parent a5700c33ff764f643d1715796d2a12545b7c9cc1
Author: Zack Newman <zack@philomathiclife.com>
Date:   Fri,  5 Jun 2026 17:18:55 -0600

address lints, move unit tests

Diffstat:
MCargo.toml | 32+++++++++++++++++++++++---------
Msrc/args.rs | 270+------------------------------------------------------------------------------
Asrc/args/tests.rs | 263+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/config.rs | 139+++----------------------------------------------------------------------------
Asrc/config/tests.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/dom.rs | 481+------------------------------------------------------------------------------
Asrc/dom/tests.rs | 449+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/file.rs | 4++--
Msrc/lib.rs | 2+-
Msrc/main.rs | 154+++----------------------------------------------------------------------------
Msrc/priv_sep.rs | 4+++-
Asrc/tests.rs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
12 files changed, 1029 insertions(+), 1039 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" name = "rpz" readme = "README.md" repository = "https://git.philomathiclife.com/repos/rpz/" -version = "2.1.0" +version = "2.1.1" [lints.rust] deprecated-safe = { level = "deny", priority = -1 } @@ -32,6 +32,7 @@ deref-into-dyn-supertrait = { level = "deny", priority = -1 } ffi-unwind-calls = { level = "deny", priority = -1 } #fuzzy-provenance-casts = { level = "deny", priority = -1 } impl-trait-redundant-captures = { level = "deny", priority = -1 } +linker-info = { level = "deny", priority = -1 } linker-messages = { level = "deny", priority = -1 } #lossy-provenance-casts = { level = "deny", priority = -1 } macro-use-extern-crate = { level = "deny", priority = -1 } @@ -65,6 +66,7 @@ variant-size-differences = { level = "deny", priority = -1 } # Before publishing to crates.io, comment above and uncomment below. #warnings = { level = "allow", priority = -1 } #ambiguous-associated-items = { level = "allow", priority = -1 } +#ambiguous-glob-imports = { level = "allow", priority = -1 } #arithmetic-overflow = { level = "allow", priority = -1 } #binary-asm-labels = { level = "allow", priority = -1 } #bindings-with-variant-name = { level = "allow", priority = -1 } @@ -73,6 +75,7 @@ variant-size-differences = { level = "deny", priority = -1 } #default-overrides-default-fields = { level = "allow", priority = -1 } #dependency-on-unit-never-type-fallback = { level = "allow", priority = -1 } #deref-nullptr = { level = "allow", priority = -1 } +#duplicate-features = { level = "allow", priority = -1 } #elided-lifetimes-in-associated-constant = { level = "allow", priority = -1 } #enum-intrinsics-non-enums = { level = "allow", priority = -1 } #explicit-builtin-cfgs-in-flags = { level = "allow", priority = -1 } @@ -100,16 +103,15 @@ variant-size-differences = { level = "deny", priority = -1 } #pub-use-of-private-extern-crate = { level = "allow", priority = -1 } #repr-transparent-non-zst-fields = { level = "allow", priority = -1 } #semicolon-in-expressions-from-macros = { level = "allow", priority = -1 } -#soft-unstable = { level = "allow", priority = -1 } #test-unstable-lint = { level = "allow", priority = -1 } #text-direction-codepoint-in-comment = { level = "allow", priority = -1 } #text-direction-codepoint-in-literal = { level = "allow", priority = -1 } #unconditional-panic = { level = "allow", priority = -1 } #undropped-manually-drops = { level = "allow", priority = -1 } +#uninhabited-static = { level = "allow", priority = -1 } #unknown-crate-types = { level = "allow", priority = -1 } #useless-deprecated = { level = "allow", priority = -1 } -# Before publishing to crates.io, comment below. [lints.clippy] cargo = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 } @@ -126,6 +128,7 @@ blanket_clippy_restriction_lints = "allow" exhaustive_enums = "allow" exhaustive_structs = "allow" implicit_return = "allow" +inline_trait_bounds = "allow" min_ident_chars = "allow" missing_trait_methods = "allow" pub_with_shorthand = "allow" @@ -133,9 +136,20 @@ question_mark_used = "allow" redundant_pub_crate = "allow" ref_patterns = "allow" return_and_then = "allow" +self_named_module_files = "allow" single_call_fn = "allow" single_char_lifetime_names = "allow" unseparated_literal_suffix = "allow" +# Before publishing to crates.io, comment above and uncomment below. +#cargo = "allow" +#complexity = "allow" +#correctness = "allow" +#nursery = "allow" +#pedantic = "allow" +#perf = "allow" +#restriction = "allow" +#style = "allow" +#suspicious = "allow" [lints.rustdoc] # Before publishing to crates.io, comment below and uncomment below that. @@ -162,18 +176,18 @@ ascii_domain = { version = "0.6.9", default-features = false } num-bigint = { version = "0.4.6", default-features = false } serde = { version = "1.0.228", default-features = false } superset_map = { version = "0.3.6", default-features = false } -tokio = { version = "1.50.0", default-features = false, features = ["rt", "time"] } -toml = { version = "1.0.7", default-features = false, features = ["parse", "serde"] } +tokio = { version = "1.52.3", default-features = false, features = ["rt", "time"] } +toml = { version = "1.1.2", default-features = false, features = ["parse", "serde"] } url = { version = "2.5.8", default-features = false, features = ["serde"] } zfc = { version = "0.4.7", default-features = false } [target.'cfg(not(target_os = "openbsd"))'.dependencies] -reqwest = { version = "0.13.2", default-features = false, features = ["brotli", "deflate", "gzip", "http2", "rustls"] } +reqwest = { version = "0.13.4", default-features = false, features = ["brotli", "deflate", "gzip", "http2", "rustls"] } [target.'cfg(target_os = "openbsd")'.dependencies] -priv_sep = { version = "3.0.0-alpha.5.0", default-features = false, features = ["std"] } -reqwest = { version = "0.13.2", default-features = false, features = ["brotli", "deflate", "gzip", "http2", "rustls-no-provider"] } -rustls = { version = "0.23.37", default-features = false, features = ["ring"] } +priv_sep = { version = "3.0.0-alpha.5.1", default-features = false, features = ["std"] } +reqwest = { version = "0.13.4", default-features = false, features = ["brotli", "deflate", "gzip", "http2", "rustls-no-provider"] } +rustls = { version = "0.23.40", default-features = false, features = ["ring"] } [profile.release] codegen-units = 1 diff --git a/src/args.rs b/src/args.rs @@ -4,6 +4,9 @@ use core::{ }; use rpz::file::AbsFilePath; use std::env::{self, Args}; +/// Unit tests. +#[cfg(test)] +mod tests; /// Error returned when parsing arguments passed to the application. #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub(crate) enum ArgsErr { @@ -193,270 +196,3 @@ impl Opts { } } } -#[cfg(test)] -mod tests { - use crate::{ArgsErr, E, test_prog}; - use core::convert; - use std::io::Write as _; - use std::process::Stdio; - use std::thread; - #[expect(clippy::too_many_lines, reason = "a lot to test")] - #[test] - #[ignore = "requires I/O"] - fn args() { - test_prog::verify_files(); - assert!( - test_prog::get_command() - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::NoArgs)).into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-f") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-q") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-v") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-fq") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-qf") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-fv") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-vf") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .args(["-h", "-V"]) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .args(["-h", "-h"]) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!( - "Error: {:?}\n", - E::Args(ArgsErr::DuplicateOption("-h/--help")) - ) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .args(["-f", "/home/zack/foo", "-V"]) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .args(["-f", "home/zack/foo"]) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .args(["-f", "/home/zack/foo/"]) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .arg("-foo") - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!( - "Error: {:?}\n", - E::Args(ArgsErr::InvalidOption(String::from("-foo"))) - ) - .into_bytes() - }) - ); - assert!( - test_prog::get_command() - .args(["-f", "-"]) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .output() - .is_ok_and(|output| !output.status.success() - && output.stderr.get(..23) == Some(b"Error: TOML parse error")) - ); - assert!( - test_prog::get_command() - .args(["-f", "-"]) - .stderr(Stdio::piped()) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .spawn() - .is_ok_and(|mut cmd| { - cmd.stdin.take().is_some_and(|mut stdin| { - thread::spawn(move || { - stdin - .write_all(b"junk") - .is_ok_and(|()| stdin.flush().is_ok()) - }) - .join() - .is_ok_and(convert::identity) - }) && cmd.wait_with_output().is_ok_and(|output| { - !output.status.success() - && output.stderr.get(..23) == Some(b"Error: TOML parse error") - }) - }) - ); - assert!( - test_prog::get_command() - .arg("-h") - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .output() - .is_ok_and(|output| output.status.success() - && output.stdout.get(..crate::HELP.len()) == Some(crate::HELP.as_bytes())) - ); - assert!( - test_prog::get_command() - .arg("-V") - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .output() - .is_ok_and(|output| output.status.success() - && output.stdout.get(..crate::VERSION.len()) - == Some(crate::VERSION.as_bytes())) - ); - } -} diff --git a/src/args/tests.rs b/src/args/tests.rs @@ -0,0 +1,263 @@ +use crate::{ArgsErr, E, tests}; +use core::convert; +use std::io::Write as _; +use std::process::Stdio; +use std::thread; +#[expect(clippy::too_many_lines, reason = "a lot to test")] +#[test] +#[ignore = "requires I/O"] +fn args() { + tests::verify_files(); + assert!( + tests::get_command() + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::NoArgs)).into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-f") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-q") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-v") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-fq") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-qf") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-fv") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-vf") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .args(["-h", "-V"]) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .args(["-h", "-h"]) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!( + "Error: {:?}\n", + E::Args(ArgsErr::DuplicateOption("-h/--help")) + ) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .args(["-f", "/home/zack/foo", "-V"]) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .args(["-f", "home/zack/foo"]) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .args(["-f", "/home/zack/foo/"]) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .arg("-foo") + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| { + !output.status.success() + && output.stderr + == format!( + "Error: {:?}\n", + E::Args(ArgsErr::InvalidOption(String::from("-foo"))) + ) + .into_bytes() + }) + ); + assert!( + tests::get_command() + .args(["-f", "-"]) + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .is_ok_and(|output| !output.status.success() + && output.stderr.get(..23) == Some(b"Error: TOML parse error")) + ); + assert!( + tests::get_command() + .args(["-f", "-"]) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .spawn() + .is_ok_and(|mut cmd| { + cmd.stdin.take().is_some_and(|mut stdin| { + thread::spawn(move || { + stdin + .write_all(b"junk") + .is_ok_and(|()| stdin.flush().is_ok()) + }) + .join() + .is_ok_and(convert::identity) + }) && cmd.wait_with_output().is_ok_and(|output| { + !output.status.success() + && output.stderr.get(..23) == Some(b"Error: TOML parse error") + }) + }) + ); + assert!( + tests::get_command() + .arg("-h") + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .output() + .is_ok_and(|output| output.status.success() + && output.stdout.get(..crate::HELP.len()) == Some(crate::HELP.as_bytes())) + ); + assert!( + tests::get_command() + .arg("-V") + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .output() + .is_ok_and(|output| output.status.success() + && output.stdout.get(..crate::VERSION.len()) == Some(crate::VERSION.as_bytes())) + ); +} diff --git a/src/config.rs b/src/config.rs @@ -7,6 +7,9 @@ use core::{ use rpz::file::{AbsFilePath, HttpUrl}; use serde::de::{Deserialize, Deserializer, Error, MapAccess, SeqAccess, Unexpected, Visitor}; use std::collections::HashSet; +/// Unit tests. +#[cfg(test)] +mod tests; /// The TOML config file. #[derive(Debug)] pub(crate) struct Config { @@ -44,12 +47,10 @@ impl Display for Config { "Config {{ timeout: {} rpz: {}, local_dir: {}, ", self.timeout .map_or_else(String::new, |dur| dur.as_secs().to_string()), - &self - .rpz + self.rpz .as_ref() .map_or_else(|| Cow::Owned(String::new()), |file| file.to_string_lossy()), - &self - .local_dir + self.local_dir .as_ref() .map_or_else(|| Cow::Owned(String::new()), |dir| dir.to_string_lossy()) ) @@ -297,133 +298,3 @@ impl<'de> Deserialize<'de> for Config { deserializer.deserialize_struct("Config", &VARIANTS, ConfigVisitor) } } -#[cfg(test)] -mod tests { - use crate::Config; - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn missing_fields() { - drop(toml::from_str::<Config>("").unwrap_err()); - drop(toml::from_str::<Config>("timeout=15").unwrap_err()); - drop(toml::from_str::<Config>(r#"rpz="/foo""#).unwrap_err()); - drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); - drop(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).unwrap()); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn invalid_fields() { - drop(toml::from_str::<Config>("bob=15").unwrap_err()); - drop(toml::from_str::<Config>(r#"foo=["https://foo.com/foo"]"#).unwrap_err()); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn timeout() { - drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); - drop( - toml::from_str::<Config>( - r#"timeout=15 -local_dir="/foo/""#, - ) - .unwrap(), - ); - drop( - toml::from_str::<Config>( - r#"timeout=4294967295 -local_dir="/foo/""#, - ) - .unwrap(), - ); - drop( - toml::from_str::<Config>( - r#"timeout=-1 -local_dir="/foo/""#, - ) - .unwrap_err(), - ); - drop( - toml::from_str::<Config>( - r#"timeout=0 -local_dir="/foo/""#, - ) - .unwrap(), - ); - drop( - toml::from_str::<Config>( - r#"timeout=4294967296 -local_dir="/foo/""#, - ) - .unwrap_err(), - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn arrays() { - drop( - toml::from_str::<Config>(r#"adblock=["https://foo.com/foo","https://foo.com/foo"]"#) - .unwrap_err(), - ); - drop(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).unwrap()); - drop( - toml::from_str::<Config>( - r#"adblock=["https://foo.com/foo"] -domain=["https://foo.com/foo"]"#, - ) - .unwrap_err(), - ); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn urls() { - drop(toml::from_str::<Config>(r#"adblock=["file://foo.com/foo"]"#).unwrap_err()); - drop(toml::from_str::<Config>(r#"adblock=["foo.com/foo"]"#).unwrap_err()); - drop(toml::from_str::<Config>(r#"adblock=["http://foo.com/foo"]"#).unwrap()); - drop( - toml::from_str::<Config>( - r#"adblock=[] -domain=["https:///foo"]"#, - ) - .unwrap(), - ); - drop(toml::from_str::<Config>(r#"adblock=[""]"#).unwrap_err()); - drop(toml::from_str::<Config>(r#"adblock=["https://"]"#).unwrap_err()); - drop(toml::from_str::<Config>(r#"adblock=["ftp://foo.com/foo"]"#).unwrap_err()); - } - #[expect(clippy::unwrap_used, reason = "OK in tests")] - #[test] - fn paths() { - drop( - toml::from_str::<Config>( - r#"rpz="/foo/" -wildcard=["https://foo.com/foo"]"#, - ) - .unwrap_err(), - ); - drop( - toml::from_str::<Config>( - r#"rpz="foo" -wildcard=["https://foo.com/foo"]"#, - ) - .unwrap_err(), - ); - drop( - toml::from_str::<Config>( - r#"rpz="/foo" -wildcard=["https://foo.com/foo"]"#, - ) - .unwrap(), - ); - drop(toml::from_str::<Config>(r#"local_dir="foo/""#).unwrap_err()); - drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); - // Directories are allowed to not have a trailing `/`, but they will get it - // added. - assert!( - toml::from_str::<Config>( - r#"local_dir="/foo" -wildcard=["https://foo.com/foo"]"# - ) - .is_ok_and(|config| config - .local_dir - .is_some_and(|dir| dir.to_str().is_some_and(|val| val == "/foo/"))) - ); - } -} diff --git a/src/config/tests.rs b/src/config/tests.rs @@ -0,0 +1,127 @@ +use crate::Config; +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn missing_fields() { + drop(toml::from_str::<Config>("").unwrap_err()); + drop(toml::from_str::<Config>("timeout=15").unwrap_err()); + drop(toml::from_str::<Config>(r#"rpz="/foo""#).unwrap_err()); + drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); + drop(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).unwrap()); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn invalid_fields() { + drop(toml::from_str::<Config>("bob=15").unwrap_err()); + drop(toml::from_str::<Config>(r#"foo=["https://foo.com/foo"]"#).unwrap_err()); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn timeout() { + drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); + drop( + toml::from_str::<Config>( + r#"timeout=15 +local_dir="/foo/""#, + ) + .unwrap(), + ); + drop( + toml::from_str::<Config>( + r#"timeout=4294967295 +local_dir="/foo/""#, + ) + .unwrap(), + ); + drop( + toml::from_str::<Config>( + r#"timeout=-1 +local_dir="/foo/""#, + ) + .unwrap_err(), + ); + drop( + toml::from_str::<Config>( + r#"timeout=0 +local_dir="/foo/""#, + ) + .unwrap(), + ); + drop( + toml::from_str::<Config>( + r#"timeout=4294967296 +local_dir="/foo/""#, + ) + .unwrap_err(), + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn arrays() { + drop( + toml::from_str::<Config>(r#"adblock=["https://foo.com/foo","https://foo.com/foo"]"#) + .unwrap_err(), + ); + drop(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).unwrap()); + drop( + toml::from_str::<Config>( + r#"adblock=["https://foo.com/foo"] +domain=["https://foo.com/foo"]"#, + ) + .unwrap_err(), + ); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn urls() { + drop(toml::from_str::<Config>(r#"adblock=["file://foo.com/foo"]"#).unwrap_err()); + drop(toml::from_str::<Config>(r#"adblock=["foo.com/foo"]"#).unwrap_err()); + drop(toml::from_str::<Config>(r#"adblock=["http://foo.com/foo"]"#).unwrap()); + drop( + toml::from_str::<Config>( + r#"adblock=[] +domain=["https:///foo"]"#, + ) + .unwrap(), + ); + drop(toml::from_str::<Config>(r#"adblock=[""]"#).unwrap_err()); + drop(toml::from_str::<Config>(r#"adblock=["https://"]"#).unwrap_err()); + drop(toml::from_str::<Config>(r#"adblock=["ftp://foo.com/foo"]"#).unwrap_err()); +} +#[expect(clippy::unwrap_used, reason = "OK in tests")] +#[test] +fn paths() { + drop( + toml::from_str::<Config>( + r#"rpz="/foo/" +wildcard=["https://foo.com/foo"]"#, + ) + .unwrap_err(), + ); + drop( + toml::from_str::<Config>( + r#"rpz="foo" +wildcard=["https://foo.com/foo"]"#, + ) + .unwrap_err(), + ); + drop( + toml::from_str::<Config>( + r#"rpz="/foo" +wildcard=["https://foo.com/foo"]"#, + ) + .unwrap(), + ); + drop(toml::from_str::<Config>(r#"local_dir="foo/""#).unwrap_err()); + drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); + // Directories are allowed to not have a trailing `/`, but they will get it + // added. + assert!( + toml::from_str::<Config>( + r#"local_dir="/foo" +wildcard=["https://foo.com/foo"]"# + ) + .is_ok_and(|config| config + .local_dir + .is_some_and(|dir| dir.to_str().is_some_and(|val| val == "/foo/"))) + ); +} diff --git a/src/dom.rs b/src/dom.rs @@ -20,6 +20,9 @@ use std::{ }; use superset_map::SetOrd; use zfc::{BoundedCardinality, Cardinality, Set}; +/// Unit tests. +#[cfg(test)] +mod tests; /// One. const ONE: NonZeroU8 = NonZeroU8::new(1).unwrap(); /// Error returned when an invalid string is passed to [`Adblock::parse_value`], [`DomainOnly::parse_value`], @@ -57,7 +60,6 @@ impl error::Error for FirefoxDomainErr {} const CHARS: &AllowedAscii<[u8; 78]> = &ASCII_FIREFOX; /// Parses a `[u8]` into a `Domain` using `CHARS` with the added restriction that the `Domain` has a TLD /// that is either all letters or has length of at least five and begins with `b"xn--"`. -#[expect(single_use_lifetimes, reason = "false positive")] #[expect(clippy::indexing_slicing, reason = "we verify manually")] fn domain_icann_tld<'a: 'b, 'b>(val: &'a [u8]) -> Result<Domain<&'b str>, FirefoxDomainErr> { Domain::try_from_bytes(val, CHARS) @@ -235,7 +237,6 @@ pub trait ParsedDomain<'a>: Sized { /// # Errors /// /// Errors iff `val` is unable to be parsed into a `Value`. - #[expect(single_use_lifetimes, reason = "false positive")] fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error>; /// Reference to the contained `Domain`. fn domain(&self) -> &Domain<&'a str>; @@ -637,7 +638,6 @@ impl<'a> Deref for Adblock<'a> { } impl<'a> ParsedDomain<'a> for Adblock<'a> { type Error = FirefoxDomainErr; - #[expect(single_use_lifetimes, reason = "false positive")] #[expect( unsafe_code, clippy::indexing_slicing, @@ -930,7 +930,6 @@ impl<'a> Deref for DomainOnly<'a> { } impl<'a> ParsedDomain<'a> for DomainOnly<'a> { type Error = FirefoxDomainErr; - #[expect(single_use_lifetimes, reason = "false positive")] #[expect( unsafe_code, clippy::arithmetic_side_effects, @@ -954,7 +953,7 @@ impl<'a> ParsedDomain<'a> for DomainOnly<'a> { value[..value .iter() .try_fold(0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) }) - .map_or_else(convert::identity, convert::identity)] + .unwrap_or_else(convert::identity)] .trim_ascii_end(), ) .map(|domain| Value::Domain(Self { domain })) @@ -1159,7 +1158,6 @@ impl<'a> Deref for Hosts<'a> { } impl<'a> ParsedDomain<'a> for Hosts<'a> { type Error = FirefoxDomainErr; - #[expect(single_use_lifetimes, reason = "false positive")] #[expect( unsafe_code, clippy::arithmetic_side_effects, @@ -1222,7 +1220,7 @@ impl<'a> ParsedDomain<'a> for Hosts<'a> { 0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) }, ) - .map_or_else(convert::identity, convert::identity)] + .unwrap_or_else(convert::identity)] .trim_ascii_end(), ) .map(|domain| Value::Domain(Self { domain })) @@ -1505,7 +1503,6 @@ impl<'a> Deref for Wildcard<'a> { } impl<'a> ParsedDomain<'a> for Wildcard<'a> { type Error = FirefoxDomainErr; - #[expect(single_use_lifetimes, reason = "false positive")] #[expect( unsafe_code, clippy::arithmetic_side_effects, @@ -1539,7 +1536,7 @@ impl<'a> ParsedDomain<'a> for Wildcard<'a> { val2[..val2 .iter() .try_fold(0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) }) - .map_or_else(convert::identity, convert::identity)] + .unwrap_or_else(convert::identity)] .trim_ascii_end(), ) .and_then(|domain| { @@ -1853,7 +1850,6 @@ impl<'a: 'b, 'b> From<Wildcard<'a>> for RpzDomain<'b> { } impl<'a> ParsedDomain<'a> for RpzDomain<'a> { type Error = FirefoxDomainErr; - #[expect(single_use_lifetimes, reason = "false positive")] #[inline] fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> { DomainOnly::parse_value(val).map_or_else( @@ -1914,468 +1910,3 @@ impl<'a> ParsedDomain<'a> for RpzDomain<'a> { } } } -#[cfg(test)] -mod tests { - use super::{ - Adblock, DomainOnly, FirefoxDomainErr, Hosts, ParsedDomain as _, RpzDomain, Value, Wildcard, - }; - use ascii_domain::dom::DomainErr; - use num_bigint::BigUint; - use superset_map::SupersetSet; - #[test] - fn adblock_parse() { - // Test subdomains. - assert!( - Adblock::parse_value("||www.example.com").is_ok_and(|val| match val { - Value::Domain(ref dom) => - dom.subdomains && dom.domain.as_bytes() == b"www.example.com", - Value::Comment(_) | Value::Blank => false, - }) - ); - // Test whitespace and '^' removal. - assert!( - Adblock::parse_value(" \t\t ||\t\t \twww.example.com \t\t ^ \t\t ").is_ok_and( - |val| match val { - Value::Domain(ref dom) => - dom.subdomains && dom.domain.as_bytes() == b"www.example.com", - Value::Comment(_) | Value::Blank => false, - } - ) - ); - assert!( - Adblock::parse_value("\t\t \twww.example.com \t\t \t\t ").is_ok_and(|val| { - match val { - Value::Domain(ref dom) => { - !dom.subdomains && dom.domain.as_bytes() == b"www.example.com" - } - Value::Comment(_) | Value::Blank => false, - } - }) - ); - assert!(Adblock::parse_value("www .example.com").is_err_and( - |err| err == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), - )); - assert!(Adblock::parse_value("||www.ExAMPle.COm").is_ok_and(|val| { - match val { - Value::Domain(ref dom) => { - Adblock::parse_value("||www.example.com").is_ok_and(|val_2| match val_2 { - Value::Domain(ref dom2) => { - dom == dom2 - && dom.subdomains - && dom2.subdomains - && dom.cmp(dom2).is_eq() - } - Value::Comment(_) | Value::Blank => false, - }) - } - Value::Comment(_) | Value::Blank => false, - } - })); - // Test comment - assert!( - Adblock::parse_value(" \t\t #hi").is_ok_and(|val| match val { - Value::Comment(com) => com == "hi", - Value::Domain(_) | Value::Blank => false, - }) - ); - assert!( - Adblock::parse_value(" \t\t !! foo").is_ok_and(|val| match val { - Value::Comment(com) => com == "! foo", - Value::Domain(_) | Value::Blank => false, - }) - ); - // Test blank - assert!(Adblock::parse_value(" \t\t ").is_ok_and(|val| matches!(val, Value::Blank))); - } - #[test] - fn domain_only_parse_value() { - // Test whitespace and comment. - assert!( - DomainOnly::parse_value(" \t\t \t\t \twww.example.com#asdflkj asdf alskdfj ") - .is_ok_and(|val| match val { - Value::Domain(ref dom) => dom.domain.as_bytes() == b"www.example.com", - Value::Comment(_) | Value::Blank => false, - }) - ); - assert!( - DomainOnly::parse_value(" \t\t \t\t \twww.example.com \t\t ^ \t\t ") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) - ); - // Test case-insensitivity. - assert!( - DomainOnly::parse_value("www.ExAMPle.CoM").is_ok_and(|val| match val { - Value::Domain(ref dom) => - DomainOnly::parse_value("www.example.com").is_ok_and(|val2| match val2 { - Value::Domain(ref dom2) => dom.cmp(dom2).is_eq(), - Value::Comment(_) | Value::Blank => false, - }), - Value::Comment(_) | Value::Blank => false, - }) - ); - // Test comment. - assert!( - DomainOnly::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { - Value::Comment(com) => com == " hi", - Value::Domain(_) | Value::Blank => false, - }) - ); - // Test blank. - assert!( - DomainOnly::parse_value(" \t\t \t\t \t ") - .is_ok_and(|val| matches!(val, Value::Blank)) - ); - // Test blank. - assert!( - DomainOnly::parse_value("example.xn--abc") - .is_ok_and(|val| matches!(val, Value::Domain(_))) - ); - // Test invalid TLD. - assert!( - DomainOnly::parse_value("www.c1m") - .is_err_and(|err| err == FirefoxDomainErr::InvalidTld) - ); - } - #[test] - fn hosts_parse_value() { - // Test whitespace and comment. - assert!( - Hosts::parse_value(" \t\t 127.0.0.1\t\t \twww.example.com#asdflkj asdf alskdfj ") - .is_ok_and(|val| match val { - Value::Domain(ref dom) => dom.domain.as_bytes() == b"www.example.com", - Value::Comment(_) | Value::Blank => false, - }) - ); - assert!( - Hosts::parse_value(" \t\t 0.0.0.0\t\t \twww.example.com \t\t ^ \t\t ") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) - ); - assert!( - Hosts::parse_value("::1\twww .example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) - ); - // Test invalid IP - assert!( - Hosts::parse_value("::2 www.example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) - ); - assert!( - Hosts::parse_value(":2 www.example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) - ); - assert!( - Hosts::parse_value("www.example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) - ); - assert!( - Hosts::parse_value("10.4.2.256 www.example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) - ); - // Test case-insensitivity. - assert!( - Hosts::parse_value(":: www.ExAMPle.Com").is_ok_and(|val| match val { - Value::Domain(ref dom) => Hosts::parse_value("127.0.0.1 www.example.com") - .is_ok_and(|val2| match val2 { - Value::Domain(ref dom2) => dom.cmp(dom2).is_eq(), - Value::Comment(_) | Value::Blank => false, - }), - Value::Comment(_) | Value::Blank => false, - }) - ); - // Test comment. - assert!( - Hosts::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { - Value::Comment(com) => com == " hi", - Value::Domain(_) | Value::Blank => false, - }) - ); - // Test blank. - assert!( - Hosts::parse_value(" \t\t \t\t \t ").is_ok_and(|val| matches!(val, Value::Blank)) - ); - } - #[test] - fn wildcard_parse_value() { - // Test bad asterisk. - assert!( - Wildcard::parse_value("*") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) - ); - assert!( - Wildcard::parse_value("www*.example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) - ); - assert!( - Wildcard::parse_value("www.*.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) - ); - assert!( - Wildcard::parse_value("*..com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::EmptyLabel)) - ); - assert!( - Wildcard::parse_value("www.com*") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) - ); - assert!( - Wildcard::parse_value("ww*w.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) - ); - // Test case-insensitivity. - assert!( - Wildcard::parse_value("*.wWw.ExamPLE.com").is_ok_and(|val| match val { - Value::Domain(ref dom) => - Wildcard::parse_value("*.www.example.com").is_ok_and(|val2| match val2 { - Value::Domain(ref dom2) => - dom.cmp(dom2).is_eq() - && dom == dom2 - && dom.proper_subdomains - && dom2.proper_subdomains, - Value::Comment(_) | Value::Blank => false, - }), - Value::Comment(_) | Value::Blank => false, - }) - ); - // Test proper subdomains. - assert!( - Wildcard::parse_value("*.www.example.com").is_ok_and(|val| match val { - Value::Domain(ref dom) => - dom.domain.as_bytes() == b"www.example.com" && dom.proper_subdomains, - Value::Comment(_) | Value::Blank => false, - }) - ); - // Test comment. - assert!( - Wildcard::parse_value(" \t\t \t\t \t*.www.example.com#asdflkj asdf alskdfj ") - .is_ok_and(|val| match val { - Value::Domain(ref dom) => - dom.domain.as_bytes() == b"www.example.com" && dom.proper_subdomains, - Value::Comment(_) | Value::Blank => false, - }) - ); - assert!( - Wildcard::parse_value(" \t\t \t\t \twww.example.com #asdflkj asdf alskdfj ") - .is_ok_and(|val| match val { - Value::Domain(ref dom) => - dom.domain.as_bytes() == b"www.example.com" && !dom.proper_subdomains, - Value::Comment(_) | Value::Blank => false, - }) - ); - // Test whitespace removal. - assert!( - Wildcard::parse_value(" \t\t *.www.example.com \t\t \t ").is_ok_and(|val| { - match val { - Value::Domain(ref dom) => { - dom.domain.as_bytes() == b"www.example.com" && dom.proper_subdomains - } - Value::Comment(_) | Value::Blank => false, - } - }) - ); - assert!( - Wildcard::parse_value("\t\t \twww.example.com \t\t \t\t ").is_ok_and(|val| { - match val { - Value::Domain(ref dom) => { - dom.domain.as_bytes() == b"www.example.com" && !dom.proper_subdomains - } - Value::Comment(_) | Value::Blank => false, - } - }) - ); - assert!( - Wildcard::parse_value("www .example.com") - .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) - ); - // Test 127 labels after wildcard error. - assert!(Wildcard::parse_value("*.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_err_and(|e| e == FirefoxDomainErr::InvalidWildcardDomain)); - // Test 126 labels after wildcard is ok. - assert!(Wildcard::parse_value("*.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_ok_and(|val| match val { - Value::Domain(ref dom) => dom.domain.iter().count() == 126 && dom.proper_subdomains, - Value::Comment(_) | Value::Blank => false, - })); - // Test comment. - assert!( - Wildcard::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { - Value::Comment(com) => com == " hi", - Value::Domain(_) | Value::Blank => false, - }) - ); - // Test blank. - assert!( - Wildcard::parse_value(" \t\t \t\t \t ") - .is_ok_and(|val| matches!(val, Value::Blank)) - ); - } - #[test] - fn rpz_parse_value() { - assert!( - RpzDomain::parse_value("*.www.example.com").is_ok_and(|val| { - let dom = val.unwrap_domain(); - dom.is_proper_subdomains() && dom.domain().as_bytes() == b"www.example.com" - }) - ); - assert!( - RpzDomain::parse_value("||www.example.com").is_ok_and(|val| { - let dom = val.unwrap_domain(); - dom.is_subdomains() && dom.domain().as_bytes() == b"www.example.com" - }) - ); - assert!( - RpzDomain::parse_value("0.0.0.0 www.example.com").is_ok_and(|val| { - let dom = val.unwrap_domain(); - !(dom.is_subdomains() || dom.is_proper_subdomains()) - && dom.domain().as_bytes() == b"www.example.com" - }) - ); - assert!(RpzDomain::parse_value("www.example.com").is_ok_and(|val| { - let dom = val.unwrap_domain(); - !(dom.is_subdomains() || dom.is_proper_subdomains()) - && dom.domain().as_bytes() == b"www.example.com" - })); - // Test case-insensitivity. - assert!( - RpzDomain::parse_value("*.Www.ExaMPle.COm").is_ok_and(|val| { - let dom = val.unwrap_domain(); - RpzDomain::parse_value("*.www.example.com").is_ok_and(|val2| { - let dom2 = val2.unwrap_domain(); - dom.is_proper_subdomains() - && dom2.is_proper_subdomains() - && dom == dom2 - && dom.cmp(&dom2).is_eq() - }) - }) - ); - // Test comment. - assert!( - RpzDomain::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { - Value::Comment(com) => com == " hi", - Value::Domain(_) | Value::Blank => false, - }) - ); - assert!( - RpzDomain::parse_value(" \t\t \t\t \t ! hi").is_ok_and(|val| match val { - Value::Comment(com) => com == " hi", - Value::Domain(_) | Value::Blank => false, - }) - ); - // Test blank. - assert!( - RpzDomain::parse_value(" \t\t \t\t \t ") - .is_ok_and(|val| matches!(val, Value::Blank)) - ); - } - #[expect(clippy::expect_used, clippy::unwrap_in_result, reason = "OK in tests")] - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[expect(clippy::nonminimal_bool, reason = "want to test all ord ops")] - #[test] - fn rpz_ord_and_eq() -> Result<(), &'static str> { - "www.bar.com,*.www.bar.com,||www.bar.com,*.bar.com,||bar.com,Example.com,WwW.exaMple.com,*.www.example.com,||www.example.com,*.example.com,||example.com,FOo.coM,Www.foo.com,*.foo.com,*.coM,example.net,*.net".split(',').try_fold(RpzDomain::DomainOnly(DomainOnly::parse_value("bar.com").expect("bug in DomainOnly::parse_value").unwrap_domain()), |prev, slice| { - let cur = if slice.as_bytes()[0] == b'|' { - RpzDomain::Adblock(Adblock::parse_value(slice).expect("Bug in Adblock::parse_value").unwrap_domain()) - } else { - RpzDomain::Wildcard(Wildcard::parse_value(slice).expect("Bug in Wildcard::parse_value").unwrap_domain()) - }; - if prev < cur && cur > prev && prev == prev && cur == cur { - Ok(cur) - } else { - Err("PartialEq or Ord are not correctly implemented for RpzDomain.") - } - }).map(|_| ()) - } - #[expect(clippy::expect_used, reason = "OK in tests")] - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[test] - fn superset_set() { - let mut iter = "*.NeT,*.net,www.bar.com,*.net,*.www.bar.com,||www.bar.com,*.bar.com,||bar.com,example.com,www.example.com,*.www.example.com,||www.example.com,*.example.com,||example.com,foo.com,www.foo.com,*.foo.com,*.com,example.net,*.abc.abc,||aawww.abc,abc.abc".split(',').fold(SupersetSet::new(), |mut doms, slice| { - _ = doms.insert(if slice.as_bytes()[0] == b'|' { - RpzDomain::Adblock(Adblock::parse_value(slice).expect("Bug in Adblock::parse_value").unwrap_domain()) - } else { - RpzDomain::Wildcard(Wildcard::parse_value(slice).expect("Bug in Wildcard::parse_value").unwrap_domain()) - }); - doms - }).into_iter(); - assert!( - iter.next() - .is_some_and(|d| { d.domain().as_bytes() == b"aawww.abc" && d.is_subdomains() }) - ); - assert!( - iter.next() - .is_some_and(|d| { d.domain().as_bytes() == b"abc.abc" && d.is_domain() }) - ); - assert!( - iter.next().is_some_and(|d| { - d.domain().as_bytes() == b"abc.abc" && d.is_proper_subdomains() - }) - ); - assert!( - iter.next() - .is_some_and(|d| { d.domain().as_bytes() == b"com" && d.is_proper_subdomains() }) - ); - assert!( - iter.next() - .is_some_and(|d| { d.domain().as_bytes() == b"NeT" && d.is_proper_subdomains() }) - ); - assert!(iter.next().is_none()); - } - #[test] - fn card() { - // Geometric series. - // We can have two labels each with one character, - // one label with one to three characters, or 0 labels. - // This is 1 + 52 + 52^2 + 52^3 + 52^2 = (1-52^4)/(1-52) + 52^2 = (52^4 - 1)/51 + 52^2 = 146069. - assert!(Adblock::parse_value("||a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_ok_and(|val| { let dom = val.unwrap_domain(); dom.domain.len().get() == 249 && dom.domain.iter().count() == 125 && dom.domain_count() == BigUint::new(vec![146_069]) })); - // A subdomain of length 252 or 253 gets converted to a domain. - assert!(Adblock::parse_value("||a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_ok_and(|val| { let dom = val.unwrap_domain(); dom.domain.iter().count() == 127 && !dom.subdomains && dom.domain_count() == BigUint::new(vec![1]) })); - // Pre-calculated manually. - // This is the number of domains possible between 2 and 252 characters. - assert!(Wildcard::parse_value("*.a").is_ok_and(|val| { - val.unwrap_domain().domain_count() - == BigUint::new(vec![ - 375_288_404, - 2_460_223_985, - 1_334_358_771, - 2_543_621_408, - 2_519_466_280, - 1_133_682_239, - 3_589_178_618, - 348_125_705, - 1_709_233_643, - 958_334_503, - 3_780_539_710, - 2_181_893_897, - 2_457_156_833, - 3_204_765_645, - 2_728_103_430, - 1_817_547_150, - 3_102_358_416, - 444_185_044, - 3_659_003_776, - 10_341_713, - 306_326_206, - 1_336_386_425, - 3_942_332_649, - 2_036_577_878, - 2_460_939_277, - 3_976_861_337, - 2_101_094_571, - 2_241_770_079, - 2_667_853_164, - 3_687_350_273, - 109_356_153, - 3_455_569_358, - 2_333_076_459, - 2_433_207_896, - 1_553_903_141, - 2_621_943_843, - 4_223_295_645, - 1_753_858_368, - 130_924_388, - 965_594_304, - 3_942_586_845, - 1_573_844_087, - 4_237_886_128, - 481_383_133, - 56_931_017, - ]) - })); - } -} diff --git a/src/dom/tests.rs b/src/dom/tests.rs @@ -0,0 +1,449 @@ +use super::{ + Adblock, DomainOnly, FirefoxDomainErr, Hosts, ParsedDomain as _, RpzDomain, Value, Wildcard, +}; +use ascii_domain::dom::DomainErr; +use num_bigint::BigUint; +use superset_map::SupersetSet; +#[test] +fn adblock_parse() { + // Test subdomains. + assert!( + Adblock::parse_value("||www.example.com").is_ok_and(|val| match val { + Value::Domain(ref dom) => dom.subdomains && dom.domain.as_bytes() == b"www.example.com", + Value::Comment(_) | Value::Blank => false, + }) + ); + // Test whitespace and '^' removal. + assert!( + Adblock::parse_value(" \t\t ||\t\t \twww.example.com \t\t ^ \t\t ").is_ok_and( + |val| match val { + Value::Domain(ref dom) => + dom.subdomains && dom.domain.as_bytes() == b"www.example.com", + Value::Comment(_) | Value::Blank => false, + } + ) + ); + assert!( + Adblock::parse_value("\t\t \twww.example.com \t\t \t\t ").is_ok_and(|val| { + match val { + Value::Domain(ref dom) => { + !dom.subdomains && dom.domain.as_bytes() == b"www.example.com" + } + Value::Comment(_) | Value::Blank => false, + } + }) + ); + assert!(Adblock::parse_value("www .example.com").is_err_and( + |err| err == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), + )); + assert!(Adblock::parse_value("||www.ExAMPle.COm").is_ok_and(|val| { + match val { + Value::Domain(ref dom) => { + Adblock::parse_value("||www.example.com").is_ok_and(|val_2| match val_2 { + Value::Domain(ref dom2) => { + dom == dom2 && dom.subdomains && dom2.subdomains && dom.cmp(dom2).is_eq() + } + Value::Comment(_) | Value::Blank => false, + }) + } + Value::Comment(_) | Value::Blank => false, + } + })); + // Test comment + assert!( + Adblock::parse_value(" \t\t #hi").is_ok_and(|val| match val { + Value::Comment(com) => com == "hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + assert!( + Adblock::parse_value(" \t\t !! foo").is_ok_and(|val| match val { + Value::Comment(com) => com == "! foo", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank + assert!(Adblock::parse_value(" \t\t ").is_ok_and(|val| matches!(val, Value::Blank))); +} +#[test] +fn domain_only_parse_value() { + // Test whitespace and comment. + assert!( + DomainOnly::parse_value(" \t\t \t\t \twww.example.com#asdflkj asdf alskdfj ") + .is_ok_and(|val| match val { + Value::Domain(ref dom) => dom.domain.as_bytes() == b"www.example.com", + Value::Comment(_) | Value::Blank => false, + }) + ); + assert!( + DomainOnly::parse_value(" \t\t \t\t \twww.example.com \t\t ^ \t\t ") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) + ); + // Test case-insensitivity. + assert!( + DomainOnly::parse_value("www.ExAMPle.CoM").is_ok_and(|val| match val { + Value::Domain(ref dom) => + DomainOnly::parse_value("www.example.com").is_ok_and(|val2| match val2 { + Value::Domain(ref dom2) => dom.cmp(dom2).is_eq(), + Value::Comment(_) | Value::Blank => false, + }), + Value::Comment(_) | Value::Blank => false, + }) + ); + // Test comment. + assert!( + DomainOnly::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!( + DomainOnly::parse_value(" \t\t \t\t \t ").is_ok_and(|val| matches!(val, Value::Blank)) + ); + // Test blank. + assert!( + DomainOnly::parse_value("example.xn--abc").is_ok_and(|val| matches!(val, Value::Domain(_))) + ); + // Test invalid TLD. + assert!( + DomainOnly::parse_value("www.c1m").is_err_and(|err| err == FirefoxDomainErr::InvalidTld) + ); +} +#[test] +fn hosts_parse_value() { + // Test whitespace and comment. + assert!( + Hosts::parse_value(" \t\t 127.0.0.1\t\t \twww.example.com#asdflkj asdf alskdfj ") + .is_ok_and(|val| match val { + Value::Domain(ref dom) => dom.domain.as_bytes() == b"www.example.com", + Value::Comment(_) | Value::Blank => false, + }) + ); + assert!( + Hosts::parse_value(" \t\t 0.0.0.0\t\t \twww.example.com \t\t ^ \t\t ") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) + ); + assert!( + Hosts::parse_value("::1\twww .example.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) + ); + // Test invalid IP + assert!( + Hosts::parse_value("::2 www.example.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) + ); + assert!( + Hosts::parse_value(":2 www.example.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) + ); + assert!( + Hosts::parse_value("www.example.com").is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) + ); + assert!( + Hosts::parse_value("10.4.2.256 www.example.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) + ); + // Test case-insensitivity. + assert!( + Hosts::parse_value(":: www.ExAMPle.Com").is_ok_and(|val| match val { + Value::Domain(ref dom) => + Hosts::parse_value("127.0.0.1 www.example.com").is_ok_and(|val2| match val2 { + Value::Domain(ref dom2) => dom.cmp(dom2).is_eq(), + Value::Comment(_) | Value::Blank => false, + }), + Value::Comment(_) | Value::Blank => false, + }) + ); + // Test comment. + assert!( + Hosts::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!(Hosts::parse_value(" \t\t \t\t \t ").is_ok_and(|val| matches!(val, Value::Blank))); +} +#[test] +fn wildcard_parse_value() { + // Test bad asterisk. + assert!( + Wildcard::parse_value("*") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) + ); + assert!( + Wildcard::parse_value("www*.example.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) + ); + assert!( + Wildcard::parse_value("www.*.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) + ); + assert!( + Wildcard::parse_value("*..com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::EmptyLabel)) + ); + assert!( + Wildcard::parse_value("www.com*") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) + ); + assert!( + Wildcard::parse_value("ww*w.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*'))) + ); + // Test case-insensitivity. + assert!( + Wildcard::parse_value("*.wWw.ExamPLE.com").is_ok_and(|val| match val { + Value::Domain(ref dom) => + Wildcard::parse_value("*.www.example.com").is_ok_and(|val2| match val2 { + Value::Domain(ref dom2) => + dom.cmp(dom2).is_eq() + && dom == dom2 + && dom.proper_subdomains + && dom2.proper_subdomains, + Value::Comment(_) | Value::Blank => false, + }), + Value::Comment(_) | Value::Blank => false, + }) + ); + // Test proper subdomains. + assert!( + Wildcard::parse_value("*.www.example.com").is_ok_and(|val| match val { + Value::Domain(ref dom) => + dom.domain.as_bytes() == b"www.example.com" && dom.proper_subdomains, + Value::Comment(_) | Value::Blank => false, + }) + ); + // Test comment. + assert!( + Wildcard::parse_value(" \t\t \t\t \t*.www.example.com#asdflkj asdf alskdfj ") + .is_ok_and(|val| match val { + Value::Domain(ref dom) => + dom.domain.as_bytes() == b"www.example.com" && dom.proper_subdomains, + Value::Comment(_) | Value::Blank => false, + }) + ); + assert!( + Wildcard::parse_value(" \t\t \t\t \twww.example.com #asdflkj asdf alskdfj ") + .is_ok_and(|val| match val { + Value::Domain(ref dom) => + dom.domain.as_bytes() == b"www.example.com" && !dom.proper_subdomains, + Value::Comment(_) | Value::Blank => false, + }) + ); + // Test whitespace removal. + assert!( + Wildcard::parse_value(" \t\t *.www.example.com \t\t \t ").is_ok_and(|val| { + match val { + Value::Domain(ref dom) => { + dom.domain.as_bytes() == b"www.example.com" && dom.proper_subdomains + } + Value::Comment(_) | Value::Blank => false, + } + }) + ); + assert!( + Wildcard::parse_value("\t\t \twww.example.com \t\t \t\t ").is_ok_and(|val| { + match val { + Value::Domain(ref dom) => { + dom.domain.as_bytes() == b"www.example.com" && !dom.proper_subdomains + } + Value::Comment(_) | Value::Blank => false, + } + }) + ); + assert!( + Wildcard::parse_value("www .example.com") + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) + ); + // Test 127 labels after wildcard error. + assert!(Wildcard::parse_value("*.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_err_and(|e| e == FirefoxDomainErr::InvalidWildcardDomain)); + // Test 126 labels after wildcard is ok. + assert!(Wildcard::parse_value("*.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_ok_and(|val| match val { + Value::Domain(ref dom) => dom.domain.iter().count() == 126 && dom.proper_subdomains, + Value::Comment(_) | Value::Blank => false, + })); + // Test comment. + assert!( + Wildcard::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!( + Wildcard::parse_value(" \t\t \t\t \t ").is_ok_and(|val| matches!(val, Value::Blank)) + ); +} +#[test] +fn rpz_parse_value() { + assert!( + RpzDomain::parse_value("*.www.example.com").is_ok_and(|val| { + let dom = val.unwrap_domain(); + dom.is_proper_subdomains() && dom.domain().as_bytes() == b"www.example.com" + }) + ); + assert!( + RpzDomain::parse_value("||www.example.com").is_ok_and(|val| { + let dom = val.unwrap_domain(); + dom.is_subdomains() && dom.domain().as_bytes() == b"www.example.com" + }) + ); + assert!( + RpzDomain::parse_value("0.0.0.0 www.example.com").is_ok_and(|val| { + let dom = val.unwrap_domain(); + !(dom.is_subdomains() || dom.is_proper_subdomains()) + && dom.domain().as_bytes() == b"www.example.com" + }) + ); + assert!(RpzDomain::parse_value("www.example.com").is_ok_and(|val| { + let dom = val.unwrap_domain(); + !(dom.is_subdomains() || dom.is_proper_subdomains()) + && dom.domain().as_bytes() == b"www.example.com" + })); + // Test case-insensitivity. + assert!( + RpzDomain::parse_value("*.Www.ExaMPle.COm").is_ok_and(|val| { + let dom = val.unwrap_domain(); + RpzDomain::parse_value("*.www.example.com").is_ok_and(|val2| { + let dom2 = val2.unwrap_domain(); + dom.is_proper_subdomains() + && dom2.is_proper_subdomains() + && dom == dom2 + && dom.cmp(&dom2).is_eq() + }) + }) + ); + // Test comment. + assert!( + RpzDomain::parse_value(" \t\t \t\t \t # hi").is_ok_and(|val| match val { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + assert!( + RpzDomain::parse_value(" \t\t \t\t \t ! hi").is_ok_and(|val| match val { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!( + RpzDomain::parse_value(" \t\t \t\t \t ").is_ok_and(|val| matches!(val, Value::Blank)) + ); +} +#[expect(clippy::expect_used, clippy::unwrap_in_result, reason = "OK in tests")] +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[expect(clippy::nonminimal_bool, reason = "want to test all ord ops")] +#[test] +fn rpz_ord_and_eq() -> Result<(), &'static str> { + "www.bar.com,*.www.bar.com,||www.bar.com,*.bar.com,||bar.com,Example.com,WwW.exaMple.com,*.www.example.com,||www.example.com,*.example.com,||example.com,FOo.coM,Www.foo.com,*.foo.com,*.coM,example.net,*.net".split(',').try_fold(RpzDomain::DomainOnly(DomainOnly::parse_value("bar.com").expect("bug in DomainOnly::parse_value").unwrap_domain()), |prev, slice| { + let cur = if slice.as_bytes()[0] == b'|' { + RpzDomain::Adblock(Adblock::parse_value(slice).expect("Bug in Adblock::parse_value").unwrap_domain()) + } else { + RpzDomain::Wildcard(Wildcard::parse_value(slice).expect("Bug in Wildcard::parse_value").unwrap_domain()) + }; + if prev < cur && cur > prev && prev == prev && cur == cur { + Ok(cur) + } else { + Err("PartialEq or Ord are not correctly implemented for RpzDomain.") + } + }).map(|_| ()) +} +#[expect(clippy::expect_used, reason = "OK in tests")] +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[test] +fn superset_set() { + let mut iter = "*.NeT,*.net,www.bar.com,*.net,*.www.bar.com,||www.bar.com,*.bar.com,||bar.com,example.com,www.example.com,*.www.example.com,||www.example.com,*.example.com,||example.com,foo.com,www.foo.com,*.foo.com,*.com,example.net,*.abc.abc,||aawww.abc,abc.abc".split(',').fold(SupersetSet::new(), |mut doms, slice| { + _ = doms.insert(if slice.as_bytes()[0] == b'|' { + RpzDomain::Adblock(Adblock::parse_value(slice).expect("Bug in Adblock::parse_value").unwrap_domain()) + } else { + RpzDomain::Wildcard(Wildcard::parse_value(slice).expect("Bug in Wildcard::parse_value").unwrap_domain()) + }); + doms + }).into_iter(); + assert!( + iter.next() + .is_some_and(|d| { d.domain().as_bytes() == b"aawww.abc" && d.is_subdomains() }) + ); + assert!( + iter.next() + .is_some_and(|d| { d.domain().as_bytes() == b"abc.abc" && d.is_domain() }) + ); + assert!( + iter.next() + .is_some_and(|d| { d.domain().as_bytes() == b"abc.abc" && d.is_proper_subdomains() }) + ); + assert!( + iter.next() + .is_some_and(|d| { d.domain().as_bytes() == b"com" && d.is_proper_subdomains() }) + ); + assert!( + iter.next() + .is_some_and(|d| { d.domain().as_bytes() == b"NeT" && d.is_proper_subdomains() }) + ); + assert!(iter.next().is_none()); +} +#[test] +fn card() { + // Geometric series. + // We can have two labels each with one character, + // one label with one to three characters, or 0 labels. + // This is 1 + 52 + 52^2 + 52^3 + 52^2 = (1-52^4)/(1-52) + 52^2 = (52^4 - 1)/51 + 52^2 = 146069. + assert!(Adblock::parse_value("||a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_ok_and(|val| { let dom = val.unwrap_domain(); dom.domain.len().get() == 249 && dom.domain.iter().count() == 125 && dom.domain_count() == BigUint::new(vec![146_069]) })); + // A subdomain of length 252 or 253 gets converted to a domain. + assert!(Adblock::parse_value("||a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a").is_ok_and(|val| { let dom = val.unwrap_domain(); dom.domain.iter().count() == 127 && !dom.subdomains && dom.domain_count() == BigUint::new(vec![1]) })); + // Pre-calculated manually. + // This is the number of domains possible between 2 and 252 characters. + assert!(Wildcard::parse_value("*.a").is_ok_and(|val| { + val.unwrap_domain().domain_count() + == BigUint::new(vec![ + 375_288_404, + 2_460_223_985, + 1_334_358_771, + 2_543_621_408, + 2_519_466_280, + 1_133_682_239, + 3_589_178_618, + 348_125_705, + 1_709_233_643, + 958_334_503, + 3_780_539_710, + 2_181_893_897, + 2_457_156_833, + 3_204_765_645, + 2_728_103_430, + 1_817_547_150, + 3_102_358_416, + 444_185_044, + 3_659_003_776, + 10_341_713, + 306_326_206, + 1_336_386_425, + 3_942_332_649, + 2_036_577_878, + 2_460_939_277, + 3_976_861_337, + 2_101_094_571, + 2_241_770_079, + 2_667_853_164, + 3_687_350_273, + 109_356_153, + 3_455_569_358, + 2_333_076_459, + 2_433_207_896, + 1_553_903_141, + 2_621_943_843, + 4_223_295_645, + 1_753_858_368, + 130_924_388, + 965_594_304, + 3_942_586_845, + 1_573_844_087, + 4_237_886_128, + 481_383_133, + 56_931_017, + ]) + })); +} diff --git a/src/file.rs b/src/file.rs @@ -8,6 +8,7 @@ use core::{ error::Error, fmt::{self, Display, Formatter}, hash::Hash, + io::ErrorKind, ops::Deref, time::Duration, }; @@ -15,8 +16,7 @@ use reqwest::Client; use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor}; use std::{ collections::{HashMap, HashSet}, - fs, - io::{self, ErrorKind}, + fs, io, path::{Path, PathBuf}, }; use superset_map::SupersetSet; diff --git a/src/lib.rs b/src/lib.rs @@ -27,7 +27,7 @@ unstable_features, reason = "the only reason we require nightly is for the below features" )] -#![feature(io_error_more)] +#![feature(core_io, io_error_more)] /// Module for hostname-like domains including parsing [`str`]s /// from a variety of formats. pub mod dom; diff --git a/src/main.rs b/src/main.rs @@ -9,7 +9,7 @@ clippy::multiple_crate_versions, reason = "dependencies haven't updated to newest crates" )] -#![feature(never_type)] +#![feature(core_io, never_type)] /// Contains a wrapper of block and unblock `RpzDomain`s /// which can be used to write to a `File` or `stdout`. mod app; @@ -20,6 +20,9 @@ mod config; /// Contains functions for `pledge(2)` and `unveil(2)` on OpenBSD platforms when compiled /// with the `priv_sep` feature; otherwise almost all functions are no-ops. mod priv_sep; +/// Unit tests. +#[cfg(test)] +mod tests; use crate::{ app::Domains, args::{ArgsErr, ConfigPath, Opts}, @@ -425,152 +428,3 @@ fn main() -> Result<(), E> { .map_err(E::Io) } } -#[cfg(test)] -pub(crate) mod test_prog { - use std::{fs, process::Command}; - /// The path to the program dir with no block subdirectories. - pub(crate) const PROG_DIR_NO_SUB: &str = "/home/zack/projects/rpz/target/"; - /// The path to the program dir with block subdirectories. - pub(crate) const PROG_DIR: &str = "/home/zack/projects/rpz/target/release/"; - /// The path to the program. - const PROG: &str = "/home/zack/projects/rpz/target/release/rpz"; - /// Message to append to the appropriate variable above if an issue occurs. - pub(crate) const ERR_MSG: &str = " does not exist, so program testing cannot occur"; - /// Verify the correct directories and files exist. - #[expect(clippy::panic, reason = "OK in tests")] - pub(crate) fn verify_files() { - if !fs::metadata(PROG) - .unwrap_or_else(|_| panic!("{PROG}{ERR_MSG}")) - .is_file() - { - panic!("{PROG} is not an executable file") - } else if !fs::metadata(PROG_DIR) - .unwrap_or_else(|_| panic!("{PROG_DIR}{ERR_MSG}")) - .is_dir() - { - panic!("{PROG_DIR} is not a directory") - } else if !fs::metadata(PROG_DIR_NO_SUB) - .unwrap_or_else(|_| panic!("{PROG_DIR_NO_SUB}{ERR_MSG}")) - .is_dir() - { - panic!("{PROG_DIR_NO_SUB} is not a directory") - } else { - let mut files = String::from(PROG_DIR); - let len = files.len(); - files.push_str("block/adblock/foo"); - if fs::metadata(files.as_str()) - .unwrap_or_else(|_| panic!("{files}{ERR_MSG}. it must only contain '||bar.com'")) - .is_file() - { - files.truncate(len); - files.push_str("block/domain/foo"); - if fs::metadata(files.as_str()) - .unwrap_or_else(|_| { - panic!("{files}{ERR_MSG}. it must only contain 'www.example.com'") - }) - .is_file() - { - files.truncate(len); - files.push_str("unblock/hosts/foo"); - if fs::metadata(files.as_str()) - .unwrap_or_else(|_| { - panic!("{files}{ERR_MSG}. it must only contain '0.0.0.0 www.bar.com'") - }) - .is_file() - { - files.truncate(len); - files.push_str("unblock/wildcard/foo"); - if !fs::metadata(files.as_str()) - .unwrap_or_else(|_| { - panic!("{files}{ERR_MSG}. it must only contain '*.foo.com'") - }) - .is_file() - { - panic!("{files} is not a file"); - } - } else { - panic!("{files} is not a file"); - } - } else { - panic!("{files} is not a file"); - } - } else { - panic!("{files} is not a file"); - } - } - } - pub(crate) fn get_command() -> Command { - Command::new(PROG) - } -} -#[cfg(test)] -mod tests { - use crate::{E, test_prog}; - use std::{io::Write as _, process::Stdio, thread}; - #[test] - #[ignore = "performs I/O"] - fn app() { - test_prog::verify_files(); - assert!( - test_prog::get_command() - .args(["-f", "-"]) - .stderr(Stdio::piped()) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .spawn() - .is_ok_and(|mut cmd| { - cmd.stdin.take().is_some_and(|mut stdin| { - thread::spawn(move || { - stdin - .write_all( - format!("local_dir=\"{}\"", test_prog::PROG_DIR_NO_SUB) - .as_bytes(), - ) - .is_ok() - }) - .join() - .is_ok_and(|v| v) - }) && cmd.wait_with_output().is_ok_and(|output| { - !output.status.success() - && output.stderr - == format!("Error: {:?}\n", E::NoBlockEntries).into_bytes() - }) - }) - ); - assert!(test_prog::get_command() - .args(["-vf", "-"]) - .stderr(Stdio::null()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .is_ok_and(|mut cmd| { - cmd.stdin.take().is_some_and(|mut stdin| { - thread::spawn(move || { - stdin - .write_all(format!("local_dir=\"{}\"", test_prog::PROG_DIR).as_bytes()) - .is_ok() - }) - .join() - .is_ok_and(|v| v) - }) && cmd.wait_with_output().is_ok_and(|output| { - output.status.success() - && output.stdout - == b"www.bar.com CNAME rpz-passthru. -bar.com CNAME . -*.bar.com CNAME . -www.example.com CNAME . -(Hosts) /home/zack/projects/rpz/target/release/unblock/hosts/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 -(Wildcard) /home/zack/projects/rpz/target/release/unblock/wildcard/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 -(Adblock) /home/zack/projects/rpz/target/release/block/adblock/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 -(Domain-only) /home/zack/projects/rpz/target/release/block/domain/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 -unblock count written: 1 -block count written: 3 -total lines written: 4 -domains parsed: 4 -comments parsed: 0 -blanks parsed: 0 -parsing errors: 0\n" - }) - })); - } -} diff --git a/src/priv_sep.rs b/src/priv_sep.rs @@ -4,12 +4,14 @@ extern crate alloc; use alloc::ffi::CString; #[cfg(target_os = "openbsd")] use core::ffi::CStr; +#[cfg(not(target_os = "openbsd"))] +use core::io::ErrorKind; #[cfg(target_os = "openbsd")] use priv_sep::{Errno, Permissions, Promise, Promises}; #[cfg(target_os = "openbsd")] use std::env; #[cfg(not(target_os = "openbsd"))] -use std::{fs, io::ErrorKind}; +use std::fs; use std::{io::Error, path::Path}; /// Calls `pledge` with only the sys calls necessary for a minimal application /// to run. Specifically, the `Promise`s `Cpath`, `Dns`, `Inet`, `Rpath`, `Stdio`, `Unveil`, and `Wpath` diff --git a/src/tests.rs b/src/tests.rs @@ -0,0 +1,143 @@ +use crate::E; +use std::{ + fs, + io::Write as _, + process::{Command, Stdio}, + thread, +}; +/// The path to the program dir with no block subdirectories. +pub(crate) const PROG_DIR_NO_SUB: &str = "/home/zack/projects/rpz/target/"; +/// The path to the program dir with block subdirectories. +pub(crate) const PROG_DIR: &str = "/home/zack/projects/rpz/target/release/"; +/// The path to the program. +const PROG: &str = "/home/zack/projects/rpz/target/release/rpz"; +/// Message to append to the appropriate variable above if an issue occurs. +pub(crate) const ERR_MSG: &str = " does not exist, so program testing cannot occur"; +/// Verify the correct directories and files exist. +#[expect(clippy::panic, reason = "OK in tests")] +pub(crate) fn verify_files() { + if !fs::metadata(PROG) + .unwrap_or_else(|_| panic!("{PROG}{ERR_MSG}")) + .is_file() + { + panic!("{PROG} is not an executable file") + } else if !fs::metadata(PROG_DIR) + .unwrap_or_else(|_| panic!("{PROG_DIR}{ERR_MSG}")) + .is_dir() + { + panic!("{PROG_DIR} is not a directory") + } else if !fs::metadata(PROG_DIR_NO_SUB) + .unwrap_or_else(|_| panic!("{PROG_DIR_NO_SUB}{ERR_MSG}")) + .is_dir() + { + panic!("{PROG_DIR_NO_SUB} is not a directory") + } else { + let mut files = String::from(PROG_DIR); + let len = files.len(); + files.push_str("block/adblock/foo"); + if fs::metadata(files.as_str()) + .unwrap_or_else(|_| panic!("{files}{ERR_MSG}. it must only contain '||bar.com'")) + .is_file() + { + files.truncate(len); + files.push_str("block/domain/foo"); + if fs::metadata(files.as_str()) + .unwrap_or_else(|_| { + panic!("{files}{ERR_MSG}. it must only contain 'www.example.com'") + }) + .is_file() + { + files.truncate(len); + files.push_str("unblock/hosts/foo"); + if fs::metadata(files.as_str()) + .unwrap_or_else(|_| { + panic!("{files}{ERR_MSG}. it must only contain '0.0.0.0 www.bar.com'") + }) + .is_file() + { + files.truncate(len); + files.push_str("unblock/wildcard/foo"); + if !fs::metadata(files.as_str()) + .unwrap_or_else(|_| { + panic!("{files}{ERR_MSG}. it must only contain '*.foo.com'") + }) + .is_file() + { + panic!("{files} is not a file"); + } + } else { + panic!("{files} is not a file"); + } + } else { + panic!("{files} is not a file"); + } + } else { + panic!("{files} is not a file"); + } + } +} +pub(crate) fn get_command() -> Command { + Command::new(PROG) +} +#[test] +#[ignore = "performs I/O"] +fn app() { + verify_files(); + assert!( + get_command() + .args(["-f", "-"]) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .spawn() + .is_ok_and(|mut cmd| { + cmd.stdin.take().is_some_and(|mut stdin| { + thread::spawn(move || { + stdin + .write_all(format!("local_dir=\"{PROG_DIR_NO_SUB}\"").as_bytes()) + .is_ok() + }) + .join() + .is_ok_and(|v| v) + }) && cmd.wait_with_output().is_ok_and(|output| { + !output.status.success() + && output.stderr == format!("Error: {:?}\n", E::NoBlockEntries).into_bytes() + }) + }) + ); + assert!(get_command() + .args(["-vf", "-"]) + .stderr(Stdio::null()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .is_ok_and(|mut cmd| { + cmd.stdin.take().is_some_and(|mut stdin| { + thread::spawn(move || { + stdin + .write_all(format!("local_dir=\"{PROG_DIR}\"").as_bytes()) + .is_ok() + }) + .join() + .is_ok_and(|v| v) + }) && cmd.wait_with_output().is_ok_and(|output| { + output.status.success() + && output.stdout + == b"www.bar.com CNAME rpz-passthru. +bar.com CNAME . +*.bar.com CNAME . +www.example.com CNAME . +(Hosts) /home/zack/projects/rpz/target/release/unblock/hosts/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 +(Wildcard) /home/zack/projects/rpz/target/release/unblock/wildcard/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 +(Adblock) /home/zack/projects/rpz/target/release/block/adblock/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 +(Domain-only) /home/zack/projects/rpz/target/release/block/domain/foo - domains parsed: 1, comments parsed: 0, blanks parsed: 0, parsing errors: 0 +unblock count written: 1 +block count written: 3 +total lines written: 4 +domains parsed: 4 +comments parsed: 0 +blanks parsed: 0 +parsing errors: 0\n" + }) + })); +}