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:
| M | Cargo.toml | | | 32 | +++++++++++++++++++++++--------- |
| M | src/args.rs | | | 270 | +------------------------------------------------------------------------------ |
| A | src/args/tests.rs | | | 263 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/config.rs | | | 139 | +++---------------------------------------------------------------------------- |
| A | src/config/tests.rs | | | 127 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/dom.rs | | | 481 | +------------------------------------------------------------------------------ |
| A | src/dom/tests.rs | | | 449 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | src/file.rs | | | 4 | ++-- |
| M | src/lib.rs | | | 2 | +- |
| M | src/main.rs | | | 154 | +++---------------------------------------------------------------------------- |
| M | src/priv_sep.rs | | | 4 | +++- |
| A | src/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"
+ })
+ }));
+}