rpz

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

commit 25565a5e2001cd4169d0c1e372d92bd47088ffe5
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sun, 29 Oct 2023 17:47:41 -0600

init

Diffstat:
A.gitignore | 2++
ACargo.toml | 50++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-MIT | 20++++++++++++++++++++
AREADME.md | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild.rs | 12++++++++++++
Arust-toolchain.toml | 2++
Asrc/app.rs | 256+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/args.rs | 430+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/config.rs | 415+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dom.rs | 3265+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/dom_count_auto_gen.rs | 1532+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/file.rs | 952+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 45+++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.rs | 546+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/priv_sep.rs | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 8115 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/** diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,50 @@ +[package] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["command-line-utilities"] +description = "RPZ file generator based on HTTP(S) URLs and local file paths entered into a config file." +documentation = "https://crates.io/crates/rpz" +edition = "2021" +keywords = ["adblock", "rpz", "unbound"] +license = "MIT OR Apache-2.0" +name = "rpz" +readme = "README.md" +repository = "https://git.philomathiclife.com/repos/rpz/" +version = "0.1.0" + +[lib] +name = "rpz" +path = "src/lib.rs" + +[[bin]] +name = "rpz" +path = "src/main.rs" + +[dependencies] +num-bigint = { version = "0.4.4", default-features = false } +priv_sep = { version = "0.8.0", default-features = false, features = ["openbsd"], optional = true } +reqwest = { version = "0.11.22", default-features = false, features = ["brotli", "deflate", "gzip", "rustls-tls-native-roots", "trust-dns"] } +serde = { version = "1.0.190", default-features = false } +superset_map = { version = "0.2.1", default-features = false } +tokio = { version = "1.33.0", default-features = false, features = ["rt", "time"] } +toml = { version = "0.8.6", default-features = false, features = ["parse"] } +url = { version = "2.4.1", default-features = false, features = ["serde"] } +zfc = { version = "0.3.1", default-features = false } + +[build-dependencies] +rustc_version = "0.4.0" + +[features] +priv_sep = ["dep:priv_sep"] +default = ["priv_sep"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[badges] +maintenance = { status = "actively-developed" } + +[profile.release] +lto = true +panic = 'abort' +strip = true diff --git a/LICENSE-APACHE b/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright © 2023 Zack Newman + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,191 @@ +# rpz + +`rpz` consists of a binary crate and [library crate](https://docs.rs/rpz/latest/rpz). +The binary crate, `rpz`, is an application that downloads, parses, and transforms ad-(un)block files from +URLs and local file paths into a response policy zone (RPZ) file. This RPZ file can be consumed +by a DNS server that supports such files (e.g., [Unbound](https://nlnetlabs.nl/projects/unbound/about/)). + +## rpz in action + +In this example it is assumed [`unbound.conf(5)`](https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html) is properly configured +and has `name` and `zonefile` in the `rpz` section set to `.` and `/var/unbound/db/rpz` respectively in addition to `control-enable` set to `true` +in the `remote-control` section. + +```bash +[zack@laptop ~]$ cat<<EOF>/usr/local/etc/rpz/config +> timeout = 15 +> rpz = "/var/unbound/db/rpz" +> local_dir = "/usr/local/etc/rpz/" +> adblock = [ + "https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/BaseFilter/sections/adservers.txt", + "https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/BaseFilter/sections/adservers_firstparty.txt", + "https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/MobileFilter/sections/adservers.txt", + "https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/SpywareFilter/sections/mobile.txt", + "https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/SpywareFilter/sections/tracking_servers.txt", + "https://raw.githubusercontent.com/AdguardTeam/AdguardFilters/master/SpywareFilter/sections/tracking_servers_firstparty.txt", + "https://raw.githubusercontent.com/easylist/easylist/master/easylist/easylist_adservers.txt", + "https://raw.githubusercontent.com/easylist/easylist/master/easylist/easylist_thirdparty.txt", + "https://raw.githubusercontent.com/easylist/easylist/master/easyprivacy/easyprivacy_thirdparty.txt", + "https://raw.githubusercontent.com/easylist/easylist/master/easyprivacy/easyprivacy_trackingservers.txt", + "https://malware-filter.gitlab.io/malware-filter/urlhaus-filter-agh.txt" + ] +domain = ["https://www.stopforumspam.com/downloads/toxic_domains_whole.txt"] +hosts = ["https://raw.githubusercontent.com/AdAway/adaway.github.io/master/hosts.txt", "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"] +wildcard = ["https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblock&showintro=0&mimetype=plaintext"] +> EOF +[zack@laptop ~]$ cat /usr/local/etc/rpz/unblock/domain/unbound +dpm.demdex.net # ESPN app on PS5 needs this. +[zack@laptop ~]$ rpz -f /usr/local/etc/rpz/config +unblock count written: 1 +block count written: 271559 +total lines written: 271560 +domains parsed: 254147 +comments parsed: 6629 +blanks parsed: 4519 +parsing errors: 24624 +[zack@laptop ~]$ head -1 /var/unbound/db/rpz +dpm.demdex.net CNAME rpz-passthru. +[zack@laptop ~]$ tail -6 /var/unbound/db/rpz +stats.zone-telechargement CNAME . +*.stats.zone-telechargement CNAME . +5wh.co.zw CNAME . +www.5wh.co.zw CNAME . +pandi.co.zw CNAME . +www.pandi.co.zw CNAME . +[zack@laptop ~]$ unbound-control -q auth_zone_reload . && unbound-control -q flush_zone . && unbound-control -q flush_negative +``` + +## Ad-(un)block file format and encoding + +All ad-(un)block files must be valid UTF-8; however for a given domain, each label must only contain 1–63 Unicode scalar values from the set: +`!`, `$`, `&`, `'`, `(`, `)`, `+`, `,`, `-`, `0`–`9`, `;`, `=`, `_`, `` ` ``, `A`–`Z`, `a`–`z`, `{`, `}`, and `~`. Labels must be delimited +by `.`. Domains in the file must be delimited by a line feed or carriage return and line feed. A domain must be less than 254 characters in length +including the `.` label separator. Domains are treated as case-insensitive with uppercase letters treated as lowercase. Domains must not be an +[Ipv4Addr](https://doc.rust-lang.org/std/net/struct.Ipv4Addr.html). + +### Adblock-style + +Domain constructed from an [Adblock-style rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#adblock-style-syntax) +with the requirement that the rule conforms to the following extended regex: + +`^<ws>*(\|\|)?<ws>*<domain><ws>*\^?<ws>*$` + +where `<domain>` conforms to a valid [`Domain`](https://docs.rs/rpz/latest/rpz/dom/struct.Domain.html) with the added requirement that it does not contain `$`, and +`<ws>` is any sequence of [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace). + +Lines that begin with `||` cause all subdomains to be blocked (i.e., the domain itself and all proper subdomains); without +`||`, only the specific domain is blocked. + +Due to the conservative nature in how these files are processed, one is encouraged to still use an application-level +ad blocker (e.g., [uBlock Origin](https://ublockorigin.com/)). Adblock-style files often contain paths as well as +additional information (e.g., “third-party”) that require application-level information to process correctly as such +entries will be considered “parsing errors” by `rpz`. + +### Domain-style + +Domain constructed from a [domains-only rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#domains-only-syntax) +with the requirement that the rule conforms to the following regex: + +`^<ws>*<domain><ws>*(#.*)?$` + +where `<domain>` conforms to a valid `Domain`, and `<ws>` is any sequence of ASCII whitespace. + +Domains only represent themselves (i.e., proper subdomains will not be blocked). + +### Hosts-style + +Domain constructed from a [`hosts(5)`-style rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#etc-hosts-syntax) +with the requirement that the rule conforms to the following extended regex: + +`^<ws>*<ip><ws>+<domain><ws>*(#.*)?$` + +where `<domain>` conforms to a valid `Domain`, `<ws>` is any sequence of ASCII whitespace, and `<ip>` is one of the following: + +`::`, `::1`, `0.0.0.0`, or `127.0.0.1`. + +Domains only represent themselves (i.e., proper subdomains will not be blocked). + +### Wildcard-style + +Domain constructed from a [wildcard domain rule](https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblock&showintro=0&mimetype=plaintext) +with the requirement that the rule conforms to the following extended regex: + +`^<ws>*(\*\.)?<domain><ws>*(#.*)?$` + +where `<domain>` conforms to a valid `Domain`, and `<ws>` is any sequence of ASCII whitespace. + +If `domain` begins with `*.`, then `domain` must have length less than 252 and all proper subdomains are blocked—this +does _not_ include the domain itself; otherwise, only the `domain` is blocked. + +## Config file + +Either `-` or the absolute path to the TOML config file must be passed via the `-f`/`--file` CLI option. If `-` is passed, then `stdin` will be read. The +format of this file must conform to the following: + +```bash +timeout = <timeout_in_seconds> +rpz = <absolute_file_path_to_the_RPZ_file_to_be_written> +local_dir = <absolute_file_path_to_the_directory_containing_local_files> +adblock = [<HTTP(S)_URLs>] +domain = [<HTTP(S)_URLs>] +hosts = [<HTTP(S)_URLs>] +wildcard = [<HTTP(S)_URLs>] +``` + +If `rpz` does not exist, then the file will be written to `stdout`. If `local_dir` is specified, `block/` and `unblock/` subdirectories are searched; and for each of those subdirectories, +`adblock/`, `domain/`, `hosts/`, and `wildcard/` subdirectories are searched for files which are parsed according to the directory they are in. It is not +an error if any of the directories do not exist. + +In the event keys are specified corresponding to arrays, URLs must be unique across all arrays. The files these URLs +point to are interpreted as block files (i.e., unblock files are only allowed on the local file system). + +The `timeout` corresponds to the maximum _seconds_ allowed for an HTTP(S) file to be downloaded. +If it does not exist or has a value of 0, then a timeout of one hour will be used. If the value specified exceeds one hour, +then it will be truncated to one hour. + +## RPZ file + +Unless `stdout` is the destination, a temporary RPZ file is written in the same location as the `rpz` value in the config file except with `tmp` appended to the name. Upon success, this file +is renamed to the `rpz` value in the config file. The contents of this file contain the minimum number of lines possible with unblock entries taking precedence +over block entries. + +In the event there are no block entries or the temp file already exists, the program will abort. + +## Options + +When `rpz` is passed `-V`/`--version`, the version of `rpz` will be printed to `stdout`. When passed `-h`/`--help`, +information about the program and its options will be printed to `stdout`. When passed `-f`/`--file` along with +`-` or the absolute path to the TOML config file, `rpz` will run normally printing summary information to `stdout` +upon completion. One can additionally pass `-q`/`--quiet` along with `-f`/`--file` in order to suppress summary +information from being printed to `stdout`. When `-v`/`--verbose` is passed along with `-f`/`--file`, in addition to +the normal summary information being printed to `stdout`, itemized summary information for each input file +including the kinds of errors and counts of errors will be printed to `stdout`. + +### Example + +If `www.example.com`, `*.example.com`, and `foo.com` are to be blocked while `foo.example.com` and `||foo.com` are to be unblocked, the RPZ file would look like the following: + +```bash +foo.example.com CNAME rpz-passthru. +*.example.com CNAME . +``` + +Upon success, the quantity of unblock, block, and total lines written is written to `stdout` in addition +to the total number of domains, comments, blanks, and parsing errors. + +## Errors + +Parsing errors are ignored; all other errors are written to `stderr` before program abortion. + +### Status + +This package will be actively maintained until it is deemed “feature complete”. + +The crates are only tested on the `x86_64-unknown-linux-gnu` and `x86_64-unknown-openbsd` targets, but +they should work on any [Tier 1 with Host Tools](https://doc.rust-lang.org/beta/rustc/platform-support.html) +target. + +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`. Note that the `rust-ring` port must also be installed with +the `[patch]` section of `Cargo.toml` or `~/.cargo/config.toml` configured appropriately. diff --git a/build.rs b/build.rs @@ -0,0 +1,12 @@ +use rustc_version::{version_meta, Channel}; + +fn main() { + // Set cfg flags depending on release channel + let channel = match version_meta().unwrap().channel { + Channel::Stable => "CHANNEL_STABLE", + Channel::Beta => "CHANNEL_BETA", + Channel::Nightly => "CHANNEL_NIGHTLY", + Channel::Dev => "CHANNEL_DEV", + }; + println!("cargo:rustc-cfg={}", channel) +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/app.rs b/src/app.rs @@ -0,0 +1,256 @@ +#![allow( + clippy::into_iter_on_ref, + clippy::ref_patterns, + clippy::single_char_lifetime_names, + clippy::unseparated_literal_suffix +)] +use core::convert; +use rpz::dom::{ + Adblock, DomainErr, DomainOnly, Hosts, ParsedDomain, RpzAction, RpzDomain, Value, Wildcard, +}; +use rpz::file::{AbsFilePath, File, Files, Kind, LocalFiles, Summary}; +use std::collections::HashMap; +use std::fs; +use std::io::{self, Error, Write}; +use superset_map::SupersetSet; +/// Helper that returns the `Kind` of a file. +pub trait Helper { + /// Returns the `Kind` of file. + fn kind() -> Kind; +} +impl Helper for Adblock<'_> { + #[inline] + fn kind() -> Kind { + Kind::Adblock + } +} +impl Helper for DomainOnly<'_> { + #[inline] + fn kind() -> Kind { + Kind::DomainOnly + } +} +impl Helper for Hosts<'_> { + #[inline] + fn kind() -> Kind { + Kind::Hosts + } +} +impl Helper for Wildcard<'_> { + #[inline] + fn kind() -> Kind { + Kind::Wildcard + } +} +/// Container of `Domain`s to block and unblock. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct Domains<'unblock, 'block> { + /// `RpzDomain`s to not block. + unblock: SupersetSet<RpzDomain<'unblock>>, + /// `RpzDomain`s to block. + block: SupersetSet<RpzDomain<'block>>, +} +impl<'unblock, 'block> Domains<'unblock, 'block> { + /// Returns a reference to the `RpzDomain`s to block. + #[inline] + pub const fn block(&self) -> &SupersetSet<RpzDomain<'block>> { + &self.block + } + /// Returns an empty `Domains`. + #[inline] + pub const fn new() -> Self { + Self { + unblock: SupersetSet::new(), + block: SupersetSet::new(), + } + } + /// Returns `Domains` based on `LocalFiles` along + /// with a `Vec` of `Summary`. + #[inline] + pub fn new_with<'c: 'unblock + 'block>( + local: &'c LocalFiles, + ) -> (Self, Vec<Summary<'c, DomainErr>>) { + let mut val = Self { + unblock: SupersetSet::new(), + block: SupersetSet::new(), + }; + let summaries = val.add_local_files(local); + (val, summaries) + } + /// Parses each line in `file` as an `RpzDomain` before + /// adding it to `Domains::block` iff `Domains::unblock` does not contain + /// a superset of it. + /// + /// All parsing errors are ignored. + #[inline] + fn add_block_file< + 'c: 'block, + T: Into<RpzDomain<'block>> + ParsedDomain<'block, Error = DomainErr> + Helper, + >( + &mut self, + file: &'c File, + summaries: &mut Vec<Summary<'c, DomainErr>>, + ) { + let mut summary = Summary { + file, + kind: T::kind(), + domain_count: 0, + comment_count: 0, + blank_count: 0, + errors: HashMap::new(), + }; + file.data + .lines() + .fold((), |(), line| match T::parse_value(line) { + Ok(val) => match val { + Value::Domain(dom) => { + summary.domain_count = summary.domain_count.saturating_add(1); + let domain = dom.into(); + if !self.unblock.contains_superset(&domain) { + self.block.insert(domain); + } + } + Value::Comment(_) => { + summary.comment_count = summary.comment_count.saturating_add(1); + } + Value::Blank => { + summary.blank_count = summary.blank_count.saturating_add(1); + } + }, + Err(err) => { + let count = summary.errors.entry(err).or_insert(0); + *count = count.saturating_add(1); + } + }); + summaries.push(summary); + } + /// Parses each line in the files in `files` as an `RpzDomain` before + /// adding it to `Domains::block` iff `Domains::unblock` does not contain + /// a superset of it. + /// + /// All parsing errors are ignored. + #[inline] + pub fn add_block_files<'c: 'block>( + &mut self, + files: &'c Files, + summaries: &mut Vec<Summary<'c, DomainErr>>, + ) { + /// Parses each line in the `String`s in `files` as an `RpzDomain` before + /// adding it to `Domains::block` iff `Domains::unblock` does not contain + /// a superset of it. + /// + /// All parsing errors are ignored. + #[inline] + fn add_files< + 'unblock, + 'block, + 'c: 'block, + T: Into<RpzDomain<'block>> + ParsedDomain<'block, Error = DomainErr> + Helper, + >( + doms: &mut Domains<'unblock, 'block>, + files: &'c [File], + summaries: &mut Vec<Summary<'c, DomainErr>>, + ) { + files + .into_iter() + .fold((), |(), file| doms.add_block_file::<T>(file, summaries)); + } + add_files::<Adblock>(self, &files.adblock, summaries); + add_files::<DomainOnly>(self, &files.domain, summaries); + add_files::<Hosts>(self, &files.hosts, summaries); + add_files::<Wildcard>(self, &files.wildcard, summaries); + } + /// Parses each line in the files in `files` as an `RpzDomain`. + /// For unblock files, the domain is added to `Domains::unblock`; + /// for block files, the domain is added to `Domains::block` iff + /// `Domains::unblock` does not contain a superset of it. + /// + /// All parsing errors are ignored. + #[inline] + fn add_local_files<'c: 'unblock + 'block>( + &mut self, + files: &'c LocalFiles, + ) -> Vec<Summary<'c, DomainErr>> { + let mut summaries = files.unblock.add_to_superset(&mut self.unblock); + self.add_block_files(&files.block, &mut summaries); + summaries + } + /// Writes all necessary unblock `RpzDomain`s as `<domain> CNAME rpz-passthru.` + /// followed by all block `RpzDomain`s as `<domain> CNAME .` + /// + /// When subdomains exist, `*.<domain>` is used. + /// + /// When `path` is `None`, `stdout` is written to; otherwise, + /// `path.unwrap().1` is written to before being renamed + /// to `path.unwrap().0`. + /// + /// Returns the quantity of unblock lines and block lines written + /// respectively. + /// + /// Note that by "necessary unblock" we mean that there exists + /// a proper superset of it in the blocked `RpzDomain`s; as if not, + /// there is no need to write it since a lack of a blocked entry + /// is equivalent to an unblock entry. + /// + /// # Errors + /// + /// Returns `Error` iff `writeln` or `fs::rename` do. + #[inline] + pub fn write( + self, + path: Option<(AbsFilePath<false>, AbsFilePath<false>)>, + ) -> Result<(usize, usize), Error> { + /// When `exclude.is_none()`, calls `Rpz::write_rpz_line` with + /// `RpzAction::Nxdomain` for each `RpzDomain` in `doms`; + /// otherwise calls it with `RpzAction::Passthru` + /// so long as `exclude` contains a proper superset of the domain. + /// + /// Returns the total number of lines written. + #[inline] + fn write_domain<W: Write>( + doms: &SupersetSet<RpzDomain<'_>>, + exclude: Option<&SupersetSet<RpzDomain<'_>>>, + mut writer: W, + ) -> Result<usize, Error> { + let action = exclude.map_or(RpzAction::Nxdomain, |_| RpzAction::Passthru); + doms.into_iter().try_fold(0usize, |count, rpz| { + if exclude.map_or(true, |other| other.contains_proper_superset(rpz)) { + rpz.write_to_rpz(action, &mut writer) + .map(|()| count.saturating_add(if rpz.is_subdomains() { 2 } else { 1 })) + } else { + Ok(count) + } + }) + } + path.map_or_else( + || { + let mut std = io::stdout().lock(); + write_domain(&self.unblock, Some(&self.block), &mut std).and_then(|unblock_count| { + write_domain(&self.block, None, std) + .map(|block_count| (unblock_count, block_count)) + }) + }, + |(rpz, rpz_tmp)| { + fs::File::options() + .read(false) + .write(true) + .create_new(true) + .open(rpz_tmp.as_path()) + .and_then(|file| { + write_domain(&self.unblock, Some(&self.block), &file).and_then( + |unblock_count| { + write_domain(&self.block, None, file).and_then(|block_count| { + fs::rename(rpz_tmp.as_path(), rpz) + .map(|()| (unblock_count, block_count)) + .map_err(|err| { + fs::remove_file(rpz_tmp) + .map_or_else(convert::identity, |()| err) + }) + }) + }, + ) + }) + }, + ) + } +} diff --git a/src/args.rs b/src/args.rs @@ -0,0 +1,430 @@ +#![allow(clippy::question_mark_used, clippy::ref_patterns)] +use core::fmt::{self, Display, Formatter}; +use rpz::file::AbsFilePath; +use std::env::{self, Args}; +use std::error::Error; +/// Error returned when parsing arguments passed to the application. +#[allow(clippy::exhaustive_enums, clippy::module_name_repetitions)] +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum ArgsErr { + /// Error when no arguments were passed to the application. + NoArgs, + /// Error when `-f`/`--file` is not passed or is passed + /// without a path to the file. + ConfigPathNotPassed, + /// Error when an invalid option is passed. The contained [`String`] + /// is the value of the invalid option. + InvalidOption(String), + /// Some options when passed must be the only option passed. + /// For such options, this is the error when other options are passed. + MoreThanOneOption, + /// Error when the passed path to the config file is not `-` nor an absolute file path to a file. + InvalidConfigPath, + /// Error when an option is passed more than once. + DuplicateOption(&'static str), + /// Error when the quiet and verbose options were passed. + QuietAndVerbose, +} +impl Display for ArgsErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::NoArgs => write!(f, "no arguments were passed, but at least two are required containing the option '-f' and its value which must be an absolute path to the config file"), + Self::ConfigPathNotPassed => f.write_str("'-f' followed by '-' or the absolute file path to the config file was not passed"), + Self::InvalidOption(ref arg) => write!(f, "{arg} is an invalid option. Only '-f'/'--file' followed by the absolute path to the config file, '-q'/'--quiet', '-h'/'--help', '-v'/'--verbose' and '-V'/'--version' are allowed"), + Self::MoreThanOneOption => f.write_str("'-V'/'--version' or '-h'/'--help' was passed with other options; but when those options are passed, they must be the only one"), + Self::InvalidConfigPath => write!(f, "an absolute file path to the config file or '-' was not passed"), + Self::DuplicateOption(arg) => write!(f, "{arg} was passed more than once"), + Self::QuietAndVerbose => f.write_str("'-q'/'--quiet' and '-v'/'--verbose' were both passed, but at most only one of them is allowed to be passed"), + } + } +} +impl Error for ArgsErr {} +/// The location of the configuration file. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum ConfigPath { + /// The config file is to be read from `stdin`. + Stdin, + /// The config file resides on the local file system. + Path(AbsFilePath<false>), +} +/// The options passed to the application. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum Opts { + /// Variant when no arguments were passed. + None, + /// Variant when the help argument was passed. + Help, + /// Variant when the version argument was passed. + Version, + /// Variant when the quiet argument was passed. + Quiet, + /// Variant when the verbose argument was passed. + Verbose, + /// Variant when the file argument with the path to the file was passed. + Config(ConfigPath), + /// Variant when the quiet argument and the file argument with the path to the file + /// were passed. + ConfigQuiet(ConfigPath), + /// Variant when the verbose argument and the file argument with the path to the file + /// were passed. + ConfigVerbose(ConfigPath), +} +impl Opts { + /// Returns `Opts` based on arguments passed to the application. + #[allow(clippy::too_many_lines)] + #[inline] + pub fn from_args() -> Result<Self, ArgsErr> { + /// Attempts to parse the next `Arg` into `-` or an absolute + /// path to a file. + #[allow(clippy::option_if_let_else)] + #[inline] + fn get_path(args: &mut Args) -> Result<ConfigPath, ArgsErr> { + args.next() + .map_or(Err(ArgsErr::ConfigPathNotPassed), |path| { + if path == "-" { + Ok(ConfigPath::Stdin) + } else { + AbsFilePath::<false>::from_string(path) + .map_or(Err(ArgsErr::InvalidConfigPath), |config| { + Ok(ConfigPath::Path(config)) + }) + } + }) + } + let mut args = env::args(); + if args.next().is_some() { + let mut opts = Self::None; + while let Some(arg) = args.next() { + match arg.as_str() { + "-h" | "--help" => match opts { + Self::None => { + opts = Self::Help; + } + Self::Help => return Err(ArgsErr::DuplicateOption("-h/--help")), + Self::Verbose + | Self::Quiet + | Self::Version + | Self::Config(_) + | Self::ConfigQuiet(_) + | Self::ConfigVerbose(_) => return Err(ArgsErr::MoreThanOneOption), + }, + "-V" | "--version" => match opts { + Self::None => { + opts = Self::Version; + } + Self::Version => return Err(ArgsErr::DuplicateOption("-V/--version")), + Self::Verbose + | Self::Quiet + | Self::Help + | Self::Config(_) + | Self::ConfigQuiet(_) + | Self::ConfigVerbose(_) => return Err(ArgsErr::MoreThanOneOption), + }, + "-f" | "--file" => match opts { + Self::None => { + opts = Self::Config(get_path(&mut args)?); + } + Self::Quiet => { + opts = Self::ConfigQuiet(get_path(&mut args)?); + } + Self::Verbose => { + opts = Self::ConfigVerbose(get_path(&mut args)?); + } + Self::Config(_) | Self::ConfigQuiet(_) | Self::ConfigVerbose(_) => { + return Err(ArgsErr::DuplicateOption("-f/--file")); + } + Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), + }, + "-q" | "--quiet" => match opts { + Self::None => { + opts = Self::Quiet; + } + Self::Config(path) => { + opts = Self::ConfigQuiet(path); + } + Self::Quiet | Self::ConfigQuiet(_) => { + return Err(ArgsErr::DuplicateOption("-q/--quiet")); + } + Self::Verbose | Self::ConfigVerbose(_) => { + return Err(ArgsErr::QuietAndVerbose) + } + Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), + }, + "-v" | "--verbose" => match opts { + Self::None => { + opts = Self::Verbose; + } + Self::Config(path) => { + opts = Self::ConfigVerbose(path); + } + Self::Quiet | Self::ConfigQuiet(_) => return Err(ArgsErr::QuietAndVerbose), + Self::Verbose | Self::ConfigVerbose(_) => { + return Err(ArgsErr::DuplicateOption("-v/--verbose")); + } + Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), + }, + "-fq" | "-qf" => match opts { + Self::None => { + opts = Self::ConfigQuiet(get_path(&mut args)?); + } + Self::Config(_) | Self::ConfigQuiet(_) => { + return Err(ArgsErr::DuplicateOption("-f/--file")) + } + Self::Quiet => return Err(ArgsErr::DuplicateOption("-q/--quiet")), + Self::Verbose | Self::ConfigVerbose(_) => { + return Err(ArgsErr::QuietAndVerbose) + } + Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), + }, + "-fv" | "-vf" => match opts { + Self::None => { + opts = Self::ConfigVerbose(get_path(&mut args)?); + } + Self::Config(_) | Self::ConfigVerbose(_) => { + return Err(ArgsErr::DuplicateOption("-f/--file")) + } + Self::Verbose => return Err(ArgsErr::DuplicateOption("-v/--verbose")), + Self::Quiet | Self::ConfigQuiet(_) => return Err(ArgsErr::QuietAndVerbose), + Self::Help | Self::Version => return Err(ArgsErr::MoreThanOneOption), + }, + _ => return Err(ArgsErr::InvalidOption(arg)), + } + } + Ok(opts) + } else { + Err(ArgsErr::NoArgs) + } + } +} +#[cfg(test)] +mod tests { + use crate::{test_prog, ArgsErr, E}; + use core::convert; + use std::io::Write; + use std::process::Stdio; + use std::thread; + #[test] + #[ignore] + fn test_args() { + test_prog::verify_files(); + assert!(test_prog::get_command() + .stderr(Stdio::piped()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .output() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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() + .map_or(false, |mut cmd| { + cmd.stdin.take().map_or(false, |mut stdin| { + thread::spawn(move || { + stdin + .write_all(b"junk") + .map_or(false, |_| stdin.flush().map_or(false, |_| true)) + }) + .join() + .map_or(false, convert::identity) + }) && cmd.wait_with_output().map_or(false, |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() + .map_or(false, |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() + .map_or(false, |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 @@ -0,0 +1,415 @@ +#![allow( + clippy::exhaustive_structs, + clippy::implicit_return, + clippy::into_iter_on_ref, + clippy::missing_trait_methods, + clippy::question_mark_used, + clippy::single_char_lifetime_names +)] +extern crate alloc; +use alloc::borrow::Cow; +use core::fmt::{self, Display, Formatter}; +use core::time::Duration; +use rpz::file::{AbsFilePath, HttpUrl}; +use serde::de::{Deserialize, Deserializer, Error, MapAccess, SeqAccess, Unexpected, Visitor}; +use std::collections::HashSet; +/// The TOML config file. +#[derive(Debug)] +pub struct Config { + /// The maximum amount of time allowed for an HTTP(S) file to be downloaded. + pub timeout: Option<Duration>, + /// The absolute file path for the [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) file. + pub rpz: Option<AbsFilePath<false>>, + /// The absolute file path to the directory that contains local (un)block files. + pub local_dir: Option<AbsFilePath<true>>, + /// The unique absolute HTTP(S) URLs to [Adblock-style](https://adguard-dns.io/kb/general/dns-filtering-syntax/#adblock-style-syntax) + /// block lists. + pub adblock: HashSet<HttpUrl>, + /// The unique absolute HTTP(S) URLs to [domains-only](https://adguard-dns.io/kb/general/dns-filtering-syntax/#domains-only-syntax) + /// block lists. + pub domain: HashSet<HttpUrl>, + /// The unique absolute HTTP(S) URLs to [`hosts(5)`-style](https://adguard-dns.io/kb/general/dns-filtering-syntax/#etc-hosts-syntax) + /// block lists. + pub hosts: HashSet<HttpUrl>, + /// The unique absolute HTTP(S) URLs to [wildcard domain](https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblock&showintro=0&mimetype=plaintext) + /// block lists. + pub wildcard: HashSet<HttpUrl>, +} +impl Display for Config { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + /// Helper function that writes the `Url`s in a `HashSet<HttpUrl>`. + #[allow(clippy::min_ident_chars)] + #[inline] + fn keys(set: &HashSet<HttpUrl>, f: &mut Formatter<'_>, name: &str) -> fmt::Result { + write!(f, "{name}: [").and_then(|()| { + set.into_iter() + .try_fold((), |(), url| write!(f, "{url}, ")) + .and_then(|()| f.write_str("], ")) + }) + } + write!( + f, + "Config {{ timeout: {} rpz: {}, local_dir: {}, ", + self.timeout + .map_or_else(String::new, |dur| dur.as_secs().to_string()), + &self + .rpz + .as_ref() + .map_or_else(|| Cow::Owned(String::new()), |file| file.to_string_lossy()), + &self + .local_dir + .as_ref() + .map_or_else(|| Cow::Owned(String::new()), |dir| dir.to_string_lossy()) + ) + .and_then(|()| { + keys(&self.adblock, f, "adblock") + .and_then(|()| keys(&self.domain, f, "domain")) + .and_then(|()| keys(&self.hosts, f, "hosts")) + .and_then(|()| keys(&self.wildcard, f, "wildcard")) + .and_then(|()| f.write_str("}")) + }) + } +} +impl<'de> Deserialize<'de> for Config { + #[allow(clippy::too_many_lines)] + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// Config file fields. + enum Field { + /// Timeout field. + Timeout, + /// RPZ field. + Rpz, + /// Local directory field. + LocalDir, + /// Adblock URLs field. + Adblock, + /// Domain-only URLs field. + Domain, + /// hosts(5) URLs field. + Hosts, + /// Wildcard URLs field. + Wildcard, + } + impl<'d> Deserialize<'d> for Field { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'d>, + { + /// `Visitor` for `Field`. + struct FieldVisitor; + impl<'de> Visitor<'de> for FieldVisitor { + type Value = Field; + #[inline] + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str( + "'timeout', 'rpz', 'local_dir', 'adblock', 'domain', 'hosts', or 'wildcard'", + ) + } + #[allow(clippy::min_ident_chars)] + #[inline] + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: Error, + { + match v { + "timeout" => Ok(Field::Timeout), + "rpz" => Ok(Field::Rpz), + "local_dir" => Ok(Field::LocalDir), + "adblock" => Ok(Field::Adblock), + "domain" => Ok(Field::Domain), + "hosts" => Ok(Field::Hosts), + "wildcard" => Ok(Field::Wildcard), + _ => Err(E::unknown_field(v, &VARIANTS)), + } + } + } + deserializer.deserialize_identifier(FieldVisitor) + } + } + /// `Visitor` for `Config`. + struct ConfigVisitor; + impl<'d> Visitor<'d> for ConfigVisitor { + type Value = Config; + #[inline] + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("struct Config") + } + #[allow(clippy::as_conversions, clippy::cast_lossless, clippy::too_many_lines)] + #[inline] + fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'d>, + { + /// Verifies that the `HashSet`s are pairwise disjoint. + #[allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)] + #[inline] + fn hash_overlap<E: Error>(maps: &[&HashSet<HttpUrl>]) -> Result<(), E> { + /// Verifies the intersection of `left` and `right` is empty. + #[inline] + fn url_overlap<E: Error>( + left: &HashSet<HttpUrl>, + right: &HashSet<HttpUrl>, + ) -> Result<(), E> { + let (mut iter, urls) = if left.len() <= right.len() { + (left.into_iter(), right) + } else { + (right.into_iter(), left) + }; + iter.try_fold((), |(), url| { + if urls.contains(url) { + Err(E::invalid_type( + Unexpected::Other(url.to_string().as_str()), + &"unique URLs across the block list types", + )) + } else { + Ok(()) + } + }) + } + maps.into_iter().enumerate().try_fold((), |(), (idx, map)| { + maps[idx + 1..] + .into_iter() + .try_fold((), |(), map2| url_overlap(map, map2)) + }) + } + /// Wrapper around a `HashSet` that is deserializable. + struct Urls(HashSet<HttpUrl>); + impl<'de> Deserialize<'de> for Urls { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `Urls`. + struct HashVisitor; + impl<'d> Visitor<'d> for HashVisitor { + type Value = Urls; + #[inline] + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("struct Urls") + } + #[inline] + fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> + where + A: SeqAccess<'d>, + { + let mut urls = HashSet::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(url) = seq.next_element::<HttpUrl>()? { + urls.replace(url).map_or(Ok(()), |dup| { + Err(Error::invalid_value( + Unexpected::Other(dup.to_string().as_str()), + &"a set of unique URLs", + )) + })?; + } + Ok(Urls(urls)) + } + } + deserializer.deserialize_seq(HashVisitor) + } + } + let mut timeout = None; + let mut rpz = None; + let mut local_dir = None; + let mut ad = None; + let mut dom = None; + let mut hst = None; + let mut wc = None; + while let Some(key) = map.next_key()? { + match key { + Field::Timeout => { + if timeout.is_some() { + return Err(Error::duplicate_field("timeout")); + } + timeout = Some(Duration::from_secs(map.next_value::<u32>()? as u64)); + } + Field::Rpz => { + if rpz.is_some() { + return Err(Error::duplicate_field("rpz")); + } + rpz = Some(map.next_value::<AbsFilePath<false>>()?); + } + Field::LocalDir => { + if local_dir.is_some() { + return Err(Error::duplicate_field("local_dir")); + } + local_dir = Some(map.next_value::<AbsFilePath<true>>()?); + } + Field::Adblock => { + if ad.is_some() { + return Err(Error::duplicate_field("adblock")); + } + ad = Some(map.next_value::<Urls>()?); + } + Field::Domain => { + if dom.is_some() { + return Err(Error::duplicate_field("domain")); + } + dom = Some(map.next_value::<Urls>()?); + } + Field::Hosts => { + if hst.is_some() { + return Err(Error::duplicate_field("hosts")); + } + hst = Some(map.next_value::<Urls>()?); + } + Field::Wildcard => { + if wc.is_some() { + return Err(Error::duplicate_field("wildcard")); + } + wc = Some(map.next_value::<Urls>()?); + } + } + } + if local_dir.is_none() + && ad.as_ref().map_or(true, |urls| urls.0.is_empty()) + && dom.as_ref().map_or(true, |urls| urls.0.is_empty()) + && hst.as_ref().map_or(true, |urls| urls.0.is_empty()) + && wc.as_ref().map_or(true, |urls| urls.0.is_empty()) + { + Err(Error::invalid_type( + Unexpected::Other("no block list URLs or directory"), + &"at least one block list entry (i.e., 'local_dir', 'adblock', 'domain', 'hosts', or 'wildcard' must exist and not be empty)", + )) + } else { + let adblock = ad.map_or_else(HashSet::new, |urls| urls.0); + let domain = dom.map_or_else(HashSet::new, |urls| urls.0); + let hosts = hst.map_or_else(HashSet::new, |urls| urls.0); + let wildcard = wc.map_or_else(HashSet::new, |urls| urls.0); + hash_overlap([&adblock, &domain, &hosts, &wildcard].as_slice()).map(|()| { + Config { + timeout, + rpz, + local_dir, + adblock, + domain, + hosts, + wildcard, + } + }) + } + } + } + /// `Config` fields. + const VARIANTS: [&str; 7] = [ + "timeout", + "rpz", + "local_dir", + "adblock", + "domain", + "hosts", + "wildcard", + ]; + deserializer.deserialize_struct("Config", &VARIANTS, ConfigVisitor) + } +} +#[cfg(test)] +mod tests { + use crate::Config; + use toml; + #[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()); + } + #[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()); + } + #[test] + fn test_timeout() { + assert!(toml::from_str::<Config>(r#"local_dir="/foo/""#).is_ok()); + assert!(toml::from_str::<Config>( + r#"timeout=15 +local_dir="/foo/""# + ) + .is_ok()); + assert!(toml::from_str::<Config>( + r#"timeout=4294967295 +local_dir="/foo/""# + ) + .is_ok()); + assert!(toml::from_str::<Config>( + r#"timeout=-1 +local_dir="/foo/""# + ) + .is_err()); + assert!(toml::from_str::<Config>( + r#"timeout=0 +local_dir="/foo/""# + ) + .is_ok()); + assert!(toml::from_str::<Config>( + r#"timeout=4294967296 +local_dir="/foo/""# + ) + .is_err()); + } + #[test] + fn test_arrays() { + assert!(toml::from_str::<Config>( + r#"adblock=["https://foo.com/foo","https://foo.com/foo"]"# + ) + .is_err()); + assert!(toml::from_str::<Config>(r#"adblock=["https://foo.com/foo"]"#).is_ok()); + assert!(toml::from_str::<Config>( + r#"adblock=["https://foo.com/foo"] +domain=["https://foo.com/foo"]"# + ) + .is_err()); + } + #[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!(toml::from_str::<Config>( + r#"adblock=[] +domain=["https:///foo"]"# + ) + .is_ok()); + 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()); + } + #[test] + fn test_paths() { + assert!(toml::from_str::<Config>( + r#"rpz="/foo/" +wildcard=["https://foo.com/foo"]"# + ) + .is_err()); + assert!(toml::from_str::<Config>( + r#"rpz="foo" +wildcard=["https://foo.com/foo"]"# + ) + .is_err()); + assert!(toml::from_str::<Config>( + r#"rpz="/foo" +wildcard=["https://foo.com/foo"]"# + ) + .is_ok()); + assert!(toml::from_str::<Config>(r#"local_dir="foo/""#).is_err()); + assert!(toml::from_str::<Config>(r#"local_dir="/foo/""#).is_ok()); + // 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"]"# + ) + .map_or(false, |config| config.local_dir.map_or(false, |dir| dir + .to_str() + .map_or(false, |val| val == String::from("/foo/"))))); + } +} diff --git a/src/dom.rs b/src/dom.rs @@ -0,0 +1,3265 @@ +#![allow( + clippy::arithmetic_side_effects, + clippy::missing_trait_methods, + clippy::implicit_return, + clippy::into_iter_on_ref, + clippy::question_mark_used, + clippy::ref_patterns, + clippy::single_char_lifetime_names, + clippy::unseparated_literal_suffix, + clippy::wildcard_enum_match_arm +)] +use crate::dom_count_auto_gen::proper_subdomain_count; +use core::borrow::Borrow; +use core::cmp::Ordering; +use core::convert::{self, AsRef}; +use core::fmt::{self, Display, Formatter}; +use core::hash::{Hash, Hasher}; +use core::iter::FusedIterator; +use core::num::NonZeroU8; +use core::ops::Deref; +use core::str; +use num_bigint::BigUint; +use std::error; +use std::io::{Error, Write}; +use std::net::Ipv4Addr; +use superset_map::SetOrd; +use zfc::{BoundedCardinality, Cardinality, Set}; +/// A flag used to indicate information about the characters +/// in a `Domain`. This flag is used to perform more efficient +/// comparisons that can potentially avoid temporary +/// memory allocations to treat uppercase letters as if they +/// were lowercase. +#[allow(clippy::exhaustive_enums)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum CharFlag { + /// No letters, ticks, or underscores. + None, + /// No uppercase letters; but there are lowercase letters, ticks, or underscores. + LowerOrTick, + /// No lowercase letters, ticks, or underscores; but there are uppercase letters. + Upper, + /// There are uppercase letters and ticks or underscores. + /// Note this means there can also be lowercase letters, but that is fine. + UpperAndTick, + /// There are both lowercase and uppercase letters, but there are no ticks or underscores. + Mixed, +} +impl CharFlag { + /// Returns a `bool` that indicates + /// if equivalence must be done in a case + /// insensitive way. + #[inline] + const fn eq_ignore_case(self, other: Self) -> bool { + match self { + Self::None => false, + Self::LowerOrTick => !matches!(other, Self::None | Self::LowerOrTick), + Self::Upper | Self::UpperAndTick => { + !matches!(other, Self::None | Self::Upper | Self::UpperAndTick) + } + Self::Mixed => !matches!(other, Self::None), + } + } +} +/// A domain that consists of at least one [`Label`]. +/// The total length of a `Domain` is at most 253 +/// characters in length including the `.` seperator. +/// The trailing `.`, if one exists, is always removed. +/// +/// This is more restrictive than what a domain is allowed to be +/// per the [Domain Name System (DNS)](https://www.rfc-editor.org/rfc/rfc2181), +/// but it is more permissive than what [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) +/// and [RFC 5891](https://datatracker.ietf.org/doc/html/rfc5891) allow. +/// In particular only the ASCII/UTF-8 encoding of the following Unicode scalar values is allowed in a `Label`: +/// +/// `!`, `$`, `&`, `'`, `(`, `)`, `+`, `,`, `-`, `0`–`9`, `;`, `=`, `_`, `` ` ``, `A`–`Z`, `a`–`z`, `{`, `}`, `~`. +/// +/// with each `Label` delimited by `.`. Uppercase letters are treated as lowercase; +/// however for better comparison performance that doesn't lead to intermediate memory allocations, +/// two `Domain`s should consist entirely of the same case. `Domain`s must not be an [`Ipv4Addr`]. +/// +/// Those Unicode scalar values were chosen based on what [Firefox](https://www.mozilla.org/en-US/firefox/) +/// allows as of 2023-09-03T20:50+00:00. +#[derive(Clone, Debug)] +pub struct Domain<'a> { + /// The domain value. + /// Guaranteed to have length between 1 and 253. + /// Guaranteed to be the UTF-8 encoding of a + /// sequence of Unicode scalar values from the set: + /// `!`, `$`, `&`, `'`, `(`, `)`, `+`, `,`, `-`, `0`–`9`, + /// `;`, `=`, `_`, `` ` ``, `A`–`Z`, `a`–`z`, `.`, `{`, `}`, and `~`. + /// This is stored as a slice of bytes to + /// allow for easier construction of `Label`s. + value: &'a [u8], + /// The lengths of each label. + /// Guaranteed to have length between 1 and 127 + /// with each value being between 1 and 63. + label_lens: Vec<NonZeroU8>, + /// Flag that contain information about the kind of + /// characters in `value`. + flag: CharFlag, +} +impl<'a> Domain<'a> { + /// The maximum length of a `Domain`. + /// `Domain`s don't include the trailing `.` nor the 0-octet root label, + /// so this is 253. + // SAFETY: 0 < 253 < 256. + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + pub const MAX_LEN: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(253) }; + /// The minimum length of a `Domain`. + /// `Domain`s don't include the trailing `.` nor the 0-octet root label, + /// so this is 1. + // SAFETY: 0 < 1 < 256. + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + pub const MIN_LEN: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1) }; + /// The domain which always has at least one label. + /// The last label is _not_ trailed by `.` + #[allow(unsafe_code)] + #[inline] + #[must_use] + pub const fn as_str(&self) -> &'a str { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + unsafe { str::from_utf8_unchecked(self.value) } + } + /// Returns the count of [`Label`]s. + /// Due to length requirements of `Label` and `Domain`, + /// this is less than `128`. + #[inline] + #[must_use] + #[allow(unsafe_code, clippy::as_conversions, clippy::cast_possible_truncation)] + pub fn label_count(&self) -> NonZeroU8 { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + // Due to the length requirements of both `Domain` and `Label`, + // `label_lens` has between 1 and 127 values which is a valid + // `NonZeroU8`. + unsafe { NonZeroU8::new_unchecked(self.label_lens.len() as u8) } + } + /// The length of the `Domain`. + /// This is the same as `self.as_str().len()`. + #[inline] + #[must_use] + #[allow(unsafe_code, clippy::as_conversions, clippy::cast_possible_truncation)] + pub const fn len(&self) -> NonZeroU8 { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + // Due to the length requirements of `Domain` , + // `self.value` is guaranteed to have length > 0 and < 254. + unsafe { NonZeroU8::new_unchecked(self.value.len() as u8) } + } + /// Returns an [`Iterator`] of [`Label`]s without + /// consuming the `Domain`. + #[inline] + #[must_use] + pub fn iter(&self) -> LabelIter<'a, '_> { + self.into_iter() + } + /// Recursively checks from the TLD if each label is the same + /// until one domain has no more labels. + #[inline] + fn same_labels(&self, other: &Domain<'_>) -> bool { + self.into_iter() + .zip(other) + .try_fold((), |(), (label, label2)| { + if label.value == label2.value { + Ok(()) + } else { + Err(()) + } + }) + .map_or(false, |()| true) + } + /// Recursively checks from the TLD if each label is the same ignoring case + /// until one domain has no more labels. + #[inline] + fn same_labels_ignore_case(&self, other: &Domain<'_>) -> bool { + self.into_iter() + .zip(other) + .try_fold((), |(), (label, label2)| { + if label.value.eq_ignore_ascii_case(label2.value) { + Ok(()) + } else { + Err(()) + } + }) + .map_or(false, |()| true) + } + /// Function that transforms a slice of bytes into a `Domain`. + /// It is not public since we only want to allow valid `str`s. + /// Trailing `.` is removed first. + #[allow( + clippy::as_conversions, + clippy::indexing_slicing, + clippy::option_if_let_else, + clippy::unreachable + )] + #[inline] + fn try_from_slice<'b: 'a>(mut value: &'b [u8]) -> Result<Self, DomainErr> { + if Ipv4Addr::parse_ascii(value).is_ok() { + return Err(DomainErr::Ipv4); + } + value = value.last().map_or(value, |byt| { + if *byt == b'.' { + &value[..value.len() - 1] + } else { + value + } + }); + let len = value.len(); + if len < Self::MIN_LEN.get() as usize { + Err(DomainErr::Empty) + } else if value.len() > Self::MAX_LEN.get() as usize { + Err(DomainErr::LenExceeds253(len)) + } else { + let mut label_lens = Vec::with_capacity(3); + let mut label_len = 0; + // Bitwise flag that means: + // 0 => no letters, ticks, or underscores, + // 1 => lowercase letters; but no uppercase letters, ticks, or underscores. + // 2 => uppercase letters; but no lowercase letters, ticks, or underscores. + // 4 => ticks or underscores, but no letters. + let mut flag = 0u8; + match value.into_iter().try_fold((), |(), byt| { + match *byt { + b'.' => { + return NonZeroU8::new(label_len).map_or( + Err(DomainErr::EmptyLabel), + |length| { + label_lens.push(length); + label_len = 0; + Ok(()) + }, + ) + } + b'A'..=b'Z' => flag |= 2, + b'`' | b'_' => flag |= 4, + b'a'..=b'z' => flag |= 1, + 0..=b' ' + | b'"'..=b'#' + | b'%' + | b'*' + | b'/' + | b':' + | b'<' + | b'>'..=b'@' + | b'['..=b'^' + | b'|' + | 127.. => return Err(DomainErr::InvalidByte(*byt)), + _ => (), + } + if label_len == 63 { + Err(DomainErr::LabelLenExceeds63) + } else { + label_len += 1; + Ok(()) + } + }) { + Ok(()) => match NonZeroU8::new(label_len) { + Some(length) => { + label_lens.push(length); + Ok(Self { + value, + label_lens, + flag: match flag { + 0 => CharFlag::None, + 1 | 4 | 5 => CharFlag::LowerOrTick, + 2 => CharFlag::Upper, + 3 => CharFlag::Mixed, + 6 | 7 => CharFlag::UpperAndTick, + _ => unreachable!("there is a bug in Domain::try_from_slice"), + }, + }) + } + None => Err(DomainErr::EmptyLabel), + }, + Err(err) => Err(err), + } + } + } +} +impl PartialEq<Domain<'_>> for Domain<'_> { + #[inline] + fn eq(&self, other: &Domain<'_>) -> bool { + if self.flag.eq_ignore_case(other.flag) { + self.value.eq_ignore_ascii_case(other.value) + } else { + self.value == other.value + } + } +} +impl Eq for Domain<'_> {} +impl PartialOrd<Domain<'_>> for Domain<'_> { + #[inline] + fn partial_cmp(&self, other: &Domain<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl<'a> AsRef<str> for Domain<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl<'a> Deref for Domain<'a> { + type Target = str; + #[inline] + fn deref(&self) -> &Self::Target { + self.as_str() + } +} +/// `Ok(())` is returned iff the `Domain`s have the same labels +/// with one `Domain` having more labels than the other. +#[inline] +fn cmp_doms(left: &Domain<'_>, right: &Domain<'_>) -> Result<(), Ordering> { + let left_input: Vec<u8>; + let right_input: Vec<u8>; + let left_dom: Domain<'_>; + let right_dom: Domain<'_>; + let (left_ref, right_ref) = match (left.flag, right.flag) { + (CharFlag::None, _) + | (_, CharFlag::None) + | (CharFlag::LowerOrTick, CharFlag::LowerOrTick) + | (CharFlag::Upper, CharFlag::Upper) => (left, right), + (CharFlag::LowerOrTick, _) => { + right_input = right.value.to_ascii_lowercase(); + right_dom = Domain { + value: right_input.as_slice(), + label_lens: right.label_lens.clone(), + flag: CharFlag::LowerOrTick, + }; + (left, &right_dom) + } + (CharFlag::Upper, CharFlag::Mixed) => { + right_input = right.value.to_ascii_uppercase(); + right_dom = Domain { + value: right_input.as_slice(), + label_lens: right.label_lens.clone(), + flag: CharFlag::Upper, + }; + (left, &right_dom) + } + (_, CharFlag::LowerOrTick) => { + left_input = left.value.to_ascii_lowercase(); + left_dom = Domain { + value: left_input.as_slice(), + label_lens: left.label_lens.clone(), + flag: CharFlag::LowerOrTick, + }; + (&left_dom, right) + } + (CharFlag::Mixed, CharFlag::Upper) => { + left_input = left.value.to_ascii_uppercase(); + left_dom = Domain { + value: left_input.as_slice(), + label_lens: left.label_lens.clone(), + flag: CharFlag::Upper, + }; + (&left_dom, right) + } + (_, _) => { + left_input = left.value.to_ascii_lowercase(); + left_dom = Domain { + value: left_input.as_slice(), + label_lens: left.label_lens.clone(), + flag: CharFlag::LowerOrTick, + }; + right_input = right.value.to_ascii_lowercase(); + right_dom = Domain { + value: right_input.as_slice(), + label_lens: right.label_lens.clone(), + flag: CharFlag::LowerOrTick, + }; + (&left_dom, &right_dom) + } + }; + // Faster to compare the entire value when we can instead of each label. + if left_ref.value == right_ref.value { + Err(Ordering::Equal) + } else { + left_ref + .into_iter() + .zip(right_ref) + .try_fold((), |(), (label, label2)| { + match label.value.cmp(label2.value) { + Ordering::Less => Err(Ordering::Less), + Ordering::Equal => Ok(()), + Ordering::Greater => Err(Ordering::Greater), + } + }) + } +} +impl Ord for Domain<'_> { + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. Return the comparison of `Label` counts. + /// + /// For example, `com` `<` `example.com` `<` `net` `<` `example.net`. + /// + /// This is the same as the [canonical DNS name order](https://datatracker.ietf.org/doc/html/rfc4034#section-6.1). + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + cmp_doms(self, other).map_or_else(convert::identity, |()| { + self.label_count().cmp(&other.label_count()) + }) + } +} +impl Hash for Domain<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + match self.flag { + CharFlag::None | CharFlag::LowerOrTick => self.value.hash(state), + _ => self.value.to_ascii_lowercase().hash(state), + } + } +} +impl Display for Domain<'_> { + #[inline] + #[allow(clippy::min_ident_chars)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} +/// Error returned when an invalid string +/// is passed to [`Domain::try_from`], [`Adblock::parse_value`], +/// [`DomainOnly::parse_value`], [`Hosts::parse_value`], [`Wildcard::parse_value`], +/// or [`RpzDomain::parse_value`]. +#[allow(clippy::exhaustive_enums)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum DomainErr { + /// The domain was empty. + Empty, + /// The length of the string had length greater than 253 + /// not counting a terminating `.` if there was one. + LenExceeds253(usize), + /// The domain contained at least one empty label. + EmptyLabel, + /// The domain contained at least one label whose length exceeded 63. + LabelLenExceeds63, + /// The domain contained an invalid byte value. + /// + /// Note the contained `u8` is ASCII iff it is `<= 127`; otherwise + /// it is the first UTF-8 code unit of a multi-byte Unicode scalar value. + InvalidByte(u8), + /// The domain was an [`Ipv4Addr`]. + Ipv4, + /// The string passed to [`Adblock::parse_value`] contained `$`. + InvalidAdblockDomain, + /// The string passed to [`Hosts::parse_value`] did not conform + /// to the required [`Hosts`] format. + InvalidHostsIP, + /// The length of the non-wildcard portion of the string passed to + /// [`Wildcard::parse_value`] was at least 252 which means there are + /// no proper subdomains. + InvalidWildcardDomain, +} +impl Display for DomainErr { + #[inline] + #[allow(unsafe_code, clippy::min_ident_chars)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Empty => f.write_str("domain is empty"), + Self::LenExceeds253(len) => write!( + f, + "domain has length {len} which is greater than the max length of 253" + ), + Self::EmptyLabel => f.write_str("domain has an empty label"), + Self::LabelLenExceeds63 => { + f.write_str("domain has a label that exceeds the max length of 63") + } + Self::InvalidByte(byt) => { + if byt > 31 && byt < 127 { + let utf8 = [byt]; + // SAFETY: + // `byt` is inclusively between 32 and 126 which is valid ASCII which + // in turn is valid UTF-8. + let printable_ascii = unsafe { str::from_utf8_unchecked(utf8.as_slice()) }; + write!(f, "domain has a label with the invalid character '{printable_ascii}'") + } else { + write!(f, "domain has a label with the invalid byte value {byt}") + } + } + Self::Ipv4 => f.write_str("domain was an IPv4 address"), + Self::InvalidAdblockDomain => f.write_str("Adblock-style domain contained a '$'"), + Self::InvalidHostsIP => f.write_str("hosts-style domain does not begin with the IP '::', '::1', '0.0.0.0', or '127.0.0.1' followed by at least one space or tab"), + Self::InvalidWildcardDomain => f.write_str("non-wildcard portion of a wildcard domain had length of at least 252 which means there are 0 proper subdomains"), + } + } +} +impl error::Error for DomainErr {} +impl<'a: 'b, 'b> From<Domain<'a>> for &'b str { + #[inline] + fn from(value: Domain<'a>) -> Self { + value.as_str() + } +} +impl<'a: 'c, 'b, 'c> From<&'b Domain<'a>> for &'c str { + #[inline] + fn from(value: &'b Domain<'a>) -> Self { + value.as_str() + } +} +impl<'a: 'b, 'b> TryFrom<&'a str> for Domain<'b> { + type Error = DomainErr; + #[inline] + fn try_from(val: &'a str) -> Result<Self, Self::Error> { + Self::try_from_slice(val.as_bytes()) + } +} +/// A label of a [`Domain`]. +/// The total length of a `Label` is between 1 and 63 +/// bytes with each Unicode scalar value being one of the following: +/// +/// `!`, `$`, `&`, `'`, `(`, `)`, `+`, `,`, `-`, `0`–`9`, `;`, `=`, `_`, `` ` ``, `A`–`Z`, `a`–`z`, `{`, `}`, `~`. +/// +/// Note that the uppercase letters are treated as lowercase. +#[derive(Debug, Clone, Copy, Hash)] +pub struct Label<'a> { + /// The label value. + value: &'a str, +} +impl<'a> Label<'a> { + /// The maximum length of a `Label` which is 63. + // SAFETY: 0 < 63 < 256. + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + pub const MAX_LEN: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(63) }; + /// The minimum length of a `Label` which is 1. + // SAFETY: 0 < 1 < 256. + #[allow(unsafe_code, clippy::undocumented_unsafe_blocks)] + pub const MIN_LEN: NonZeroU8 = unsafe { NonZeroU8::new_unchecked(1) }; + /// The label. + #[inline] + #[must_use] + pub const fn as_str(self) -> &'a str { + self.value + } +} +impl Display for Label<'_> { + #[inline] + #[allow(clippy::min_ident_chars)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(self.value) + } +} +impl PartialEq<Label<'_>> for Label<'_> { + #[inline] + fn eq(&self, other: &Label<'_>) -> bool { + self.value.eq_ignore_ascii_case(other.value) + } +} +impl Eq for Label<'_> {} +impl PartialOrd<Label<'_>> for Label<'_> { + #[inline] + fn partial_cmp(&self, other: &Label<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for Label<'_> { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.value + .to_ascii_lowercase() + .cmp(&other.value.to_ascii_lowercase()) + } +} +impl<'a: 'b, 'b> From<Label<'a>> for &'b str { + #[inline] + fn from(value: Label<'a>) -> Self { + value.as_str() + } +} +impl<'a> AsRef<str> for Label<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl Deref for Label<'_> { + type Target = str; + #[inline] + fn deref(&self) -> &Self::Target { + self.as_str() + } +} +/// [`Iterator`] that iterates [`Label`]s +/// from a [`Domain`] starting from the TLD +/// down. +pub struct IntoLabelIter<'a> { + /// Domain that contains `Label`s to iterate. + domain: Domain<'a>, + /// Starts at domain.label_count().get() - 1 + /// which is valid since domain.label_count().get() > 0. + /// idx is 255 when the iterator is exhausted. + /// Since idx is decremented each time and it starts + /// at a value less than 254, this is a valid value to use + /// as a flag. + idx: u8, + /// This is used to mark the start of a label before + /// the length of the label has been subtracted. + /// After a label is read, 1 must be subtracted + /// to account for '.'. + start: u8, + /// Starts at 0 which is valid since domain.label_count().get() > 0. + /// idx_back is 255 when the iterator is exhausted. + /// Since idx_back is incremented each time and the max label count + /// is 127, this is a valid value to use as a flag. + idx_back: u8, + /// This is used to mark the start of a label before + /// the length of the label has been added. + /// After a label is read, 1 must be added + /// to account for '.'. + start_back: u8, +} +impl<'a> IntoLabelIter<'a> { + /// Helper function to construct an instance. + #[inline] + #[must_use] + fn new(domain: Domain<'a>) -> IntoLabelIter<'a> { + Self { + idx: domain.label_count().get() - 1, + start: domain.len().get(), + idx_back: 0, + start_back: 0, + domain, + } + } +} +impl<'a> Iterator for IntoLabelIter<'a> { + type Item = Label<'a>; + #[inline] + #[allow( + unsafe_code, + clippy::as_conversions, + clippy::cast_possible_truncation, + clippy::indexing_slicing + )] + fn next(&mut self) -> Option<Self::Item> { + self.domain.label_lens.get(self.idx as usize).map(|len| { + self.start -= len.get(); + let lbl = &self.domain.value[self.start as usize..(self.start + len.get()) as usize]; + let label = Label { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + value: unsafe { str::from_utf8_unchecked(lbl) }, + }; + if self.idx == 0 || self.idx <= self.idx_back { + self.idx = 255; + self.idx_back = 255; + } else { + self.idx -= 1; + self.start -= 1; + } + label + }) + } +} +impl FusedIterator for IntoLabelIter<'_> {} +impl ExactSizeIterator for IntoLabelIter<'_> { + #[allow(clippy::as_conversions)] + #[inline] + fn len(&self) -> usize { + if self.idx == 255 { + 0 + } else { + (self.idx - self.idx_back + 1) as usize + } + } +} +impl DoubleEndedIterator for IntoLabelIter<'_> { + #[inline] + #[allow( + unsafe_code, + clippy::as_conversions, + clippy::cast_possible_truncation, + clippy::indexing_slicing + )] + fn next_back(&mut self) -> Option<Self::Item> { + self.domain + .label_lens + .get(self.idx_back as usize) + .map(|len| { + let lbl = &self.domain.value + [self.start_back as usize..(self.start_back + len.get()) as usize]; + let label = Label { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + value: unsafe { str::from_utf8_unchecked(lbl) }, + }; + if self.idx_back + 1 == self.domain.label_count().get() || self.idx_back >= self.idx + { + self.idx = 255; + self.idx_back = 255; + } else { + self.idx_back += 1; + self.start_back += len.get() + 1; + } + label + }) + } +} +/// [`Iterator`] that iterates [`Label`]s +/// from a borrowed [`Domain`] starting from the TLD +/// down. +pub struct LabelIter<'a, 'b> { + /// Domain that contains `Label`s to iterate. + domain: &'b Domain<'a>, + /// Starts at domain.label_count().get() - 1 + /// which is valid since domain.label_count().get() > 0. + /// idx is 255 when the iterator is exhausted. + /// Since idx is decremented each time and it starts + /// at a value less than 254, this is a valid value to use + /// as a flag. + idx: u8, + /// This is used to mark the start of a label before + /// the length of the label has been subtracted. + /// After a label is read, 1 must be subtracted + /// to account for '.'. + start: u8, + /// Starts at 0 which is valid since domain.label_count().get() > 0. + /// idx_back is 255 when the iterator is exhausted. + /// Since idx_back is incremented each time and the max label count + /// is 127, this is a valid value to use as a flag. + idx_back: u8, + /// This is used to mark the start of a label before + /// the length of the label has been added. + /// After a label is read, 1 must be added + /// to account for '.'. + start_back: u8, +} +impl<'a, 'b> LabelIter<'a, 'b> { + /// Helper function to construct an instance. + #[inline] + #[must_use] + fn new(domain: &'b Domain<'a>) -> LabelIter<'a, 'b> { + Self { + idx: domain.label_count().get() - 1, + start: domain.len().get(), + idx_back: 0, + start_back: 0, + domain, + } + } +} +impl<'a> Iterator for LabelIter<'a, '_> { + type Item = Label<'a>; + #[inline] + #[allow( + unsafe_code, + clippy::as_conversions, + clippy::cast_possible_truncation, + clippy::indexing_slicing + )] + fn next(&mut self) -> Option<Self::Item> { + self.domain.label_lens.get(self.idx as usize).map(|len| { + self.start -= len.get(); + let lbl = &self.domain.value[self.start as usize..(self.start + len.get()) as usize]; + let label = Label { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + value: unsafe { str::from_utf8_unchecked(lbl) }, + }; + if self.idx == 0 || self.idx <= self.idx_back { + self.idx = 255; + self.idx_back = 255; + } else { + self.idx -= 1; + self.start -= 1; + } + label + }) + } +} +impl FusedIterator for LabelIter<'_, '_> {} +impl ExactSizeIterator for LabelIter<'_, '_> { + #[allow(clippy::as_conversions)] + #[inline] + fn len(&self) -> usize { + if self.idx == 255 { + 0 + } else { + (self.idx - self.idx_back + 1) as usize + } + } +} +impl DoubleEndedIterator for LabelIter<'_, '_> { + #[inline] + #[allow( + unsafe_code, + clippy::as_conversions, + clippy::cast_possible_truncation, + clippy::indexing_slicing + )] + fn next_back(&mut self) -> Option<Self::Item> { + self.domain + .label_lens + .get(self.idx_back as usize) + .map(|len| { + let lbl = &self.domain.value + [self.start_back as usize..(self.start_back + len.get()) as usize]; + let label = Label { + // SAFETY: + // The only way to construct a `Domain` is via + // `try_from` which only uses a valid ASCII + // substring from the original `str` input. + // `Domain` is immutable ensuring such invariants are kept. + value: unsafe { str::from_utf8_unchecked(lbl) }, + }; + if self.idx_back + 1 == self.domain.label_count().get() || self.idx_back >= self.idx + { + self.idx = 255; + self.idx_back = 255; + } else { + self.idx_back += 1; + self.start_back += len.get() + 1; + } + label + }) + } +} +impl<'a> IntoIterator for Domain<'a> { + type Item = Label<'a>; + type IntoIter = IntoLabelIter<'a>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + IntoLabelIter::new(self) + } +} +impl<'a, 'b> IntoIterator for &'b Domain<'a> { + type Item = Label<'a>; + type IntoIter = LabelIter<'a, 'b>; + #[inline] + fn into_iter(self) -> Self::IntoIter { + LabelIter::new(self) + } +} +/// Action taken by a DNS server when a domain +/// matches. +#[allow(clippy::exhaustive_enums)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum RpzAction { + /// Send `NXDOMAIN` reply. + Nxdomain, + /// Send `NODATA` reply. + Nodata, + /// Do nothing; continue as normal. + Passthru, + /// Drop the query. + Drop, + /// Answer over TCP. + TcpOnly, +} +impl Display for RpzAction { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Nxdomain => f.write_str("NXDOMAIN"), + Self::Nodata => f.write_str("NODATA"), + Self::Passthru => f.write_str("PASSTHRU"), + Self::Drop => f.write_str("DROP"), + Self::TcpOnly => f.write_str("TCP-Only"), + } + } +} +/// Writes the following line with `writer` based on `action`: +/// * `RpzAction::Nxdomain`: `<dom> CNAME .`. +/// * `RpzAction::Nodata`: `<dom> CNAME *.`. +/// * `RpzAction::Passthru`: `<dom> CNAME rpz-passthru.`. +/// * `RpzAction::Drop`: `<dom> CNAME rpz-drop.`. +/// * `RpzAction::TcpOnly`: `<dom> CNAME rpz-tcp-only.`. +/// +/// `*.` is prepended to `<dom>` iff `wildcard`. +/// +/// # Errors +/// +/// Returns [`Error`] iff [`writeln`] does. +#[inline] +pub fn write_rpz_line<W: Write>( + mut writer: W, + dom: &Domain<'_>, + action: RpzAction, + wildcard: bool, +) -> Result<(), Error> { + writeln!( + writer, + "{}{} CNAME {}.", + if wildcard { "*." } else { "" }, + dom, + match action { + RpzAction::Nxdomain => "", + RpzAction::Nodata => "*", + RpzAction::Passthru => "rpz-passthru", + RpzAction::Drop => "rpz-drop", + RpzAction::TcpOnly => "rpz-tcp-only", + } + ) +} +/// Type that can be returned by [`Domain`]-like +/// parsers (e.g., [`Adblock`]). +#[allow(clippy::exhaustive_enums)] +#[derive(Clone, Copy, Debug)] +pub enum Value<'a, T: ParsedDomain<'a>> { + /// The parsed value is a domain. + Domain(T), + /// The parsed value is a comment. + Comment(&'a str), + /// The parsed value is blank or just [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace). + Blank, +} +impl<'a, T: ParsedDomain<'a>> Value<'a, T> { + /// Returns `true` iff `self` is a [`Self::Domain`]. + #[inline] + pub const fn is_domain(&self) -> bool { + match *self { + Self::Domain(_) => true, + Self::Comment(_) | Self::Blank => false, + } + } + /// Returns `true` iff `self` is a [`Self::Comment`]. + #[inline] + pub const fn is_comment(&self) -> bool { + match *self { + Self::Comment(_) => true, + Self::Domain(_) | Self::Blank => false, + } + } + /// Returns `true` iff `self` is a [`Self::Blank`]. + #[inline] + pub const fn is_blank(&self) -> bool { + matches!(*self, Value::Blank) + } + /// Returns the contained [`Self::Domain`] value. + /// + /// # Panics + /// + /// Panics iff `self` is [`Self::Comment`] or [`Self::Blank`]. + #[allow(clippy::panic)] + #[inline] + pub fn unwrap_domain(self) -> T { + match self { + Self::Domain(dom) => dom, + Self::Comment(_) | Self::Blank => { + panic!("called `ParsedDomain::unwrap_domain()` on a `Comment` or `Blank` value") + } + } + } + /// Returns the contained [`prim@str`] in [`Self::Comment`]. + /// + /// # Panics + /// + /// Panics iff `self` is [`Self::Domain`] or [`Self::Blank`]. + #[allow(clippy::panic)] + #[inline] + pub fn unwrap_comment(self) -> &'a str { + match self { + Self::Comment(com) => com, + Self::Domain(_) | Self::Blank => { + panic!("called `ParsedDomain::unwrap_comment()` on a `Domain` or `Blank` value") + } + } + } + /// Returns [`unit`] when `self` is [`Self::Blank`]. + /// + /// # Panics + /// + /// Panics iff `self` is [`Self::Domain`] or [`Self::Comment`]. + #[allow(clippy::panic)] + #[inline] + pub fn unwrap_blank(self) { + match self { + Self::Blank => {} + Self::Domain(_) | Self::Comment(_) => { + panic!("called `ParsedDomain::unwrap_blank()` on a `Domain` or `Comment` value") + } + } + } +} +/// Structure of a [`Domain`]-like type that can parse [`prim@str`]s into [`Value`]s. +/// When parsed into a [`Value::Domain`], the domain can +/// be written to a [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) +/// file. +pub trait ParsedDomain<'a>: Sized { + type Error; + /// Parses a `str` into a `Value`. + /// # Errors + /// + /// Errors iff `val` is unable to be parsed into a `Value`. + fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error>; + /// Reference to the contained `Domain`. + fn domain(&self) -> &Domain<'a>; + /// Writes `self` as RPZ lines via `writer`. + /// + /// # Errors + /// + /// Errors iff `writer` errors. + fn write_to_rpz<W: Write>(&self, action: RpzAction, writer: W) -> Result<(), Error>; +} +/// Domain constructed from an [Adblock-style rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#adblock-style-syntax) +/// with the requirement that the rule conforms to the following extended regex: +/// +/// `^<ws>*(\|\|)?<ws>*<domain><ws>*\^?<ws>*$` +/// +/// where `<domain>` conforms to a valid [`Domain`] with the added requirement that it does not contain `$`, and +/// `<ws>` is any sequence of [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace). +/// +/// Comments are any lines that start with `!` or `#` (ignoring whitespace). Any in-line comments +/// after a valid domain are ignored and will be parsed into a [`Value::Domain`]. +/// +/// Note that this means some valid Adblock-style rules are not considered valid since +/// such rules often contain path information or modifiers (e.g., “third-party”), but this only +/// considers domain-only rules. +#[derive(Clone, Debug)] +pub struct Adblock<'a> { + /// The `Domain`. + domain: Domain<'a>, + /// `true` iff `domain` represents all subdomains. + /// Note that this includes `domain` itself. + subdomains: bool, +} +impl<'a> Adblock<'a> { + /// Returns `true` iff the contained [`Domain`] represents all subdomains. + /// Note this includes the `Domain` itself. + #[inline] + #[must_use] + pub const fn is_subdomains(&self) -> bool { + self.subdomains + } + /// Since `DomainOnly` and `Hosts` are treated the same, + /// we have this helper function that can be used for both. + #[inline] + #[must_use] + fn cmp_dom(&self, other: &Domain<'_>) -> Ordering { + cmp_doms(&self.domain, other).map_or_else( + |ord| { + if ord == Ordering::Equal && self.subdomains { + Ordering::Greater + } else { + ord + } + }, + |()| { + // At this point `self` and `other` have different number of labels; + // otherwise they would have the same domain which was already + // checked for in `cmp_doms`. + if self.subdomains { + Ordering::Greater + } else { + self.domain.label_count().cmp(&other.label_count()) + } + }, + ) + } + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. If `self` represents a single `Domain` (i.e., `!self.is_subdomains()`), + /// then return the comparison of label counts. + /// 4. `self` is greater. + /// + /// For example, `com` `<` `example.com` `<` `||example.com` `<` `||com` `<` `net` `<` `example.net` `<` `||example.net` `<` `||net`. + #[inline] + #[must_use] + pub fn cmp_domain_only(&self, other: &DomainOnly<'_>) -> Ordering { + self.cmp_dom(&other.domain) + } + /// Same as [`Adblock::cmp_domain_only`]. + #[inline] + #[must_use] + pub fn cmp_hosts(&self, other: &Hosts<'_>) -> Ordering { + self.cmp_dom(&other.domain) + } + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. If both domains represent a single `Domain`, then return the comparison + /// of label counts. + /// 4. If one domain represents a single `Domain`, then return that that domain is less. + /// 5. If the label counts are the same, `self` is greater. + /// 6. Return the inverse of the comparison of label counts. + /// + /// 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` + #[inline] + #[must_use] + pub fn cmp_wildcard(&self, other: &Wildcard<'_>) -> Ordering { + cmp_doms(&self.domain, &other.domain).map_or_else( + |ord| { + if ord == Ordering::Equal { + if self.subdomains { + Ordering::Greater + } else if other.proper_subdomains { + Ordering::Less + } else { + ord + } + } else { + ord + } + }, + |()| { + // At this point `self` and `other` have different number of labels; + // otherwise they would have the same domain which was already + // checked for in `cmp_doms`. + if self.subdomains { + if !other.proper_subdomains + || self.domain.label_count() < other.domain.label_count() + { + Ordering::Greater + } else { + Ordering::Less + } + } else if other.proper_subdomains + || self.domain.label_count() < other.domain.label_count() + { + Ordering::Less + } else { + Ordering::Greater + } + }, + ) + } + /// Same as [`Adblock::cardinality`] except + /// that a `BigUint` is returned. + /// Note the count _includes_ the `Domain` itself + /// when `self.is_subdomains()`. + /// + /// `!self.is_subdomains()` ⇔ `self.domain_count() == BigUint::new(vec![1])`. + #[inline] + #[must_use] + pub fn domain_count(&self) -> BigUint { + if self.subdomains { + proper_subdomain_count(&self.domain) + BigUint::new(vec![1]) + } else { + BigUint::new(vec![1]) + } + } +} +impl Display for Adblock<'_> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}", + if self.subdomains { "||" } else { "" }, + self.domain + ) + } +} +impl PartialEq<Adblock<'_>> for Adblock<'_> { + #[inline] + fn eq(&self, other: &Adblock<'_>) -> bool { + self.domain == other.domain && self.subdomains == other.subdomains + } +} +impl PartialEq<DomainOnly<'_>> for Adblock<'_> { + #[inline] + fn eq(&self, other: &DomainOnly<'_>) -> bool { + !self.subdomains && self.domain == other.domain + } +} +impl PartialEq<Hosts<'_>> for Adblock<'_> { + #[inline] + fn eq(&self, other: &Hosts<'_>) -> bool { + !self.subdomains && self.domain == other.domain + } +} +impl PartialEq<Wildcard<'_>> for Adblock<'_> { + #[allow(clippy::suspicious_operation_groupings)] + #[inline] + fn eq(&self, other: &Wildcard<'_>) -> bool { + !(self.subdomains || other.proper_subdomains) && self.domain == other.domain + } +} +impl Eq for Adblock<'_> {} +impl Hash for Adblock<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + self.domain.hash(state); + } +} +impl PartialOrd<Adblock<'_>> for Adblock<'_> { + #[inline] + fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for Adblock<'_> { + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. If both domains represent a single `Domain`, then return the comparison + /// of label counts. + /// 4. If one domain represents a single `Domain`, then return that that domain is less. + /// 5. Return the inverse of the comparison of label counts. + /// + /// For example, `com` `<` `example.com` `<` `||example.com` `<` `||com` `<` `net` `<` `example.net` `<` `||example.net` `<` `||net`. + #[inline] + #[must_use] + fn cmp(&self, other: &Self) -> Ordering { + cmp_doms(&self.domain, &other.domain).map_or_else( + |ord| { + if ord == Ordering::Equal { + if self.subdomains { + if other.subdomains { + ord + } else { + Ordering::Greater + } + } else if other.subdomains { + Ordering::Less + } else { + ord + } + } else { + ord + } + }, + |()| { + // At this point `self` and `other` have different number of labels; + // otherwise they would have the same domain which was already + // checked for in `cmp_doms`. + if self.subdomains { + if !other.subdomains || self.domain.label_count() < other.domain.label_count() { + Ordering::Greater + } else { + Ordering::Less + } + } else if other.subdomains || self.domain.label_count() < other.domain.label_count() + { + Ordering::Less + } else { + Ordering::Greater + } + }, + ) + } +} +impl PartialOrd<DomainOnly<'_>> for Adblock<'_> { + #[inline] + fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> { + Some(self.cmp_domain_only(other)) + } +} +impl PartialOrd<Hosts<'_>> for Adblock<'_> { + #[inline] + fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> { + Some(self.cmp_hosts(other)) + } +} +impl PartialOrd<Wildcard<'_>> for Adblock<'_> { + #[inline] + fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> { + Some(self.cmp_wildcard(other)) + } +} +impl<'a> Set for Adblock<'a> { + type Elem = Domain<'a>; + #[inline] + fn bounded_cardinality(&self) -> BoundedCardinality { + BoundedCardinality::from_biguint_exact(self.domain_count()) + } + #[inline] + fn cardinality(&self) -> Option<Cardinality> { + Some(Cardinality::Finite(self.domain_count())) + } + #[inline] + fn contains<Q>(&self, elem: &Q) -> bool + where + Q: Borrow<Self::Elem> + Eq + ?Sized, + { + if self.subdomains { + let dom2 = elem.borrow(); + self.domain.label_count() <= dom2.label_count() + && if self.domain.flag.eq_ignore_case(dom2.flag) { + self.domain.same_labels_ignore_case(dom2) + } else { + self.domain.same_labels(dom2) + } + } else { + self.domain == *elem.borrow() + } + } + #[inline] + fn is_proper_subset(&self, val: &Self) -> bool { + // A single domain can never be a proper superset. + // Subdomains` cannot be a proper superset + // if it has more labels or the same number of labels + // as another subdomains. + // In all other cases, we need to recursively check from the TLD + // that the labels are the same. + val.subdomains + && match val.domain.label_count().cmp(&self.domain.label_count()) { + Ordering::Less => { + if self.domain.flag.eq_ignore_case(val.domain.flag) { + val.domain.same_labels_ignore_case(&self.domain) + } else { + val.domain.same_labels(&self.domain) + } + } + Ordering::Equal => { + !self.subdomains + && if val.domain.flag.eq_ignore_case(self.domain.flag) { + val.domain.value.eq_ignore_ascii_case(self.domain.value) + || val.domain.same_labels_ignore_case(&self.domain) + } else { + val.domain.value == self.domain.value + || val.domain.same_labels(&self.domain) + } + } + Ordering::Greater => false, + } + } + #[inline] + fn is_subset(&self, val: &Self) -> bool { + self == val || self.is_proper_subset(val) + } +} +impl SetOrd for Adblock<'_> {} +impl<'a> AsRef<Domain<'a>> for Adblock<'a> { + #[inline] + fn as_ref(&self) -> &Domain<'a> { + &self.domain + } +} +impl<'a> AsRef<str> for Adblock<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl<'a> Deref for Adblock<'a> { + type Target = Domain<'a>; + #[inline] + fn deref(&self) -> &Self::Target { + &self.domain + } +} +impl<'a> ParsedDomain<'a> for Adblock<'a> { + type Error = DomainErr; + #[allow(unsafe_code, clippy::indexing_slicing)] + #[inline] + fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> { + // First remove leading whitepace. + // Then check for comments via '#' and '!'. + // Return Blank iff empty. + // Return Comment iff '#' or '!' is the first character. + // Remove trailing whitespace. + // Next remove the last byte if it is '^' as well as whitespace before. + // Next track and remove '||' at the beginning and any subsequent whitespace. + let mut value = val.as_bytes().trim_ascii_start(); + value.first().map_or_else( + || Ok(Value::Blank), + |byt| { + if *byt == b'#' || *byt == b'!' { + // SAFETY: + // `value` came from `val` with leading ASCII whitespace removed + // which is still valid UTF-8. Since the first byte is '#' or '$' + // the remaining bytes is still valid UTF-8. + let comment = unsafe { str::from_utf8_unchecked(&value[1..]) }; + Ok(Value::Comment(comment)) + } else { + value = value.trim_ascii_end(); + let len = value.len().wrapping_sub(1); + value = value.get(len).map_or(value, |byt2| { + if *byt2 == b'^' { + value[..len].trim_ascii_end() + } else { + value + } + }); + let (subdomains, val2) = value.get(..2).map_or_else( + || (false, value), + |fst| { + if fst == b"||" { + (true, value[2..].trim_ascii_start()) + } else { + (false, value) + } + }, + ); + // `Domain`s allow `$`, but we don't want to allow that symbol + // for Adblock-style rules. + val2.into_iter() + .try_fold((), |(), byt2| { + if *byt2 == b'$' { + Err(DomainErr::InvalidAdblockDomain) + } else { + Ok(()) + } + }) + .and_then(|()| { + Domain::try_from_slice(val2).map(|domain| { + // A domain of length 252 or 253 can't have subdomains + // due to there not being enough characters. + Value::Domain(Self { + subdomains: if domain.len().get() > 251 { + false + } else { + subdomains + }, + domain, + }) + }) + }) + } + }, + ) + } + #[inline] + fn domain(&self) -> &Domain<'a> { + &self.domain + } + #[inline] + fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> { + write_rpz_line(&mut writer, self.domain(), action, false).and_then(|()| { + if self.subdomains { + write_rpz_line(writer, self.domain(), action, true) + } else { + Ok(()) + } + }) + } +} +/// Domain constructed from a [domains-only rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#domains-only-syntax) +/// with the requirement that the rule conforms to the following regex: +/// +/// `^<ws>*<domain><ws>*(#.*)?$` +/// +/// where `<domain>` conforms to a valid [`Domain`], and `<ws>` is any sequence of +/// [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace). +/// +/// Comments are any lines that start with `#` (ignoring whitespace). Any in-line comments +/// after a valid domain are ignored and will be parsed into a [`Value::Domain`]. +#[derive(Clone, Debug)] +pub struct DomainOnly<'a> { + /// The `Domain`. + domain: Domain<'a>, +} +impl<'a> DomainOnly<'a> { + /// Read [`Adblock::cmp_domain_only`]. + #[inline] + #[must_use] + pub fn cmp_adblock(&self, other: &Adblock<'_>) -> Ordering { + other.cmp_domain_only(self).reverse() + } + /// Read [`Domain::cmp`]. + #[inline] + #[must_use] + pub fn cmp_hosts(&self, other: &Hosts<'_>) -> Ordering { + self.domain.cmp(&other.domain) + } + /// Read [`Wildcard::cmp_domain_only`]. + #[inline] + #[must_use] + pub fn cmp_wildcard(&self, other: &Wildcard<'_>) -> Ordering { + other.cmp_domain_only(self).reverse() + } + /// Same as [`DomainOnly::cardinality`] except + /// that a `NonZeroU8` is returned. + /// + /// The value is always 1. + #[allow(unsafe_code)] + #[inline] + #[must_use] + pub const fn domain_count(&self) -> NonZeroU8 { + // SAFETY: + // 0 < 1 < 256. + unsafe { NonZeroU8::new_unchecked(1) } + } +} +impl PartialEq<DomainOnly<'_>> for DomainOnly<'_> { + #[inline] + fn eq(&self, other: &DomainOnly<'_>) -> bool { + self.domain == other.domain + } +} +impl PartialEq<Adblock<'_>> for DomainOnly<'_> { + #[inline] + fn eq(&self, other: &Adblock<'_>) -> bool { + other == self + } +} +impl PartialEq<Hosts<'_>> for DomainOnly<'_> { + #[inline] + fn eq(&self, other: &Hosts<'_>) -> bool { + self.domain == other.domain + } +} +impl PartialEq<Wildcard<'_>> for DomainOnly<'_> { + #[inline] + fn eq(&self, other: &Wildcard<'_>) -> bool { + !other.proper_subdomains && self.domain == other.domain + } +} +impl Eq for DomainOnly<'_> {} +impl Hash for DomainOnly<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + self.domain.hash(state); + } +} +impl PartialOrd<DomainOnly<'_>> for DomainOnly<'_> { + #[inline] + fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for DomainOnly<'_> { + /// Read [`Domain::cmp`]. + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.domain.cmp(&other.domain) + } +} +impl PartialOrd<Adblock<'_>> for DomainOnly<'_> { + #[inline] + fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> { + Some(self.cmp_adblock(other)) + } +} +impl PartialOrd<Hosts<'_>> for DomainOnly<'_> { + #[inline] + fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> { + Some(self.cmp_hosts(other)) + } +} +impl PartialOrd<Wildcard<'_>> for DomainOnly<'_> { + #[inline] + fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> { + Some(self.cmp_wildcard(other)) + } +} +impl Display for DomainOnly<'_> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.domain.fmt(f) + } +} +impl<'a> Set for DomainOnly<'a> { + type Elem = Domain<'a>; + #[inline] + fn bounded_cardinality(&self) -> BoundedCardinality { + BoundedCardinality::from_biguint_exact(self.domain_count().get().into()) + } + #[inline] + fn cardinality(&self) -> Option<Cardinality> { + Some(Cardinality::Finite(self.domain_count().get().into())) + } + #[inline] + fn contains<Q>(&self, elem: &Q) -> bool + where + Q: Borrow<Self::Elem> + Eq + ?Sized, + { + self.domain == *elem.borrow() + } + #[inline] + fn is_proper_subset(&self, _: &Self) -> bool { + false + } + #[inline] + fn is_subset(&self, val: &Self) -> bool { + self == val + } +} +impl SetOrd for DomainOnly<'_> {} +impl<'a> AsRef<Domain<'a>> for DomainOnly<'a> { + #[inline] + fn as_ref(&self) -> &Domain<'a> { + &self.domain + } +} +impl<'a> AsRef<str> for DomainOnly<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl<'a> Borrow<Domain<'a>> for DomainOnly<'a> { + #[inline] + fn borrow(&self) -> &Domain<'a> { + &self.domain + } +} +impl<'a> Deref for DomainOnly<'a> { + type Target = Domain<'a>; + #[inline] + fn deref(&self) -> &Self::Target { + &self.domain + } +} +impl<'a> ParsedDomain<'a> for DomainOnly<'a> { + type Error = DomainErr; + #[allow(unsafe_code, clippy::indexing_slicing)] + #[inline] + fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> { + let value = val.as_bytes().trim_ascii_start(); + value.first().map_or_else( + || Ok(Value::Blank), + |byt| { + if *byt == b'#' { + // SAFETY: + // `value` came from `val` with leading ASCII whitespace removed + // which is still valid UTF-8. Since the first byte is '#' or '$' + // the remaining bytes is still valid UTF-8. + let comment = unsafe { str::from_utf8_unchecked(&value[1..]) }; + Ok(Value::Comment(comment)) + } else { + Domain::try_from_slice( + value[..value + .into_iter() + .try_fold(0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) }) + .map_or_else(convert::identity, convert::identity)] + .trim_ascii_end(), + ) + .map(|domain| Value::Domain(Self { domain })) + } + }, + ) + } + #[inline] + fn domain(&self) -> &Domain<'a> { + &self.domain + } + #[inline] + fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> { + write_rpz_line(&mut writer, self.domain(), action, false) + } +} +/// Domain constructed from a [`hosts(5)`-style rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#etc-hosts-syntax) +/// with the requirement that the rule conforms to the following extended regex: +/// +/// `^<ws>*<ip><ws>+<domain><ws>*(#.*)?$` +/// +/// where `<domain>` conforms to a valid [`Domain`], `<ws>` is any sequence of +/// [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace), and +/// `<ip>` is one of the following: +/// +/// `::`, `::1`, `0.0.0.0`, or `127.0.0.1`. +/// +/// Comments are any lines that start with `#` (ignoring whitespace). Any in-line comments +/// after a valid domain are ignored and will be parsed into a [`Value::Domain`]. +#[derive(Clone, Debug)] +pub struct Hosts<'a> { + /// The `Domain`. + domain: Domain<'a>, +} +impl<'a> Hosts<'a> { + /// Read [`Adblock::cmp_hosts`]. + #[inline] + #[must_use] + pub fn cmp_adblock(&self, other: &Adblock<'_>) -> Ordering { + other.cmp_hosts(self).reverse() + } + /// Read [`DomainOnly::cmp_hosts`]. + #[inline] + #[must_use] + pub fn cmp_domain_only(&self, other: &DomainOnly<'_>) -> Ordering { + other.cmp_hosts(self).reverse() + } + /// Read [`Wildcard::cmp_hosts`]. + #[inline] + #[must_use] + pub fn cmp_wildcard(&self, other: &Wildcard<'_>) -> Ordering { + other.cmp_hosts(self).reverse() + } + /// Same as [`Hosts::cardinality`] except + /// that a `NonZeroU8` is returned. + /// + /// The value is always 1. + #[allow(unsafe_code)] + #[inline] + #[must_use] + pub const fn domain_count(&self) -> NonZeroU8 { + // SAFETY: + // 0 < 1 < 256. + unsafe { NonZeroU8::new_unchecked(1) } + } +} +impl PartialEq<Hosts<'_>> for Hosts<'_> { + #[inline] + fn eq(&self, other: &Hosts<'_>) -> bool { + self.domain == other.domain + } +} +impl PartialEq<Adblock<'_>> for Hosts<'_> { + #[inline] + fn eq(&self, other: &Adblock<'_>) -> bool { + other == self + } +} +impl PartialEq<DomainOnly<'_>> for Hosts<'_> { + #[inline] + fn eq(&self, other: &DomainOnly<'_>) -> bool { + other == self + } +} +impl PartialEq<Wildcard<'_>> for Hosts<'_> { + #[inline] + fn eq(&self, other: &Wildcard<'_>) -> bool { + !other.proper_subdomains && self.domain == other.domain + } +} +impl Eq for Hosts<'_> {} +impl Hash for Hosts<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + self.domain.hash(state); + } +} +impl PartialOrd<Hosts<'_>> for Hosts<'_> { + #[inline] + fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for Hosts<'_> { + /// Read [`Domain::cmp`]. + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.domain.cmp(&other.domain) + } +} +impl PartialOrd<Adblock<'_>> for Hosts<'_> { + #[inline] + fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> { + Some(self.cmp_adblock(other)) + } +} +impl PartialOrd<DomainOnly<'_>> for Hosts<'_> { + #[inline] + fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> { + Some(self.cmp_domain_only(other)) + } +} +impl PartialOrd<Wildcard<'_>> for Hosts<'_> { + #[inline] + fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> { + Some(self.cmp_wildcard(other)) + } +} +impl Display for Hosts<'_> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.domain.fmt(f) + } +} +impl<'a> Set for Hosts<'a> { + type Elem = Domain<'a>; + #[inline] + fn bounded_cardinality(&self) -> BoundedCardinality { + BoundedCardinality::from_biguint_exact(self.domain_count().get().into()) + } + #[inline] + fn cardinality(&self) -> Option<Cardinality> { + Some(Cardinality::Finite(self.domain_count().get().into())) + } + #[inline] + fn contains<Q>(&self, elem: &Q) -> bool + where + Q: Borrow<Self::Elem> + Eq + ?Sized, + { + self.domain == *elem.borrow() + } + #[inline] + fn is_proper_subset(&self, _: &Self) -> bool { + false + } + #[inline] + fn is_subset(&self, val: &Self) -> bool { + self == val + } +} +impl SetOrd for Hosts<'_> {} +impl<'a> AsRef<Domain<'a>> for Hosts<'a> { + #[inline] + fn as_ref(&self) -> &Domain<'a> { + &self.domain + } +} +impl<'a> AsRef<str> for Hosts<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl<'a> Borrow<Domain<'a>> for Hosts<'a> { + #[inline] + fn borrow(&self) -> &Domain<'a> { + &self.domain + } +} +impl<'a> Deref for Hosts<'a> { + type Target = Domain<'a>; + #[inline] + fn deref(&self) -> &Self::Target { + &self.domain + } +} +impl<'a> ParsedDomain<'a> for Hosts<'a> { + type Error = DomainErr; + #[allow(unsafe_code, clippy::indexing_slicing)] + #[inline] + fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> { + let mut value = val.as_bytes().trim_ascii_start(); + value.first().map_or_else( + || Ok(Value::Blank), + |byt| { + if *byt == b'#' { + // SAFETY: + // `value` came from `val` with leading ASCII whitespace removed + // which is still valid UTF-8. Since the first byte is '#' or '$' + // the remaining bytes is still valid UTF-8. + let comment = unsafe { str::from_utf8_unchecked(&value[1..]) }; + Ok(Value::Comment(comment)) + } else { + value = value + .get(..3) + .ok_or(DomainErr::InvalidHostsIP) + .and_then(|fst| { + if fst == b"::1" { + Ok(&value[3..]) + } else if &value[..2] == b"::" { + Ok(&value[2..]) + } else { + value + .get(..7) + .ok_or(DomainErr::InvalidHostsIP) + .and_then(|fst2| { + if fst2 == b"0.0.0.0" { + Ok(&value[7..]) + } else { + value + .get(..9) + .ok_or(DomainErr::InvalidHostsIP) + .and_then(|fst3| { + if fst3 == b"127.0.0.1" { + Ok(&value[9..]) + } else { + Err(DomainErr::InvalidHostsIP) + } + }) + } + }) + } + })?; + let len = value.len(); + value = value.trim_ascii_start(); + if len == value.len() { + // There has to be at least one space or tab between + // the IP and domain. + Err(DomainErr::InvalidHostsIP) + } else { + Domain::try_from_slice( + value[..value + .into_iter() + .try_fold( + 0, + |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) }, + ) + .map_or_else(convert::identity, convert::identity)] + .trim_ascii_end(), + ) + .map(|domain| Value::Domain(Self { domain })) + } + } + }, + ) + } + #[inline] + fn domain(&self) -> &Domain<'a> { + &self.domain + } + #[inline] + fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> { + write_rpz_line(&mut writer, self.domain(), action, false) + } +} +/// Domain constructed from a [wildcard domain rule](https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblock&showintro=0&mimetype=plaintext) +/// with the requirement that the rule conforms to the following extended regex: +/// +/// `^<ws>*(\*\.)?<domain><ws>*(#.*)?$` +/// +/// where `<domain>` conforms to a valid [`Domain`], and `<ws>` is any sequence of +/// [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace). +/// +/// If `domain` begins with `*.`, then `domain` must have length less than 252. +/// +/// Comments are any lines that start with `#` (ignoring whitespace). Any in-line comments +/// after a valid domain are ignored and will be parsed into a [`Value::Domain`]. +#[derive(Clone, Debug)] +pub struct Wildcard<'a> { + /// The `Domain`. + domain: Domain<'a>, + /// `true` iff `domain` represents all proper subdomains. + /// Note that this does _not_ include `domain` itself. + proper_subdomains: bool, +} +impl<'a> Wildcard<'a> { + /// Returns `true` iff the contained [`Domain`] represents all proper subdomains. + /// Note this does _not_ include the `Domain` itself. + #[inline] + #[must_use] + pub const fn is_proper_subdomains(&self) -> bool { + self.proper_subdomains + } + /// Read [`Adblock::cmp_wildcard`]. + #[inline] + #[must_use] + pub fn cmp_adblock(&self, other: &Adblock<'_>) -> Ordering { + other.cmp_wildcard(self).reverse() + } + /// Since `DomainOnly` and `Hosts` are treated the same, + /// we have this helper function that can be used for both. + #[inline] + #[must_use] + fn cmp_dom(&self, other: &Domain<'_>) -> Ordering { + cmp_doms(&self.domain, other).map_or_else( + |ord| { + if ord == Ordering::Equal && self.proper_subdomains { + Ordering::Greater + } else { + ord + } + }, + |()| { + // At this point `self` and `other` have different number of labels; + // otherwise they would have the same domain which was already + // checked for in `cmp_doms`. + if self.proper_subdomains { + Ordering::Greater + } else { + self.domain.label_count().cmp(&other.label_count()) + } + }, + ) + } + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. If `self` represents a single `Domain` (i.e., `!self.is_proper_subdomains()`), + /// then return the comparison of label counts. + /// 4. Return `self` is greater. + /// + /// For example, `com` `<` `example.com` `<` `*.example.com` `<` `*.com` `<` `net` `<` `example.net` `<` `*.example.net` `<` `*.net`. + #[inline] + #[must_use] + pub fn cmp_domain_only(&self, other: &DomainOnly<'_>) -> Ordering { + self.cmp_dom(&other.domain) + } + /// Read [`Wildcard::cmp_domain_only`]. + #[inline] + #[must_use] + pub fn cmp_hosts(&self, other: &Hosts<'_>) -> Ordering { + self.cmp_dom(&other.domain) + } + /// Same as [`Wildcard::cardinality`] except + /// that a `BigUint` is returned. + /// Note the count does _not_ include the `Domain` itself + /// when `self.is_proper_subdomains()`. + /// + /// `!self.is_proper_subdomains()` ⇔ `self.domain_count() == BigUint::new(vec![1])`. + #[inline] + #[must_use] + pub fn domain_count(&self) -> BigUint { + if self.proper_subdomains { + proper_subdomain_count(&self.domain) + } else { + BigUint::new(vec![1]) + } + } +} +impl PartialEq<Wildcard<'_>> for Wildcard<'_> { + #[inline] + fn eq(&self, other: &Wildcard<'_>) -> bool { + self.domain == other.domain && self.proper_subdomains == other.proper_subdomains + } +} +impl PartialEq<Adblock<'_>> for Wildcard<'_> { + #[inline] + fn eq(&self, other: &Adblock<'_>) -> bool { + other == self + } +} +impl PartialEq<DomainOnly<'_>> for Wildcard<'_> { + #[inline] + fn eq(&self, other: &DomainOnly<'_>) -> bool { + other == self + } +} +impl PartialEq<Hosts<'_>> for Wildcard<'_> { + #[inline] + fn eq(&self, other: &Hosts<'_>) -> bool { + other == self + } +} +impl Eq for Wildcard<'_> {} +impl Hash for Wildcard<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + self.domain.hash(state); + } +} +impl PartialOrd<Wildcard<'_>> for Wildcard<'_> { + #[inline] + fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for Wildcard<'_> { + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. If both domains represent a single `Domain`, then return the comparison + /// of label counts. + /// 4. If one domain represents a single `Domain`, then return that that domain is less. + /// 5. Return the inverse of the comparison of label counts. + /// + /// For example, `com` `<` `example.com` `<` `*.example.com` `<` `*.com` `<` `net` `<` `example.net` `<` `*.example.net` `<` `*.net`. + #[inline] + #[must_use] + fn cmp(&self, other: &Self) -> Ordering { + cmp_doms(&self.domain, &other.domain).map_or_else( + |ord| { + if ord == Ordering::Equal { + if self.proper_subdomains { + if other.proper_subdomains { + ord + } else { + Ordering::Greater + } + } else if other.proper_subdomains { + Ordering::Less + } else { + ord + } + } else { + ord + } + }, + |()| { + // At this point `self` and `other` have different number of labels; + // otherwise they would have the same domain which was already + // checked for in `cmp_doms`. + if self.proper_subdomains { + if !other.proper_subdomains + || self.domain.label_count() < other.domain.label_count() + { + Ordering::Greater + } else { + Ordering::Less + } + } else if other.proper_subdomains + || self.domain.label_count() < other.domain.label_count() + { + Ordering::Less + } else { + Ordering::Greater + } + }, + ) + } +} +impl PartialOrd<Adblock<'_>> for Wildcard<'_> { + #[inline] + fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> { + Some(self.cmp_adblock(other)) + } +} +impl PartialOrd<DomainOnly<'_>> for Wildcard<'_> { + #[inline] + fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> { + Some(self.cmp_domain_only(other)) + } +} +impl PartialOrd<Hosts<'_>> for Wildcard<'_> { + #[inline] + fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> { + Some(self.cmp_hosts(other)) + } +} +impl Display for Wildcard<'_> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{}{}", + if self.proper_subdomains { "*." } else { "" }, + self.domain + ) + } +} +impl<'a> Set for Wildcard<'a> { + type Elem = Domain<'a>; + #[inline] + fn bounded_cardinality(&self) -> BoundedCardinality { + BoundedCardinality::from_biguint_exact(self.domain_count()) + } + #[inline] + fn cardinality(&self) -> Option<Cardinality> { + Some(Cardinality::Finite(self.domain_count())) + } + #[inline] + fn contains<Q>(&self, elem: &Q) -> bool + where + Q: Borrow<Self::Elem> + Eq + ?Sized, + { + if self.proper_subdomains { + let dom2 = elem.borrow(); + self.domain.label_count() < dom2.label_count() + && if self.domain.flag.eq_ignore_case(dom2.flag) { + self.domain.same_labels_ignore_case(dom2) + } else { + self.domain.same_labels(dom2) + } + } else { + self.domain == *elem.borrow() + } + } + #[inline] + fn is_proper_subset(&self, val: &Self) -> bool { + // A single domain can never be a proper superset. + // Proper subdomains cannot be a proper superset + // if it has more labels or the same number of labels + // as another domain. + // In all other cases, we need to recursively check from the TLD + // that the labels are the same. + val.proper_subdomains + && val.domain.label_count() < self.domain.label_count() + && if self.domain.flag.eq_ignore_case(val.domain.flag) { + val.domain.same_labels_ignore_case(&self.domain) + } else { + val.domain.same_labels(&self.domain) + } + } + #[inline] + fn is_subset(&self, val: &Self) -> bool { + self == val || self.is_proper_subset(val) + } +} +impl SetOrd for Wildcard<'_> {} +impl<'a> AsRef<Domain<'a>> for Wildcard<'a> { + #[inline] + fn as_ref(&self) -> &Domain<'a> { + &self.domain + } +} +impl<'a> AsRef<str> for Wildcard<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl<'a> Deref for Wildcard<'a> { + type Target = Domain<'a>; + #[inline] + fn deref(&self) -> &Self::Target { + &self.domain + } +} +impl<'a> ParsedDomain<'a> for Wildcard<'a> { + type Error = DomainErr; + #[allow(unsafe_code, clippy::indexing_slicing)] + #[inline] + fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> { + let value = val.as_bytes().trim_ascii_start(); + value.first().map_or_else( + || Ok(Value::Blank), + |byt| { + if *byt == b'#' { + // SAFETY: + // `value` came from `val` with leading ASCII whitespace removed + // which is still valid UTF-8. Since the first byte is '#' or '$' + // the remaining bytes is still valid UTF-8. + let comment = unsafe { str::from_utf8_unchecked(&value[1..]) }; + Ok(Value::Comment(comment)) + } else { + let (proper_subdomains, val2) = value.get(..2).map_or_else( + || (false, value), + |fst| { + if fst == b"*." { + (true, &value[2..]) + } else { + (false, value) + } + }, + ); + Domain::try_from_slice( + val2[..val2 + .into_iter() + .try_fold(0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) }) + .map_or_else(convert::identity, convert::identity)] + .trim_ascii_end(), + ) + .and_then(|domain| { + if proper_subdomains { + if domain.len().get() > 251 { + Err(DomainErr::InvalidWildcardDomain) + } else { + Ok(Value::Domain(Self { + domain, + proper_subdomains: true, + })) + } + } else { + Ok(Value::Domain(Self { + domain, + proper_subdomains, + })) + } + }) + } + }, + ) + } + #[inline] + fn domain(&self) -> &Domain<'a> { + &self.domain + } + #[inline] + fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> { + write_rpz_line(&mut writer, self.domain(), action, self.proper_subdomains) + } +} +/// A [`Domain`] in a [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) +/// file. +#[allow(clippy::exhaustive_enums)] +#[derive(Clone, Debug)] +pub enum RpzDomain<'a> { + /// An `Adblock` domain. + Adblock(Adblock<'a>), + /// A `DomainOnly` domain. + DomainOnly(DomainOnly<'a>), + /// A `Hosts` domain. + Hosts(Hosts<'a>), + /// A `Wildcard` domain. + Wildcard(Wildcard<'a>), +} +impl<'a> RpzDomain<'a> { + /// Returns `true` iff `self` represents a single [`Domain`]. + #[inline] + #[must_use] + pub const fn is_domain(&self) -> bool { + match *self { + Self::Adblock(ref dom) => !dom.subdomains, + Self::DomainOnly(_) | Self::Hosts(_) => true, + Self::Wildcard(ref dom) => !dom.proper_subdomains, + } + } + /// Returns `true` iff `self` represents proper subdomains of + /// the contained [`Domain`] (i.e., is a [`Wildcard`] such that + /// [`Wildcard::is_proper_subdomains`]). + #[inline] + #[must_use] + pub const fn is_proper_subdomains(&self) -> bool { + match *self { + Self::Adblock(_) | Self::DomainOnly(_) | Self::Hosts(_) => false, + Self::Wildcard(ref dom) => dom.proper_subdomains, + } + } + /// Returns `true` iff `self` represents subdomains of + /// the contained [`Domain`] (i.e., is an [`Adblock`] such that + /// [`Adblock::is_subdomains`]). + #[inline] + #[must_use] + pub const fn is_subdomains(&self) -> bool { + match *self { + Self::Adblock(ref dom) => dom.subdomains, + Self::DomainOnly(_) | Self::Hosts(_) | Self::Wildcard(_) => false, + } + } + /// Returns the count of [`Domain`]s represented by `self`. + /// This function is the same as [`RpzDomain::cardinality`] + /// except that it returns a `BigUint`. + #[inline] + #[must_use] + pub fn domain_count(&self) -> BigUint { + match *self { + Self::Adblock(ref dom) => dom.domain_count(), + Self::DomainOnly(ref dom) => dom.domain_count().get().into(), + Self::Hosts(ref dom) => dom.domain_count().get().into(), + Self::Wildcard(ref dom) => dom.domain_count(), + } + } +} +impl PartialEq<RpzDomain<'_>> for RpzDomain<'_> { + #[inline] + fn eq(&self, other: &RpzDomain<'_>) -> bool { + match *self { + Self::Adblock(ref dom) => match *other { + RpzDomain::Adblock(ref dom2) => dom == dom2, + RpzDomain::DomainOnly(ref dom2) => dom == dom2, + RpzDomain::Hosts(ref dom2) => dom == dom2, + RpzDomain::Wildcard(ref dom2) => dom == dom2, + }, + Self::DomainOnly(ref dom) => match *other { + RpzDomain::Adblock(ref dom2) => dom == dom2, + RpzDomain::DomainOnly(ref dom2) => dom == dom2, + RpzDomain::Hosts(ref dom2) => dom == dom2, + RpzDomain::Wildcard(ref dom2) => dom == dom2, + }, + Self::Hosts(ref dom) => match *other { + RpzDomain::Adblock(ref dom2) => dom == dom2, + RpzDomain::DomainOnly(ref dom2) => dom == dom2, + RpzDomain::Hosts(ref dom2) => dom == dom2, + RpzDomain::Wildcard(ref dom2) => dom == dom2, + }, + Self::Wildcard(ref dom) => match *other { + RpzDomain::Adblock(ref dom2) => dom == dom2, + RpzDomain::DomainOnly(ref dom2) => dom == dom2, + RpzDomain::Hosts(ref dom2) => dom == dom2, + RpzDomain::Wildcard(ref dom2) => dom == dom2, + }, + } + } +} +impl Eq for RpzDomain<'_> {} +impl Hash for RpzDomain<'_> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + self.domain().hash(state); + } +} +impl PartialOrd<RpzDomain<'_>> for RpzDomain<'_> { + #[inline] + fn partial_cmp(&self, other: &RpzDomain<'_>) -> Option<Ordering> { + Some(self.cmp(other)) + } +} +impl Ord for RpzDomain<'_> { + /// The total order that is defined follows the following hierarchy: + /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. + /// 2. If 1. evaluates as not equivalent, then return the result. + /// 3. If both domains represent a single `Domain`, then return the comparison + /// of label counts. + /// 4. If one domain represents a single `Domain`, then return that that domain is less. + /// 5. If the label counts are the same and exactly one domain represents proper subdomains, the other domain is greater. + /// 6. Return the inverse of the comparison of label counts. + /// + /// 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` + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + match *self { + Self::Adblock(ref dom) => match *other { + Self::Adblock(ref dom2) => dom.cmp(dom2), + Self::DomainOnly(ref dom2) => dom.cmp_domain_only(dom2), + Self::Hosts(ref dom2) => dom.cmp_hosts(dom2), + Self::Wildcard(ref dom2) => dom.cmp_wildcard(dom2), + }, + Self::DomainOnly(ref dom) => match *other { + Self::Adblock(ref dom2) => dom.cmp_adblock(dom2), + Self::DomainOnly(ref dom2) => dom.cmp(dom2), + Self::Hosts(ref dom2) => dom.cmp_hosts(dom2), + Self::Wildcard(ref dom2) => dom.cmp_wildcard(dom2), + }, + Self::Hosts(ref dom) => match *other { + Self::Adblock(ref dom2) => dom.cmp_adblock(dom2), + Self::DomainOnly(ref dom2) => dom.cmp_domain_only(dom2), + Self::Hosts(ref dom2) => dom.cmp(dom2), + Self::Wildcard(ref dom2) => dom.cmp_wildcard(dom2), + }, + Self::Wildcard(ref dom) => match *other { + Self::Adblock(ref dom2) => dom.cmp_adblock(dom2), + Self::DomainOnly(ref dom2) => dom.cmp_domain_only(dom2), + Self::Hosts(ref dom2) => dom.cmp_hosts(dom2), + Self::Wildcard(ref dom2) => dom.cmp(dom2), + }, + } + } +} +impl Display for RpzDomain<'_> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Adblock(ref dom) => dom.fmt(f), + Self::DomainOnly(ref dom) => dom.fmt(f), + Self::Hosts(ref dom) => dom.fmt(f), + Self::Wildcard(ref dom) => dom.fmt(f), + } + } +} +impl<'a> Set for RpzDomain<'a> { + type Elem = Domain<'a>; + #[inline] + fn bounded_cardinality(&self) -> BoundedCardinality { + BoundedCardinality::from_biguint_exact(self.domain_count()) + } + #[inline] + fn cardinality(&self) -> Option<Cardinality> { + Some(Cardinality::Finite(self.domain_count())) + } + #[inline] + fn contains<Q>(&self, elem: &Q) -> bool + where + Q: Borrow<Self::Elem> + Eq + ?Sized, + { + match *self { + Self::Adblock(ref dom) => dom.contains(elem), + Self::DomainOnly(ref dom) => dom.contains(elem), + Self::Hosts(ref dom) => dom.contains(elem), + Self::Wildcard(ref dom) => dom.contains(elem), + } + } + #[inline] + fn is_proper_subset(&self, val: &Self) -> bool { + /// Helper function that verifies all labels are the same. + #[inline] + fn helper(left: &Domain<'_>, right: &Domain<'_>) -> bool { + left.label_count() <= right.label_count() + && if left.flag.eq_ignore_case(right.flag) { + left.same_labels_ignore_case(right) + } else { + left.same_labels(right) + } + } + match *val { + Self::Adblock(ref dom) => { + dom.subdomains + && match *self { + Self::Adblock(ref dom2) => { + dom.domain.label_count() < dom2.domain.label_count() + && helper(&dom.domain, &dom2.domain) + } + Self::DomainOnly(ref dom2) => { + match dom.domain.label_count().cmp(&dom2.domain.label_count()) { + Ordering::Less => helper(&dom.domain, &dom2.domain), + Ordering::Equal => { + dom.domain == dom2.domain || helper(&dom.domain, &dom2.domain) + } + Ordering::Greater => false, + } + } + Self::Hosts(ref dom2) => { + match dom.domain.label_count().cmp(&dom2.domain.label_count()) { + Ordering::Less => helper(&dom.domain, &dom2.domain), + Ordering::Equal => { + dom.domain == dom2.domain || helper(&dom.domain, &dom2.domain) + } + Ordering::Greater => false, + } + } + Self::Wildcard(ref dom2) => { + match dom.domain.label_count().cmp(&dom2.domain.label_count()) { + Ordering::Less => helper(&dom.domain, &dom2.domain), + Ordering::Equal => { + dom.domain == dom2.domain || helper(&dom.domain, &dom2.domain) + } + Ordering::Greater => false, + } + } + } + } + Self::DomainOnly(_) | Self::Hosts(_) => false, + Self::Wildcard(ref dom) => { + dom.proper_subdomains + && match *self { + Self::Adblock(ref dom2) => { + dom.domain.label_count() < dom2.domain.label_count() + && helper(&dom.domain, &dom2.domain) + } + Self::DomainOnly(ref dom2) => { + dom.domain.label_count() < dom2.domain.label_count() + && helper(&dom.domain, &dom2.domain) + } + Self::Hosts(ref dom2) => { + dom.domain.label_count() < dom2.domain.label_count() + && helper(&dom.domain, &dom2.domain) + } + Self::Wildcard(ref dom2) => { + dom.domain.label_count() < dom2.domain.label_count() + && helper(&dom.domain, &dom2.domain) + } + } + } + } + } + #[inline] + fn is_subset(&self, val: &Self) -> bool { + self == val || self.is_proper_subset(val) + } +} +impl SetOrd for RpzDomain<'_> {} +impl<'a> AsRef<Domain<'a>> for RpzDomain<'a> { + #[inline] + fn as_ref(&self) -> &Domain<'a> { + self.domain() + } +} +impl<'a> AsRef<str> for RpzDomain<'a> { + #[inline] + fn as_ref(&self) -> &'a str { + self.as_str() + } +} +impl<'a> Deref for RpzDomain<'a> { + type Target = Domain<'a>; + #[inline] + fn deref(&self) -> &Self::Target { + self.domain() + } +} +impl<'a: 'b, 'b> From<Adblock<'a>> for RpzDomain<'b> { + #[inline] + fn from(value: Adblock<'a>) -> Self { + Self::Adblock(value) + } +} +impl<'a: 'b, 'b> From<DomainOnly<'a>> for RpzDomain<'b> { + #[inline] + fn from(value: DomainOnly<'a>) -> Self { + Self::DomainOnly(value) + } +} +impl<'a: 'b, 'b> From<Hosts<'a>> for RpzDomain<'b> { + #[inline] + fn from(value: Hosts<'a>) -> Self { + Self::Hosts(value) + } +} +impl<'a: 'b, 'b> From<Wildcard<'a>> for RpzDomain<'b> { + #[inline] + fn from(value: Wildcard<'a>) -> Self { + Self::Wildcard(value) + } +} +impl<'a> ParsedDomain<'a> for RpzDomain<'a> { + type Error = DomainErr; + #[inline] + fn parse_value<'b: 'a>(value: &'b str) -> Result<Value<'a, Self>, Self::Error> { + DomainOnly::parse_value(value).map_or_else( + |_| { + Hosts::parse_value(value).map_or_else( + |_| { + Wildcard::parse_value(value).map_or_else( + |_| { + Adblock::parse_value(value).map(|val| match val { + Value::Domain(dom) => Value::Domain(Self::Adblock(dom)), + Value::Comment(com) => Value::Comment(com), + Value::Blank => Value::Blank, + }) + }, + |val| { + Ok(match val { + Value::Domain(dom) => Value::Domain(Self::Wildcard(dom)), + Value::Comment(com) => Value::Comment(com), + Value::Blank => Value::Blank, + }) + }, + ) + }, + |val| { + Ok(match val { + Value::Domain(dom) => Value::Domain(Self::Hosts(dom)), + Value::Comment(com) => Value::Comment(com), + Value::Blank => Value::Blank, + }) + }, + ) + }, + |val| { + Ok(match val { + Value::Domain(dom) => Value::Domain(Self::DomainOnly(dom)), + Value::Comment(com) => Value::Comment(com), + Value::Blank => Value::Blank, + }) + }, + ) + } + #[inline] + fn domain(&self) -> &Domain<'a> { + match *self { + Self::Adblock(ref dom) => dom.domain(), + Self::DomainOnly(ref dom) => dom.domain(), + Self::Hosts(ref dom) => dom.domain(), + Self::Wildcard(ref dom) => dom.domain(), + } + } + #[inline] + fn write_to_rpz<W: Write>(&self, action: RpzAction, writer: W) -> Result<(), Error> { + match *self { + Self::Adblock(ref dom) => dom.write_to_rpz(action, writer), + Self::DomainOnly(ref dom) => dom.write_to_rpz(action, writer), + Self::Hosts(ref dom) => dom.write_to_rpz(action, writer), + Self::Wildcard(ref dom) => dom.write_to_rpz(action, writer), + } + } +} +#[cfg(test)] +mod tests { + use super::{ + Adblock, Domain, DomainErr, DomainOnly, Hosts, ParsedDomain, RpzDomain, Value, Wildcard, + }; + use core::cmp::Ordering; + use num_bigint::BigUint; + use superset_map::SupersetSet; + #[test] + fn test_dom_parse() { + // Test Ipv4Addr is error. + assert!(Domain::try_from("1.1.1.1").map_or_else(|e| e == DomainErr::Ipv4, |_| false)); + // Test empty is error. + assert!(Domain::try_from("").map_or_else(|e| e == DomainErr::Empty, |_| false)); + // Test empty label is error. + assert!(Domain::try_from("a..com").map_or_else(|e| e == DomainErr::EmptyLabel, |_| false)); + // Test label too long. + let val = "www.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com"; + // 4 + 64 + 4 + assert!(val.len() == 72); + assert!(Domain::try_from(val).map_or_else(|e| e == DomainErr::LabelLenExceeds63, |_| false)); + assert!(Domain::try_from( + "www.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com" + ) + .map_or(false, |d| d.len().get() == 71)); + // Test domain too long. + assert!(Domain::try_from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").map_or_else(|e| e == DomainErr::LenExceeds253(254), |_| false)); + assert!(Domain::try_from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").map_or(false, |d| d.len().get() == 253 )); + // Test max labels. + assert!(Domain::try_from("a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.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 == DomainErr::LenExceeds253(255), |_| false)); + assert!(Domain::try_from("a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.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, |d| d.label_count().get() == 127 && d.len().get() == 253)); + assert!(Domain::try_from("a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.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, |d| d.label_count().get() == 127 && d.len().get() == 253)); + // Test removal of trailing '.'. + assert!(Domain::try_from("com").map_or(false, |d| d.value == b"com")); + // Test case-insensitivity. + assert!(Domain::try_from("wwW.ExAMple.COm") + .map_or(false, |d| Domain::try_from("www.example.com") + .map_or(false, |d2| d == d2 && d.cmp(&d2) == Ordering::Equal))); + // Test valid bytes + let mut vec = Vec::new(); + let mut counter = 0; + for i in 0u8..=255 { + vec.push(i); + match i { + b'!' + | b'$' + | b'&'..=b')' + | b'+'..=b'-' + | b'0'..=b'9' + | b';' + | b'=' + | b'A'..=b'Z' + | b'_'..=b'{' + | b'}'..=b'~' => { + counter += 1; + assert!(Domain::try_from_slice(vec.as_slice()) + .map_or(false, |d| d.value.len() == 1 && d.value[0] == i)) + } + b'.' => { + vec.push(i); + vec[0] = b'a'; + assert!( + Domain::try_from_slice(vec.as_slice()) + .map_or(false, |d| d.value.len() == 1 && d.value[0] == b'a') + && vec.pop() == Some(i) + ) + } + _ => assert!(Domain::try_from_slice(vec.as_slice()) + .map_or_else(|e| e == DomainErr::InvalidByte(i), |_| false)), + } + vec.pop(); + } + assert!(counter == 78); + } + #[test] + fn test_dom_into_iter() { + assert!(Domain::try_from("www.example.com").map_or(false, |d| { + let mut iter = d.into_iter(); + if iter.len() != 3 { + return false; + } + let Some(l) = iter.next() else { + return false; + }; + if l.value != "com" { + return false; + } + if iter.len() != 2 { + return false; + } + let Some(l) = iter.next() else { return false }; + if l.value != "example" { + return false; + } + if iter.len() != 1 { + return false; + } + let Some(l) = iter.next() else { + return false; + }; + if iter.len() != 0 { + return false; + } + if l.value != "www" { + return false; + } + iter.next().is_none() + })); + assert!(Domain::try_from("www.example.com").map_or(false, |d| { + let mut iter = d.into_iter(); + if iter.len() != 3 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "www" { + return false; + } + if iter.len() != 2 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "example" { + return false; + } + if iter.len() != 1 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "com" { + return false; + } + if iter.len() != 0 { + return false; + } + iter.next_back().is_none() + })); + assert!(Domain::try_from("www.example.com").map_or(false, |d| { + let mut iter = d.into_iter(); + if iter.len() != 3 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "www" { + return false; + } + if iter.len() != 2 { + return false; + } + let Some(l) = iter.next() else { return false }; + if l.value != "com" { + return false; + } + if iter.len() != 1 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "example" { + return false; + } + if iter.len() != 0 { + return false; + } + iter.next().is_none() && iter.next_back().is_none() + })); + } + #[test] + fn test_dom_iter() { + assert!(Domain::try_from("www.example.com").map_or(false, |d| { + let mut iter = d.iter(); + if iter.len() != 3 { + return false; + } + let Some(l) = iter.next() else { + return false; + }; + if l.value != "com" { + return false; + } + if iter.len() != 2 { + return false; + } + let Some(l) = iter.next() else { return false }; + if l.value != "example" { + return false; + } + if iter.len() != 1 { + return false; + } + let Some(l) = iter.next() else { + return false; + }; + if iter.len() != 0 { + return false; + } + if l.value != "www" { + return false; + } + iter.next().is_none() + })); + assert!(Domain::try_from("www.example.com").map_or(false, |d| { + let mut iter = d.iter(); + if iter.len() != 3 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "www" { + return false; + } + if iter.len() != 2 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "example" { + return false; + } + if iter.len() != 1 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "com" { + return false; + } + if iter.len() != 0 { + return false; + } + iter.next_back().is_none() + })); + assert!(Domain::try_from("www.example.com").map_or(false, |d| { + let mut iter = d.iter(); + if iter.len() != 3 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "www" { + return false; + } + if iter.len() != 2 { + return false; + } + let Some(l) = iter.next() else { return false }; + if l.value != "com" { + return false; + } + if iter.len() != 1 { + return false; + } + let Some(l) = iter.next_back() else { + return false; + }; + if l.value != "example" { + return false; + } + if iter.len() != 0 { + return false; + } + iter.next().is_none() && iter.next_back().is_none() + })); + } + #[test] + fn test_adblock_parse() { + // Test subdomains. + assert!( + Adblock::parse_value("||www.example.com").map_or(false, |val| match val { + Value::Domain(ref dom) => dom.subdomains && dom.domain.value == 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 ").map_or( + false, + |val| match val { + Value::Domain(ref dom) => + dom.subdomains && dom.domain.value == b"www.example.com", + Value::Comment(_) | Value::Blank => false, + } + ) + ); + assert!( + Adblock::parse_value("\t\t \twww.example.com \t\t \t\t ").map_or(false, |val| { + match val { + Value::Domain(ref dom) => { + !dom.subdomains && dom.domain.value == b"www.example.com" + } + Value::Comment(_) | Value::Blank => false, + } + }) + ); + assert!(Adblock::parse_value("www .example.com") + .map_or_else(|err| err == 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, + } + }) + ); + // Test comment + assert!( + Adblock::parse_value(" \t\t #hi").map_or(false, |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 { + 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))); + } + #[test] + fn test_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 { + Value::Domain(ref dom) => dom.domain.value == 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 == DomainErr::InvalidByte(b' '), |_| false) + ); + // Test case-insensitivity. + assert!( + DomainOnly::parse_value("www.ExAMPle.CoM").map_or(false, |val| match val { + Value::Domain(ref dom) => + DomainOnly::parse_value("www.example.com").map_or(false, |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").map_or(false, |val| match val { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!(DomainOnly::parse_value(" \t\t \t\t \t ") + .map_or(false, |val| matches!(val, Value::Blank))); + } + #[test] + fn test_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 { + Value::Domain(ref dom) => dom.domain.value == 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 == DomainErr::InvalidByte(b' '), |_| false) + ); + assert!(Hosts::parse_value("::1\twww .example.com") + .map_or_else(|e| e == DomainErr::InvalidByte(b' '), |_| false)); + // Test invalid IP + assert!(Hosts::parse_value("::2 www.example.com") + .map_or_else(|e| e == DomainErr::InvalidHostsIP, |_| false)); + assert!(Hosts::parse_value(":2 www.example.com") + .map_or_else(|e| e == DomainErr::InvalidHostsIP, |_| false)); + assert!(Hosts::parse_value("www.example.com") + .map_or_else(|e| e == DomainErr::InvalidHostsIP, |_| false)); + assert!(Hosts::parse_value("10.4.2.256 www.example.com") + .map_or_else(|e| e == DomainErr::InvalidHostsIP, |_| false)); + // 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 + { + 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").map_or(false, |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))); + } + #[test] + fn test_wildcard_parse_value() { + // Test bad asterisk. + assert!(Wildcard::parse_value("*") + .map_or_else(|e| e == DomainErr::InvalidByte(b'*'), |_| false)); + assert!(Wildcard::parse_value("www*.example.com") + .map_or_else(|e| e == DomainErr::InvalidByte(b'*'), |_| false)); + assert!(Wildcard::parse_value("www.*.com") + .map_or_else(|e| e == DomainErr::InvalidByte(b'*'), |_| false)); + assert!( + Wildcard::parse_value("*..com").map_or_else(|e| e == DomainErr::EmptyLabel, |_| false) + ); + assert!(Wildcard::parse_value("www.com*") + .map_or_else(|e| e == DomainErr::InvalidByte(b'*'), |_| false)); + assert!(Wildcard::parse_value("ww*w.com") + .map_or_else(|e| e == DomainErr::InvalidByte(b'*'), |_| false)); + // Test case-insensitivity. + assert!( + Wildcard::parse_value("*.wWw.ExamPLE.com").map_or(false, |val| match val { + Value::Domain(ref dom) => + Wildcard::parse_value("*.www.example.com").map_or(false, |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").map_or(false, |val| match val { + Value::Domain(ref dom) => + dom.domain.value == 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 ") + .map_or(false, |val| match val { + Value::Domain(ref dom) => + dom.domain.value == 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 ") + .map_or(false, |val| match val { + Value::Domain(ref dom) => + dom.domain.value == 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 ").map_or(false, |val| { + match val { + Value::Domain(ref dom) => { + dom.domain.value == 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 ").map_or(false, |val| { + match val { + Value::Domain(ref dom) => { + dom.domain.value == b"www.example.com" && !dom.proper_subdomains + } + Value::Comment(_) | Value::Blank => false, + } + }) + ); + assert!(Wildcard::parse_value("www .example.com") + .map_or_else(|e| e == DomainErr::InvalidByte(b' '), |_| false)); + // 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 == DomainErr::InvalidWildcardDomain, |_| false)); + // 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 { + Value::Domain(ref dom) => dom.domain.label_count().get() == 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 { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!(Wildcard::parse_value(" \t\t \t\t \t ") + .map_or(false, |val| matches!(val, Value::Blank))); + } + #[test] + fn test_rpz_parse_value() { + assert!( + RpzDomain::parse_value("*.www.example.com").map_or(false, |val| { + let dom = val.unwrap_domain(); + dom.is_proper_subdomains() && dom.domain().value == b"www.example.com" + }) + ); + assert!( + RpzDomain::parse_value("||www.example.com").map_or(false, |val| { + let dom = val.unwrap_domain(); + dom.is_subdomains() && dom.domain().value == 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().value == b"www.example.com" + }) + ); + assert!( + RpzDomain::parse_value("www.example.com").map_or(false, |val| { + let dom = val.unwrap_domain(); + !(dom.is_subdomains() || dom.is_proper_subdomains()) + && dom.domain().value == b"www.example.com" + }) + ); + // Test case-insensitivity. + assert!( + RpzDomain::parse_value("*.Www.ExaMPle.COm").map_or(false, |val| { + let dom = val.unwrap_domain(); + RpzDomain::parse_value("*.www.example.com").map_or(false, |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").map_or(false, |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 { + Value::Comment(com) => com == " hi", + Value::Domain(_) | Value::Blank => false, + }) + ); + // Test blank. + assert!(RpzDomain::parse_value(" \t\t \t\t \t ") + .map_or(false, |val| matches!(val, Value::Blank))); + } + #[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| { + 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(|_| ()) + } + #[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| { + 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().map_or(false, |d| { + d.domain().value == b"aawww.abc" && d.is_subdomains() + })); + assert!(iter.next().map_or(false, |d| { + d.domain().value == b"abc.abc" && d.is_domain() + })); + assert!(iter.next().map_or(false, |d| { + d.domain().value == b"abc.abc" && d.is_proper_subdomains() + })); + assert!(iter.next().map_or(false, |d| { + d.domain().value == b"com" && d.is_proper_subdomains() + })); + assert!(iter.next().map_or(false, |d| { + d.domain().value == b"NeT" && d.is_proper_subdomains() + })); + assert!(iter.next().is_none()); + } + #[test] + fn test_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.label_count().get() == 125 && dom.domain_count() == BigUint::new(vec![146069]) })); + // 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.label_count().get() == 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. + // The other check is to ensure that IPv4 address subdomains are not counted. + assert!(Wildcard::parse_value("*.a").map_or(false, |val| { + let val = val.unwrap_domain().domain_count(); + val == 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, + ]) && Wildcard::parse_value("*.1").map_or(false, |val2| { + val2.unwrap_domain().domain_count() == (val - BigUint::new(vec![256u32.pow(3)])) + }) + })); + } +} diff --git a/src/dom_count_auto_gen.rs b/src/dom_count_auto_gen.rs @@ -0,0 +1,1532 @@ +use crate::dom::Domain; +use num_bigint::BigUint; +/// The count of proper subdomains for both `Adblock` and `Wildcard` `Domain` when subdomains +/// and proper subdomains are represented respectively. +#[allow( + clippy::arithmetic_side_effects, + clippy::as_conversions, + clippy::implicit_return, + clippy::indexing_slicing, + clippy::too_many_lines, + clippy::unreadable_literal, + clippy::unseparated_literal_suffix +)] +#[inline] +pub fn proper_subdomain_count(dom: &Domain<'_>) -> BigUint { + /// Returns how many proper subdomains are IPv4 addresses. + /// We need to calculate this so that we can subtract this value + /// from the cached cardinalities. Note that we don't have to worry + /// about the `Domain` itself since an IPv4 address can't be parsed into + /// a `Domain`. + #[allow(clippy::cast_lossless)] + #[inline] + fn ip_count(dom: &Domain<'_>) -> u32 { + // `Domain`s that have 4 or more `Label`s can't be an IPv4 address. + // Also `Domain`s must have at least one `Label`, so + // `0 < 4 - dom.label_count < 4` and `256^3 <= u32::MAX`. + if dom.label_count().get() < 4 { + dom.into_iter() + .try_fold((), |(), label| { + // Only a sequence of decimal numbers between 0 and 255 without leading + // 0s can be an IPv4 address. For `Domain`s that have such `Label`s, + // the total number of IPv4 addresses is simply 256^(4- label count). + // Note that `Label`s always have a length of at least 1 and + // any `Label` longer than 3 cannot be a valid `u8` without leading + // 0s. + match label.len() { + 1 => match label.as_str().as_bytes()[0] { + b'0'..=b'9' => Ok(()), + _ => Err(()), + }, + 2 => label.as_str().parse::<u8>().map_or(Err(()), |val| { + if val < 10 { + Err(()) + } else { + Ok(()) + } + }), + 3 => label.as_str().parse::<u8>().map_or(Err(()), |val| { + if val < 100 { + Err(()) + } else { + Ok(()) + } + }), + _ => Err(()), + } + }) + .map_or(0, |()| 256u32.pow(4 - dom.label_count().get() as u32)) + } else { + 0 + } + } + // The commented out code at the end of the function was used to calculate the cardinalities + // for each possible value of domain length allowing IPv4 addresses; however it takes + // as much as 16 seconds to calculate for a give value of n, so we cache the results + // instead. + // The array is ordered based on the descending order of length of `Domain`s + // (e.g., index 0 corresponds to the number of subdomains for `Domain`s of length 251 which + // is the max length of a `Domain` with at least one proper subdomain). + [ + BigUint::new(vec![52]), + BigUint::new(vec![2756]), + BigUint::new(vec![146068]), + BigUint::new(vec![7738900]), + BigUint::new(vec![410018388]), + BigUint::new(vec![248542548, 5]), + BigUint::new(vec![4180397652, 267]), + BigUint::new(vec![2671623764, 14197]), + BigUint::new(vec![4117795412, 752210]), + BigUint::new(vec![862478932, 39853246]), + BigUint::new(vec![1276228180, 2111483772]), + BigUint::new(vec![3838587476, 200375265, 26]), + BigUint::new(vec![3977409108, 4252552993, 1379]), + BigUint::new(vec![2704896596, 3919002822, 73113]), + BigUint::new(vec![3882512980, 4014107452, 3873682]), + BigUint::new(vec![3242881620, 204873911, 205233436]), + BigUint::new(vec![1153331796, 343698866, 2283635595, 2]), + BigUint::new(vec![969830996, 2755980681, 572170738, 134]), + BigUint::new(vec![3030282836, 2269546517, 2473041289, 7106]), + BigUint::new(vec![1847489108, 3629376584, 3732202808, 376516]), + BigUint::new(vec![241070676, 1801323295, 550145915, 19948419]), + BigUint::new(vec![1230926420, 3223519493, 3638801565, 1056896671]), + BigUint::new(vec![3529405012, 3593787233, 3076904220, 161369882, 13]), + BigUint::new(vec![2724098644, 2312631537, 1324349926, 3220318693, 690]), + BigUint::new(vec![3059642964, 2191098099, 1231948975, 4049114113, 36596]), + BigUint::new(vec![106852948, 2265707158, 4078524026, 53383894, 1938960]), + BigUint::new(vec![ + 1449030228, 4120606714, 1266689161, 2876498924, 102728961, + ]), + BigUint::new(vec![ + 3596513876, 1375839570, 3073178857, 2030051240, 1147764631, 1, + ]), + BigUint::new(vec![ + 375288404, 2347365293, 2334837610, 1737538116, 601157403, 67, + ]), + BigUint::new(vec![ + 375288404, 333124604, 2043982089, 2641118257, 749632597, 3557, + ]), + BigUint::new(vec![ + 375288404, 1946521181, 65357692, 56864761, 1521603317, 188464, + ]), + BigUint::new(vec![ + 375288404, 2577463837, 2311486239, 2856163489, 2140150568, 9985119, + ]), + BigUint::new(vec![ + 375288404, 3318986961, 3336800178, 1153613668, 1432641031, 529026360, + ]), + BigUint::new(vec![ + 375288404, 1672763489, 1653117627, 2349982024, 1101569468, 2258793175, 6, + ]), + BigUint::new(vec![ + 375288404, 1872985649, 1777688160, 1798349612, 2929927110, 3232695082, 345, + ]), + BigUint::new(vec![ + 375288404, 3990328753, 2308241830, 964880313, 3479391898, 2089547876, 18318, + ]), + BigUint::new(vec![ + 375288404, 4244638193, 2014962046, 1954035381, 2572106657, 1878726949, 970540, + ]), + BigUint::new(vec![ + 375288404, 3016518897, 1468302259, 1459760780, 1145312287, 191860765, 51420664, + ]), + BigUint::new(vec![ + 375288404, 3918013937, 741117515, 1423741278, 32256809, 296378773, 2724342633, + ]), + BigUint::new(vec![ + 375288404, 4113421809, 3220678635, 3913218978, 1104050882, 3913619510, 2605770681, 33, + ]), + BigUint::new(vec![ + 375288404, 1022831089, 4149936985, 2644026415, 3253425148, 4171545929, 2287985434, 1780, + ]), + BigUint::new(vec![ + 375288404, 797178353, 1019922958, 1674344141, 3250454247, 3816775168, 1072247613, 94335, + ]), + BigUint::new(vec![ + 375288404, 151210481, 2544744706, 1216969582, 3194279504, 3075836706, 2933426700, + 4998020, + ]), + BigUint::new(vec![ + 375288404, 2071579121, 679124811, 24458279, 118705999, 1933531958, 2136634151, + 264802508, + ]), + BigUint::new(vec![ + 375288404, 3915909617, 137490366, 129739371, 476554331, 2789132808, 1650159256, + 1144725629, 3, + ]), + BigUint::new(vec![ + 375288404, 2111769073, 3809283612, 3723310513, 888766089, 765431967, 3639728901, + 281019137, 173, + ]), + BigUint::new(vec![ + 375288404, 4201646577, 3368784016, 2790098399, 2277185150, 153774588, 196277263, + 1124283864, 9169, + ]), + BigUint::new(vec![ + 375288404, 1880099313, 3892329276, 3689814422, 1420707258, 554100642, 1903824923, + 61312066, 485801, + ]), + BigUint::new(vec![ + 375288404, 2718173681, 3915736505, 1948017691, 3311844270, 2449773636, 1831131280, + 1521446241, 25738454, + ]), + BigUint::new(vec![ + 375288404, 2886994417, 2292494843, 1109493842, 1279543652, 1582639857, 944194272, + 699053385, 1363661279, + ]), + BigUint::new(vec![ + 375288404, 3705932273, 705482963, 76809839, 2528970701, 3527071483, 2583007984, + 3796830889, 3529309406, 16, + ]), + BigUint::new(vec![ + 375288404, 3529771505, 1276023335, 1558249304, 474250754, 3711984670, 3025890941, + 1857748306, 1031405210, 891, + ]), + BigUint::new(vec![ + 375288404, 2594441713, 4254079775, 3418696835, 1548693023, 2768765240, 3899935355, + 1980342079, 933958820, 47219, + ]), + BigUint::new(vec![ + 375288404, 631507441, 4097520258, 1103161534, 2113861360, 1991546256, 3660681902, + 2012204487, 3414681798, 2501743, + ]), + BigUint::new(vec![ + 375288404, 245631473, 491504859, 3208401305, 1474266946, 2723061964, 2310073485, + 1453991315, 2791012792, 132546076, + ]), + BigUint::new(vec![ + 375288404, 2661550577, 2406104814, 862968291, 1899078236, 346491611, 1241634869, + 4148522640, 573571521, 2727519367, 1, + ]), + BigUint::new(vec![ + 375288404, 849611249, 351847671, 1257821523, 3615257673, 702995988, 5240717, + 3567916871, 3159692503, 2694515012, 86, + ]), + BigUint::new(vec![ + 375288404, 2191788529, 1679608494, 2906887961, 3277625757, 3033747662, 413021044, + 1822896059, 856201021, 2772913513, 4589, + ]), + BigUint::new(vec![ + 375288404, 3533965809, 2556505512, 1816528392, 1947652842, 1037141563, 274775137, + 1149398125, 2668033105, 838441812, 243166, + ]), + BigUint::new(vec![ + 375288404, 1386482161, 1234596285, 804514535, 1131547557, 1232842259, 1405663093, + 4235442216, 2871548155, 3106883214, 12883303, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3863765183, 3150246073, 1204630827, 2075041789, 1483442067, + 838823512, 295416753, 3293438507, 682576435, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3121791326, 3784088765, 1222191727, 211278684, 4204580296, + 1868812834, 1473418029, 2104247738, 1804168085, 8, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2471685663, 4103126092, 1640721307, 2924547633, 3719386775, + 3358136588, 1785095480, 1506810521, 461696225, 446, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3097994655, 2113279511, 1888061798, 1596718373, 3213304653, + 152654476, 2978780767, 2083463032, 1742258559, 16363, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1860567763, 1130544223, 2153567102, 625138177, 3206578786, + 1106733064, 3948231794, 1002658338, 2817930503, 866822, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 147208035, 1175109684, 660928834, 1730986888, 3083362323, + 412094060, 2997340098, 3976540641, 3212428124, 45540257, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1325126451, 3929886196, 1506154326, 283097260, 2162374086, + 1083031484, 2385562882, 1083286932, 3864853917, 2393119200, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3546949299, 3466780721, 1591926664, 3866228006, 3769344032, + 3294539415, 761827863, 3320756801, 1055962996, 1194022465, 29, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 4239835891, 2374590398, 341723740, 1768865321, 3009735295, + 640743363, 760884641, 1286523815, 1425636762, 1404846044, 1538, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1185904115, 3103587562, 1485183809, 1874654482, 3208312786, + 2785384481, 313087062, 1389949551, 3463745521, 1039435803, 80821, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2965606131, 1397412449, 3110808017, 2226053202, 3474793386, + 257483838, 2073200290, 2070753250, 1854563799, 2728715379, 4245916, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1130168051, 2123766638, 316945545, 132357966, 3454088252, + 3364776445, 2707843191, 452091523, 2613197394, 888667529, 223041660, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2526860019, 2712686141, 2768201501, 3657483061, 763339927, + 2809075408, 4010198047, 1079127981, 404473813, 1744216475, 3125773904, 2, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1186898675, 2387441384, 2136278685, 2507538597, 4291433051, + 4120455470, 4126082953, 4124167878, 1816064621, 56141297, 1164585958, 143, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 4136891123, 3213626288, 3178750640, 3483039856, 2585028864, + 307761499, 612469337, 2705587210, 2331449101, 2528560486, 2004283004, 7524, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1959162611, 3492710176, 4176981138, 187691056, 2590522248, + 1426342635, 2139644698, 2023712095, 4279998285, 2289341587, 1312229383, 395147, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3462181619, 837145225, 2194072164, 1472386366, 1537487790, + 439148352, 2717361996, 3390324277, 1458645090, 4049643816, 731583843, 20749520, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2737025779, 1814181525, 1816957655, 4281341807, 4095807471, + 814369634, 1431209630, 3374966462, 3448108709, 31622598, 1983475365, 1089487382, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 236237555, 430037603, 1852070087, 2118228517, 3252136865, + 3422495617, 2590246972, 3459047098, 3973588529, 3171827264, 92475618, 1366057699, 13, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 4285838067, 735277699, 3811534046, 3138836190, 397415289, + 2582819123, 1510113859, 2843520944, 231283193, 1071216799, 881488229, 734567190, 699, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3219698419, 466853614, 1331826237, 875975020, 1508049190, + 2008133206, 1300868705, 3490208720, 2376565918, 2037490725, 1237796068, 4043718151, + 36701, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3740840691, 2381286222, 1266084405, 3478844871, 138766328, + 2627058549, 1862482887, 3802739663, 3303530586, 670509360, 729738179, 1103040603, + 1926447, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1170780915, 2074383492, 1961754613, 1827774001, 916656902, + 3259243073, 210875113, 335228650, 3589141125, 431855961, 918438149, 3617060868, + 101108337, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2001253107, 4061558499, 3398113640, 2159180498, 3282472469, + 2781426599, 2457937046, 705967069, 1864750529, 2770072726, 4100119564, 3195678530, + 1011162313, 1, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1737011955, 1241403666, 1629794265, 1113534036, 219462432, + 3724660491, 1632655042, 2458055004, 4216935476, 2686950025, 703127910, 202085749, + 3560425265, 64, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1116254963, 876125681, 2080401951, 2417594553, 1281467958, + 1222232179, 673709201, 3936590470, 2560633599, 1145420807, 450422830, 2135989143, + 2462786277, 3401, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2340991731, 2737343678, 1289015076, 1347075108, 2512475061, + 1412108126, 4100065885, 1354686988, 95090307, 1491981215, 1626720129, 1489450116, + 1884072652, 178463, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 3683169011, 3216812981, 2744855154, 4058157834, 3543196209, + 4060346256, 3819377805, 880180604, 3698897414, 2155732057, 219486495, 2801311956, + 196802852, 9362171, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 4018713331, 378501028, 3741989739, 4071246630, 3223764155, + 944505703, 4060511320, 3998749690, 1376306696, 541422658, 3194641855, 1822586890, + 2833605965, 491089841, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1065923315, 2272734833, 2112108986, 105514710, 1835282898, + 73796845, 594435594, 3623404924, 3134226987, 1565418351, 2713974924, 557744684, + 2768639568, 4282534202, 5, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 2408100595, 425311361, 2226869327, 1674710348, 1445832078, + 312177116, 2231562669, 3746125130, 3498765241, 1939808910, 1301732400, 854687535, + 3104899461, 2200045581, 314, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 260616947, 2859448658, 3702809247, 3320531005, 3151878689, + 4207742540, 2040955801, 252969925, 386120380, 1743412687, 3776929748, 2230776619, + 1412589968, 2363163963, 16492, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3303796476, 2667360554, 3871609700, 911605988, + 27609975, 1290159954, 1842020606, 3449686738, 4185142679, 3778140731, 3809292562, + 2284720990, 3785464148, 864751, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2661167083, 3024326089, 3454077871, 3099468518, + 2527626308, 1167917430, 1316291304, 2175736981, 3846068744, 1555994873, 2137553745, + 2603347602, 3190393459, 45336466, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 940459788, 2102940239, 3591716736, 3834980050, + 1875345014, 528139358, 3296012157, 3763402287, 624898860, 1256164751, 3436662455, + 2303595502, 90138154, 2376595497, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2601003596, 2817854902, 3738069448, 49753232, + 395556631, 4278421792, 1332236424, 2899221004, 2382694996, 1653174695, 1396595153, + 2947866156, 2578251457, 15892544, 29, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3767469568, 671287769, 3791280922, 2921453395, + 4023886981, 3031459970, 3699531449, 4253680083, 3347977273, 3228548537, 2887358797, + 2042847151, 1961985952, 257705967, 1520, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 448122768, 3534770692, 578405276, 433247806, + 4175667383, 3340326313, 851853855, 3728964000, 1960633968, 431785542, 1966122388, + 1627483431, 2802782821, 2538621092, 79655, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 167469408, 2159686082, 821089061, 2324782401, + 578797663, 3194338755, 2071266770, 3465944956, 683610840, 2498722363, 2423090999, + 2865272105, 48834160, 912286961, 4173677, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 1946022112, 2246986986, 3745071011, 3383180416, + 807758270, 1127886323, 1915334517, 2927471789, 4218978062, 441286161, 2533902413, + 89124065, 3692088392, 3505188042, 218658982, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2527376672, 4001711028, 3217742207, 1841952496, + 3072670916, 3204261182, 3208730903, 3551501038, 3441960928, 3295757255, 299807985, + 1166589037, 2169989204, 3733494551, 2864151189, 2, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 688502816, 1002760441, 377188254, 794405575, + 1675507559, 1446917124, 3708509626, 1867858013, 920339131, 2428156058, 2092486826, + 3748575832, 1953057757, 2363909823, 2924476765, 139, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 4016976160, 727489525, 65915787, 1183254543, + 2888071423, 3049200727, 2690107908, 2198215983, 4119894348, 3875556523, 442037750, + 1898863513, 160023236, 989734902, 90609241, 7315, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 4166738208, 2266663227, 673332435, 550627422, + 3488007317, 3671482230, 59074521, 4227541302, 2262677408, 1149291744, 921863748, + 3112047771, 3399675251, 652952636, 3354949157, 383031, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 351384864, 3565098702, 4155390886, 3220789349, + 2608312059, 2103834788, 944768032, 949615699, 4118569665, 1685119122, 980757317, + 1694451395, 393065143, 2754423072, 3616955801, 20053644, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3014165792, 796920501, 3672616261, 1130012766, + 4077271733, 2971834292, 252963730, 1652648553, 69566497, 1999168979, 3104695664, + 710159863, 1735470542, 4064425944, 1573131256, 1049759101, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3209942304, 1679710063, 1755690075, 4251598478, + 1049675705, 1717017534, 705215572, 2260440090, 2765904623, 975350201, 1756060381, + 1886465482, 2733251981, 3115673342, 472751457, 3404646711, 12, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 1531073824, 2423748678, 1729816840, 1740611885, + 2777212627, 2969100597, 501119910, 1886104013, 1931706566, 96118838, 3281936552, + 4069997219, 1398231818, 2988777868, 2856575295, 2009829544, 669, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 1719702816, 1119467944, 3767307448, 1949393548, + 1966720572, 2729045600, 3753905954, 3937507823, 3731918463, 3146610355, 2967238817, + 2505065361, 2485535643, 1893300314, 166365062, 874087629, 35029, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 1536660768, 2051648810, 2202156949, 1371566363, + 1821496179, 1684562462, 1200503648, 1234387596, 2182170796, 1165941114, 1382310371, + 1755514751, 2474898046, 2611907239, 456112578, 2749340137, 1832576, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 1827181856, 4177292154, 2680738868, 1607480411, + 1794618371, 3703803888, 2600970549, 2966298121, 174262665, 1367904220, 905247628, + 2766298775, 3949556221, 1103455785, 3652079576, 1227653919, 95856967, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3121124640, 4270361123, 2054576521, 3629448573, + 2564600244, 132302420, 4285170993, 2743649285, 133493916, 3207419472, 3048390000, + 2806218966, 2081300703, 4181751659, 1789896323, 1910815868, 718209863, 1, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3908867360, 3679284426, 2607521588, 512862844, + 2732858915, 2032099596, 1011088742, 4023691910, 2343553854, 2039346656, 791945032, + 3298005749, 1732434189, 2233637154, 2006257184, 4156212473, 144011146, 61, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 487363872, 3552686372, 3532184366, 1586939227, + 3724812754, 2465159099, 2444171857, 3424835511, 2900544237, 1283399694, 2991378323, + 2924594123, 3463506096, 3517618882, 1646903520, 850003639, 3693629995, 3190, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 970757408, 593337656, 2621124778, 2362798218, + 1847405156, 3380032365, 3917922903, 1684495821, 3187390150, 3115120277, 3126047712, + 1298279344, 3937900583, 3741958231, 1599463652, 2918936608, 2485862381, 166789, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2807862560, 3332862828, 3314859132, 1713311062, + 3684236752, 781458918, 1258707760, 795101932, 1642963495, 1566360302, 2027884895, + 2599672732, 4134932573, 4218553122, 3065721011, 3586743940, 3098048383, 8716654, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3214710048, 491973160, 2927525970, 3000056542, + 3840979468, 354856496, 2429638914, 1493528488, 2621804923, 1074562622, 2337847084, + 805113811, 2215452942, 2043738784, 862449625, 3952422953, 2935835032, 455457980, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3936130336, 3810953987, 1542106736, 3855036022, + 737849359, 946161952, 3426822660, 140914304, 1709004311, 1784614413, 2398718531, + 3365991385, 2536976033, 1995705147, 903783129, 3958112258, 744347273, 2318865108, 5, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2476512544, 2901890493, 1516229638, 4279451811, + 1158581914, 3413180859, 2662373916, 138227816, 3500608007, 1021825042, 1542512059, + 1657638841, 2546098455, 2167488194, 2639913991, 1190253808, 3657171949, 1517999094, + 289, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2744948000, 3663540216, 1529140403, 1719575268, + 788302244, 3133780986, 1080231567, 1297291346, 2555205130, 1013378818, 3710964045, + 254267420, 169782776, 444582309, 3263263535, 4006521636, 3677731565, 142388726, 15110, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 933008672, 292991406, 2733939278, 3620414616, + 610832446, 2435459475, 3940308335, 1824151617, 1013413903, 1529153948, 3841031730, + 2270845647, 2514900921, 352319593, 4070392803, 1250818383, 2493419008, 4180057617, + 788877, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2275185952, 2069192335, 3442270414, 3613722894, + 3792005945, 782718190, 2966897497, 2085152667, 4262162053, 1687190049, 507318413, + 3050269090, 4242352422, 3290464898, 312510041, 1161865422, 2075798102, 3170157616, + 41177419, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 3617363232, 767481141, 2798605879, 216592806, + 3009776831, 2287157616, 746314070, 533365218, 1941679715, 3683198332, 1287225523, + 1783505619, 2435916847, 30895875, 2414036950, 3357534644, 2488836996, 1476424431, + 2148871451, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 1469879584, 3966110914, 524124560, 152391733, + 2511082427, 2824602280, 2073871626, 1879641108, 629469854, 1723285870, 1308591480, + 1139848879, 1338115761, 1345716577, 2490757277, 2518816577, 2890283632, 3045945884, + 445080315, 26, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3821629204, 1251879183, 2891264873, + 862372749, 2730649920, 3946196409, 386054333, 495072142, 3997453427, 1929934700, + 2286014611, 3132574989, 3816564976, 3613717384, 137659154, 3503341474, 2723213443, + 2543045884, 1361, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3723538515, 1352727828, 2409267632, + 1399204016, 963872732, 2078104532, 2376079728, 3106353504, 2941033470, 1552894073, + 1150412642, 3250864799, 1200024207, 1381405911, 1845036695, 2091830815, 696155422, + 2031249559, 71004, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3994675668, 1490424801, 2018541335, + 3386372446, 1887579055, 1070949466, 3015236068, 2125386458, 2576663800, 787844066, + 3899955011, 3896043957, 3245195291, 3451998588, 4137134477, 2870460987, 2223995227, + 1856577899, 3701804, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 108189908, 1009895052, 2417361073, + 3716302432, 1206236872, 1714392538, 2423637157, 1446646222, 2951583407, 381426507, + 4171738569, 2486690329, 1225283083, 81479387, 655056393, 1387263240, 3653525540, + 158300381, 192941957, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1088623368, 362459840, 1427608929, + 4026337049, 17759160, 2131962935, 3078358333, 3350714126, 2005794791, 1986667923, + 3069103584, 2589397401, 872859540, 3464091314, 2827312770, 2771039149, 2983482937, + 3282652449, 1516545866, 2, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 297759128, 1837823977, 1597754151, + 2594200211, 3938329176, 1514863451, 170335670, 4104939771, 2122122959, 204320869, + 1514324102, 1749816223, 2472922285, 1345800768, 672452179, 1118197205, 2132458193, + 2742243656, 985429928, 123, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1565423976, 1940455119, 2001544857, + 3190388342, 2147138011, 3053711670, 4016476330, 2167732712, 1190433567, 1594135970, + 291103371, 1626580129, 1301317960, 1559418209, 738610514, 205838008, 1400741198, + 4214199197, 796192726, 6453, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 589251816, 2391831009, 1728723516, + 181130724, 4285375407, 2929351545, 3233212788, 2309651665, 2730928462, 146568749, + 2657314986, 3107049692, 1532662895, 1514403156, 103999350, 3374554676, 3436565642, + 2034666958, 2828653903, 337921, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2861969704, 1135425604, 1039070983, + 1500008042, 756650173, 1226466646, 2884374739, 2720613726, 4037861095, 1154327514, + 948102900, 2402380469, 518831132, 287068554, 455744436, 2132737622, 3684840433, + 2753298665, 1829483875, 17694581, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1562870824, 2223563800, 627601685, + 3931191767, 1963804481, 2068642021, 3182461511, 3440237650, 3372627476, 3882745516, + 155135794, 328106351, 153352687, 532473956, 384060564, 2089202149, 2414435551, + 2221820394, 1383076804, 926503403, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 651451688, 2063603536, 4195344935, + 628247983, 3694049076, 409781270, 3866516567, 3575005349, 477908172, 1365517798, + 4219664552, 2267226689, 1273207489, 715477852, 2130518214, 407639283, 1646907501, + 3664713902, 3419206668, 1265920647, 11, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1668631848, 3084215717, 2535623602, + 1355370279, 1711209510, 3565276581, 334763960, 1637078921, 1157348908, 2867965050, + 4160429863, 1209329262, 1964536455, 3844490096, 3112905541, 3015147941, 609034362, + 3114335197, 2289791300, 1525249047, 591, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2873237800, 593475146, 3957752737, + 3607753543, 998755275, 2973630508, 3812764564, 729867179, 3117173245, 1205481369, + 3859663194, 2382784159, 4166471027, 338216186, 2248990593, 4122863298, 2190421143, + 962475347, 3363479543, 644463577, 30960, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2441998632, 1456210221, 3517676631, + 2125186749, 2336040486, 1803295599, 497828393, 3354229517, 2315762146, 2390170442, + 2471250722, 3262196615, 3636458986, 1948827006, 3805560749, 1978273630, 2198623259, + 4237805852, 1489294306, 3466543098, 1620841, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 4002365736, 2699270358, 4035612103, + 4236649769, 2481511290, 286754901, 2192344738, 3423993860, 3397041319, 2594484709, + 3540687144, 447363625, 488059790, 444324058, 1568636462, 2963210948, 924176437, + 1842840823, 23040779, 22811001, 84851830, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2587476264, 531471700, 565661081, + 3173396807, 560588409, 2397243561, 2939066677, 1112532089, 4016222432, 4054750870, + 1034402982, 1728888132, 2408752621, 3982402714, 3118679989, 1289461419, 1709813332, + 2514987870, 1439101459, 4218408052, 146893508, 1, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1562378536, 3984676162, 1533759221, + 2265150196, 4077915242, 3191635425, 1391128044, 79630882, 1799432732, 798767144, + 788136604, 3877311258, 2344732049, 2771994519, 730903558, 1075120241, 2762438885, + 1653779067, 641228852, 4115900764, 587300920, 54, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3532063016, 2106301204, 1161579613, + 3977699201, 517035130, 2794580228, 3841970396, 3789755434, 721327799, 1673345532, + 77309593, 2039372378, 2275232492, 247383471, 1521519421, 3259734418, 42022609, + 2135694052, 1236139276, 2334490590, 3251143850, 2833, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1110966568, 2393056800, 1350110272, + 2924655289, 1459090315, 3475237696, 1257511530, 4036949063, 723610430, 1552731592, + 1205849493, 3220845583, 3701829803, 2849977890, 1258404014, 238739126, 2442636624, + 4014062691, 3433106490, 1901814374, 3462971528, 148325, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3407348008, 1233228594, 3411181417, + 2093918876, 1827015250, 1338390636, 4278954490, 2855758896, 2194310770, 3410983728, + 620234408, 1826608367, 3617389928, 246027064, 1642755342, 1870814843, 1879774701, + 904530615, 3902271887, 3396039084, 4245280487, 7763447, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1217134888, 3078093129, 3061112270, + 1278695255, 3616386859, 4024538543, 2988611858, 2589454103, 787801141, 2215111346, + 4020818330, 3231249235, 3792889391, 1656781417, 1381049381, 2527801670, 1083965285, + 1627715319, 2367340571, 1283593976, 1268495279, 406327846, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2442920232, 45276574, 2032627375, + 917984412, 2986861814, 2373466326, 1802961357, 3126051563, 2002628262, 2936766148, + 3750548047, 3767161939, 2780915648, 1069487964, 4036604440, 1550559861, 1170372835, + 3083458002, 1323016514, 1640149106, 4074778593, 4085980586, 4, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3832283432, 2696280962, 2572683480, + 4175448237, 3404610727, 2136661146, 4257763704, 892952018, 2218618404, 1250412368, + 2141474832, 700052702, 1861341822, 1903102532, 539549951, 987106772, 2352847513, + 2795598340, 142672976, 3553933053, 3432928320, 547528798, 259, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2381054248, 21917494, 2024589020, + 3035885024, 1544851454, 3873165476, 52544173, 584620594, 999613054, 53857787, + 2953709759, 3669630452, 1795146519, 968325082, 4236363572, 3962694578, 607022979, + 1147753971, 3246682774, 1373916352, 3588841297, 3896070368, 13560, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3458990376, 3102212629, 2923606015, + 2610526647, 2862495030, 2462184740, 247107426, 2375588414, 1615032415, 2018689308, + 23577407, 757254239, 3469952833, 2523407951, 236907216, 2696977587, 3342638617, + 3218520741, 1307512362, 1441329142, 1553236610, 3099072064, 709657, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1227620648, 2735822829, 3182207984, + 2655874152, 1911959699, 3202269726, 3352265041, 2482043729, 1096361615, 1827680999, + 2108667791, 4109372722, 3751633681, 4168743365, 1483427286, 3668772749, 371413044, + 891157989, 3614903301, 379159372, 2554584355, 2523739390, 37135930, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1378615592, 2124979467, 3869608993, + 1182792460, 2640281798, 94061031, 3072662239, 1765178173, 522921752, 1599801713, + 2392193932, 3491954298, 3409559666, 995484266, 287535901, 1813090899, 3858071186, + 2932421652, 2727235639, 47972540, 1760330552, 1367320824, 1943234256, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 573309224, 2848462570, 297419517, + 784731094, 2768546732, 295310148, 1482184039, 3289378734, 1626845618, 4088238483, + 572708640, 1418696534, 4059942084, 984728194, 2831713861, 2810244614, 2425826462, + 502741229, 2484597449, 2051110097, 1309436654, 1582624320, 2897210858, 23, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 908853544, 115794502, 3565064611, + 4224203971, 3003673416, 2040538988, 3494903392, 2174787101, 730958942, 1587426713, + 1216059146, 2222641978, 458956447, 2568862701, 1118079692, 3242301930, 3084300380, + 4119311250, 3093156219, 1587649575, 1429127399, 2586105371, 3232553230, 1238, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2251030824, 3012358716, 278114290, + 2220419113, 3976552303, 2715134477, 1681099792, 1589972998, 3000442551, 404778394, + 832783608, 2396613513, 1673183307, 1942089317, 1845205033, 3482430837, 4084553639, + 2323389363, 3896457087, 348002344, 4044436031, 287449663, 3114057219, 64814, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 3593208104, 2945023736, 2227032504, + 2193119976, 808267449, 3216385101, 3033836400, 456555884, 1125870240, 464859713, + 10507958, 270631227, 2210300992, 1703891855, 551285133, 2264386612, 1922395156, + 3034507121, 1040591836, 922801132, 1637967686, 3017872049, 3114302110, 3391169, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 1445724456, 4036055873, 26662430, + 2849460476, 126144669, 3134242821, 3267407193, 3385299635, 2084731548, 3086885011, + 2933551302, 1118233540, 334662835, 2478562709, 3520965186, 286926085, 2281497960, + 4239060620, 2277599706, 456079410, 3755990621, 289475438, 3137271329, 177424049, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1433733179, 1983551259, + 3183236740, 3213866899, 4208537401, 2945052152, 3940374272, 3882475903, 1509970502, + 3309566084, 1926141956, 3284707559, 3230861295, 3043572573, 2579360888, 3177799575, + 1588286429, 932093071, 3711427490, 368598896, 1429105521, 906409877, 692522539, 2, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 156035530, 1133686565, + 1743320647, 3375072318, 2012342394, 1153370874, 2481071177, 583451137, 327194059, + 397399572, 2128700879, 944339639, 1239680795, 3991450197, 3388101214, 3344426675, + 3597872431, 4206908931, 2447851829, 1889574113, 1472740103, 3986860253, 294017406, 113, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 258440619, 2864366690, + 1036994977, 1375962384, 623996537, 3277116958, 3199396551, 4141879303, 592996622, + 3477310669, 492237602, 337428166, 1626177557, 299025377, 994816793, 779280139, + 2761101027, 3415256044, 2363507292, 3483241717, 2337671414, 2257176492, 784913708, + 5915, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 3567736939, 1422128831, + 1359905647, 3749719229, 127483939, 4223227770, 2169435423, 2138123196, 1126010966, + 613271075, 605448982, 3068413273, 1547048905, 2583389829, 1453489243, 8760026, + 1648613590, 2555144115, 401564871, 4196029032, 1612167424, 302476558, 550178662, + 309445, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 587583775, 3536224817, + 2781219922, 2582698859, 2983955949, 913689216, 2656586171, 486487766, 3837012000, + 3679195413, 1772417733, 2985095058, 1205369396, 1096725427, 2527158883, 3112953302, + 45546555, 685048376, 2320196409, 3593580469, 3481366272, 3387590826, 1954897998, + 16187820, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 523158703, 4113109057, + 2100971674, 217956614, 596199284, 2380878930, 3063776566, 1398583896, 475510041, + 2647065, 569539020, 3670413946, 1728895160, 1053315873, 2514048110, 2778013497, + 3821961787, 4267822458, 13820863, 1167343593, 3268996059, 1641040263, 140134694, + 846803892, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1118880383, 2310160004, + 3019308007, 4008582343, 2896674950, 330609170, 817589387, 2952646134, 1654073678, + 8987835, 2423235159, 664912750, 3046216472, 489593826, 2216686240, 1791381573, + 2387259047, 2476423337, 3775812603, 4086181528, 2412691423, 184457215, 464564228, + 1346638432, 10, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 2976500223, 2979299174, + 3407692876, 1015168976, 1405129545, 89483162, 1793466320, 4107506794, 451927044, + 3854789239, 2292720344, 3033627639, 3414458444, 4206167043, 3911629651, 1549165729, + 2609588033, 1282189107, 3868152888, 2198179607, 187872385, 1757276935, 1970119063, + 2103526322, 539, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1701240383, 4150727432, + 4229846246, 1491411143, 3448010050, 2748284773, 435433000, 2628046556, 2404309504, + 1338368081, 3510713777, 1756851377, 3559685262, 3651796331, 2244954464, 847590960, + 1051616462, 2516424913, 980131917, 619425458, 246904961, 3660903002, 4148712762, + 2358648193, 28219, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1919189311, 1075985887, + 3115672713, 3401657272, 3896084877, 525825118, 876173054, 3168154221, 2976809555, + 2149006125, 4178177611, 2236024378, 2590801160, 2938337185, 2230946803, 3406716434, + 1896413717, 485546906, 2995780162, 2252419063, 951366215, 1875230279, 3453444322, + 2272567594, 1476076, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 2773596735, 887942758, + 3663982038, 1807451680, 3462435410, 4176426144, 3909880531, 569807074, 2047733993, + 32965839, 1144605978, 2238484561, 243114785, 530662820, 1119792380, 2182736718, + 2324124763, 1631774642, 2784602890, 1606266084, 1344911530, 3252479184, 699521876, + 2806160939, 77207632, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 2701552191, 3021831563, + 3761866094, 1495182712, 3005256748, 1578935760, 729160555, 1248336637, 96469620, + 2453445089, 3694966433, 33145778, 582073547, 2108534160, 910164355, 1709948987, + 4081974507, 962001181, 1665189504, 3356106364, 2947861254, 1856731644, 1088109335, + 571966670, 4038357842, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 434748991, 1126591621, + 2683503626, 3186698004, 2540597168, 2833518046, 2921126828, 3105171050, 1989368494, + 902639402, 2679724105, 1682425435, 2556927276, 3696061619, 1949936733, 2929994801, + 1995337434, 2496165830, 1577335356, 1326658277, 3155215019, 4233228516, 707341872, + 1525371270, 770589498, 49, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 3368717887, 651430580, + 1331245725, 1870762458, 952297851, 288351487, 3062668627, 1959269072, 110037462, + 4211895868, 88728104, 3759138601, 2492810714, 2210040404, 2053569231, 1675388541, + 3095602713, 1096209388, 842591810, 4253831717, 1328811468, 2020607979, 1049249342, + 1275113326, 1155387700, 2572, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 3701595711, 1944631056, + 3913388044, 1972358401, 2211743384, 2468061449, 1825696645, 3950254819, 4175887213, + 658190054, 988997182, 79409135, 3369430523, 2182610800, 1656518321, 3430281761, + 467517116, 1215120427, 1477681381, 1103183017, 1606806971, 410824471, 966071681, + 404550408, 3941253598, 134537, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1778933311, 1533008756, + 993702014, 1124262251, 3395509530, 3217741713, 1279240497, 3816440002, 1903989291, + 51592826, 788752418, 435974174, 1298746546, 2043470592, 3595479373, 1154431373, + 474663849, 2645512080, 1612537241, 2883787901, 2770004941, 3764226311, 820346693, + 2815966245, 3411107300, 7036700, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 714513983, 130433633, + 43631165, 1061573839, 3636110016, 3105933977, 4119850069, 3850315024, 3307706156, + 1888708654, 1089500020, 3483168113, 4246385694, 148388959, 3339203696, 50446275, + 3495488798, 1475116976, 3302398184, 3737270005, 335208268, 3241199250, 1916720821, + 1631957933, 2590429742, 368036023, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 5086783, 281448113, + 2198189083, 2950462223, 1083532748, 999096768, 120317796, 3666454154, 3267420670, + 3370080400, 3355726828, 577850252, 948557141, 2836218692, 1427932166, 1569892183, + 3940072213, 585643425, 376346774, 2570723472, 3703624351, 241067127, 2244633063, + 388840025, 2897967481, 2069179149, 4, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 2254347839, 3919771391, + 2083221487, 1248859317, 2584251500, 2309118643, 2966295632, 530227479, 3941029541, + 3829792983, 1331460141, 794511141, 4238539339, 3614639971, 3888520003, 671875281, + 1318280390, 1017040504, 4102400675, 3908602649, 3081942537, 3325786435, 2657424156, + 2789779288, 2421537869, 1739491396, 234, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 721329727, 3396839210, + 3681663045, 2164575428, 2501982576, 1849109597, 300912659, 2388520714, 1411624874, + 2624693519, 167813227, 3136212958, 3590039522, 366933383, 3827298677, 1462508872, + 4104316159, 3408037632, 75945980, 142801927, 110906538, 224733420, 3587144888, + 2079024206, 1816483967, 3487529667, 12259, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 3606224447, 2188419014, + 1682084083, 3590599894, 890252841, 412438068, 1372266175, 373594701, 3372748264, + 2179273181, 4139821800, 3352899463, 3096202992, 4118590511, 1812536094, 933259209, + 2398707774, 2707207274, 2998411347, 3811914682, 3520084344, 1714267093, 1063807448, + 3971931589, 2091560152, 941864838, 641211, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 889364031, 2352408643, + 2785101230, 1733772378, 434784163, 4073566559, 2380432466, 31602380, 4066003450, + 4237626852, 40313701, 575299096, 3781620268, 2441227883, 169978061, 469088410, + 4035978047, 1815582963, 950034051, 3187464554, 3077778800, 959496553, 627861401, + 1054229804, 2989469638, 476513413, 33536656, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1037213247, 3876594009, + 2266627829, 1919250987, 537147373, 2107714153, 2288848718, 3541705719, 457011493, + 1804491592, 2322151177, 1323959538, 2711719661, 3758789829, 2391880189, 1967683422, + 1660874094, 762547319, 2152582589, 3218958934, 3364300115, 2092930734, 112375585, + 1053460104, 1303864359, 1010650355, 1754045079, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 592617023, 1467380502, + 2920044402, 3863208706, 2968900529, 3891775804, 1937888664, 1517322414, 4079190218, + 2261620489, 922271986, 1130329623, 3364875314, 3847148069, 2618495464, 190888988, + 3288980521, 966383648, 731142836, 624127045, 1186738324, 1079024412, 4233796097, + 3186498315, 2896863071, 2664720291, 1547038600, 21, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 2341641791, 2690557422, + 1657533239, 3963446746, 2703859152, 249614421, 2885391040, 1382595792, 1418129263, + 3838274635, 366387568, 576270801, 474192704, 531259356, 3296862651, 3613193853, + 3267779581, 1895643517, 2825715628, 2202733356, 130729796, 488623509, 1763848835, + 2605511812, 2862954514, 2332937230, 894415183, 1117, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1452449343, 1146197058, + 2863763601, 93635278, 4064052986, 1491278843, 3759242632, 2357543838, 1663095871, + 2905572294, 1392422334, 650761418, 970832320, 478005943, 1896661229, 1639868621, + 3674871690, 3321586441, 3192994991, 4009230510, 1248211706, 4222945771, 818509991, + 1745442306, 3321513893, 2773719933, 1718321319, 58434, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 3214057023, 1624527164, + 3510708953, 572913639, 976867443, 3600176463, 566184589, 202266931, 3799707683, + 2676529998, 2657330927, 146625937, 3197015680, 3446603163, 3159890695, 385445176, + 985476360, 2726297137, 3136737090, 180444142, 3558697084, 1848158780, 2837294127, + 3264483530, 3316877529, 114183317, 1315990856, 3056397, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1335008831, 2025528607, + 2551216052, 1231522767, 1577354711, 2011572376, 3223747309, 1201751868, 3525934298, + 2868471710, 1932753307, 2293006463, 3175890606, 310909112, 3878142961, 943828189, + 3647227586, 2588718047, 722658033, 4186505903, 685808546, 604140331, 4121595882, + 798648471, 3597867602, 1023618253, 2393446371, 159867062, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 3818036799, 506128897, + 3210177243, 2823203764, 3664221334, 92701100, 3929787299, 3240683251, 2162523887, + 241905739, 1168759323, 1987737378, 215876082, 2620200269, 766727543, 1367504002, + 256654718, 1602659492, 3580579779, 877599467, 3288208776, 403329079, 3503114788, + 1822959735, 1853927691, 2913901605, 2974172155, 4067172038, 1, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 865246783, 2478961165, + 2279369525, 1520299851, 1366585487, 1882728022, 4008137538, 1871447639, 3136473970, + 3318579424, 872785890, 2718034001, 1178511011, 2797504317, 3859536305, 3499293615, + 3699614382, 1622854171, 3883461695, 3549424136, 3601002501, 3866372034, 2873861091, + 2935925015, 178042204, 3684726535, 1974077257, 3615893361, 101, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 2207424063, 287650399, + 3981042031, 1021320006, 1997542145, 1279380243, 2113901254, 185032623, 2126737977, + 2069376527, 3345854844, 944924203, 1381750296, 3143952166, 2542102859, 3968815518, + 1505847453, 392713703, 3301576050, 4089524993, 105444836, 137335223, 2482596656, + 3198079093, 1239483262, 1952216162, 3413804971, 1291819903, 5327, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 59940415, 1811670372, + 4069128060, 323976449, 972848863, 4196783059, 2091578826, 4136797996, 2137688135, + 2072447942, 3494899784, 3293304898, 3077389977, 3297643560, 4127008437, 906830581, + 2345350258, 3830948402, 181768411, 1490075293, 2758544777, 2659560401, 1217847221, + 2331559151, 1117191399, 3989967619, 223309308, 236525387, 278677, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1472287494, + 499807712, 3091974659, 4285256010, 3709032953, 2652354345, 1492839133, 3720959829, + 3181700988, 243307285, 2635473859, 3928938903, 1318083514, 3873894276, 3864396159, + 4074500574, 3372801244, 4206609721, 195422496, 1336336801, 119286259, 377214804, + 3067415634, 3479414776, 849565694, 2893846532, 850758156, 14578400, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2943874277, + 3085475042, 1945597866, 333623124, 611451271, 623193460, 401877214, 721271982, + 1599632279, 2905205146, 2666546460, 1055507639, 2433365035, 2462715959, 571334422, + 2536517743, 2916272350, 2556731527, 1027980980, 3685046324, 1363456080, 2137673941, + 4179722900, 4282073441, 721763253, 2564496459, 3570344581, 762666481, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1688935206, + 3475125672, 1130811827, 1933921485, 531309505, 2114739632, 4153679106, 3454868205, + 3216657819, 3610045576, 200928018, 667211958, 1718836400, 3546350352, 365452608, + 4278360604, 1394651330, 1897113925, 2604987881, 393693183, 2422383856, 2173309089, + 2039658552, 1552739446, 1806832201, 202993211, 643893967, 1245681714, 9, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 69714342, + 3572836939, 917813014, 2872424929, 3320167609, 3915314664, 2158359498, 4096929447, + 1758773917, 786611217, 775992986, 2828036016, 1548582321, 3515773534, 1812941543, + 2323394627, 2767537218, 1984982431, 1938557130, 2280845712, 2129545450, 33303255, + 3422157941, 2567101694, 2032365961, 287136117, 218347635, 205070838, 486, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 937253082, + 3145851772, 399354380, 3966343270, 166744482, 268238601, 3920682663, 2684648910, + 3319860812, 3123677841, 3438421899, 1745377561, 2255572901, 750005822, 2952830203, + 3372110582, 185116763, 2002594349, 2433711601, 4063410951, 2286486142, 1772751515, + 3216019658, 4000395493, 4000253346, 789132859, 1524346577, 3799756468, 25430, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 504488298, + 3203478147, 623275496, 484461782, 1186077606, 1635005427, 3292075576, 2904368173, + 3518426662, 181481468, 1620487576, 425716765, 604055179, 805684953, 805173333, + 3762705229, 2028717707, 801243590, 2881187444, 846270972, 3082213948, 1607262303, + 1052034558, 157330487, 100016280, 3992222713, 1068806644, 2968059137, 1330568, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1637897530, + 1176690152, 2518524412, 1571689290, 564727034, 2889640772, 189920587, 971299436, + 197455917, 506450197, 314150642, 2142474017, 1145233556, 2799889119, 149868198, + 1878569113, 2059335395, 1444099206, 3839586651, 278212380, 1911724378, 3294879169, + 3920817795, 858717088, 3156125105, 3400670886, 2237009409, 266827588, 69615850, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3711670458, + 1859501024, 3610388180, 946947779, 188562960, 1743243970, 1574884712, 226401137, + 1143881879, 4062190916, 229671045, 2229726790, 999524008, 1316690469, 4167061421, + 2498109216, 4190372998, 879299639, 2242805718, 497842390, 1722101840, 3774019732, + 4234321243, 3960322840, 2849777158, 2529803791, 1822070296, 4066700810, 3642286032, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2981418234, + 692167403, 1848601396, 1851033287, 3082881006, 3029106926, 1056224599, 1747081078, + 2663632879, 154522774, 2209597776, 2772758834, 3547012403, 2334215292, 3375366183, + 457373055, 1042267069, 1683395470, 3464887484, 3950738381, 2895002845, 3347042372, + 2078125748, 4255125750, 2065510086, 4099872968, 2407024158, 1480172293, 1582961262, 44, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 4125018106, + 1266788247, 1827479118, 2740028768, 2177761845, 4240657249, 2664765720, 2699426397, + 3614214301, 1454649263, 4133484154, 3036887598, 2683101280, 3074486753, 835341147, + 3368320629, 1229459788, 452669746, 56808418, 1624613901, 4220917568, 2053078848, + 2242861251, 3442203080, 3939617165, 3443289142, 614889031, 3317401451, 1303453973, + 2321, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 4144259322, + 510494920, 1764954127, 83312554, 3748961014, 313729141, 1602272849, 1269321234, + 2686911170, 490015867, 1664750701, 2562471748, 3259156338, 2516500245, 1735770725, + 663832606, 3550903612, 581616448, 2323885727, 1028803259, 1389737432, 2800431785, + 4132179859, 1971645827, 1316284844, 589480419, 3424138973, 4047738950, 1015097954, + 121446, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 187486458, + 3948427707, 2684007304, 2019110728, 297954, 19026412, 2220806674, 2961165664, + 2731046581, 2226946501, 99313115, 2011440358, 2223279933, 3825085095, 1243730519, + 1520855745, 753516187, 3495541403, 1121681341, 2249905758, 1470892432, 2528006013, + 2635297570, 1569210250, 820756082, 4161553654, 3820200787, 2695080773, 1601345601, + 6353776, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1594270970, + 1659758811, 2150372209, 203424308, 1450821561, 1840071673, 2158310511, 1256614075, + 852448241, 2516168341, 1280629025, 3243030782, 866104124, 3526173307, 2529026022, + 3647096254, 1307891285, 2747650692, 1894152882, 3020927393, 1257295519, 2541719605, + 753092931, 1207937333, 406183594, 3734804116, 4235922774, 294736150, 3803850933, + 332411224, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2138862842, + 1291938968, 1063251815, 3170601015, 2671654210, 113890197, 2650092083, 2105084901, + 2357022675, 1649775569, 2595302470, 1910508674, 1571126118, 3725528327, 2509219534, + 1328702106, 2413808307, 1504672593, 3563655142, 909319039, 3320511195, 778354275, + 1567520491, 2609521647, 1419322921, 3424589238, 1666238921, 247329657, 2462539832, + 210767815, 4, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 531219706, + 593478036, 450119827, 3749065210, 2086394584, 2910876912, 614108214, 4115375801, + 44864403, 2006634342, 1680960375, 3812244716, 1231206091, 4257658636, 3572109458, + 1925186103, 1437593381, 283293330, 2722021809, 1726475417, 337402759, 2228226360, + 2318797623, 3821440697, 3602072373, 2563060900, 3870061838, 2190672447, 3834326569, + 3573601848, 211, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1087128826, + 981452571, 2880291694, 690450234, 2453641974, 3692566137, 598633614, 3877527001, + 768346722, 3382159864, 161950338, 2269297588, 2586046528, 1220588270, 1193232140, + 2163563226, 643752406, 1617927903, 3402316016, 682532534, 2135929957, 2020237801, + 42718290, 1243975999, 65815584, 2766923586, 3228600988, 508222157, 2234744696, + 649167307, 11082, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2231534842, + 2016029098, 2102547670, 3923335417, 1984901852, 350880092, 2844195792, 458613143, + 275085070, 4008059670, 925677591, 264491974, 3331668198, 909553008, 1056346490, + 4172757287, 2231906487, 3027579414, 2137629704, 3313017339, 3340434766, 3868134082, + 3178418324, 1989113004, 2491891383, 3924450077, 1227802997, 1133757557, 4201533026, + 1655299608, 579766, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 453608698, + 2974240311, 4190540419, 1410368863, 833699122, 3566653284, 1221166785, 2701860513, + 4209709410, 134442109, 3051903255, 723203335, 3738552855, 3445694436, 1098450603, + 3456666556, 2312759672, 390733580, 1065635372, 3833332957, 1784038691, 365685078, + 4203571811, 3960413215, 2644005498, 1308680782, 2528127772, 2922357531, 2538871130, + 89406648, 30330440, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1870300410, + 3519987679, 1785307871, 2076018399, 2631762895, 3337063364, 2197180164, 1044451958, + 3461946801, 2428986168, 3118024345, 1762268358, 231516531, 822067348, 1654625105, + 2372345571, 2157667181, 2651495586, 2166886651, 3634216025, 92645252, 1757253515, + 193416704, 2676968478, 102624486, 1554339635, 601848384, 2183691411, 83598378, + 1020113734, 1586723574, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 265979130, + 121455263, 2188672198, 3408550078, 2397184039, 2427373929, 3201791133, 2777668048, + 1115407813, 3103231190, 3537501048, 760881182, 1284277906, 3353426689, 1853209179, + 396964304, 1239487750, 1277749024, 1370670850, 3779603290, 1520771030, 140592522, + 1810338136, 3929439595, 4176243011, 561601281, 3519529757, 3757993485, 3139295079, + 1630853870, 1403800759, 19, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3394143482, + 2100488124, 3706196543, 4119429252, 943109685, 1644436876, 2936915684, 3287021127, + 3957754123, 1028688673, 2578375259, 2520030764, 530159313, 3457850287, 1263475043, + 607228571, 1147164734, 3334565589, 2049984080, 2117393090, 224359410, 4143333089, + 29920363, 3675525811, 2314357996, 2934312505, 589492290, 890888488, 1586529790, + 1388279943, 267009447, 1011, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1029604602, + 1300955315, 1830990254, 1970899103, 1916726520, 3486386395, 1053589970, 2175764065, + 2379095055, 754945042, 531625132, 4014252696, 3829929750, 2474352983, 189287338, + 1723055400, 1365757113, 3099920986, 3432163066, 1799736303, 3216463597, 434456139, + 1543423970, 3109762217, 3137941216, 509720093, 1827337688, 720424945, 2707331184, + 816691531, 1092968965, 52892, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2083423482, + 2505415884, 3212144860, 710902257, 3320088221, 3147567479, 3091994784, 4201390360, + 4143787420, 1074850321, 2060158878, 3635644273, 4179910906, 2722055679, 2939194020, + 2466925622, 202688522, 372284267, 3277804064, 3401490865, 1773346774, 506145463, + 1388421892, 3493417405, 20233904, 3395525644, 4196390909, 1444793471, 3972498474, + 1443159463, 745252462, 2766966, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2645460218, + 2086822908, 2707204293, 1114209013, 825850397, 873488971, 2792723373, 36856421, + 3667233282, 1582246548, 651520439, 1928100686, 474104159, 2536694282, 2452371321, + 2833948873, 2693133614, 1565755751, 1806845266, 3169331798, 2417037154, 3779543345, + 1304854875, 3783283689, 507107699, 299935788, 4261007077, 4249521952, 797303627, + 2793347355, 243877443, 144748256, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 770606330, + 2264804, 329439611, 2332764211, 3451269867, 1927897295, 3114736445, 3016443594, + 1060032281, 82130215, 1783271780, 3996233488, 2763222839, 1195102010, 3973812197, + 3274054474, 1182302338, 1246796164, 1177746551, 227103909, 22573863, 514858324, + 1220606725, 1982433693, 3507357714, 1424442578, 2527561086, 2442928632, 1529612946, + 3953493533, 4285474694, 3277208968, 1, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1223591162, + 2982394804, 2895911336, 367523356, 2047351792, 371938123, 1378078781, 873612867, + 3202209579, 1262032369, 714144373, 3437733071, 602639567, 3984064747, 612978490, + 3471864183, 674481286, 1394271724, 1763826443, 2990163968, 3301989925, 614316342, + 37291547, 1513490987, 4181721666, 449071607, 3349264364, 4080788191, 992432549, + 110857720, 2412516358, 982433320, 92, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 300844282, + 2307493123, 2055145423, 1171144040, 2971339012, 869159436, 4016520557, 2700899514, + 1661166827, 1912303342, 936071126, 2875533686, 3868206806, 2282746627, 2149397043, + 4175901529, 1080663404, 1982433417, 1398961609, 2644130322, 1226711637, 2742866472, + 245919861, 1192438573, 796345551, 1845430816, 4013717915, 1845523693, 219966706, + 15237374, 1933961567, 2987494186, 4824, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1643021562, + 1920281417, 1446539515, 672005290, 1339606105, 1060185642, 2928538624, 1412353665, + 502198486, 1114659620, 2915704511, 2151278059, 2021357877, 3160496898, 2352263002, + 1249509765, 4146969416, 1655382877, 4172807454, 20332050, 3769739973, 2432429984, + 1945135808, 620977496, 3648234946, 1743994192, 2945771402, 2947990629, 3617215824, + 3994616034, 552805990, 3762013631, 252389, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 1978565882, + 2524960146, 4193922506, 4076590529, 2516223737, 391695821, 2190762613, 522673139, + 1987128608, 440254412, 2807372635, 359959156, 3737477879, 2747000479, 581553201, + 2511797953, 702434943, 2652697923, 3695193898, 3613772647, 1816774943, 2722879248, + 2302144282, 3030219558, 3287548153, 218079512, 3420566846, 1009458358, 935580230, + 1506862800, 4272011209, 2867187996, 13202995, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3320743162, + 948343474, 3405636809, 4014767551, 2612885443, 3972612845, 824281737, 4137837656, + 414658304, 1027983595, 1814268684, 1190496019, 2283765592, 2254837733, 1402998045, + 3229446146, 2921499078, 1872277523, 40810166, 1881592946, 1676856550, 4237240479, + 954097323, 3540364155, 2427558438, 4097986023, 1876897108, 2259500003, 410781233, + 47810089, 1460850268, 3796693287, 690671804, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 367953146, + 1947178011, 857303701, 4252140757, 2685473375, 170237623, 681228584, 2546374475, + 647727024, 574155167, 3835998138, 3156977392, 1377964963, 879242026, 2397433252, + 3151903944, 321716629, 2915001872, 2034067052, 4197479390, 239762274, 411187050, + 1913498586, 1999752094, 3974430733, 3337922549, 3113592958, 2973554659, 262245046, + 3390159210, 3748777389, 3567389094, 1770416790, 8, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 2515436794, + 1967278043, 2104607690, 2279407792, 4042270982, 2493824756, 2093025952, 3057235161, + 1947246360, 2051169369, 148967671, 2639851938, 447496030, 1169299488, 2272991543, + 2665184954, 859991147, 2025887591, 1226166406, 1287398010, 3430001396, 89366649, + 4067196564, 961555706, 1728104225, 3397127484, 4040801740, 2318882210, 3672174130, + 1476555222, 3748652491, 797956256, 236712014, 440, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 3412268069, 1023086602, 2026210508, 2948640365, 1990377411, 3418447020, 2628410977, + 1991879622, 3672905839, 1223731781, 2017025521, 2737855981, 2257171014, 890828011, + 2761812033, 1866749028, 3636324627, 776987488, 3662393815, 1860335431, 3546671068, + 559330021, 3174795765, 244385001, 399604355, 141731703, 1407315466, 3798132614, + 2901035943, 3891090695, 2549732229, 3844051172, 23019, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2287539796, 4275088352, 2524788671, 428301997, 3800466684, 3002290381, 589736055, + 1142130780, 373738384, 3768739486, 2628080437, 2388674081, 2423831981, 2386510872, + 3309501747, 3119841775, 3519820, 4127203326, 2410571999, 3790961085, 393878211, + 1807298400, 787387858, 3771609922, 2083085827, 3431083982, 1779441946, 1173778119, + 2530463497, 102816871, 3379839254, 3462765990, 1204200, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1761281781, 1175965788, 1954342487, 1084525753, 3257851189, 1650471046, 589323451, + 1708627683, 2913630328, 327508870, 65450921, 387930981, 868838718, 1131223535, + 2045168060, 2182484972, 1305171510, 3513059695, 2783764003, 1352841236, 4223790765, + 2355760065, 1705130248, 2408827385, 1032430657, 2597771791, 2207430797, 1314791031, + 2272867397, 3418340483, 4056746188, 2275032614, 62993239, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1809340725, 535748848, 2651279796, 2972702886, 3798816580, 1689558595, 2563380497, + 2676026999, 735820630, 506791325, 3000489346, 1796919308, 3526045192, 486243855, + 372489664, 1906228610, 3800714730, 1747845295, 2957993825, 668549798, 2621876870, + 407136765, 591566264, 2632073022, 1636313827, 891821689, 2281824154, 1923313633, + 958388250, 1285318798, 1658461377, 856491293, 3295251525, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2712792809, 3658590250, 1894120586, 698265268, 1162171068, 870321121, 3815695773, + 1680991420, 4253709444, 648283645, 1126625410, 3601123345, 2808686883, 1180925142, + 807407113, 560604794, 4237259425, 1207497088, 484611426, 2618666933, 3408262350, + 2884145631, 1261484694, 700732417, 2424618666, 267590588, 747041969, 769087774, + 4178986315, 138609815, 2888997761, 3066983590, 579742411, 40, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 651758713, 3906043406, 2548994640, 2228757432, 2121881214, 472348093, 878530644, + 971248989, 2081688077, 1949321364, 2647657092, 1107849908, 3882250508, 1952827114, + 859336559, 744223792, 3992135533, 68806754, 2609188276, 4080249809, 297055681, + 885074930, 2678708362, 3615821021, 2182633519, 760522724, 3081526858, 2240134876, + 3574753748, 1020578834, 4050681264, 124268560, 2177028858, 2099, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 587036233, 3067701272, 1697918617, 3416744348, 957762033, 2784479345, 3660801700, + 2341257627, 2870220782, 3127255235, 1850426667, 2590711144, 193256504, 3092252746, + 858791940, 3195757850, 2789572728, 2250403466, 4223626301, 883283817, 343136822, + 754616125, 4172822213, 377411409, 3910770529, 1514450968, 868753818, 333097290, + 2246022828, 2503075601, 462100262, 282524657, 2620007649, 109827, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1716843977, 2406245462, 4100164558, 730871533, 2076274051, 1849094521, 441146666, + 463301781, 1449951415, 2543043405, 2506092364, 2186797030, 4026578175, 3937378246, + 698864271, 604408052, 870457054, 3903374973, 1022854923, 4281001692, 279067829, + 3748974629, 1724584816, 814693658, 848422032, 2238904762, 3671458914, 4166573676, + 2052797806, 200140248, 1816868384, 1300920824, 3180657116, 5745209, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1266702857, 1726163715, 1868411348, 3369952439, 3242131435, 1534222815, 2135626846, + 3416782314, 2087636967, 1971732998, 3841794106, 322082412, 2445768432, 604649625, + 1736128197, 2376764098, 2774390933, 1146681592, 677359293, 1813892062, 968415609, + 1576242251, 3798010013, 3827678188, 902520545, 2549131665, 3909779088, 1506236052, + 2938795369, 485390229, 2269504120, 2792821107, 3274905154, 300538757, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2249628937, 685687495, 2193556864, 849482254, 1892955482, 2678790792, 3215816948, + 3418405555, 4263405829, 2982365918, 3251176925, 1827020223, 3699084302, 4097605129, + 1965684458, 4076326152, 1772127055, 3933922126, 479474035, 1840427833, 3012899581, + 3831069482, 3298156499, 4038576278, 2408364303, 4056410551, 2104476906, 193784559, + 3125211841, 1505789080, 2696864045, 827610944, 2518243838, 2836651081, 3, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 4184643081, 1410986433, 1843600019, 2400675340, 929523537, 2459688753, 947275637, + 3237179039, 4130345007, 3653734165, 2190755476, 4082624876, 758859577, 215491460, + 3989305297, 2894966395, 1376384020, 4106882930, 292746535, 2711172166, 2768244740, + 146454730, 2700634288, 2811878475, 406039235, 3376346153, 2204857609, 1752048742, + 2908338330, 2827562179, 2659032057, 2206054502, 795376852, 2075947017, 191, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1298712073, 2201636989, 3613796443, 3750533912, 813399020, 2846080568, 232014587, + 2397968910, 1395889404, 2894036997, 2880261938, 2445503214, 1309873853, 705309998, + 3769805381, 3352712840, 1793952063, 2039983215, 4181620852, 3563741840, 3416348297, + 3870361186, 3297272487, 740951942, 3216429413, 493979526, 3749505290, 2611250942, + 2114114653, 832062692, 3217332227, 580286736, 2603067450, 3236710367, 10016, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 3390642697, 3721599337, 871664873, 2911612331, 1048817226, 2119214535, 3867031815, + 713033506, 1637429796, 146757919, 604782956, 3611789462, 1105349601, 8293620, + 1235958732, 4107004201, 22662698, 4227745661, 615330606, 3555602145, 2758006042, + 1871034164, 1089875898, 3010016080, 3984027314, 1148724871, 2505171071, 2916506843, + 2063038348, 1635052446, 1554832576, 3305290580, 1432708148, 3316934573, 523990, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 757328393, 3614386048, 612921981, 3671275341, 229647166, 922873307, 4264649519, + 1719555435, 2266736286, 1533889711, 2441905738, 280079152, 828611854, 3201843831, + 3602591324, 280090900, 1469826922, 1875299088, 3069200698, 3724633479, 1990993253, + 4183411048, 2801573192, 3331946271, 3218951071, 3335901655, 2490262384, 3495191922, + 3995041812, 142479205, 912525419, 1349100142, 3879969713, 1156441875, 27410757, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2670148105, 4062893077, 1020178806, 3590763106, 340483064, 735422676, 2016484003, + 123165102, 335678566, 2404537076, 3105061293, 262388713, 346139379, 4279497998, + 4229372948, 1689322293, 2741425721, 679181659, 3127270803, 4076916912, 754865685, + 3412746642, 1580171686, 983324276, 1665980275, 3530216836, 4061821633, 665902558, + 4257628544, 1810364691, 4287621734, 682959171, 1988619710, 3911294335, 1433901286, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 3859134985, 335331068, 49729055, 2180659011, 2028716409, 698050378, 1054422637, + 118763064, 3248361728, 4170950490, 1271907245, 2427996554, 3295652926, 3972266060, + 417838330, 3974975679, 196215687, 1069744590, 3392617455, 3081697941, 2439553383, + 3952135723, 1301176694, 3409091030, 1929659121, 429183607, 3113036387, 2212349291, + 2869725706, 2012444045, 9727018, 4144199701, 3596901873, 584228079, 1995418814, 17, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1944320521, 1623163986, 628812511, 3164503401, 3895298003, 1054018629, 340174001, + 3153318879, 3928889269, 885285768, 3997011857, 1566203560, 1265094257, 2802058726, + 1331675844, 2827468672, 3060230221, 948837327, 1325629880, 2200958864, 1888334393, + 625826929, 2768815320, 2019796144, 3120134802, 1259302831, 758014572, 4152415859, + 4121411433, 4104878577, 4050987119, 4003867353, 365502017, 3906849072, 2599511236, 913, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2855991817, 3606270125, 3181925397, 1257739756, 1915489712, 4000844533, 2935459934, + 3199937223, 585493537, 2183742943, 4127321345, 2262180642, 3815400108, 2289695961, + 2815189647, 4136674591, 4257830634, 1089901505, 2531093721, 2877560724, 3445987330, + 1090531247, 2485278853, 443621013, 801806268, 1637665528, 206526733, 142888530, + 215304120, 562513453, 1670416775, 769471011, 2404374923, 1720644795, 2109662988, 47792, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2232154633, 1896409237, 2365622837, 3265658089, 2304950344, 629781221, 2808696483, + 3607626459, 1613690854, 3240893670, 2034809217, 3701333687, 4019267560, 2934788607, + 1000006471, 841934072, 3804698306, 1028929358, 37860002, 2667482229, 2989744713, + 1734489179, 3783538269, 3966691719, 1370314139, 3520981926, 4001410774, 3344358977, + 422292633, 3797056842, 4224698104, 2178099822, 2363633118, 840586894, 2027155544, + 2500125, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 19659273, 3220260405, 219408304, 2653515606, 2382656772, 1175565998, 4236323019, + 945581179, 2684487750, 2728069737, 1760710078, 3383293733, 225823767, 2592633266, + 2575153034, 1843036524, 2188424523, 2242423936, 1053404805, 1099379566, 3683175185, + 2856152948, 560129007, 2365107635, 4126904878, 2478475286, 3985790097, 2156013108, + 4114005084, 3855494087, 1718783362, 2995569012, 3215050506, 3538134199, 1735144505, + 130787134, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2854222345, 327624114, 3930972968, 3338609940, 4160100491, 1369247557, 336580958, + 3829711641, 2260926043, 3169241416, 1890583300, 1600428045, 2513137876, 2569532300, + 1153079777, 2076762262, 1991046151, 2097142034, 3767179424, 2482412768, 1617290924, + 1831653476, 2536667053, 2459756649, 3342948645, 1411557415, 1984219451, 1838558342, + 2101966663, 4061975165, 3145853166, 3246339919, 1163935729, 1811615434, 902266163, + 2546816526, 1, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 842005001, 355176350, 2114974833, 494686742, 2433365255, 3877574949, 2315539152, + 876546272, 600231499, 611489188, 1562884203, 1619233555, 238670040, 278329366, + 253541034, 2893307126, 3022623747, 3858635470, 3361253054, 1047785714, 760632267, + 3451717120, 2363692180, 1104748202, 887485704, 4216544507, 666010480, 340335913, + 2196648833, 1519847578, 3079887219, 2197423964, 2161700369, 828284167, 2924785644, + 1428498487, 83, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 654309897, 1694660860, 53644162, 719830500, 1346144126, 673702875, 1861379378, + 1248111153, 88483473, 1214871104, 760018784, 748015845, 3920112330, 1451106470, + 2416521600, 139673472, 1631116243, 3635739447, 2140639545, 747719032, 4103149476, + 2448347463, 1918631311, 1045764051, 3937222717, 3527506827, 3791575540, 1130257547, + 1111955208, 3781906601, 1655764382, 546384510, 540931477, 692596877, 4183948736, + 1491597320, 4359, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2222979593, 4061094889, 953466330, 4164056915, 2681813943, 3111113686, 3324352345, + 1740781125, 871329561, 3511837688, 2785070824, 2608723999, 167310482, 3357531027, + 3336475205, 1743224291, 1506125517, 1464703126, 3570815192, 1479890140, 1467987273, + 2535695435, 4011119207, 34118114, 2909809094, 2708212011, 3619957842, 3819607067, + 551243015, 4243601098, 2181644241, 4199986351, 3438670995, 1047519114, 3252714832, + 2109756729, 228049, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1019214345, 3495330614, 3276646235, 2917694430, 202635504, 3127232740, 3096620825, + 2422150918, 2598217057, 2312710030, 1794242995, 3114292296, 4187839285, 2679606467, + 2363551913, 2720666805, 2294334399, 3171297205, 2597289631, 4007201490, 241513290, + 487844326, 159657130, 2759576089, 3694698832, 1216640832, 4275866767, 2977309375, + 1545701229, 3060587272, 1164198153, 2925687791, 153485005, 3909822127, 756091994, + 2803779685, 11929927, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 2814376457, 2640877314, 546262917, 2038320337, 4134503216, 6312622, 3742317570, + 2818695184, 1883758307, 1936304904, 940198419, 2574351302, 4202998030, 2096070289, + 1919640665, 3365064531, 2402066125, 3392759355, 808613323, 3281727309, 4218506968, + 1288863240, 2317106034, 1550598984, 3398356838, 2885063453, 967415299, 1655572686, + 311025610, 1003926595, 2301518401, 335703777, 1059867251, 2301363583, 1843601789, + 1172249251, 624090442, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 3502242313, 1804007453, 1735714352, 613223925, 1030198935, 952721098, 2082843195, + 2734934294, 3842351295, 831495393, 1110138176, 466303268, 3623141358, 1647759366, + 1128907386, 3234636962, 3317881823, 661273616, 2610082853, 2328028526, 481010401, + 3758025887, 3642284429, 2122902860, 1662465428, 3875062690, 2381397737, 1723696361, + 1080501446, 2606552252, 1487821436, 58750251, 3599280126, 2261211288, 4119403670, + 2613793840, 2583351593, 7, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 3770677769, 4049516327, 1450027359, 4002593979, 3413012441, 1624438220, 898545018, + 2498016501, 4137840039, 1065606392, 1492184036, 3226447526, 944013522, 3559656857, + 1179750270, 2389232650, 3467392352, 2728744532, 1465630338, 1488576798, 54611393, + 3518420558, 2772448600, 1309221855, 2829832393, 1606212912, 2057808156, 2116635731, + 3963120029, 2007695148, 1941388729, 1486623309, 1416380476, 510649109, 571816553, + 2275852204, 2826859509, 397, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 1958738441, 4284300983, 3080186296, 2744787675, 1702526358, 374933851, 2837853614, + 1077815519, 2215259023, 145245666, 549730322, 1889984953, 2172093180, 1553664732, + 1369083690, 2494515454, 3176957800, 2566989312, 2807639888, 3857504457, 3760142116, + 1535230380, 2027375774, 999441160, 2895617276, 2294641768, 3499880006, 3499376604, + 3713739526, 970936659, 4088926661, 3785432868, 3449522887, 1980098243, 2319665041, + 2860149894, 3536548067, 20802, + ]), + BigUint::new(vec![ + 375288404, 2460223985, 1334358771, 2543621408, 2519466280, 1133682239, 3589178618, + 3300915721, 115578348, 1790222711, 1858434925, 4115884344, 1661122603, 3566611261, + 540903953, 3108944241, 3649809669, 1068275229, 486997643, 886790309, 1831002139, + 2594785146, 23978661, 576512999, 3811470156, 3874159424, 2484542501, 3745592469, + 329497784, 2078580168, 2837461864, 3992415299, 91937857, 415278725, 1129439873, + 2753976754, 3981038028, 4127738104, 4170126886, 3452333906, 3051709874, 3061052331, + 1878810143, 2918565786, 1088266, + ]), + 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, + ]), + ][251 - dom.len().get() as usize] + .clone() + - BigUint::new(vec![ip_count(dom)]) + // #![feature(int_roundings)] + // use num_bigint::BigUint; + // use std::fs::File; + // use std::io::{BufWriter, Write}; + // fn main() { + // let mut writer = BufWriter::new( + // File::options() + // .read(false) + // .write(true) + // .create_new(true) + // .open("/home/zack/cards") + // .unwrap(), + // ); + // writer.write_all(b" [").unwrap(); + // let mut count = BigUint::new(Vec::new()); + // // We only count proper subdomains which means + // // the max length of a domain is 252. + // // We dont count proper subdomains of the root domain, + // // so the minimum length is 2. + // for i in 2usize..=252 { + // for j in i.div_ceil(64)..=i / 2 { + // count += a_restricted_comp(i, j); + // } + // writer + // .write_fmt(format_args!( + // "BigUint::new(vec!{:?}), ", + // count.to_u32_digits() + // )) + // .unwrap(); + // } + // writer + // .write_all(b"][251 - dom.len().get() as usize].clone()") + // .unwrap(); + // } + // fn poly_expand(left: &[BigUint], right: &[BigUint], n: usize) -> Vec<BigUint> { + // let mut prod = vec![BigUint::new(Vec::new()); usize::min(n + 1, left.len() + right.len() - 1)]; + // for i in 0..usize::min(left.len(), n + 1) { + // for j in 0..usize::min(right.len(), n + 1 - i) { + // prod[i + j] += &left[i] * &right[j]; + // } + // } + // prod + // } + // fn a_restricted_comp(n: usize, k: usize) -> BigUint { + // let init = { + // let start = usize::max(2, (n + 64).saturating_sub(64 * k)); + // let end = usize::min(64, n + 2 - (2 * k)); + // let mut tmp = Vec::with_capacity(end + 1); + // for _ in 0..start { + // tmp.push(BigUint::new(Vec::new())); + // } + // for _ in start..=end { + // tmp.push(BigUint::new(vec![1])); + // } + // tmp + // }; + // let mut two_power = 1; + // let mut two_powers = Vec::<(Vec<BigUint>, usize)>::with_capacity(7); + // let mut k_copy = k; + // while k_copy > 0 { + // if (k_copy & two_power) == two_power { + // let (mut two_power_start, mut two) = two_powers + // .last() + // .map_or_else(|| (init.clone(), 1), |t| (t.0.clone(), t.1)); + // while two < two_power { + // two_power_start = + // poly_expand(two_power_start.as_slice(), two_power_start.as_slice(), n); + // two <<= 1; + // } + // two_powers.push((two_power_start, two_power)); + // k_copy -= two_power; + // } + // two_power <<= 1; + // } + // let mut iter = two_powers.into_iter(); + // let init = iter.next().unwrap().0; + // iter.fold(init, |prev, cur| { + // poly_expand(cur.0.as_slice(), prev.as_slice(), n) + // }) + // .into_iter() + // .skip(n) + // .next() + // .unwrap() + // * BigUint::new(vec![52]).pow((n - k) as u32) + // } +} diff --git a/src/file.rs b/src/file.rs @@ -0,0 +1,952 @@ +#![allow( + clippy::exhaustive_structs, + clippy::implicit_return, + clippy::into_iter_on_ref, + clippy::missing_trait_methods, + clippy::multiple_unsafe_ops_per_block, + clippy::question_mark_used, + clippy::ref_patterns, + clippy::single_char_lifetime_names, + clippy::wildcard_enum_match_arm +)] +extern crate alloc; +use crate::dom::{Adblock, DomainErr, DomainOnly, Hosts, ParsedDomain, RpzDomain, Value, Wildcard}; +use alloc::string::FromUtf8Error; +use core::borrow::Borrow; +use core::fmt::{self, Display, Formatter}; +use core::hash::Hash; +use core::ops::Deref; +use core::time::Duration; +use reqwest::Client; +use serde::de::{self, Deserialize, Deserializer, Unexpected, Visitor}; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::fs; +use std::io::{self, ErrorKind}; +use std::path::{Path, PathBuf}; +use superset_map::SupersetSet; +use tokio::task::{JoinError, JoinSet}; +use tokio::time::{self, error::Elapsed}; +use url::Url; +/// Wrapper around an absolute [`PathBuf`] to a directory or file depending on `IS_DIR`. +/// +/// Note that `IS_DIR` iff the wrapped `PathBuf` ends with `/`. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct AbsFilePath<const IS_DIR: bool> { + /// The file or directory. + path: PathBuf, +} +impl<const IS_DIR: bool> AbsFilePath<IS_DIR> { + /// Returns `true` iff `val` was appended to `self`. + #[inline] + pub fn append(&mut self, val: &str) -> bool { + let bytes = val.as_bytes(); + if IS_DIR { + self.path.as_mut_os_string().push(val); + bytes.last().map_or(true, |byt| { + if *byt != b'/' { + self.path.as_mut_os_string().push("/"); + } + true + }) + // When `!IS_DIR`, we have to verify `val` does not end + // with `/` as well as verify the last component is not + // `..` which is true iff `val == ".."` or the last 3 + // characters of `val` is `/..`. + } else if bytes.last().map_or(false, |byt| { + *byt == b'/' + || bytes + .get(bytes.len().wrapping_sub(2)..) + .map_or(false, |byts| { + byts == b".." + && bytes + .get(bytes.len().wrapping_sub(3)) + .map_or(true, |byt2| *byt2 == b'/') + }) + }) { + false + } else { + self.path.as_mut_os_string().push(val); + true + } + } + /// Returns `self` as a [`Path`] reference. + #[inline] + #[must_use] + pub fn as_path(&self) -> &Path { + self.path.as_path() + } + /// Returns an `AbsFilePath` iff `val` conforms to the following: + /// * `PathBuf::from(val).is_absolute()`. + /// * `PathBuf::from(val).as_bytes().last().unwrap() == b'/'` ⇒ `IS_DIR`. + /// * `PathBuf::from(val).file_name().is_none()` ⇒ `IS_DIR`. + /// + /// If `IS_DIR` and `PathBuf::from(val).as_bytes().last().unwrap() != b'/'`, `val` + /// will have `/` appended to it. + #[allow(clippy::option_if_let_else)] + #[inline] + #[must_use] + pub fn from_string(val: String) -> Option<Self> { + match val.as_bytes().last() { + Some(byt) => { + let last = *byt; + let mut path = PathBuf::from(val); + if path.is_absolute() { + if last == b'/' { + IS_DIR.then_some(Self { path }) + } else if IS_DIR { + path.as_mut_os_string().push("/"); + Some(Self { path }) + } else { + path.file_name().is_some().then_some(Self { path }) + } + } else { + None + } + } + None => None, + } + } +} +impl<const IS_DIR: bool> AsRef<Path> for AbsFilePath<IS_DIR> { + #[inline] + fn as_ref(&self) -> &Path { + self.as_path() + } +} +impl<const IS_DIR: bool> Borrow<Path> for AbsFilePath<IS_DIR> { + #[inline] + fn borrow(&self) -> &Path { + self.as_path() + } +} +impl<const IS_DIR: bool> Deref for AbsFilePath<IS_DIR> { + type Target = Path; + #[inline] + fn deref(&self) -> &Self::Target { + self.as_path() + } +} +impl<const IS_DIR: bool> From<AbsFilePath<IS_DIR>> for PathBuf { + #[inline] + fn from(value: AbsFilePath<IS_DIR>) -> Self { + value.path + } +} +impl<'de, const IS_DIR: bool> Deserialize<'de> for AbsFilePath<IS_DIR> { + #[allow(clippy::too_many_lines)] + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `AbsFilePath`. + struct FilePathVisitor<const IS_DIR: bool>; + impl<'de, const IS_DIR: bool> Visitor<'de> for FilePathVisitor<IS_DIR> { + type Value = AbsFilePath<IS_DIR>; + #[inline] + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("struct AbsFilePath") + } + #[allow(clippy::arithmetic_side_effects, clippy::min_ident_chars)] + #[inline] + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: de::Error, + { + v.as_bytes().last().map_or_else( + || { + Err(E::invalid_value( + Unexpected::Str(v), + &"an absolute file path", + )) + }, + |byt| { + if *byt == b'/' { + if IS_DIR { + let mut path = PathBuf::with_capacity(v.len()); + path.push(v); + if path.is_absolute() { + Ok(AbsFilePath { path }) + } else { + Err(E::invalid_value( + Unexpected::Str(v), + &"an absolute file path to a directory", + )) + } + } else { + Err(E::invalid_value( + Unexpected::Str(v), + &"an absolute file path to a file", + )) + } + } else if IS_DIR { + let mut path = PathBuf::with_capacity(v.len() + 1); + path.push(v); + path.as_mut_os_string().push("/"); + if path.is_absolute() { + Ok(AbsFilePath { path }) + } else { + Err(E::invalid_value( + Unexpected::Str(v), + &"an absolute file path to a directory", + )) + } + } else { + let mut path = PathBuf::with_capacity(v.len()); + path.push(v); + if path.is_absolute() && path.file_name().is_some() { + Ok(AbsFilePath { path }) + } else { + Err(E::invalid_value( + Unexpected::Str(v), + &"an absolute file path to a file", + )) + } + } + }, + ) + } + #[allow(clippy::min_ident_chars)] + #[inline] + fn visit_string<E>(self, v: String) -> Result<Self::Value, E> + where + E: de::Error, + { + match v.as_bytes().last() { + Some(byt) => { + if *byt == b'/' { + if IS_DIR { + let path = PathBuf::from(v); + if path.is_absolute() { + Ok(AbsFilePath { path }) + } else { + Err(E::invalid_value( + Unexpected::Str(path.to_string_lossy().as_ref()), + &"an absolute file path to a directory", + )) + } + } else { + Err(E::invalid_value( + Unexpected::Str(v.as_str()), + &"an absolute file path to a file", + )) + } + } else if IS_DIR { + let mut path = PathBuf::from(v); + path.as_mut_os_string().push("/"); + if path.is_absolute() { + Ok(AbsFilePath { path }) + } else { + Err(E::invalid_value( + Unexpected::Str(path.to_string_lossy().as_ref()), + &"an absolute file path to a directory", + )) + } + } else { + let path = PathBuf::from(v); + if path.is_absolute() && path.file_name().is_some() { + Ok(AbsFilePath { path }) + } else { + Err(E::invalid_value( + Unexpected::Str(path.to_string_lossy().as_ref()), + &"an absolute file path to a file", + )) + } + } + } + None => Err(E::invalid_value( + Unexpected::Str(v.as_str()), + &"an absolute file path", + )), + } + } + } + deserializer.deserialize_string(FilePathVisitor) + } +} +impl<const IS_DIR: bool> Display for AbsFilePath<IS_DIR> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(&self.path.to_string_lossy()) + } +} +/// Wrapper around an absolute HTTP(S) [`Url`]. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct HttpUrl { + /// The HTTP(S) `Url`. + url: Url, +} +impl HttpUrl { + /// Returns `self` as a [`Url`] reference. + #[inline] + #[must_use] + pub const fn as_url(&self) -> &Url { + &self.url + } + /// Returns an `HttpUrl` iff `url` has a host and HTTP(S) scheme. + #[inline] + #[must_use] + pub fn from_url(url: Url) -> Option<Self> { + (url.has_host() && (url.scheme() == "http" || url.scheme() == "https")) + .then_some(Self { url }) + } +} +impl AsRef<Url> for HttpUrl { + #[inline] + fn as_ref(&self) -> &Url { + self.as_url() + } +} +impl Borrow<Url> for HttpUrl { + #[inline] + fn borrow(&self) -> &Url { + self.as_url() + } +} +impl Deref for HttpUrl { + type Target = Url; + #[inline] + fn deref(&self) -> &Self::Target { + self.as_url() + } +} +impl Display for HttpUrl { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.url.fmt(f) + } +} +impl From<HttpUrl> for Url { + #[inline] + fn from(value: HttpUrl) -> Self { + value.url + } +} +impl<'de> Deserialize<'de> for HttpUrl { + #[inline] + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + /// `Visitor` for `HttpUrl`. + struct UrlVisitor; + impl<'d> Visitor<'d> for UrlVisitor { + type Value = HttpUrl; + #[inline] + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str("struct HttpUrl") + } + #[allow(clippy::min_ident_chars)] + #[inline] + fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> + where + E: de::Error, + { + Url::parse(v).map_or_else( + |_| { + Err(E::invalid_type( + Unexpected::Str(v), + &"an absolute URL with HTTP(S) scheme", + )) + }, + |url| { + if url.has_host() && (url.scheme() == "http" || url.scheme() == "https") { + Ok(HttpUrl { url }) + } else { + Err(E::invalid_value( + Unexpected::Other(v), + &"an absolute URL with HTTP(S) scheme", + )) + } + }, + ) + } + } + deserializer.deserialize_str(UrlVisitor) + } +} +/// Represents the kind of [`ParsedDomain`]s a [`File`] +/// contains. +#[allow(clippy::exhaustive_enums)] +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Kind { + /// [`Adblock`] domains. + Adblock, + /// [`DomainOnly`] domains. + DomainOnly, + /// [`Hosts`] domains. + Hosts, + /// [`Wildcard`] domains. + Wildcard, +} +impl Display for Kind { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Adblock => f.write_str("Adblock"), + Self::DomainOnly => f.write_str("Domain-only"), + Self::Hosts => f.write_str("Hosts"), + Self::Wildcard => f.write_str("Wildcard"), + } + } +} +/// The name where a [`File`] was sourced from. +#[allow(clippy::exhaustive_enums)] +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Name { + /// The `File` came from the contained `AbsFilePath`. + Path(AbsFilePath<false>), + /// The `File` came from the contained `HttpUrl`. + Url(HttpUrl), +} +impl Display for Name { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Path(ref path) => path.fmt(f), + Self::Url(ref url) => url.fmt(f), + } + } +} +/// 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. + pub name: Name, + /// The contents of the file. + pub data: String, +} +impl Display for File { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.name.fmt(f) + } +} +/// A summary of a [`File`] after it has been parsed +/// into [`Value`]s. +#[derive(Clone, Debug)] +pub struct Summary<'a, E: Eq + Hash> { + /// The `File` that was parsed. + pub file: &'a File, + /// The kind of file. + pub kind: Kind, + /// The quantity of domains parsed. + pub domain_count: usize, + /// The quantity of comments parsed. + pub comment_count: usize, + /// The quantity of blank lines parsed. + pub blank_count: usize, + /// Parsing errors and their counts. + pub errors: HashMap<E, usize>, +} +impl<E: Display + Eq + Hash> Display for Summary<'_, E> { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "({}) {} - domains parsed: {}, comments parsed: {}, blanks parsed: {}, parsing errors: {}", + self.kind, self.file, self.domain_count, self.comment_count, self.blank_count, self.errors.values().sum::<usize>() + ).and_then(|()| { + if self.errors.is_empty() { + Ok(()) + } else { + f.write_str(", errors: [").and_then(|()| { + self.errors.iter().try_fold((), |(), tup| { + write!(f, "{}: {}, ", tup.0, tup.1) + }) + }).and_then(|()| f.write_str("]")) + } + }) + } +} +impl<E: Eq + Hash> PartialEq<Summary<'_, E>> for Summary<'_, E> { + #[inline] + fn eq(&self, other: &Summary<'_, E>) -> bool { + self.file == other.file + && self.kind == other.kind + && self.domain_count == other.domain_count + && self.comment_count == other.comment_count + && self.blank_count == other.blank_count + && self.errors == other.errors + } +} +impl<E: Eq + Hash> Eq for Summary<'_, E> {} +/// Container of [`Adblock`], [`DomainOnly`], [`Hosts`], and [`Wildcard`] +/// files. +#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub struct Files { + /// [`Adblock`] files. + pub adblock: Vec<File>, + /// [`DomainOnly`] files. + pub domain: Vec<File>, + /// [`Hosts`] files. + pub hosts: Vec<File>, + /// [`Wildcard`] files. + pub wildcard: Vec<File>, +} +/// Helper trait to track what kind of file. +trait Helper { + /// Returns the `Kind`. + fn kind() -> Kind; +} +impl Helper for Adblock<'_> { + #[inline] + fn kind() -> Kind { + Kind::Adblock + } +} +impl Helper for DomainOnly<'_> { + #[inline] + fn kind() -> Kind { + Kind::DomainOnly + } +} +impl Helper for Hosts<'_> { + #[inline] + fn kind() -> Kind { + Kind::Hosts + } +} +impl Helper for Wildcard<'_> { + #[inline] + fn kind() -> Kind { + Kind::Wildcard + } +} +impl Files { + /// Attempts to convert an `ExternalFiles` into a `Files`. + /// + /// # Errors + /// + /// Returns `ExtFileErr` iff any of the files in `ExternalFiles` + /// errors when downloading them and transforming them into a [`String`]. + #[inline] + pub async fn from_external(ext: ExternalFiles) -> Result<Self, ExtFileErr> { + /// `await`s each `Result` in `set` propagating any errors; otherwise + /// the `String` is `push`ed into `files`. + /// + /// If an error occurs, then the function immediately returns. + async fn add_file( + files: &mut Vec<File>, + mut set: JoinSet<Result<Result<File, ExtFileErr>, Elapsed>>, + ) -> Result<(), ExtFileErr> { + while let Some(val) = set.join_next().await { + files.push(val???); + } + Ok(()) + } + let mut files = Self { + adblock: Vec::with_capacity(ext.adblock.len()), + domain: Vec::with_capacity(ext.domain.len()), + hosts: Vec::with_capacity(ext.hosts.len()), + wildcard: Vec::with_capacity(ext.wildcard.len()), + }; + add_file(&mut files.adblock, ext.adblock).await?; + add_file(&mut files.domain, ext.domain).await?; + add_file(&mut files.hosts, ext.hosts).await?; + add_file(&mut files.wildcard, ext.wildcard).await?; + Ok(files) + } + /// Creates an empty instance of `Files`. + #[inline] + #[must_use] + pub const fn new() -> Self { + Self { + adblock: Vec::new(), + domain: Vec::new(), + hosts: Vec::new(), + wildcard: Vec::new(), + } + } + /// Reads each [`String`] from each field and attempts + /// to transform each line into an `RpzDomain` before adding + /// the domain into `doms`. + /// + /// Returns a `Vec` containing `Summary` information for each + /// [`File`] that was parsed. + #[allow(clippy::arithmetic_side_effects)] + #[inline] + pub fn add_to_superset<'a: 'b, 'b>( + &'a self, + doms: &mut SupersetSet<RpzDomain<'b>>, + ) -> Vec<Summary<'a, DomainErr>> { + /// Iterates each `String` from `files` and transforms each line + /// into `T` before adding it as an `RpzDomain` into `doms`. + #[inline] + fn insert< + 'a, + 'b: 'a, + T: Into<RpzDomain<'a>> + ParsedDomain<'a, Error = DomainErr> + Helper, + >( + doms: &mut SupersetSet<RpzDomain<'a>>, + files: &'b [File], + summaries: &mut Vec<Summary<'b, DomainErr>>, + ) { + let kind = T::kind(); + files.into_iter().fold((), |(), file| { + let mut summary = Summary { + file, + kind, + domain_count: 0, + comment_count: 0, + blank_count: 0, + errors: HashMap::new(), + }; + file.data + .lines() + .fold((), |(), line| match T::parse_value(line) { + Ok(val) => match val { + Value::Domain(dom) => { + summary.domain_count = summary.domain_count.saturating_add(1); + doms.insert(dom.into()); + } + Value::Comment(_) => { + summary.comment_count = summary.comment_count.saturating_add(1); + } + Value::Blank => { + summary.blank_count = summary.blank_count.saturating_add(1); + } + }, + Err(err) => { + let count = summary.errors.entry(err).or_insert(0); + *count = count.saturating_add(1); + } + }); + summaries.push(summary); + }); + } + let mut summaries = Vec::with_capacity( + self.adblock.len() + self.domain.len() + self.hosts.len() + self.wildcard.len(), + ); + insert::<Adblock>(doms, self.adblock.as_slice(), &mut summaries); + insert::<DomainOnly>(doms, self.domain.as_slice(), &mut summaries); + insert::<Hosts>(doms, self.hosts.as_slice(), &mut summaries); + insert::<Wildcard>(doms, self.wildcard.as_slice(), &mut summaries); + summaries + } + /// Returns `true` iff there are no files. + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { + self.adblock.is_empty() + && self.domain.is_empty() + && self.hosts.is_empty() + && self.wildcard.is_empty() + } +} +/// Block and unblock [`Files`] stored on the local file system. +#[derive(Debug)] +pub struct LocalFiles { + /// Local file system files containing domains to not block. + pub unblock: Files, + /// Local file system files containing domains to block. + pub block: Files, +} +impl LocalFiles { + /// Reads all block and unblock files in the `adblock/`, `domain/`, `hosts/`, and `wildcard/` directories + /// under `dir/block/` and `dir/unblock/` respectively. + /// + /// # Errors + /// + /// Returns [`io::Error`] iff reading said files causes an error. Note that + /// it is _not_ an error if a directory does not exist. + #[inline] + pub fn from_path(dir: AbsFilePath<true>) -> Result<Option<Self>, io::Error> { + /// Checks if `path` exists. + #[inline] + fn exists<P: AsRef<Path>>(path: P) -> Result<bool, io::Error> { + fs::metadata(path).map_or_else( + |err| match err.kind() { + ErrorKind::NotFound => Ok(false), + _ => Err(err), + }, + |_| Ok(true), + ) + } + /// Reads all files stored in `adblock/`, `domain/`, `hosts/`, and `wildcard/` + /// directories under `dir/name/`. + #[inline] + fn get_files( + mut dir: PathBuf, + files: &mut Files, + name: &str, + ) -> Result<PathBuf, io::Error> { + /// Reads all files under `dir/name/`. + #[inline] + fn get_file( + files: &mut Vec<File>, + mut dir: PathBuf, + name: &str, + ) -> Result<PathBuf, io::Error> { + dir.push(name); + if exists(dir.as_path())? { + for entry in fs::read_dir(dir.as_path())? { + let file = entry?; + if !file.file_type()?.is_dir() { + let path = file.path(); + files.push(fs::read_to_string(path.as_path()).map(|data| File { + name: Name::Path(AbsFilePath::<false> { path }), + data, + })?); + } + } + } + dir.pop(); + Ok(dir) + } + dir.push(name); + if exists(dir.as_path())? { + get_file(&mut files.adblock, dir, "adblock/") + .and_then(|dir2| get_file(&mut files.domain, dir2, "domain/")) + .and_then(|dir2| get_file(&mut files.hosts, dir2, "hosts/")) + .and_then(|dir2| get_file(&mut files.wildcard, dir2, "wildcard/")) + .map(|mut dir2| { + dir2.pop(); + dir2 + }) + } else { + dir.pop(); + Ok(dir) + } + } + let mut unblock = Files::new(); + let mut block = Files::new(); + get_files(PathBuf::from(dir), &mut unblock, "unblock/").and_then(|dir2| { + get_files(dir2, &mut block, "block/").map(|_| { + if unblock.is_empty() && block.is_empty() { + None + } else { + Some(Self { unblock, block }) + } + }) + }) + } +} +/// Error returned when downloading text files from HTTP(S) servers. +#[allow(clippy::exhaustive_enums)] +#[derive(Debug)] +pub enum ExtFileErr { + /// Error when a task exceeds the specified timeout. + Timeout(Elapsed), + /// HTTP(S) error when attempting to download a file. + Http(reqwest::Error), + /// Error when a task fails to complete. + Join(JoinError), + /// Error when a file is not valid UTF-8. + InvalidUtf8(FromUtf8Error), +} +impl Display for ExtFileErr { + #[allow(clippy::min_ident_chars)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Timeout(ref err) => err.fmt(f), + Self::Http(ref err) => err.fmt(f), + Self::Join(ref err) => err.fmt(f), + Self::InvalidUtf8(ref err) => err.fmt(f), + } + } +} +impl From<Elapsed> for ExtFileErr { + #[inline] + fn from(value: Elapsed) -> Self { + Self::Timeout(value) + } +} +impl From<reqwest::Error> for ExtFileErr { + #[inline] + fn from(value: reqwest::Error) -> Self { + Self::Http(value) + } +} +impl From<JoinError> for ExtFileErr { + #[inline] + fn from(value: JoinError) -> Self { + Self::Join(value) + } +} +impl From<FromUtf8Error> for ExtFileErr { + #[inline] + fn from(value: FromUtf8Error) -> Self { + Self::InvalidUtf8(value) + } +} +impl Error for ExtFileErr {} +/// Tasks of downloaded files from HTTP(S) servers. +#[derive(Debug)] +pub struct ExternalFiles { + /// [`Adblock`]-based files. + pub adblock: JoinSet<Result<Result<File, ExtFileErr>, Elapsed>>, + /// [`DomainOnly`]-based files. + pub domain: JoinSet<Result<Result<File, ExtFileErr>, Elapsed>>, + /// [`Hosts`]-based files. + pub hosts: JoinSet<Result<Result<File, ExtFileErr>, Elapsed>>, + /// [`Wildcard`]-based files. + pub wildcard: JoinSet<Result<Result<File, ExtFileErr>, Elapsed>>, +} +impl ExternalFiles { + /// The maximum timeout for a task. + pub const MAX_TIMEOUT: Duration = Duration::from_secs(3600); + /// The minimum timeout for a task. + pub const MIN_TIMEOUT: Duration = Duration::from_secs(1); + /// Returns `ExternalFiles` containing files downloaded + /// from the `HashSet`s with a timeout of [`Self::MAX_TIMEOUT`] + /// for each task. + #[inline] + #[must_use] + pub fn new_with_urls( + client: &'static Client, + adblock_urls: HashSet<HttpUrl>, + domain_urls: HashSet<HttpUrl>, + hosts_urls: HashSet<HttpUrl>, + wildcard_urls: HashSet<HttpUrl>, + ) -> Self { + Self::new_with_urls_and_timeout( + client, + Self::MAX_TIMEOUT, + adblock_urls, + domain_urls, + hosts_urls, + wildcard_urls, + ) + } + /// Returns `ExternalFiles` containing files downloaded + /// from the `HashSet`s with a timeout for each task set to `timeout`. + /// + /// Note that when `timeout` is `<` [`Self::MIN_TIMEOUT`], it is set to + /// `MIN_TIMEOUT`; similarly when it is `>` [`Self::MAX_TIMEOUT`], it is set + /// to `MAX_TIMEOUT`. + #[inline] + #[must_use] + pub fn new_with_urls_and_timeout( + client: &'static Client, + timeout: Duration, + adblock_urls: HashSet<HttpUrl>, + domain_urls: HashSet<HttpUrl>, + hosts_urls: HashSet<HttpUrl>, + wildcard_urls: HashSet<HttpUrl>, + ) -> Self { + let mut val = Self::new(); + val.add_adblock(client, timeout, adblock_urls); + val.add_domain(client, timeout, domain_urls); + val.add_hosts(client, timeout, hosts_urls); + val.add_wildcard(client, timeout, wildcard_urls); + val + } + /// Returns an empty `ExternalFiles`. + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + adblock: JoinSet::new(), + domain: JoinSet::new(), + hosts: JoinSet::new(), + wildcard: JoinSet::new(), + } + } + /// Downloads the [`Adblock`] files from `urls` and adds them to [`ExternalFiles::adblock`] + /// with a timeout for each task set to `timeout`. + /// + /// Note that when `timeout` is `<` [`Self::MIN_TIMEOUT`], it is set to + /// `MIN_TIMEOUT`; similarly when it is `>` [`Self::MAX_TIMEOUT`], it is set + /// to `MAX_TIMEOUT`. + #[inline] + pub fn add_adblock( + &mut self, + client: &'static Client, + timeout: Duration, + urls: HashSet<HttpUrl>, + ) { + Self::get_external_files(&mut self.adblock, client, timeout, urls); + } + /// Downloads the [`DomainOnly`] files from `urls` and adds them to [`ExternalFiles::domain`] + /// with a timeout for each task set to `timeout`. + /// + /// Note that when `timeout` is `<` [`Self::MIN_TIMEOUT`], it is set to + /// `MIN_TIMEOUT`; similarly when it is `>` [`Self::MAX_TIMEOUT`], it is set + /// to `MAX_TIMEOUT`. + #[inline] + pub fn add_domain( + &mut self, + client: &'static Client, + timeout: Duration, + urls: HashSet<HttpUrl>, + ) { + Self::get_external_files(&mut self.domain, client, timeout, urls); + } + /// Downloads the [`Hosts`] files from `urls` and adds them to [`ExternalFiles::hosts`] + /// with a timeout for each task set to `timeout`. + /// + /// Note that when `timeout` is `<` [`Self::MIN_TIMEOUT`], it is set to + /// `MIN_TIMEOUT`; similarly when it is `>` [`Self::MAX_TIMEOUT`], it is set + /// to `MAX_TIMEOUT`. + #[inline] + pub fn add_hosts( + &mut self, + client: &'static Client, + timeout: Duration, + urls: HashSet<HttpUrl>, + ) { + Self::get_external_files(&mut self.hosts, client, timeout, urls); + } + /// Downloads the [`Wildcard`] files from `urls` and adds them to [`ExternalFiles::wildcard`] + /// with a timeout for each task set to `timeout`. + /// + /// Note that when `timeout` is `<` [`Self::MIN_TIMEOUT`], it is set to + /// `MIN_TIMEOUT`; similarly when it is `>` [`Self::MAX_TIMEOUT`], it is set + /// to `MAX_TIMEOUT`. + #[inline] + pub fn add_wildcard( + &mut self, + client: &'static Client, + timeout: Duration, + urls: HashSet<HttpUrl>, + ) { + Self::get_external_files(&mut self.wildcard, client, timeout, urls); + } + /// Downloads the files from `urls` and converts them to `String`s adding the + /// tasks to `set`. + #[inline] + fn get_external_files( + set: &mut JoinSet<Result<Result<File, ExtFileErr>, Elapsed>>, + client: &'static Client, + mut timeout: Duration, + urls: HashSet<HttpUrl>, + ) { + timeout = timeout.clamp(Self::MIN_TIMEOUT, Self::MAX_TIMEOUT); + for url in Into::<HashSet<HttpUrl>>::into(urls) { + let url_clone = url.clone(); + set.spawn(time::timeout(timeout, async { + let resp = client + .get::<Url>(url_clone.into()) + .send() + .await? + .error_for_status()?; + resp.bytes().await.map_or_else( + |err| Err(ExtFileErr::Http(err)), + |bytes| { + String::from_utf8(bytes.into()) + .map_err(ExtFileErr::InvalidUtf8) + .map(|data| File { + name: Name::Url(url), + data, + }) + }, + ) + })); + } + } +} +impl Default for ExternalFiles { + #[inline] + fn default() -> Self { + Self::new() + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,45 @@ +//! # `rpz` +//! +//! The primary module is [`mod@dom`] which contains types which can parse +//! [`str`]s into different kinds of domains. +//! +//! [`mod@file`] contains types that can read UTF-8 files on the local file system +//! or hosted on HTTP(S) servers. +//! +//! The purpose of these types is to make fetching, parsing, and transforming +//! ad-blocking files into a [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) +//! file easier. +#![feature(addr_parse_ascii)] +#![feature(btree_cursors)] +#![feature(byte_slice_trim_ascii)] +#![feature(io_error_more)] +#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![deny( + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::complexity, + clippy::correctness, + clippy::nursery, + clippy::pedantic, + clippy::perf, + clippy::restriction, + clippy::style, + clippy::suspicious +)] +#![allow( + clippy::blanket_clippy_restriction_lints, + clippy::multiple_crate_versions, + clippy::single_call_fn +)] +/// Module for hostname-like domains including parsing [`str`]s +/// from a variety of formats. +pub mod dom; +/// Contains a single function, `proper_subdomain_count`, that has a lot of lines +/// that were pre-generated. +mod dom_count_auto_gen; +/// Module for fetching and parsing local and HTTP(S) files into +/// [`dom::RpzDomain`]s. +pub mod file; diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,546 @@ +//! # `rpz` +//! +//! Consult [`README.md`](https://crates.io/crates/rpz). +#![feature(never_type)] +#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![deny( + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::complexity, + clippy::correctness, + clippy::nursery, + clippy::pedantic, + clippy::perf, + clippy::restriction, + clippy::style, + clippy::suspicious +)] +#![allow( + clippy::blanket_clippy_restriction_lints, + clippy::implicit_return, + clippy::min_ident_chars, + clippy::missing_trait_methods, + clippy::multiple_crate_versions, + clippy::question_mark_used, + clippy::single_call_fn, + clippy::unseparated_literal_suffix +)] +/// Contains a wrapper of block and unblock `RpzDomain`s +/// which can be used to write to a `File` or `stdout`. +mod app; +/// Module for reading and parsing passed arguments. +mod args; +/// Module for the TOML config file. +mod config; +#[allow(clippy::doc_markdown)] +/// 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; +use crate::app::Domains; +use crate::args::{ArgsErr, ConfigPath, Opts}; +use crate::config::Config; +use core::fmt::{self, Display, Formatter}; +use core::time::Duration; +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +use priv_sep::UnveilErr; +use reqwest::Client; +use rpz::dom::DomainErr; +use rpz::file::{AbsFilePath, ExtFileErr, ExternalFiles, Files, HttpUrl, LocalFiles, Summary}; +use std::collections::HashSet; +use std::error::Error; +use std::fs; +use std::io::{self, Read, Write}; +use std::sync::OnceLock; +use tokio::runtime::Builder; +use toml::{self, de}; +/// The HTTP(S) client that is used to download all files. +/// It is initialized exactly once in `main` before being used. +static CLIENT: OnceLock<Client> = OnceLock::new(); +/// The output printed to `stdout` when `-h`/`--help` are passed +/// to the program. +const HELP: &str = "Response policy zone (RPZ) file generator + +Usage: rpz [OPTIONS] + +Options: + -V, --version Print version info and exit + -h, --help Print help + -q, --quiet Do not print messages + -v, --verbose Print summary parsing information about each file + -f, --file <CONFIG_FILE> Required option to pass '-' for stdin or the absolute path to the config file"; +/// The output printed to `stdout` when `-v`/`--version` are passed +/// to the program. +const VERSION: &str = concat!("rpz ", env!("CARGO_PKG_VERSION")); +/// The User-Agent header value sent to HTTP(S) servers. +const USER_AGENT: &str = concat!("rpz/", env!("CARGO_PKG_VERSION")); +/// Error returned from the program. +#[allow(clippy::exhaustive_enums)] +enum E { + /// Variant for errors due to incorrect arguments being passed. + 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(UnveilErr), + /// 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, +} +impl fmt::Debug for E { + #[allow(clippy::ref_patterns)] + #[inline] + 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), + } + } +} +impl Display for E { + #[allow(clippy::ref_patterns)] + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + 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"), + } + } +} +impl Error for E {} +impl From<ArgsErr> for E { + #[inline] + fn from(value: ArgsErr) -> Self { + Self::Args(value) + } +} +impl From<de::Error> for E { + #[inline] + fn from(value: de::Error) -> Self { + Self::Config(value) + } +} +impl From<io::Error> for E { + #[inline] + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} +impl From<ExtFileErr> for E { + #[inline] + fn from(value: ExtFileErr) -> Self { + Self::ExtFile(value) + } +} +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +impl From<UnveilErr> for E { + #[inline] + fn from(value: UnveilErr) -> Self { + Self::Unveil(value) + } +} +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +impl From<!> for E { + #[inline] + fn from(value: !) -> Self { + value + } +} +/// Reads `Config` from `conf`. +#[inline] +fn get_config(conf: ConfigPath) -> Result<Config, E> { + toml::from_str::<Config>( + match conf { + ConfigPath::Stdin => { + let mut file = String::new(); + io::stdin().lock().read_to_string(&mut file)?; + file + } + ConfigPath::Path(path) => { + priv_sep::unveil_read_file(path.as_path())?; + let file = fs::read_to_string(path.as_path())?; + priv_sep::unveil_none(path)?; + file + } + } + .as_str(), + ) + .map_err(E::Config) +} +/// Gets `LocalFiles` from `local_dir`. +#[inline] +fn get_local_files(local_dir: Option<AbsFilePath<true>>) -> Result<Option<LocalFiles>, E> { + local_dir.map_or_else( + || Ok(None), + |dir| { + priv_sep::unveil_read_dir(dir.as_path()) + .map_err(E::from) + .and_then(|exists| { + if exists { + LocalFiles::from_path(dir.clone()) + .map_err(E::Io) + .and_then(|files| { + priv_sep::unveil_none(dir).map_err(E::from).map(|()| files) + }) + } else { + Ok(None) + } + }) + }, + ) +} +/// Downloads block files from HTTP(S) servers. +#[allow(clippy::unwrap_used)] +#[inline] +fn get_external_files( + timeout: Duration, + adblock: HashSet<HttpUrl>, + domain: HashSet<HttpUrl>, + hosts: HashSet<HttpUrl>, + wildcard: HashSet<HttpUrl>, +) -> Result<Files, E> { + Builder::new_current_thread() + .enable_all() + .build() + .map_or_else( + |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(); + let client = CLIENT.get().unwrap(); + files.add_adblock(client, timeout, adblock); + files.add_domain(client, timeout, domain); + files.add_hosts(client, timeout, hosts); + files.add_wildcard(client, timeout, wildcard); + files + }) + .await + .map_err(E::ExtFile) + }) + }, + ) +} +/// Verbosity of what is written to `stdout`. +#[derive(Clone, Copy)] +enum Verbosity { + /// Suppress all summary info. + None, + /// Write normal amount of info. + Normal, + /// Write verbose information. + High, +} +/// Writes to `stdout` the summary information in the event the quiet +/// option was not passed. +#[inline] +fn write_summary( + summaries: Vec<Summary<'_, DomainErr>>, + verbose: bool, + unblock_count: usize, + block_count: usize, +) -> Result<(), io::Error> { + let mut stdout = io::stdout().lock(); + let mut domain_count = 0usize; + let mut comment_count = 0usize; + let mut blank_count = 0usize; + let mut error_count = 0usize; + if verbose { + summaries.into_iter().try_fold((), |(), summary| { + domain_count = domain_count.saturating_add(summary.domain_count); + comment_count = comment_count.saturating_add(summary.comment_count); + blank_count = blank_count.saturating_add(summary.blank_count); + error_count = error_count.saturating_add(summary.errors.values().sum()); + writeln!(&mut stdout, "{summary}") + })?; + } else { + summaries.into_iter().fold((), |(), summary| { + domain_count = domain_count.saturating_add(summary.domain_count); + comment_count = comment_count.saturating_add(summary.comment_count); + blank_count = blank_count.saturating_add(summary.blank_count); + error_count = error_count.saturating_add(summary.errors.values().sum()); + }); + } + writeln!( + stdout, + "unblock count written: {}\nblock count written: {}\ntotal lines written: {}\ndomains parsed: {}\ncomments parsed: {}\nblanks parsed: {}\nparsing errors: {}", + unblock_count, + block_count, + unblock_count.saturating_add(block_count), + domain_count, + comment_count, + blank_count, + error_count, + ) +} +#[allow(clippy::arithmetic_side_effects, clippy::unwrap_used)] +fn main() -> Result<(), E> { + let mut promises = priv_sep::pledge_init()?; + priv_sep::veil_all()?; + let (conf, verbosity) = match Opts::from_args()? { + Opts::Help => return writeln!(io::stdout().lock(), "{HELP}").map_err(E::Io), + Opts::Version => return writeln!(io::stdout().lock(), "{VERSION}").map_err(E::Io), + Opts::Config(path) => (path, Verbosity::Normal), + Opts::ConfigQuiet(path) => (path, Verbosity::None), + Opts::ConfigVerbose(path) => (path, Verbosity::High), + Opts::Verbose | Opts::Quiet => return Err(E::Args(ArgsErr::ConfigPathNotPassed)), + Opts::None => return Err(E::Args(ArgsErr::NoArgs)), + }; + let config = get_config(conf)?; + // We use a temp file to write to that way we can avoid overwriting + // a file just for the process to error. Once the temp file is written to, + // we rename it to the desired name. + let tmp_rpz = config.rpz.as_ref().map_or_else( + || { + priv_sep::pledge_away_create_write(&mut promises) + .map_err(E::from) + .map(|()| None) + }, + |file| { + priv_sep::unveil_create(file.as_path()) + .and_then(|()| { + let mut rpz = file.clone(); + rpz.append("tmp"); + priv_sep::unveil_create_read_write(rpz.as_path()).map(|()| Some(rpz)) + }) + .map_err(E::from) + }, + )?; + let local_files = get_local_files(config.local_dir)?; + let (mut domains, mut summaries) = if let Some(files) = local_files.as_ref() { + Domains::new_with(files) + } else { + ( + Domains::new(), + Vec::with_capacity( + config.adblock.len() + + config.domain.len() + + config.hosts.len() + + config.wildcard.len(), + ), + ) + }; + let ext_files = if config.adblock.is_empty() + && config.domain.is_empty() + && config.hosts.is_empty() + && config.wildcard.is_empty() + { + if domains.block().is_empty() { + return Err(E::NoBlockEntries); + } + priv_sep::pledge_away_net(&mut promises) + .and_then(|()| priv_sep::pledge_away_unveil(&mut promises).map(|()| Files::new())) + .map_err(E::from) + } else { + priv_sep::unveil_https().map_err(E::from).and_then(|()| { + priv_sep::pledge_away_unveil(&mut promises) + .map_err(E::from) + .and_then(|()| { + get_external_files( + config.timeout.unwrap_or(ExternalFiles::MAX_TIMEOUT), + config.adblock, + config.domain, + config.hosts, + config.wildcard, + ) + .and_then(|files| { + priv_sep::pledge_away_net(&mut promises) + .map_err(E::from) + .map(|()| files) + }) + }) + }) + }?; + domains.add_block_files(&ext_files, &mut summaries); + if domains.block().is_empty() { + return Err(E::NoBlockEntries); + } + let (unblock_count, block_count) = + domains.write(config.rpz.map(|file| (file, tmp_rpz.unwrap())))?; + if matches!(verbosity, Verbosity::None) { + Ok(()) + } else { + priv_sep::pledge_away_all_but_stdio(&mut promises)?; + write_summary( + summaries, + matches!(verbosity, Verbosity::High), + unblock_count, + block_count, + ) + .map_err(E::Io) + } +} +#[cfg(test)] +pub(crate) mod test_prog { + use std::fs; + use std::process::Command; + /// The path to the program dir with no block subdirectories. + pub const PROG_DIR_NO_SUB: &str = "/home/zack/projects/rpz/target/"; + /// The path to the program dir with block subdirectories. + pub 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 const ERR_MSG: &str = " does not exist, so program testing cannot occur"; + /// Verify the correct directories and files exist. + pub fn verify_files() { + if !fs::metadata(PROG) + .expect(format!("{PROG}{ERR_MSG}").as_str()) + .is_file() + { + panic!("{PROG} is not an executable file") + } else if !fs::metadata(PROG_DIR) + .expect(format!("{PROG_DIR}{ERR_MSG}").as_str()) + .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()) + .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()) + .expect(format!("{files}{ERR_MSG}. it must only contain '||bar.com'").as_str()) + .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(), + ) + .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(), + ) + .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(), + ) + .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 fn get_command() -> Command { + Command::new(PROG) + } +} +#[cfg(test)] +mod tests { + use crate::{test_prog, E}; + use std::io::Write; + use std::process::Stdio; + use std::thread; + #[test] + #[ignore] + fn test_app() { + test_prog::verify_files(); + assert!(test_prog::get_command() + .args(["-f", "-"]) + .stderr(Stdio::piped()) + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .spawn() + .map_or(false, |mut cmd| { + cmd.stdin.take().map_or(false, |mut stdin| { + thread::spawn(move || { + stdin + .write_all( + format!("local_dir=\"{}\"", test_prog::PROG_DIR_NO_SUB).as_bytes(), + ) + .map_or(false, |_| true) + }) + .join() + .map_or(false, |v| v) + }) && cmd.wait_with_output().map_or(false, |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() + .map_or(false, |mut cmd| { + cmd.stdin.take().map_or(false, |mut stdin| { + thread::spawn(move || { + stdin + .write_all(format!("local_dir=\"{}\"", test_prog::PROG_DIR).as_bytes()) + .map_or(false, |_| true) + }) + .join() + .map_or(false, |v| v) + }) && cmd.wait_with_output().map_or(false, |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 @@ -0,0 +1,220 @@ +#![allow(clippy::implicit_return, clippy::pub_use, clippy::ref_patterns)] +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +pub use priv_sep::UnveilErr; +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +use priv_sep::{self, Permissions, Promise, Promises}; +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +use std::env; +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +use std::fs; +use std::io::{Error, ErrorKind}; +use std::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 struct Zst; +/// Calls `pledge` with only the sys calls necessary for a minimal application +/// to run. Specifically, the `Promise`s `Cpath`, `Inet`, `Rpath`, `Stdio`, `Unveil`, and `Wpath` +/// are passed. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn pledge_init() -> Result<Promises<6>, Error> { + let promises = Promises::new([ + Promise::Cpath, + Promise::Inet, + Promise::Rpath, + Promise::Stdio, + Promise::Unveil, + Promise::Wpath, + ]); + match promises.pledge() { + Ok(()) => Ok(promises), + Err(e) => Err(e), + } +} +/// No-op that always returns `Ok`. +#[allow(clippy::unnecessary_wraps)] +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[inline] +pub const fn pledge_init() -> Result<Zst, !> { + Ok(Zst) +} +/// Removes `Cpath` and `Wpath` `Promise`s. +/// +/// This should only be called when `stdout` is written to +/// instead of an RPZ file. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn pledge_away_create_write(promises: &mut Promises<6>) -> Result<(), Error> { + promises.remove_promises([Promise::Cpath, Promise::Wpath]); + promises.pledge() +} +/// No-op that always returns `Ok`. +#[allow(clippy::unnecessary_wraps)] +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[inline] +pub fn pledge_away_create_write(_: &mut Zst) -> Result<(), !> { + Ok(()) +} +/// Removes `Promise::Unveil`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn pledge_away_unveil(promises: &mut Promises<6>) -> Result<(), Error> { + promises.remove(Promise::Unveil); + promises.pledge() +} +/// No-op that always returns `Ok`. +#[allow(clippy::unnecessary_wraps)] +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[inline] +pub fn pledge_away_unveil(_: &mut Zst) -> Result<(), !> { + Ok(()) +} +/// Removes `Promise::Inet`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn pledge_away_net(promises: &mut Promises<6>) -> Result<(), Error> { + promises.remove(Promise::Inet); + promises.pledge() +} +/// No-op that always returns `Ok`. +#[allow(clippy::unnecessary_wraps)] +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[inline] +pub fn pledge_away_net(_: &mut Zst) -> Result<(), !> { + Ok(()) +} +/// Removes all `Promise`s except `Stdio`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn pledge_away_all_but_stdio(promises: &mut Promises<6>) -> Result<(), Error> { + promises.retain([Promise::Stdio]); + promises.pledge() +} +/// No-op that always returns `Ok`. +#[allow(clippy::unnecessary_wraps)] +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[inline] +pub 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"))] +#[inline] +pub fn unveil_none<P: AsRef<Path>>(path: P) -> Result<(), UnveilErr> { + Permissions::NONE.unveil(path) +} +/// No-op that always returns `Ok`. +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[allow(clippy::unnecessary_wraps)] +#[inline] +pub fn unveil_none<P: AsRef<Path>>(_: P) -> Result<(), !> { + Ok(()) +} +/// Calls `unveil_none` on `/`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn veil_all() -> Result<(), UnveilErr> { + unveil_none("/") +} +/// No-op that always returns `Ok`. +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[allow(clippy::unnecessary_wraps)] +#[inline] +pub const fn veil_all() -> Result<(), !> { + Ok(()) +} +/// Calls `unveil`_on `path` with `Permissions::CREATE`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn unveil_create<P: AsRef<Path>>(path: P) -> Result<(), UnveilErr> { + Permissions::CREATE.unveil(path) +} +/// No-op that always returns `Ok`. +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[allow(clippy::unnecessary_wraps)] +#[inline] +pub fn unveil_create<P: AsRef<Path>>(_: P) -> Result<(), !> { + Ok(()) +} +/// Calls `unveil`_on `path` with `Permissions::READ` and returns +/// `true` iff `path` exists. +#[allow(clippy::wildcard_enum_match_arm)] +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn unveil_read_dir<P: AsRef<Path>>(path: P) -> Result<bool, UnveilErr> { + Permissions::READ.unveil(path).map_or_else( + |err| match err { + UnveilErr::Io(ref err2) => match err2.kind() { + ErrorKind::NotFound => Ok(false), + _ => Err(err), + }, + UnveilErr::Nul(_) => Err(err), + }, + |()| Ok(true), + ) +} +/// Returns `true` iff `path` exists +#[allow(clippy::wildcard_enum_match_arm)] +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[inline] +pub fn unveil_read_dir<P: AsRef<Path>>(path: P) -> Result<bool, Error> { + fs::metadata(path).map_or_else( + |err| match err.kind() { + ErrorKind::NotFound => Ok(false), + _ => Err(err), + }, + |dir| Ok(dir.is_dir()), + ) +} +/// Calls `unveil`_on `path` with `Permissions::READ`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn unveil_read_file<P: AsRef<Path>>(path: P) -> Result<(), UnveilErr> { + Permissions::READ.unveil(path) +} +/// No-op that always returns `Ok`. +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[allow(clippy::unnecessary_wraps)] +#[inline] +pub fn unveil_read_file<P: AsRef<Path>>(_: P) -> Result<(), !> { + Ok(()) +} +/// Calls `unveil` on all files necessary for HTTP(S) with `Permissions::READ` +/// in addition to setting the `SSL_CERT_FILE` variable to `/etc/ssl/cert.pem`. +/// We rely on `SSL_CERT_FILE` as that allows one to `unveil(2)` only `/etc/ssl/cert.pem` +/// instead of `/etc/ssl/`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn unveil_https() -> Result<(), UnveilErr> { + /// The path to the root certificate store. + const CERTS: &str = "/etc/ssl/cert.pem"; + unveil_read_file("/dev/urandom").and_then(|()| { + unveil_read_file("/etc/resolv.conf") + .and_then(|()| unveil_read_file(CERTS).map(|()| env::set_var("SSL_CERT_FILE", CERTS))) + }) +} +/// No-op that always returns `Ok`. +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[allow(clippy::unnecessary_wraps)] +#[inline] +pub const fn unveil_https() -> Result<(), !> { + Ok(()) +} +/// Calls `unveil`_on `path` with create, read, and write `Permissions`. +#[cfg(all(feature = "priv_sep", target_os = "openbsd"))] +#[inline] +pub fn unveil_create_read_write<P: AsRef<Path>>(path: P) -> Result<(), UnveilErr> { + let mut perms = Permissions::ALL; + perms.execute = false; + perms.unveil(path) +} +/// No-op that always returns `Ok`. +#[cfg(not(all(feature = "priv_sep", target_os = "openbsd")))] +#[allow(clippy::unnecessary_wraps)] +#[inline] +pub fn unveil_create_read_write<P: AsRef<Path>>(_: P) -> Result<(), !> { + Ok(()) +}