rpz

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

commit 077af97bbeb8f467def905c19a9a5ea0c8b43043
parent 60bc4bf481e42e8f074051eff90bf32610c84b96
Author: Zack Newman <zack@philomathiclife.com>
Date:   Wed, 18 Mar 2026 18:06:44 -0600

update deps and lints. address lints and new deps

Diffstat:
MCargo.toml | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
MREADME.md | 13++++++-------
Msrc/app.rs | 2+-
Msrc/args.rs | 54+++++++++++++++++++++++++++---------------------------
Msrc/config.rs | 122+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/dom.rs | 332+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/file.rs | 6+++---
Msrc/lib.rs | 9+++++++--
Msrc/main.rs | 184++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/priv_sep.rs | 194++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
10 files changed, 640 insertions(+), 469 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,59 +9,108 @@ license = "MIT OR Apache-2.0" name = "rpz" readme = "README.md" repository = "https://git.philomathiclife.com/repos/rpz/" -version = "1.4.1" +version = "2.1.0" [lints.rust] -ambiguous_negative_literals = { level = "deny", priority = -1 } -closure_returning_async_block = { level = "deny", priority = -1 } -deprecated_safe = { level = "deny", priority = -1 } -deref_into_dyn_supertrait = { level = "deny", priority = -1 } -ffi_unwind_calls = { level = "deny", priority = -1 } -future_incompatible = { level = "deny", priority = -1 } -#fuzzy_provenance_casts = { level = "deny", priority = -1 } -impl_trait_redundant_captures = { level = "deny", priority = -1 } -keyword_idents = { level = "deny", priority = -1 } -let_underscore = { 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 } -meta_variable_misuse = { level = "deny", priority = -1 } -missing_copy_implementations = { level = "deny", priority = -1 } -missing_debug_implementations = { level = "deny", priority = -1 } -missing_docs = { level = "deny", priority = -1 } -#multiple_supertrait_upcastable = { level = "deny", priority = -1 } -#must_not_suspend = { level = "deny", priority = -1 } -non_ascii_idents = { level = "deny", priority = -1 } -#non_exhaustive_omitted_patterns = { level = "deny", priority = -1 } -nonstandard_style = { level = "deny", priority = -1 } -redundant_imports = { level = "deny", priority = -1 } -redundant_lifetimes = { level = "deny", priority = -1 } -refining_impl_trait = { level = "deny", priority = -1 } -rust_2018_compatibility = { level = "deny", priority = -1 } -rust_2018_idioms = { level = "deny", priority = -1 } -rust_2021_compatibility = { level = "deny", priority = -1 } -rust_2024_compatibility = { level = "deny", priority = -1 } -single_use_lifetimes = { level = "deny", priority = -1 } -#supertrait_item_shadowing_definition = { level = "deny", priority = -1 } -trivial_casts = { level = "deny", priority = -1 } -trivial_numeric_casts = { level = "deny", priority = -1 } -unit_bindings = { level = "deny", priority = -1 } -unnameable_types = { level = "deny", priority = -1 } -#unqualified_local_imports = { level = "deny", priority = -1 } -unreachable_pub = { level = "deny", priority = -1 } -unsafe_code = { level = "deny", priority = -1 } -unstable_features = { level = "deny", priority = -1 } +deprecated-safe = { level = "deny", priority = -1 } +future-incompatible = { level = "deny", priority = -1 } +keyword-idents = { level = "deny", priority = -1 } +let-underscore = { level = "deny", priority = -1 } +nonstandard-style = { level = "deny", priority = -1 } +refining-impl-trait = { level = "deny", priority = -1 } +rust-2018-compatibility = { level = "deny", priority = -1 } +rust-2018-idioms = { level = "deny", priority = -1 } +rust-2021-compatibility = { level = "deny", priority = -1 } +rust-2024-compatibility = { level = "deny", priority = -1 } +unknown-or-malformed-diagnostic-attributes = { level = "deny", priority = -1 } unused = { level = "deny", priority = -1 } -unused_crate_dependencies = { level = "deny", priority = -1 } -unused_import_braces = { level = "deny", priority = -1 } -unused_lifetimes = { level = "deny", priority = -1 } -unused_qualifications = { level = "deny", priority = -1 } -unused_results = { level = "deny", priority = -1 } -variant_size_differences = { level = "deny", priority = -1 } warnings = { level = "deny", priority = -1 } +ambiguous-negative-literals = { level = "deny", priority = -1 } +closure-returning-async-block = { level = "deny", priority = -1 } +deprecated-in-future = { level = "deny", priority = -1 } +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-messages = { level = "deny", priority = -1 } +#lossy-provenance-casts = { level = "deny", priority = -1 } +macro-use-extern-crate = { level = "deny", priority = -1 } +meta-variable-misuse = { level = "deny", priority = -1 } +missing-copy-implementations = { level = "deny", priority = -1 } +missing-debug-implementations = { level = "deny", priority = -1 } +missing-docs = { level = "deny", priority = -1 } +#multiple-supertrait-upcastable = { level = "deny", priority = -1 } +#must-not-suspend = { level = "deny", priority = -1 } +non-ascii-idents = { level = "deny", priority = -1 } +#non-exhaustive-omitted-patterns = { level = "deny", priority = -1 } +redundant-imports = { level = "deny", priority = -1 } +redundant-lifetimes = { level = "deny", priority = -1 } +#resolving-to-items-shadowing-supertrait-items = { level = "deny", priority = -1 } +#shadowing-supertrait-items = { level = "deny", priority = -1 } +single-use-lifetimes = { level = "deny", priority = -1 } +trivial-casts = { level = "deny", priority = -1 } +trivial-numeric-casts = { level = "deny", priority = -1 } +unit-bindings = { level = "deny", priority = -1 } +unnameable-types = { level = "deny", priority = -1 } +#unqualified-local-imports = { level = "deny", priority = -1 } +unreachable-pub = { level = "deny", priority = -1 } +unsafe-code = { level = "deny", priority = -1 } +unstable-features = { level = "deny", priority = -1 } +unused-crate-dependencies = { level = "deny", priority = -1 } +unused-import-braces = { level = "deny", priority = -1 } +unused-lifetimes = { level = "deny", priority = -1 } +unused-qualifications = { level = "deny", priority = -1 } +unused-results = { level = "deny", priority = -1 } +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 } +#arithmetic-overflow = { level = "allow", priority = -1 } +#binary-asm-labels = { level = "allow", priority = -1 } +#bindings-with-variant-name = { level = "allow", priority = -1 } +#conflicting-repr-hints = { level = "allow", priority = -1 } +#dangerous-implicit-autorefs = { level = "allow", 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 } +#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 } +#ill-formed-attribute-input = { level = "allow", priority = -1 } +#incomplete-include = { level = "allow", priority = -1 } +#ineffective-unstable-trait-impl = { level = "allow", priority = -1 } +#invalid-atomic-ordering = { level = "allow", priority = -1 } +#invalid-from-utf8-unchecked = { level = "allow", priority = -1 } +#invalid-macro-export-arguments = { level = "allow", priority = -1 } +#invalid-null-arguments = { level = "allow", priority = -1 } +#invalid-reference-casting = { level = "allow", priority = -1 } +#invalid-type-param-default = { level = "allow", priority = -1 } +#legacy-derive-helpers = { level = "allow", priority = -1 } +#let-underscore-lock = { level = "allow", priority = -1 } +#long-running-const-eval = { level = "allow", priority = -1 } +#macro-expanded-macro-exports-accessed-by-absolute-paths = { level = "allow", priority = -1 } +#mutable-transmutes = { level = "allow", priority = -1 } +#named-asm-labels = { level = "allow", priority = -1 } +#never-type-fallback-flowing-into-unsafe = { level = "allow", priority = -1 } +#no-mangle-const-items = { level = "allow", priority = -1 } +#out-of-scope-macro-calls = { level = "allow", priority = -1 } +#overflowing-literals = { level = "allow", priority = -1 } +#patterns-in-fns-without-body = { level = "allow", priority = -1 } +#proc-macro-derive-resolution-fallback = { level = "allow", 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 } +#unknown-crate-types = { level = "allow", priority = -1 } +#useless-deprecated = { level = "allow", priority = -1 } +# Before publishing to crates.io, comment below. [lints.clippy] -all = { level = "deny", priority = -1 } cargo = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 } correctness = { level = "deny", priority = -1 } @@ -88,28 +137,46 @@ single_call_fn = "allow" single_char_lifetime_names = "allow" unseparated_literal_suffix = "allow" -[dependencies] -ascii_domain = { version = "0.6.6", default-features = false } -num-bigint = { version = "0.4.6", default-features = false } -priv_sep = { version = "3.0.0-alpha.1.3", default-features = false, optional = true } -reqwest = { version = "0.12.23", default-features = false, features = ["brotli", "deflate", "gzip", "rustls-tls-native-roots", "trust-dns"] } -serde = { version = "1.0.219", default-features = false } -superset_map = { version = "0.3.4", default-features = false } -tokio = { version = "1.47.1", default-features = false, features = ["rt", "time"] } -toml = { version = "0.9.5", default-features = false, features = ["parse", "serde"] } -url = { version = "2.5.7", default-features = false, features = ["serde"] } -zfc = { version = "0.4.4", default-features = false } +[lints.rustdoc] +# Before publishing to crates.io, comment below and uncomment below that. +all = { level = "deny", priority = -1 } +#all = "allow" +[package.metadata.docs.rs] +default-target = "x86_64-unknown-linux-gnu" +targets = [ + "aarch64-apple-darwin", + "aarch64-pc-windows-msvc", + "aarch64-unknown-linux-gnu", + "i686-pc-windows-msvc", + "i686-unknown-linux-gnu", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "x86_64-unknown-freebsd", + "x86_64-unknown-linux-musl", + "x86_64-unknown-netbsd" +] -### FEATURES ################################################################# +[dependencies] +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"] } +url = { version = "2.5.8", default-features = false, features = ["serde"] } +zfc = { version = "0.4.7", default-features = false } -[features] -default = ["priv_sep"] +[target.'cfg(not(target_os = "openbsd"))'.dependencies] +reqwest = { version = "0.13.2", default-features = false, features = ["brotli", "deflate", "gzip", "http2", "rustls"] } -# Provide pledge and unveil for the binary crate on OpenBSD platforms. -priv_sep = ["dep:priv_sep"] +[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"] } [profile.release] +codegen-units = 1 lto = true panic = 'abort' strip = true diff --git a/README.md b/README.md @@ -210,15 +210,14 @@ at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. -Before any PR is sent, `cargo clippy` and `cargo t` should be run for both `--no-default-features` and -`--all-features`. Additionally `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features` should be run to -ensure documentation can be built. +Before any PR is sent, `cargo clippy --all-targets`, `cargo test --all-targets`, and `cargo test --doc` should be +run _for each possible combination of "features"_ using the nightly toolchain. One easy way to achieve this +is by invoking [`ci-cargo`](https://crates.io/crates/ci-cargo) as +`ci-cargo clippy --all-targets test --all-targets --include-ignored` in the `rpz` directory. + +Last, `cargo +nightly doc` should be run to ensure documentation can be built. ### Status The crate is only tested on the `x86_64-unknown-linux-gnu`, `x86_64-unknown-openbsd`, and `aarch64-apple-darwin` targets; but it should work on most platforms. - -Nightly `rustc` is required. Once `BTreeMap` [cursors are stabilized](https://github.com/rust-lang/rust/issues/107540), stable `rustc` will work. -On OpenBSD-stable, one can use the `rust` port as long as `RUSTC_BOOTSTRAP` is `export`ed with a value of `1` before invoking -`cargo build --all-features --release` or `cargo install --all-features rpz`. diff --git a/src/app.rs b/src/app.rs @@ -161,7 +161,7 @@ impl<'unblock, 'block> Domains<'unblock, 'block> { summaries } /// Writes all necessary unblock `RpzDomain`s as `<domain> CNAME rpz-passthru.` - /// followed by all block `RpzDomain`s as `<domain> CNAME .` + /// followed by all block `RpzDomain`s as `<domain> CNAME .`. /// /// When subdomains exist, `*.<domain>` is used. /// diff --git a/src/args.rs b/src/args.rs @@ -197,12 +197,13 @@ impl Opts { mod tests { use crate::{ArgsErr, E, test_prog}; use core::convert; - use std::io::Write; + use std::io::Write as _; use std::process::Stdio; use std::thread; + #[expect(clippy::too_many_lines, reason = "a lot to test")] #[test] - #[ignore] - fn test_args() { + #[ignore = "requires I/O"] + fn args() { test_prog::verify_files(); assert!( test_prog::get_command() @@ -210,7 +211,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::NoArgs)).into_bytes() @@ -223,7 +224,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -237,7 +238,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -251,7 +252,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -265,7 +266,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -279,7 +280,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -293,7 +294,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -307,7 +308,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::ConfigPathNotPassed)) @@ -321,7 +322,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) @@ -335,7 +336,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!( @@ -352,7 +353,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::MoreThanOneOption)) @@ -366,7 +367,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) @@ -380,7 +381,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!("Error: {:?}\n", E::Args(ArgsErr::InvalidConfigPath)) @@ -394,7 +395,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| { + .is_ok_and(|output| { !output.status.success() && output.stderr == format!( @@ -411,7 +412,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::null()) .output() - .map_or(false, |output| !output.status.success() + .is_ok_and(|output| !output.status.success() && output.stderr.get(..23) == Some(b"Error: TOML parse error")) ); assert!( @@ -421,16 +422,16 @@ mod tests { .stdin(Stdio::piped()) .stdout(Stdio::null()) .spawn() - .map_or(false, |mut cmd| { - cmd.stdin.take().map_or(false, |mut stdin| { + .is_ok_and(|mut cmd| { + cmd.stdin.take().is_some_and(|mut stdin| { thread::spawn(move || { stdin .write_all(b"junk") - .map_or(false, |_| stdin.flush().map_or(false, |_| true)) + .is_ok_and(|()| stdin.flush().is_ok()) }) .join() - .map_or(false, convert::identity) - }) && cmd.wait_with_output().map_or(false, |output| { + .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") }) @@ -443,9 +444,8 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::piped()) .output() - .map_or(false, |output| output.status.success() - && output.stdout.get(..crate::HELP.len()) - == Some(crate::HELP.as_bytes())) + .is_ok_and(|output| output.status.success() + && output.stdout.get(..crate::HELP.len()) == Some(crate::HELP.as_bytes())) ); assert!( test_prog::get_command() @@ -454,7 +454,7 @@ mod tests { .stdin(Stdio::null()) .stdout(Stdio::piped()) .output() - .map_or(false, |output| output.status.success() + .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 @@ -300,114 +300,120 @@ impl<'de> Deserialize<'de> for Config { #[cfg(test)] mod tests { use crate::Config; + #[expect(clippy::unwrap_used, reason = "OK in tests")] #[test] - fn test_missing_fields() { - assert!(toml::from_str::<Config>("").is_err()); - assert!(toml::from_str::<Config>("timeout=15").is_err()); - assert!(toml::from_str::<Config>(r#"rpz="/foo""#).is_err()); - assert!(toml::from_str::<Config>(r#"local_dir="/foo/""#).is_ok()); - assert!(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).is_ok()); + 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 test_invalid_fields() { - assert!(toml::from_str::<Config>("bob=15").is_err()); - assert!(toml::from_str::<Config>(r#"foo=["https://foo.com/foo"]"#).is_err()); + 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 test_timeout() { - assert!(toml::from_str::<Config>(r#"local_dir="/foo/""#).is_ok()); - assert!( + fn timeout() { + drop(toml::from_str::<Config>(r#"local_dir="/foo/""#).unwrap()); + drop( toml::from_str::<Config>( r#"timeout=15 -local_dir="/foo/""# +local_dir="/foo/""#, ) - .is_ok() + .unwrap(), ); - assert!( + drop( toml::from_str::<Config>( r#"timeout=4294967295 -local_dir="/foo/""# +local_dir="/foo/""#, ) - .is_ok() + .unwrap(), ); - assert!( + drop( toml::from_str::<Config>( r#"timeout=-1 -local_dir="/foo/""# +local_dir="/foo/""#, ) - .is_err() + .unwrap_err(), ); - assert!( + drop( toml::from_str::<Config>( r#"timeout=0 -local_dir="/foo/""# +local_dir="/foo/""#, ) - .is_ok() + .unwrap(), ); - assert!( + drop( toml::from_str::<Config>( r#"timeout=4294967296 -local_dir="/foo/""# +local_dir="/foo/""#, ) - .is_err() + .unwrap_err(), ); } + #[expect(clippy::unwrap_used, reason = "OK in tests")] #[test] - fn test_arrays() { - assert!( + fn arrays() { + drop( toml::from_str::<Config>(r#"adblock=["https://foo.com/foo","https://foo.com/foo"]"#) - .is_err() + .unwrap_err(), ); - assert!(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).is_ok()); - assert!( + 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"]"# +domain=["https://foo.com/foo"]"#, ) - .is_err() + .unwrap_err(), ); } + #[expect(clippy::unwrap_used, reason = "OK in tests")] #[test] - fn test_urls() { - assert!(toml::from_str::<Config>(r#"adblock=["file://foo.com/foo"]"#).is_err()); - assert!(toml::from_str::<Config>(r#"adblock=["foo.com/foo"]"#).is_err()); - assert!(toml::from_str::<Config>(r#"adblock=["http://foo.com/foo"]"#).is_ok()); - assert!( + 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"]"# +domain=["https:///foo"]"#, ) - .is_ok() + .unwrap(), ); - assert!(toml::from_str::<Config>(r#"adblock=[""]"#).is_err()); - assert!(toml::from_str::<Config>(r#"adblock=["https://"]"#).is_err()); - assert!(toml::from_str::<Config>(r#"adblock=["ftp://foo.com/foo"]"#).is_err()); + 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 test_paths() { - assert!( + fn paths() { + drop( toml::from_str::<Config>( r#"rpz="/foo/" -wildcard=["https://foo.com/foo"]"# +wildcard=["https://foo.com/foo"]"#, ) - .is_err() + .unwrap_err(), ); - assert!( + drop( toml::from_str::<Config>( r#"rpz="foo" -wildcard=["https://foo.com/foo"]"# +wildcard=["https://foo.com/foo"]"#, ) - .is_err() + .unwrap_err(), ); - assert!( + drop( toml::from_str::<Config>( r#"rpz="/foo" -wildcard=["https://foo.com/foo"]"# +wildcard=["https://foo.com/foo"]"#, ) - .is_ok() + .unwrap(), ); - assert!(toml::from_str::<Config>(r#"local_dir="foo/""#).is_err()); - assert!(toml::from_str::<Config>(r#"local_dir="/foo/""#).is_ok()); + 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!( @@ -415,9 +421,9 @@ wildcard=["https://foo.com/foo"]"# r#"local_dir="/foo" wildcard=["https://foo.com/foo"]"# ) - .map_or(false, |config| config.local_dir.map_or(false, |dir| dir - .to_str() - .map_or(false, |val| val == String::from("/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 @@ -1697,7 +1697,7 @@ impl Ord for RpzDomain<'_> { /// For example the following is a sequence of domains in /// ascending order: /// - /// `bar.com`, `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` + /// `bar.com`, `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`. #[inline] fn cmp(&self, other: &Self) -> Ordering { match *self { @@ -1917,16 +1917,16 @@ impl<'a> ParsedDomain<'a> for RpzDomain<'a> { #[cfg(test)] mod tests { use super::{ - Adblock, DomainOnly, FirefoxDomainErr, Hosts, ParsedDomain, RpzDomain, Value, Wildcard, + Adblock, DomainOnly, FirefoxDomainErr, Hosts, ParsedDomain as _, RpzDomain, Value, Wildcard, }; use ascii_domain::dom::DomainErr; use num_bigint::BigUint; use superset_map::SupersetSet; #[test] - fn test_adblock_parse() { + fn adblock_parse() { // Test subdomains. assert!( - Adblock::parse_value("||www.example.com").map_or(false, |val| match val { + 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, @@ -1934,8 +1934,7 @@ mod tests { ); // Test whitespace and '^' removal. assert!( - Adblock::parse_value(" \t\t ||\t\t \twww.example.com \t\t ^ \t\t ").map_or( - false, + 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", @@ -1944,7 +1943,7 @@ mod tests { ) ); assert!( - Adblock::parse_value("\t\t \twww.example.com \t\t \t\t ").map_or(false, |val| { + 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" @@ -1953,66 +1952,60 @@ mod tests { } }) ); - assert!(Adblock::parse_value("www .example.com").map_or_else( + assert!(Adblock::parse_value("www .example.com").is_err_and( |err| err == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), - |_| false )); - assert!( - Adblock::parse_value("||www.ExAMPle.COm").map_or(false, |val| { - match val { - Value::Domain(ref dom) => { - Adblock::parse_value("||www.example.com").map_or(false, |val| match val { - 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, + 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").map_or(false, |val| match val { + 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").map_or(false, |val| match val { + 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 ").map_or(false, |val| matches!(val, Value::Blank))); + assert!(Adblock::parse_value(" \t\t ").is_ok_and(|val| matches!(val, Value::Blank))); } #[test] - fn test_domain_only_parse_value() { + fn domain_only_parse_value() { // Test whitespace and comment. assert!( DomainOnly::parse_value(" \t\t \t\t \twww.example.com#asdflkj asdf alskdfj ") - .map_or(false, |val| match val { + .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 ") - .map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), - |_| false - ) + .is_err_and(|e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' '))) ); // Test case-insensitivity. assert!( - DomainOnly::parse_value("www.ExAMPle.CoM").map_or(false, |val| match val { + DomainOnly::parse_value("www.ExAMPle.CoM").is_ok_and(|val| match val { Value::Domain(ref dom) => - DomainOnly::parse_value("www.example.com").map_or(false, |val2| match val2 { + 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, }), @@ -2021,7 +2014,7 @@ mod tests { ); // Test comment. assert!( - DomainOnly::parse_value(" \t\t \t\t \t # hi").map_or(false, |val| match val { + 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, }) @@ -2029,63 +2022,59 @@ mod tests { // Test blank. assert!( DomainOnly::parse_value(" \t\t \t\t \t ") - .map_or(false, |val| matches!(val, Value::Blank)) + .is_ok_and(|val| matches!(val, Value::Blank)) ); // Test blank. assert!( DomainOnly::parse_value("example.xn--abc") - .map_or(false, |val| matches!(val, Value::Domain(_))) + .is_ok_and(|val| matches!(val, Value::Domain(_))) ); // Test invalid TLD. assert!( DomainOnly::parse_value("www.c1m") - .map_or_else(|err| err == FirefoxDomainErr::InvalidTld, |_| false) + .is_err_and(|err| err == FirefoxDomainErr::InvalidTld) ); } #[test] - fn test_hosts_parse_value() { + 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 ") - .map_or(false, |val| match val { + .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 ") - .map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), - |_| false - ) + .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' '))) ); - assert!(Hosts::parse_value("::1\twww .example.com").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), - |_| false - )); // Test invalid IP assert!( Hosts::parse_value("::2 www.example.com") - .map_or_else(|e| e == FirefoxDomainErr::InvalidHostsIP, |_| false) + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) ); assert!( Hosts::parse_value(":2 www.example.com") - .map_or_else(|e| e == FirefoxDomainErr::InvalidHostsIP, |_| false) + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) ); assert!( Hosts::parse_value("www.example.com") - .map_or_else(|e| e == FirefoxDomainErr::InvalidHostsIP, |_| false) + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) ); assert!( Hosts::parse_value("10.4.2.256 www.example.com") - .map_or_else(|e| e == FirefoxDomainErr::InvalidHostsIP, |_| false) + .is_err_and(|e| e == FirefoxDomainErr::InvalidHostsIP) ); // Test case-insensitivity. assert!( - Hosts::parse_value(":: www.ExAMPle.Com").map_or(false, |val| match val { - Value::Domain(ref dom) => - Hosts::parse_value("127.0.0.1 www.example.com").map_or(false, |val2| match val2 - { + 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, }), @@ -2094,49 +2083,48 @@ mod tests { ); // Test comment. assert!( - Hosts::parse_value(" \t\t \t\t \t # hi").map_or(false, |val| match val { + 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 ") - .map_or(false, |val| matches!(val, Value::Blank)) + Hosts::parse_value(" \t\t \t\t \t ").is_ok_and(|val| matches!(val, Value::Blank)) ); } #[test] - fn test_wildcard_parse_value() { + fn wildcard_parse_value() { // Test bad asterisk. - assert!(Wildcard::parse_value("*").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*')), - |_| false - )); - assert!(Wildcard::parse_value("www*.example.com").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*')), - |_| false - )); - assert!(Wildcard::parse_value("www.*.com").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*')), - |_| false - )); - assert!(Wildcard::parse_value("*..com").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::EmptyLabel), - |_| false - )); - assert!(Wildcard::parse_value("www.com*").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*')), - |_| false - )); - assert!(Wildcard::parse_value("ww*w.com").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b'*')), - |_| false - )); + 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").map_or(false, |val| match val { + Wildcard::parse_value("*.wWw.ExamPLE.com").is_ok_and(|val| match val { Value::Domain(ref dom) => - Wildcard::parse_value("*.www.example.com").map_or(false, |val2| match val2 { + Wildcard::parse_value("*.www.example.com").is_ok_and(|val2| match val2 { Value::Domain(ref dom2) => dom.cmp(dom2).is_eq() && dom == dom2 @@ -2149,7 +2137,7 @@ mod tests { ); // Test proper subdomains. assert!( - Wildcard::parse_value("*.www.example.com").map_or(false, |val| match val { + 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, @@ -2158,7 +2146,7 @@ mod tests { // Test comment. assert!( Wildcard::parse_value(" \t\t \t\t \t*.www.example.com#asdflkj asdf alskdfj ") - .map_or(false, |val| match val { + .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, @@ -2166,7 +2154,7 @@ mod tests { ); assert!( Wildcard::parse_value(" \t\t \t\t \twww.example.com #asdflkj asdf alskdfj ") - .map_or(false, |val| match val { + .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, @@ -2174,7 +2162,7 @@ mod tests { ); // Test whitespace removal. assert!( - Wildcard::parse_value(" \t\t *.www.example.com \t\t \t ").map_or(false, |val| { + 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 @@ -2184,7 +2172,7 @@ mod tests { }) ); assert!( - Wildcard::parse_value("\t\t \twww.example.com \t\t \t\t ").map_or(false, |val| { + 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 @@ -2193,20 +2181,20 @@ mod tests { } }) ); - assert!(Wildcard::parse_value("www .example.com").map_or_else( - |e| e == FirefoxDomainErr::InvalidDomain(DomainErr::InvalidByte(b' ')), - |_| 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").map_or_else(|e| e == FirefoxDomainErr::InvalidWildcardDomain, |_| false)); + 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").map_or(false, |val| match val { + 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").map_or(false, |val| match val { + 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, }) @@ -2214,42 +2202,40 @@ mod tests { // Test blank. assert!( Wildcard::parse_value(" \t\t \t\t \t ") - .map_or(false, |val| matches!(val, Value::Blank)) + .is_ok_and(|val| matches!(val, Value::Blank)) ); } #[test] - fn test_rpz_parse_value() { + fn rpz_parse_value() { assert!( - RpzDomain::parse_value("*.www.example.com").map_or(false, |val| { + 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").map_or(false, |val| { + 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").map_or(false, |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").map_or(false, |val| { + 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").map_or(false, |val| { + RpzDomain::parse_value("*.Www.ExaMPle.COm").is_ok_and(|val| { let dom = val.unwrap_domain(); - RpzDomain::parse_value("*.www.example.com").map_or(false, |val2| { + RpzDomain::parse_value("*.www.example.com").is_ok_and(|val2| { let dom2 = val2.unwrap_domain(); dom.is_proper_subdomains() && dom2.is_proper_subdomains() @@ -2260,13 +2246,13 @@ mod tests { ); // Test comment. assert!( - RpzDomain::parse_value(" \t\t \t\t \t # hi").map_or(false, |val| match val { + 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").map_or(false, |val| match val { + 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, }) @@ -2274,12 +2260,15 @@ mod tests { // Test blank. assert!( RpzDomain::parse_value(" \t\t \t\t \t ") - .map_or(false, |val| matches!(val, Value::Blank)) + .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 test_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(|b| b == ',').try_fold(RpzDomain::DomainOnly(DomainOnly::parse_value("bar.com").expect("bug in DomainOnly::parse_value").unwrap_domain()), |prev, slice| { + 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 { @@ -2292,9 +2281,11 @@ mod tests { } }).map(|_| ()) } + #[expect(clippy::expect_used, reason = "OK in tests")] + #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] #[test] - fn test_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(|b| b == ',').fold(SupersetSet::new(), |mut doms, slice| { + 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 { @@ -2302,45 +2293,88 @@ mod tests { }); doms }).into_iter(); - assert!(iter.next().map_or(false, |d| { - d.domain().as_bytes() == b"aawww.abc" && d.is_subdomains() - })); - assert!(iter.next().map_or(false, |d| { - d.domain().as_bytes() == b"abc.abc" && d.is_domain() - })); - assert!(iter.next().map_or(false, |d| { - d.domain().as_bytes() == b"abc.abc" && d.is_proper_subdomains() - })); - assert!(iter.next().map_or(false, |d| { - d.domain().as_bytes() == b"com" && d.is_proper_subdomains() - })); - assert!(iter.next().map_or(false, |d| { - d.domain().as_bytes() == b"NeT" && d.is_proper_subdomains() - })); + 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 test_card() { + 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").map_or(false, |val| { let dom = val.unwrap_domain(); dom.domain.len().get() == 249 && dom.domain.iter().count() == 125 && dom.domain_count() == BigUint::new(vec![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").map_or(false, |val| { let dom = val.unwrap_domain(); dom.domain.iter().count() == 127 && !dom.subdomains && dom.domain_count() == BigUint::new(vec![1]) })); + 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").map_or(false, |val| { + assert!(Wildcard::parse_value("*.a").is_ok_and(|val| { val.unwrap_domain().domain_count() == BigUint::new(vec![ - 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, - 3589178618, 348125705, 1709233643, 958334503, 3780539710, 2181893897, - 2457156833, 3204765645, 2728103430, 1817547150, 3102358416, 444185044, - 3659003776, 10341713, 306326206, 1336386425, 3942332649, 2036577878, - 2460939277, 3976861337, 2101094571, 2241770079, 2667853164, 3687350273, - 109356153, 3455569358, 2333076459, 2433207896, 1553903141, 2621943843, - 4223295645, 1753858368, 130924388, 965594304, 3942586845, 1573844087, - 4237886128, 481383133, 56931017, + 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 @@ -394,7 +394,7 @@ impl Display for Name { } } } -/// An in-memory file sourced from [`Self::name`]; +/// An in-memory file sourced from [`Self::name`]. #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct File { /// The name/origin of the file. @@ -528,9 +528,9 @@ impl Files { Ok(()) } timeout_per_file = if timeout_per_file == Duration::ZERO { - Duration::from_secs(3600) + Duration::from_hours(1) } else { - timeout_per_file.clamp(Duration::from_secs(1), Duration::from_secs(3600)) + timeout_per_file.clamp(Duration::from_secs(1), Duration::from_hours(1)) }; let mut files = Self { adblock: Vec::with_capacity(ext.adblock.len()), diff --git a/src/lib.rs b/src/lib.rs @@ -16,6 +16,10 @@ //! ad-blocking files into a [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) //! file easier. #![expect( + clippy::doc_paragraphs_missing_punctuation, + reason = "false positive for crate documentation having image links and regex expressions for domain types" +)] +#![expect( clippy::multiple_crate_versions, reason = "dependencies haven't updated to newest crates" )] @@ -23,7 +27,6 @@ unstable_features, reason = "the only reason we require nightly is for the below features" )] -#![feature(btree_cursors)] #![feature(io_error_more)] /// Module for hostname-like domains including parsing [`str`]s /// from a variety of formats. @@ -34,6 +37,8 @@ mod dom_count_auto_gen; /// Module for fetching and parsing local and HTTP(S) files into /// [`dom::RpzDomain`]s. pub mod file; -#[cfg(feature = "priv_sep")] +#[cfg(target_os = "openbsd")] use priv_sep as _; +#[cfg(target_os = "openbsd")] +use rustls as _; use toml as _; diff --git a/src/main.rs b/src/main.rs @@ -10,7 +10,6 @@ reason = "dependencies haven't updated to newest crates" )] #![feature(never_type)] -#![cfg_attr(doc, feature(doc_auto_cfg))] /// Contains a wrapper of block and unblock `RpzDomain`s /// which can be used to write to a `File` or `stdout`. mod app; @@ -26,8 +25,6 @@ use crate::{ args::{ArgsErr, ConfigPath, Opts}, config::Config, }; -#[cfg(all(feature = "priv_sep", not(target_os = "openbsd")))] -use ::priv_sep as _; use ascii_domain as _; use core::{ error::Error, @@ -35,13 +32,20 @@ use core::{ time::Duration, }; use num_bigint as _; -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -use priv_sep::NulOrIoErr; -use reqwest::Client; +use reqwest::{Client, Error as HttpErr}; use rpz::{ dom::FirefoxDomainErr, file::{AbsFilePath, ExtFileErr, ExternalFiles, Files, HttpUrl, LocalFiles, Summary}, }; +#[cfg(target_os = "openbsd")] +use rustls::{ + ClientConfig, Error as TlsErr, RootCertStore, + pki_types::{ + CertificateDer, + pem::{Error as PkiErr, PemObject as _}, + }, + version::TLS13, +}; use std::{ collections::HashSet, fs, @@ -78,25 +82,32 @@ enum E { Args(ArgsErr), /// Variant for errors due to issues with the TOML config file. Config(de::Error), - /// Variant for errors due to calls to `unveil`. - #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] - Unveil(NulOrIoErr), /// Variant for IO errors. Io(io::Error), /// Variant for errors due to downloading external HTTP(S) block files. ExtFile(ExtFileErr), /// Variant when there are no block entries to be written. NoBlockEntries, + /// Variant when there is an issue creating the HTTP(S) client. + HttpClient(HttpErr), + /// Variant when there is a `rustls` issue. + #[cfg(target_os = "openbsd")] + Tls(TlsErr), + /// Variant when there is an issue parsing the root certificate store. + #[cfg(target_os = "openbsd")] + Pki(PkiErr), } impl fmt::Debug for E { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { Self::Args(ref e) => write!(f, "{e}.\nFor more information, try '--help'."), - Self::Config(_) | Self::Io(_) | Self::ExtFile(_) | Self::NoBlockEntries => { - <Self as Display>::fmt(self, f) - } - #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] - Self::Unveil(_) => <Self as Display>::fmt(self, f), + Self::Config(_) + | Self::Io(_) + | Self::ExtFile(_) + | Self::NoBlockEntries + | Self::HttpClient(_) => <Self as Display>::fmt(self, f), + #[cfg(target_os = "openbsd")] + Self::Tls(_) | Self::Pki(_) => <Self as Display>::fmt(self, f), } } } @@ -105,11 +116,14 @@ impl Display for E { match *self { Self::Args(ref e) => e.fmt(f), Self::Config(ref e) => e.fmt(f), - #[cfg(all(feature = "priv_sep", target_os = "openbsd"))] - Self::Unveil(ref err) => write!(f, "unveil(2) failed with {err}"), Self::Io(ref e) => e.fmt(f), Self::ExtFile(ref e) => e.fmt(f), Self::NoBlockEntries => f.write_str("there are no domains to block"), + Self::HttpClient(ref e) => e.fmt(f), + #[cfg(target_os = "openbsd")] + Self::Tls(ref e) => e.fmt(f), + #[cfg(target_os = "openbsd")] + Self::Pki(ref e) => e.fmt(f), } } } @@ -134,13 +148,7 @@ impl From<ExtFileErr> for E { Self::ExtFile(value) } } -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -impl From<NulOrIoErr> for E { - fn from(value: NulOrIoErr) -> Self { - Self::Unveil(value) - } -} -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] impl From<!> for E { fn from(value: !) -> Self { value @@ -187,6 +195,41 @@ fn get_local_files(local_dir: Option<AbsFilePath<true>>) -> Result<Option<LocalF }, ) } +/// Retrieves the HTTP(S) client. +#[cfg(target_os = "openbsd")] +fn https_client() -> Result<Client, E> { + fs::read("/etc/ssl/cert.pem") + .map_err(E::Io) + .and_then(|root_cert_file| { + let mut root_store = RootCertStore::empty(); + CertificateDer::pem_slice_iter(&root_cert_file) + .try_fold((), |(), cert| { + cert.map_err(E::Pki) + .and_then(|c| root_store.add(c).map_err(E::Tls)) + }) + .and_then(|()| { + let mut conf = + ClientConfig::builder_with_protocol_versions([&TLS13].as_slice()) + .with_root_certificates(root_store) + .with_no_client_auth(); + conf.alpn_protocols = vec![vec![b'h', b'2']]; + Client::builder() + .user_agent(USER_AGENT) + .tls_backend_preconfigured(conf) + .build() + .map_err(E::HttpClient) + }) + }) +} +/// Retrieves the HTTP(S) client. +#[cfg(not(target_os = "openbsd"))] +fn https_client() -> Result<Client, E> { + Client::builder() + .user_agent(USER_AGENT) + .tls_backend_rustls() + .build() + .map_err(E::HttpClient) +} /// Downloads block files from HTTP(S) servers. #[expect(clippy::unreachable, reason = "there is a bug and we want to crash")] fn get_external_files( @@ -203,33 +246,25 @@ fn get_external_files( |e| Err(E::Io(e)), |runtime| { runtime.block_on(async { - Files::from_external( - { - let mut files = ExternalFiles::new(); - CLIENT - .set( - Client::builder() - .user_agent(USER_AGENT) - .use_rustls_tls() - .build() - .map_err(ExtFileErr::Http)?, - ) - .unwrap_or_else(|_e| { - unreachable!("there is a bug in OnceLock::set") - }); + match https_client() { + Ok(c) => { + CLIENT.set(c).unwrap_or_else(|_e| { + unreachable!("there is a bug in OnceLock::set") + }); let client = CLIENT .get() .unwrap_or_else(|| unreachable!("there is a bug in OnceLock::get")); + let mut files = ExternalFiles::new(); files.add_adblock(client, adblock); files.add_domain(client, domain); files.add_hosts(client, hosts); files.add_wildcard(client, wildcard); - files - }, - timeout, - ) - .await - .map_err(E::ExtFile) + Files::from_external(files, timeout) + .await + .map_err(E::ExtFile) + } + Err(e) => Err(e), + } }) }, ) @@ -353,7 +388,7 @@ fn main() -> Result<(), E> { .map_err(E::from) .and_then(|()| { get_external_files( - config.timeout.unwrap_or(Duration::from_secs(3600)), + config.timeout.unwrap_or(Duration::from_hours(1)), config.adblock, config.domain, config.hosts, @@ -392,8 +427,7 @@ fn main() -> Result<(), E> { } #[cfg(test)] pub(crate) mod test_prog { - use std::fs; - use std::process::Command; + 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. @@ -403,19 +437,20 @@ pub(crate) mod test_prog { /// 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) - .expect(format!("{PROG}{ERR_MSG}").as_str()) + .unwrap_or_else(|_| panic!("{PROG}{ERR_MSG}")) .is_file() { panic!("{PROG} is not an executable file") } else if !fs::metadata(PROG_DIR) - .expect(format!("{PROG_DIR}{ERR_MSG}").as_str()) + .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) - .expect(format!("{PROG_DIR_NO_SUB}{ERR_MSG}").as_str()) + .unwrap_or_else(|_| panic!("{PROG_DIR_NO_SUB}{ERR_MSG}")) .is_dir() { panic!("{PROG_DIR_NO_SUB} is not a directory") @@ -424,34 +459,31 @@ pub(crate) mod test_prog { let len = files.len(); files.push_str("block/adblock/foo"); if fs::metadata(files.as_str()) - .expect(format!("{files}{ERR_MSG}. it must only contain '||bar.com'").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()) - .expect( - format!("{files}{ERR_MSG}. it must only contain 'www.example.com'") - .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()) - .expect( - format!("{files}{ERR_MSG}. it must only contain '0.0.0.0 www.bar.com'") - .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()) - .expect( - format!("{files}{ERR_MSG}. it must only contain '*.foo.com'") - .as_str(), - ) + .unwrap_or_else(|_| { + panic!("{files}{ERR_MSG}. it must only contain '*.foo.com'") + }) .is_file() { panic!("{files} is not a file"); @@ -474,12 +506,10 @@ pub(crate) mod test_prog { #[cfg(test)] mod tests { use crate::{E, test_prog}; - use std::io::Write; - use std::process::Stdio; - use std::thread; + use std::{io::Write as _, process::Stdio, thread}; #[test] - #[ignore] - fn test_app() { + #[ignore = "performs I/O"] + fn app() { test_prog::verify_files(); assert!( test_prog::get_command() @@ -488,19 +518,19 @@ mod tests { .stdin(Stdio::piped()) .stdout(Stdio::null()) .spawn() - .map_or(false, |mut cmd| { - cmd.stdin.take().map_or(false, |mut stdin| { + .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(), ) - .map_or(false, |_| true) + .is_ok() }) .join() - .map_or(false, |v| v) - }) && cmd.wait_with_output().map_or(false, |output| { + .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() @@ -513,16 +543,16 @@ mod tests { .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() - .map_or(false, |mut cmd| { - cmd.stdin.take().map_or(false, |mut stdin| { + .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()) - .map_or(false, |_| true) + .is_ok() }) .join() - .map_or(false, |v| v) - }) && cmd.wait_with_output().map_or(false, |output| { + .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. diff --git a/src/priv_sep.rs b/src/priv_sep.rs @@ -1,25 +1,20 @@ -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) use priv_sep::NulOrIoErr; -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -use priv_sep::{Permissions, Promise, Promises}; -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] +extern crate alloc; +#[cfg(target_os = "openbsd")] +use alloc::ffi::CString; +#[cfg(target_os = "openbsd")] +use core::ffi::CStr; +#[cfg(target_os = "openbsd")] +use priv_sep::{Errno, Permissions, Promise, Promises}; +#[cfg(target_os = "openbsd")] use std::env; -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] -use std::fs; -use std::{ - io::{Error, ErrorKind}, - path::Path, -}; -/// Used instead of `()` for the parameter -/// in the `pledge` functions. This allows -/// one to avoid having to disable certain lints. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] -#[derive(Clone, Copy)] -pub(crate) struct Zst; +#[cfg(not(target_os = "openbsd"))] +use std::{fs, io::ErrorKind}; +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` /// are passed. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] pub(crate) fn pledge_init() -> Result<Promises, Error> { let promises = Promises::new([ Promise::Cpath, @@ -32,12 +27,16 @@ pub(crate) fn pledge_init() -> Result<Promises, Error> { ]); match promises.pledge() { Ok(()) => Ok(promises), - Err(e) => Err(e), + Err(e) => Err(e.into()), } } +/// ZST that doesn't trigger lint errors in contrast to `()`. +#[derive(Clone, Copy)] +#[cfg(not(target_os = "openbsd"))] +pub(crate) struct Zst; /// No-op that always returns `Ok`. #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] pub(crate) const fn pledge_init() -> Result<Zst, !> { Ok(Zst) } @@ -45,107 +44,123 @@ pub(crate) const fn pledge_init() -> Result<Zst, !> { /// /// This should only be called when `stdout` is written to /// instead of an RPZ file. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] pub(crate) fn pledge_away_create_write(promises: &mut Promises) -> Result<(), Error> { - promises.remove_promises_then_pledge([Promise::Cpath, Promise::Wpath]) + promises + .remove_promises_then_pledge([Promise::Cpath, Promise::Wpath]) + .map_err(Error::from) } /// No-op that always returns `Ok`. #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] pub(crate) const fn pledge_away_create_write(_: &mut Zst) -> Result<(), !> { Ok(()) } /// Removes `Promise::Unveil`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] pub(crate) fn pledge_away_unveil(promises: &mut Promises) -> Result<(), Error> { - promises.remove_then_pledge(Promise::Unveil) + promises + .remove_then_pledge(Promise::Unveil) + .map_err(Error::from) } /// No-op that always returns `Ok`. #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] pub(crate) const fn pledge_away_unveil(_: &mut Zst) -> Result<(), !> { Ok(()) } /// Removes `Promise::Dns` and `Promise::Inet`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] pub(crate) fn pledge_away_net(promises: &mut Promises) -> Result<(), Error> { - promises.remove_promises_then_pledge([Promise::Dns, Promise::Inet]) + promises + .remove_promises_then_pledge([Promise::Dns, Promise::Inet]) + .map_err(Error::from) } /// No-op that always returns `Ok`. #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] pub(crate) const fn pledge_away_net(_: &mut Zst) -> Result<(), !> { Ok(()) } /// Removes all `Promise`s except `Stdio`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[cfg(target_os = "openbsd")] pub(crate) fn pledge_away_all_but_stdio(promises: &mut Promises) -> Result<(), Error> { - promises.retain_then_pledge([Promise::Stdio]) + promises + .retain_then_pledge([Promise::Stdio]) + .map_err(Error::from) } /// No-op that always returns `Ok`. #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] pub(crate) const fn pledge_away_all_but_stdio(_: &mut Zst) -> Result<(), !> { Ok(()) } /// Calls `unveil`_on `path` with no `Permissions`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn unveil_none<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - Permissions::NONE.unveil(path) +#[cfg(target_os = "openbsd")] +pub(crate) fn unveil_none<P: AsRef<Path>>(path: P) -> Result<(), Error> { + CString::new(path.as_ref().as_os_str().as_encoded_bytes()) + .map_err(Error::other) + .and_then(|p| Permissions::NONE.unveil(p.as_c_str()).map_err(Error::from)) } /// No-op that always returns `Ok`. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] pub(crate) fn unveil_none<P: AsRef<Path>>(_: P) -> Result<(), !> { Ok(()) } -/// Calls `unveil_none` on `/`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn veil_all() -> Result<(), NulOrIoErr> { - unveil_none("/") +/// Calls `unveil` on `/`. +#[cfg(target_os = "openbsd")] +pub(crate) fn veil_all() -> Result<(), Error> { + Permissions::NONE.unveil(c"/").map_err(Error::from) } /// No-op that always returns `Ok`. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] pub(crate) const fn veil_all() -> Result<(), !> { Ok(()) } /// Calls `unveil`_on `path` with `Permissions::CREATE`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn unveil_create<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - Permissions::CREATE.unveil(path) +#[cfg(target_os = "openbsd")] +pub(crate) fn unveil_create<P: AsRef<Path>>(path: P) -> Result<(), Error> { + CString::new(path.as_ref().as_os_str().as_encoded_bytes()) + .map_err(Error::other) + .and_then(|p| { + Permissions::CREATE + .unveil(p.as_c_str()) + .map_err(Error::from) + }) } /// No-op that always returns `Ok`. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] pub(crate) fn unveil_create<P: AsRef<Path>>(_: P) -> Result<(), !> { Ok(()) } /// Calls `unveil`_on `path` with `Permissions::READ` and returns /// `true` iff `path` exists. -#[expect( - clippy::wildcard_enum_match_arm, - reason = "ErrorKind is non_exhaustive" -)] -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn unveil_read_dir<P: AsRef<Path>>(path: P) -> Result<bool, NulOrIoErr> { - Permissions::READ.unveil(path).map_or_else( - |err| match err { - NulOrIoErr::Io(ref err2) => match err2.kind() { - ErrorKind::NotFound => Ok(false), - _ => Err(err), - }, - NulOrIoErr::Nul(_) => Err(err), - }, - |()| Ok(true), - ) -} -/// Returns `true` iff `path` exists +#[cfg(target_os = "openbsd")] +pub(crate) fn unveil_read_dir<P: AsRef<Path>>(path: P) -> Result<bool, Error> { + CString::new(path.as_ref().as_os_str().as_encoded_bytes()) + .map_err(Error::other) + .and_then(|p| { + Permissions::READ.unveil(p.as_c_str()).map_or_else( + |err| { + if matches!(err, Errno::ENOENT) { + Ok(false) + } else { + Err(err.into()) + } + }, + |()| Ok(true), + ) + }) +} +/// Returns `true` iff `path` exists. #[expect( clippy::wildcard_enum_match_arm, reason = "too many branches to write out manually" )] -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] pub(crate) fn unveil_read_dir<P: AsRef<Path>>(path: P) -> Result<bool, Error> { fs::metadata(path).map_or_else( |err| match err.kind() { @@ -156,12 +171,14 @@ pub(crate) fn unveil_read_dir<P: AsRef<Path>>(path: P) -> Result<bool, Error> { ) } /// Calls `unveil`_on `path` with `Permissions::READ`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn unveil_read_file<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - Permissions::READ.unveil(path) +#[cfg(target_os = "openbsd")] +pub(crate) fn unveil_read_file<P: AsRef<Path>>(path: P) -> Result<(), Error> { + CString::new(path.as_ref().as_os_str().as_encoded_bytes()) + .map_err(Error::other) + .and_then(|p| Permissions::READ.unveil(p.as_c_str()).map_err(Error::from)) } /// No-op that always returns `Ok`. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] pub(crate) fn unveil_read_file<P: AsRef<Path>>(_: P) -> Result<(), !> { Ok(()) @@ -171,30 +188,43 @@ pub(crate) fn unveil_read_file<P: AsRef<Path>>(_: P) -> Result<(), !> { /// We rely on `SSL_CERT_FILE` as that allows one to `unveil(2)` only `/etc/ssl/cert.pem` /// instead of `/etc/ssl/`. #[expect(unsafe_code, reason = "safety comment justifies its correctness")] -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn unveil_https() -> Result<(), NulOrIoErr> { +#[cfg(target_os = "openbsd")] +pub(crate) fn unveil_https() -> Result<(), Error> { /// The path to the root certificate store. - const CERTS: &str = "/etc/ssl/cert.pem"; - unveil_read_file(CERTS).map(|()| { - // SAFETY: - // `unveil_https` is only called in `super::main` - // in a single-threaded context; thus this is OK. - unsafe { env::set_var("SSL_CERT_FILE", CERTS) } - }) + const CERTS: &CStr = c"/etc/ssl/cert.pem"; + const CERTS_STR: &str = match CERTS.to_str() { + Ok(val) => val, + Err(_) => panic!("/etc/ssl/cert.pem is not a valid str"), + }; + Permissions::READ + .unveil(CERTS) + .map(|()| { + // SAFETY: + // `unveil_https` is only called in `super::main` + // in a single-threaded context; thus this is OK. + unsafe { env::set_var("SSL_CERT_FILE", CERTS_STR) } + }) + .map_err(Error::from) } /// No-op that always returns `Ok`. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] pub(crate) const fn unveil_https() -> Result<(), !> { Ok(()) } /// Calls `unveil`_on `path` with create, read, and write `Permissions`. -#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] -pub(crate) fn unveil_create_read_write<P: AsRef<Path>>(path: P) -> Result<(), NulOrIoErr> { - (!Permissions::EXECUTE).unveil(path) +#[cfg(target_os = "openbsd")] +pub(crate) fn unveil_create_read_write<P: AsRef<Path>>(path: P) -> Result<(), Error> { + CString::new(path.as_ref().as_os_str().as_encoded_bytes()) + .map_err(Error::other) + .and_then(|p| { + (!Permissions::EXECUTE) + .unveil(p.as_c_str()) + .map_err(Error::from) + }) } /// No-op that always returns `Ok`. -#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[cfg(not(target_os = "openbsd"))] #[expect(clippy::unnecessary_wraps, reason = "need to align with OpenBSD code")] pub(crate) fn unveil_create_read_write<P: AsRef<Path>>(_: P) -> Result<(), !> { Ok(())