rpz

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

dom.rs (68143B)


      1 use crate::dom_count_auto_gen::proper_subdomain_count;
      2 use ascii_domain::{
      3     char_set::{ASCII_FIREFOX, AllowedAscii},
      4     dom::{Domain, DomainErr, DomainOrdering},
      5 };
      6 use core::{
      7     borrow::Borrow,
      8     cmp::Ordering,
      9     convert,
     10     fmt::{self, Display, Formatter},
     11     hash::{Hash, Hasher},
     12     num::NonZeroU8,
     13     ops::Deref,
     14     str,
     15 };
     16 use num_bigint::BigUint;
     17 use std::{
     18     error,
     19     io::{Error, Write},
     20 };
     21 use superset_map::SetOrd;
     22 use zfc::{BoundedCardinality, Cardinality, Set};
     23 /// Unit tests.
     24 #[cfg(test)]
     25 mod tests;
     26 /// One.
     27 const ONE: NonZeroU8 = NonZeroU8::new(1).unwrap();
     28 /// Error returned when an invalid string is passed to [`Adblock::parse_value`], [`DomainOnly::parse_value`],
     29 /// [`Hosts::parse_value`], [`Wildcard::parse_value`], or [`RpzDomain::parse_value`].
     30 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
     31 pub enum FirefoxDomainErr {
     32     /// The domain is invalid based on [`Domain`] using [`ASCII_FIREFOX`].
     33     InvalidDomain(DomainErr),
     34     /// The domain had a TLD that was not all letters nor length of at least five beginning with `b"xn--"`.
     35     InvalidTld,
     36     /// The string passed to [`Adblock::parse_value`] contained `$`.
     37     InvalidAdblockDomain,
     38     /// The string passed to [`Hosts::parse_value`] did not conform
     39     /// to the required [`Hosts`] format.
     40     InvalidHostsIP,
     41     /// The length of the non-wildcard portion of the string passed to
     42     /// [`Wildcard::parse_value`] was at least 252 which means there are
     43     /// no proper subdomains.
     44     InvalidWildcardDomain,
     45 }
     46 impl Display for FirefoxDomainErr {
     47     #[inline]
     48     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     49         match *self {
     50             Self::InvalidDomain(err) => err.fmt(f),
     51             Self::InvalidTld => f.write_str("domain had a TLD that was not all letters nor at least five characters long starting with 'xn--'"),
     52             Self::InvalidAdblockDomain => f.write_str("Adblock-style domain contained a '$'"),
     53             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"),
     54             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"),
     55         }
     56     }
     57 }
     58 impl error::Error for FirefoxDomainErr {}
     59 /// The ASCII we allow domains to have.
     60 const CHARS: &AllowedAscii<[u8; 78]> = &ASCII_FIREFOX;
     61 /// Parses a `[u8]` into a `Domain` using `CHARS` with the added restriction that the `Domain` has a TLD
     62 /// that is either all letters or has length of at least five and begins with `b"xn--"`.
     63 #[expect(clippy::indexing_slicing, reason = "we verify manually")]
     64 fn domain_icann_tld<'a: 'b, 'b>(val: &'a [u8]) -> Result<Domain<&'b str>, FirefoxDomainErr> {
     65     Domain::try_from_bytes(val, CHARS)
     66         .map_err(FirefoxDomainErr::InvalidDomain)
     67         .and_then(|dom| {
     68             let tld = dom.tld();
     69             // `tld.as_bytes()[..4]` won't panic since we check before that that the length is at least 5.
     70             if tld.is_alphabetic() || (tld.len().get() > 4 && tld.as_bytes()[..4] == *b"xn--") {
     71                 Ok(dom.into())
     72             } else {
     73                 Err(FirefoxDomainErr::InvalidTld)
     74             }
     75         })
     76 }
     77 /// Action taken by a DNS server when a domain matches.
     78 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     79 pub enum RpzAction {
     80     /// Send `NXDOMAIN` reply.
     81     Nxdomain,
     82     /// Send `NODATA` reply.
     83     Nodata,
     84     /// Do nothing; continue as normal.
     85     Passthru,
     86     /// Drop the query.
     87     Drop,
     88     /// Answer over TCP.
     89     TcpOnly,
     90 }
     91 impl Display for RpzAction {
     92     #[inline]
     93     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     94         match *self {
     95             Self::Nxdomain => f.write_str("NXDOMAIN"),
     96             Self::Nodata => f.write_str("NODATA"),
     97             Self::Passthru => f.write_str("PASSTHRU"),
     98             Self::Drop => f.write_str("DROP"),
     99             Self::TcpOnly => f.write_str("TCP-Only"),
    100         }
    101     }
    102 }
    103 impl PartialEq<&Self> for RpzAction {
    104     #[inline]
    105     fn eq(&self, other: &&Self) -> bool {
    106         *self == **other
    107     }
    108 }
    109 impl PartialEq<RpzAction> for &RpzAction {
    110     #[inline]
    111     fn eq(&self, other: &RpzAction) -> bool {
    112         **self == *other
    113     }
    114 }
    115 /// Writes the following line with `writer` based on `action`:
    116 /// * `RpzAction::Nxdomain`: `<dom> CNAME .`.
    117 /// * `RpzAction::Nodata`: `<dom> CNAME *.`.
    118 /// * `RpzAction::Passthru`: `<dom> CNAME rpz-passthru.`.
    119 /// * `RpzAction::Drop`: `<dom> CNAME rpz-drop.`.
    120 /// * `RpzAction::TcpOnly`: `<dom> CNAME rpz-tcp-only.`.
    121 ///
    122 /// `*.` is prepended to `<dom>` iff `wildcard`.
    123 ///
    124 /// # Errors
    125 ///
    126 /// Returns [`Error`] iff [`writeln`] does.
    127 #[inline]
    128 pub fn write_rpz_line<W: Write, T>(
    129     mut writer: W,
    130     dom: &Domain<T>,
    131     action: RpzAction,
    132     wildcard: bool,
    133 ) -> Result<(), Error>
    134 where
    135     Domain<T>: Display,
    136 {
    137     writeln!(
    138         writer,
    139         "{}{} CNAME {}.",
    140         if wildcard { "*." } else { "" },
    141         dom,
    142         match action {
    143             RpzAction::Nxdomain => "",
    144             RpzAction::Nodata => "*",
    145             RpzAction::Passthru => "rpz-passthru",
    146             RpzAction::Drop => "rpz-drop",
    147             RpzAction::TcpOnly => "rpz-tcp-only",
    148         }
    149     )
    150 }
    151 /// Type that can be returned by [`Domain`]-like parsers (e.g., [`Adblock`]).
    152 #[derive(Clone, Copy, Debug)]
    153 pub enum Value<'a, T: ParsedDomain<'a>> {
    154     /// The parsed value is a domain.
    155     Domain(T),
    156     /// The parsed value is a comment.
    157     Comment(&'a str),
    158     /// The parsed value is blank or just [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace).
    159     Blank,
    160 }
    161 impl<'a, T: ParsedDomain<'a>> Value<'a, T> {
    162     /// Returns `true` iff `self` is a [`Self::Domain`].
    163     #[inline]
    164     pub const fn is_domain(&self) -> bool {
    165         match *self {
    166             Self::Domain(_) => true,
    167             Self::Comment(_) | Self::Blank => false,
    168         }
    169     }
    170     /// Returns `true` iff `self` is a [`Self::Comment`].
    171     #[inline]
    172     pub const fn is_comment(&self) -> bool {
    173         match *self {
    174             Self::Comment(_) => true,
    175             Self::Domain(_) | Self::Blank => false,
    176         }
    177     }
    178     /// Returns `true` iff `self` is a [`Self::Blank`].
    179     #[inline]
    180     pub const fn is_blank(&self) -> bool {
    181         matches!(*self, Value::Blank)
    182     }
    183     /// Returns the contained [`Self::Domain`] value.
    184     ///
    185     /// # Panics
    186     ///
    187     /// Panics iff `self` is [`Self::Comment`] or [`Self::Blank`].
    188     #[expect(clippy::panic, reason = "bug if called incorrectly")]
    189     #[inline]
    190     pub fn unwrap_domain(self) -> T {
    191         match self {
    192             Self::Domain(dom) => dom,
    193             Self::Comment(_) | Self::Blank => {
    194                 panic!("called `ParsedDomain::unwrap_domain()` on a `Comment` or `Blank` value")
    195             }
    196         }
    197     }
    198     /// Returns the contained [`prim@str`] in [`Self::Comment`].
    199     ///
    200     /// # Panics
    201     ///
    202     /// Panics iff `self` is [`Self::Domain`] or [`Self::Blank`].
    203     #[expect(clippy::panic, reason = "bug if called incorrectly")]
    204     #[inline]
    205     pub fn unwrap_comment(self) -> &'a str {
    206         match self {
    207             Self::Comment(com) => com,
    208             Self::Domain(_) | Self::Blank => {
    209                 panic!("called `ParsedDomain::unwrap_comment()` on a `Domain` or `Blank` value")
    210             }
    211         }
    212     }
    213     /// Returns [`unit`] when `self` is [`Self::Blank`].
    214     ///
    215     /// # Panics
    216     ///
    217     /// Panics iff `self` is [`Self::Domain`] or [`Self::Comment`].
    218     #[expect(clippy::panic, reason = "bug if called incorrectly")]
    219     #[inline]
    220     pub fn unwrap_blank(self) {
    221         match self {
    222             Self::Blank => {}
    223             Self::Domain(_) | Self::Comment(_) => {
    224                 panic!("called `ParsedDomain::unwrap_blank()` on a `Domain` or `Comment` value")
    225             }
    226         }
    227     }
    228 }
    229 /// Structure of a [`Domain`]-like type that can parse [`prim@str`]s into [`Value`]s.
    230 ///
    231 /// When parsed into a [`Value::Domain`], the domain can be written to a
    232 /// [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) file.
    233 pub trait ParsedDomain<'a>: Sized {
    234     /// The error returned from [`Self::parse_value`].
    235     type Error;
    236     /// Parses a `str` into a `Value`.
    237     /// # Errors
    238     ///
    239     /// Errors iff `val` is unable to be parsed into a `Value`.
    240     fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error>;
    241     /// Reference to the contained `Domain`.
    242     fn domain(&self) -> &Domain<&'a str>;
    243     /// Writes `self` as RPZ lines via `writer`.
    244     ///
    245     /// # Errors
    246     ///
    247     /// Errors iff `writer` errors.
    248     fn write_to_rpz<W: Write>(&self, action: RpzAction, writer: W) -> Result<(), Error>;
    249 }
    250 /// Domain constructed from an
    251 /// [Adblock-style rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#adblock-style-syntax).
    252 ///
    253 /// Specifically the domain must conform to the following extended regex:
    254 ///
    255 /// `^<ws>*(\|\|)?<ws>*<domain><ws>*\^?<ws>*$`
    256 ///
    257 /// where `<domain>` conforms to a valid [`Domain`] based on [`ASCII_FIREFOX`] with the added requirement that it
    258 /// does not contain `$`, the TLD is either all letters or at least length five and begins with `xn--`, and `<ws>` is any sequence of
    259 /// [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace).
    260 ///
    261 /// Comments are any lines that start with `!` or `#` (ignoring whitespace). Any in-line comments after a valid
    262 /// domain are ignored and will be parsed into a [`Value::Domain`].
    263 ///
    264 /// Note that this means some valid Adblock-style rules are not considered valid since such rules often contain
    265 /// path information or modifiers (e.g., “third-party”), but this only considers domain-only rules.
    266 #[derive(Clone, Debug)]
    267 pub struct Adblock<'a> {
    268     /// The `Domain`.
    269     domain: Domain<&'a str>,
    270     /// `true` iff `domain` represents all subdomains. Note that this includes `domain` itself.
    271     subdomains: bool,
    272 }
    273 impl Adblock<'_> {
    274     /// Returns `true` iff the contained [`Domain`] represents all subdomains. Note this includes the
    275     /// `Domain` itself.
    276     #[inline]
    277     #[must_use]
    278     pub const fn is_subdomains(&self) -> bool {
    279         self.subdomains
    280     }
    281     /// Since `DomainOnly` and `Hosts` are treated the same, we have this helper function that can be used
    282     /// for both.
    283     #[must_use]
    284     fn cmp_dom(&self, other: &Domain<&str>) -> Ordering {
    285         match self.domain.cmp_by_domain_ordering(other) {
    286             DomainOrdering::Less => Ordering::Less,
    287             DomainOrdering::Shorter => {
    288                 if self.subdomains {
    289                     Ordering::Greater
    290                 } else {
    291                     Ordering::Less
    292                 }
    293             }
    294             DomainOrdering::Equal => {
    295                 if self.subdomains {
    296                     Ordering::Greater
    297                 } else {
    298                     Ordering::Equal
    299                 }
    300             }
    301             DomainOrdering::Longer | DomainOrdering::Greater => Ordering::Greater,
    302         }
    303     }
    304     /// The total order that is defined follows the following hierarchy:
    305     /// 1. Pairwise comparisons of each [`ascii_domain::dom::Label`] starting from the TLDs.
    306     /// 2. If 1. evaluates as not equivalent, then return the result.
    307     /// 3. If `self` represents a single `Domain` (i.e., `!self.is_subdomains()`),
    308     ///    then return the comparison of label counts.
    309     /// 4. `self` is greater.
    310     ///
    311     /// For example, `com` `<` `example.com` `<` `||example.com` `<` `||com` `<` `net` `<` `example.net` `<` `||example.net` `<` `||net`.
    312     #[inline]
    313     #[must_use]
    314     pub fn cmp_domain_only(&self, other: &DomainOnly<'_>) -> Ordering {
    315         self.cmp_dom(&other.domain)
    316     }
    317     /// Same as [`Adblock::cmp_domain_only`].
    318     #[inline]
    319     #[must_use]
    320     pub fn cmp_hosts(&self, other: &Hosts<'_>) -> Ordering {
    321         self.cmp_dom(&other.domain)
    322     }
    323     /// The total order that is defined follows the following hierarchy:
    324     /// 1. Pairwise comparisons of each [`ascii_domain::dom::Label`] starting from the TLDs.
    325     /// 2. If 1. evaluates as not equivalent, then return the result.
    326     /// 3. If both domains represent a single `Domain`, then return the comparison
    327     ///    of label counts.
    328     /// 4. If one domain represents a single `Domain`, then return that that domain is less.
    329     /// 5. If the label counts are the same, `self` is greater.
    330     /// 6. Return the inverse of the comparison of label counts.
    331     ///
    332     /// For example the following is a sequence of domains in
    333     /// ascending order:
    334     ///
    335     /// `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`
    336     #[inline]
    337     #[must_use]
    338     pub fn cmp_wildcard(&self, other: &Wildcard<'_>) -> Ordering {
    339         match self.domain.cmp_by_domain_ordering(&other.domain) {
    340             DomainOrdering::Less => Ordering::Less,
    341             DomainOrdering::Shorter => {
    342                 if self.subdomains {
    343                     Ordering::Greater
    344                 } else {
    345                     Ordering::Less
    346                 }
    347             }
    348             DomainOrdering::Equal => {
    349                 if self.subdomains {
    350                     Ordering::Greater
    351                 } else if other.proper_subdomains {
    352                     Ordering::Less
    353                 } else {
    354                     Ordering::Equal
    355                 }
    356             }
    357             DomainOrdering::Longer => {
    358                 if self.subdomains {
    359                     if other.proper_subdomains {
    360                         Ordering::Less
    361                     } else {
    362                         Ordering::Greater
    363                     }
    364                 } else if other.proper_subdomains {
    365                     Ordering::Less
    366                 } else {
    367                     Ordering::Greater
    368                 }
    369             }
    370             DomainOrdering::Greater => Ordering::Greater,
    371         }
    372     }
    373     /// Same as [`Adblock::cardinality`] except that a `BigUint` is returned. Note the count _includes_
    374     /// the `Domain` itself when `self.is_subdomains()`.
    375     ///
    376     /// `!self.is_subdomains()` ⇔ `self.domain_count() == BigUint::new(vec![1])`.
    377     #[expect(clippy::arithmetic_side_effects, reason = "arbitrary-sized arithmetic")]
    378     #[inline]
    379     #[must_use]
    380     pub fn domain_count(&self) -> BigUint {
    381         if self.subdomains {
    382             proper_subdomain_count(&self.domain) + BigUint::new(vec![1])
    383         } else {
    384             BigUint::new(vec![1])
    385         }
    386     }
    387 }
    388 impl Display for Adblock<'_> {
    389     #[inline]
    390     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    391         write!(
    392             f,
    393             "{}{}",
    394             if self.subdomains { "||" } else { "" },
    395             self.domain
    396         )
    397     }
    398 }
    399 impl PartialEq<Adblock<'_>> for Adblock<'_> {
    400     #[inline]
    401     fn eq(&self, other: &Adblock<'_>) -> bool {
    402         self.domain == other.domain && self.subdomains == other.subdomains
    403     }
    404 }
    405 impl PartialEq<&Adblock<'_>> for Adblock<'_> {
    406     #[inline]
    407     fn eq(&self, other: &&Adblock<'_>) -> bool {
    408         *self == **other
    409     }
    410 }
    411 impl PartialEq<Adblock<'_>> for &Adblock<'_> {
    412     #[inline]
    413     fn eq(&self, other: &Adblock<'_>) -> bool {
    414         **self == *other
    415     }
    416 }
    417 impl PartialEq<DomainOnly<'_>> for Adblock<'_> {
    418     #[inline]
    419     fn eq(&self, other: &DomainOnly<'_>) -> bool {
    420         !self.subdomains && self.domain == other.domain
    421     }
    422 }
    423 impl PartialEq<&DomainOnly<'_>> for Adblock<'_> {
    424     #[inline]
    425     fn eq(&self, other: &&DomainOnly<'_>) -> bool {
    426         *self == **other
    427     }
    428 }
    429 impl PartialEq<DomainOnly<'_>> for &Adblock<'_> {
    430     #[inline]
    431     fn eq(&self, other: &DomainOnly<'_>) -> bool {
    432         **self == *other
    433     }
    434 }
    435 impl PartialEq<&Adblock<'_>> for DomainOnly<'_> {
    436     #[inline]
    437     fn eq(&self, other: &&Adblock<'_>) -> bool {
    438         *self == **other
    439     }
    440 }
    441 impl PartialEq<Adblock<'_>> for &DomainOnly<'_> {
    442     #[inline]
    443     fn eq(&self, other: &Adblock<'_>) -> bool {
    444         **self == *other
    445     }
    446 }
    447 impl PartialEq<Hosts<'_>> for Adblock<'_> {
    448     #[inline]
    449     fn eq(&self, other: &Hosts<'_>) -> bool {
    450         !self.subdomains && self.domain == other.domain
    451     }
    452 }
    453 impl PartialEq<&Hosts<'_>> for Adblock<'_> {
    454     #[inline]
    455     fn eq(&self, other: &&Hosts<'_>) -> bool {
    456         *self == **other
    457     }
    458 }
    459 impl PartialEq<Hosts<'_>> for &Adblock<'_> {
    460     #[inline]
    461     fn eq(&self, other: &Hosts<'_>) -> bool {
    462         **self == *other
    463     }
    464 }
    465 impl PartialEq<&Adblock<'_>> for Hosts<'_> {
    466     #[inline]
    467     fn eq(&self, other: &&Adblock<'_>) -> bool {
    468         *self == **other
    469     }
    470 }
    471 impl PartialEq<Adblock<'_>> for &Hosts<'_> {
    472     #[inline]
    473     fn eq(&self, other: &Adblock<'_>) -> bool {
    474         **self == *other
    475     }
    476 }
    477 impl PartialEq<Wildcard<'_>> for Adblock<'_> {
    478     #[expect(clippy::suspicious_operation_groupings, reason = "false positive")]
    479     #[inline]
    480     fn eq(&self, other: &Wildcard<'_>) -> bool {
    481         !(self.subdomains || other.proper_subdomains) && self.domain == other.domain
    482     }
    483 }
    484 impl PartialEq<&Wildcard<'_>> for Adblock<'_> {
    485     #[inline]
    486     fn eq(&self, other: &&Wildcard<'_>) -> bool {
    487         *self == **other
    488     }
    489 }
    490 impl PartialEq<Wildcard<'_>> for &Adblock<'_> {
    491     #[inline]
    492     fn eq(&self, other: &Wildcard<'_>) -> bool {
    493         **self == *other
    494     }
    495 }
    496 impl PartialEq<&Adblock<'_>> for Wildcard<'_> {
    497     #[inline]
    498     fn eq(&self, other: &&Adblock<'_>) -> bool {
    499         *self == **other
    500     }
    501 }
    502 impl PartialEq<Adblock<'_>> for &Wildcard<'_> {
    503     #[inline]
    504     fn eq(&self, other: &Adblock<'_>) -> bool {
    505         **self == *other
    506     }
    507 }
    508 impl Eq for Adblock<'_> {}
    509 impl Hash for Adblock<'_> {
    510     #[inline]
    511     fn hash<H: Hasher>(&self, state: &mut H) {
    512         self.domain.hash(state);
    513     }
    514 }
    515 impl PartialOrd<Adblock<'_>> for Adblock<'_> {
    516     #[inline]
    517     fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> {
    518         Some(self.cmp(other))
    519     }
    520 }
    521 impl Ord for Adblock<'_> {
    522     /// The total order that is defined follows the following hierarchy:
    523     /// 1. Pairwise comparisons of each [`ascii_domain::dom::Label`] starting from the TLDs.
    524     /// 2. If 1. evaluates as not equivalent, then return the result.
    525     /// 3. If both domains represent a single `Domain`, then return the comparison
    526     ///    of label counts.
    527     /// 4. If one domain represents a single `Domain`, then return that that domain is less.
    528     /// 5. Return the inverse of the comparison of label counts.
    529     ///
    530     /// For example, `com` `<` `example.com` `<` `||example.com` `<` `||com` `<` `net` `<` `example.net` `<` `||example.net` `<` `||net`.
    531     #[inline]
    532     fn cmp(&self, other: &Self) -> Ordering {
    533         match self.domain.cmp_by_domain_ordering(&other.domain) {
    534             DomainOrdering::Less => Ordering::Less,
    535             DomainOrdering::Shorter => {
    536                 if self.subdomains {
    537                     Ordering::Greater
    538                 } else {
    539                     Ordering::Less
    540                 }
    541             }
    542             DomainOrdering::Equal => {
    543                 if self.subdomains {
    544                     if other.subdomains {
    545                         Ordering::Equal
    546                     } else {
    547                         Ordering::Greater
    548                     }
    549                 } else if other.subdomains {
    550                     Ordering::Less
    551                 } else {
    552                     Ordering::Equal
    553                 }
    554             }
    555             DomainOrdering::Longer => {
    556                 if self.subdomains {
    557                     if other.subdomains {
    558                         Ordering::Less
    559                     } else {
    560                         Ordering::Greater
    561                     }
    562                 } else if other.subdomains {
    563                     Ordering::Less
    564                 } else {
    565                     Ordering::Greater
    566                 }
    567             }
    568             DomainOrdering::Greater => Ordering::Greater,
    569         }
    570     }
    571 }
    572 impl PartialOrd<DomainOnly<'_>> for Adblock<'_> {
    573     #[inline]
    574     fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> {
    575         Some(self.cmp_domain_only(other))
    576     }
    577 }
    578 impl PartialOrd<Hosts<'_>> for Adblock<'_> {
    579     #[inline]
    580     fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> {
    581         Some(self.cmp_hosts(other))
    582     }
    583 }
    584 impl PartialOrd<Wildcard<'_>> for Adblock<'_> {
    585     #[inline]
    586     fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> {
    587         Some(self.cmp_wildcard(other))
    588     }
    589 }
    590 impl<'a> Set for Adblock<'a> {
    591     type Elem = Domain<&'a str>;
    592     #[inline]
    593     fn bounded_cardinality(&self) -> BoundedCardinality {
    594         BoundedCardinality::from_biguint_exact(self.domain_count())
    595     }
    596     #[inline]
    597     fn cardinality(&self) -> Option<Cardinality> {
    598         Some(Cardinality::Finite(self.domain_count()))
    599     }
    600     #[inline]
    601     fn contains<Q>(&self, elem: &Q) -> bool
    602     where
    603         Q: Borrow<Self::Elem> + Eq + ?Sized,
    604     {
    605         if self.subdomains {
    606             matches!(
    607                 self.domain.cmp_by_domain_ordering(elem.borrow()),
    608                 DomainOrdering::Shorter
    609             )
    610         } else {
    611             self.domain == *elem.borrow()
    612         }
    613     }
    614     #[inline]
    615     fn is_proper_subset(&self, val: &Self) -> bool {
    616         // A single domain can never be a proper superset. Subdomains` cannot be a proper superset if it has
    617         // more labels or the same number of labels as another subdomains. In all other cases, we need to
    618         // recursively check from the TLD that the labels are the same.
    619         val.subdomains
    620             && match val.domain.cmp_by_domain_ordering(&self.domain) {
    621                 DomainOrdering::Less | DomainOrdering::Longer | DomainOrdering::Greater => false,
    622                 DomainOrdering::Shorter => true,
    623                 DomainOrdering::Equal => !self.subdomains,
    624             }
    625     }
    626     #[inline]
    627     fn is_subset(&self, val: &Self) -> bool {
    628         self == val || self.is_proper_subset(val)
    629     }
    630 }
    631 impl SetOrd for Adblock<'_> {}
    632 impl<'a> Deref for Adblock<'a> {
    633     type Target = Domain<&'a str>;
    634     #[inline]
    635     fn deref(&self) -> &Self::Target {
    636         &self.domain
    637     }
    638 }
    639 impl<'a> ParsedDomain<'a> for Adblock<'a> {
    640     type Error = FirefoxDomainErr;
    641     #[expect(
    642         unsafe_code,
    643         clippy::indexing_slicing,
    644         reason = "we carefully verify what we are doing"
    645     )]
    646     #[inline]
    647     fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> {
    648         // First remove leading whitepace. Then check for comments via '#' and '!'. Return Blank iff empty.
    649         // Return Comment iff '#' or '!' is the first character. Remove trailing whitespace. Next remove the
    650         // last byte if it is '^' as well as whitespace before. Next track and remove '||' at the beginning
    651         // and any subsequent whitespace.
    652         let mut value = val.as_bytes().trim_ascii_start();
    653         value.first().map_or_else(
    654             || Ok(Value::Blank),
    655             |byt| {
    656                 if *byt == b'#' || *byt == b'!' {
    657                     // SAFETY:
    658                     // `value` came from `val` with leading ASCII whitespace removed which is still valid UTF-8
    659                     // since the first byte is '#' or '$' the remaining bytes is still valid UTF-8.
    660                     let comment = unsafe { str::from_utf8_unchecked(&value[1..]) };
    661                     Ok(Value::Comment(comment))
    662                 } else {
    663                     value = value.trim_ascii_end();
    664                     let len = value.len().wrapping_sub(1);
    665                     value = value.get(len).map_or(value, |byt2| {
    666                         if *byt2 == b'^' {
    667                             value[..len].trim_ascii_end()
    668                         } else {
    669                             value
    670                         }
    671                     });
    672                     let (subdomains, val2) = value.get(..2).map_or_else(
    673                         || (false, value),
    674                         |fst| {
    675                             if fst == b"||" {
    676                                 (true, value[2..].trim_ascii_start())
    677                             } else {
    678                                 (false, value)
    679                             }
    680                         },
    681                     );
    682                     // `Domain`s allow `$`, but we don't want to allow that symbol for Adblock-style rules.
    683                     val2.iter()
    684                         .try_fold((), |(), byt2| {
    685                             if *byt2 == b'$' {
    686                                 Err(FirefoxDomainErr::InvalidAdblockDomain)
    687                             } else {
    688                                 Ok(())
    689                             }
    690                         })
    691                         .and_then(|()| {
    692                             domain_icann_tld(val2).map(|domain| {
    693                                 // A domain of length 252 or 253 can't have subdomains due to there not being enough
    694                                 // characters.
    695                                 Value::Domain(Self {
    696                                     subdomains: if domain.len().get() > 251 {
    697                                         false
    698                                     } else {
    699                                         subdomains
    700                                     },
    701                                     domain,
    702                                 })
    703                             })
    704                         })
    705                 }
    706             },
    707         )
    708     }
    709     #[inline]
    710     fn domain(&self) -> &Domain<&'a str> {
    711         &self.domain
    712     }
    713     #[inline]
    714     fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> {
    715         write_rpz_line(&mut writer, self.domain(), action, false).and_then(|()| {
    716             if self.subdomains {
    717                 write_rpz_line(writer, self.domain(), action, true)
    718             } else {
    719                 Ok(())
    720             }
    721         })
    722     }
    723 }
    724 /// Domain constructed from a
    725 /// [domains-only rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#domains-only-syntax).
    726 ///
    727 /// Specifically the domain must conform to the following extended regex:
    728 ///
    729 /// `^<ws>*<domain><ws>*(#.*)?$`
    730 ///
    731 /// where `<domain>` conforms to a valid [`Domain`] based on [`ASCII_FIREFOX`], the TLD is either all letters
    732 /// or at least length five and begins with `xn--`, and `<ws>` is any sequence of [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace).
    733 ///
    734 /// Comments are any lines that start with `#` (ignoring whitespace). Any in-line comments after a valid domain
    735 /// are ignored and will be parsed into a [`Value::Domain`].
    736 #[derive(Clone, Debug)]
    737 pub struct DomainOnly<'a> {
    738     /// The `Domain`.
    739     domain: Domain<&'a str>,
    740 }
    741 impl DomainOnly<'_> {
    742     /// Read [`Adblock::cmp_domain_only`].
    743     #[inline]
    744     #[must_use]
    745     pub fn cmp_adblock(&self, other: &Adblock<'_>) -> Ordering {
    746         other.cmp_domain_only(self).reverse()
    747     }
    748     /// Read [`Domain::cmp`].
    749     #[inline]
    750     #[must_use]
    751     pub fn cmp_hosts(&self, other: &Hosts<'_>) -> Ordering {
    752         self.domain.cmp(&other.domain)
    753     }
    754     /// Read [`Wildcard::cmp_domain_only`].
    755     #[inline]
    756     #[must_use]
    757     pub fn cmp_wildcard(&self, other: &Wildcard<'_>) -> Ordering {
    758         other.cmp_domain_only(self).reverse()
    759     }
    760     /// Same as [`DomainOnly::cardinality`] except that a `NonZeroU8` is returned.
    761     ///
    762     /// The value is always 1.
    763     #[inline]
    764     #[must_use]
    765     pub const fn domain_count(&self) -> NonZeroU8 {
    766         ONE
    767     }
    768 }
    769 impl PartialEq<DomainOnly<'_>> for DomainOnly<'_> {
    770     #[inline]
    771     fn eq(&self, other: &DomainOnly<'_>) -> bool {
    772         self.domain == other.domain
    773     }
    774 }
    775 impl PartialEq<DomainOnly<'_>> for &DomainOnly<'_> {
    776     #[inline]
    777     fn eq(&self, other: &DomainOnly<'_>) -> bool {
    778         **self == *other
    779     }
    780 }
    781 impl PartialEq<&DomainOnly<'_>> for DomainOnly<'_> {
    782     #[inline]
    783     fn eq(&self, other: &&DomainOnly<'_>) -> bool {
    784         *self == **other
    785     }
    786 }
    787 impl PartialEq<Adblock<'_>> for DomainOnly<'_> {
    788     #[inline]
    789     fn eq(&self, other: &Adblock<'_>) -> bool {
    790         other == self
    791     }
    792 }
    793 impl PartialEq<Hosts<'_>> for DomainOnly<'_> {
    794     #[inline]
    795     fn eq(&self, other: &Hosts<'_>) -> bool {
    796         self.domain == other.domain
    797     }
    798 }
    799 impl PartialEq<&Hosts<'_>> for DomainOnly<'_> {
    800     #[inline]
    801     fn eq(&self, other: &&Hosts<'_>) -> bool {
    802         *self == **other
    803     }
    804 }
    805 impl PartialEq<Hosts<'_>> for &DomainOnly<'_> {
    806     #[inline]
    807     fn eq(&self, other: &Hosts<'_>) -> bool {
    808         **self == *other
    809     }
    810 }
    811 impl PartialEq<&DomainOnly<'_>> for Hosts<'_> {
    812     #[inline]
    813     fn eq(&self, other: &&DomainOnly<'_>) -> bool {
    814         *self == **other
    815     }
    816 }
    817 impl PartialEq<DomainOnly<'_>> for &Hosts<'_> {
    818     #[inline]
    819     fn eq(&self, other: &DomainOnly<'_>) -> bool {
    820         **self == *other
    821     }
    822 }
    823 impl PartialEq<Wildcard<'_>> for DomainOnly<'_> {
    824     #[inline]
    825     fn eq(&self, other: &Wildcard<'_>) -> bool {
    826         !other.proper_subdomains && self.domain == other.domain
    827     }
    828 }
    829 impl PartialEq<&Wildcard<'_>> for DomainOnly<'_> {
    830     #[inline]
    831     fn eq(&self, other: &&Wildcard<'_>) -> bool {
    832         *self == **other
    833     }
    834 }
    835 impl PartialEq<Wildcard<'_>> for &DomainOnly<'_> {
    836     #[inline]
    837     fn eq(&self, other: &Wildcard<'_>) -> bool {
    838         **self == *other
    839     }
    840 }
    841 impl PartialEq<&DomainOnly<'_>> for Wildcard<'_> {
    842     #[inline]
    843     fn eq(&self, other: &&DomainOnly<'_>) -> bool {
    844         *self == **other
    845     }
    846 }
    847 impl PartialEq<DomainOnly<'_>> for &Wildcard<'_> {
    848     #[inline]
    849     fn eq(&self, other: &DomainOnly<'_>) -> bool {
    850         **self == *other
    851     }
    852 }
    853 impl Eq for DomainOnly<'_> {}
    854 impl Hash for DomainOnly<'_> {
    855     #[inline]
    856     fn hash<H: Hasher>(&self, state: &mut H) {
    857         self.domain.hash(state);
    858     }
    859 }
    860 impl PartialOrd<DomainOnly<'_>> for DomainOnly<'_> {
    861     #[inline]
    862     fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> {
    863         Some(self.cmp(other))
    864     }
    865 }
    866 impl Ord for DomainOnly<'_> {
    867     /// Read [`Domain::cmp`].
    868     #[inline]
    869     fn cmp(&self, other: &Self) -> Ordering {
    870         self.domain.cmp(&other.domain)
    871     }
    872 }
    873 impl PartialOrd<Adblock<'_>> for DomainOnly<'_> {
    874     #[inline]
    875     fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> {
    876         Some(self.cmp_adblock(other))
    877     }
    878 }
    879 impl PartialOrd<Hosts<'_>> for DomainOnly<'_> {
    880     #[inline]
    881     fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> {
    882         Some(self.cmp_hosts(other))
    883     }
    884 }
    885 impl PartialOrd<Wildcard<'_>> for DomainOnly<'_> {
    886     #[inline]
    887     fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> {
    888         Some(self.cmp_wildcard(other))
    889     }
    890 }
    891 impl Display for DomainOnly<'_> {
    892     #[inline]
    893     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    894         self.domain.fmt(f)
    895     }
    896 }
    897 impl<'a> Set for DomainOnly<'a> {
    898     type Elem = Domain<&'a str>;
    899     #[inline]
    900     fn bounded_cardinality(&self) -> BoundedCardinality {
    901         BoundedCardinality::from_biguint_exact(self.domain_count().get().into())
    902     }
    903     #[inline]
    904     fn cardinality(&self) -> Option<Cardinality> {
    905         Some(Cardinality::Finite(self.domain_count().get().into()))
    906     }
    907     #[inline]
    908     fn contains<Q>(&self, elem: &Q) -> bool
    909     where
    910         Q: Borrow<Self::Elem> + Eq + ?Sized,
    911     {
    912         self.domain == *elem.borrow()
    913     }
    914     #[inline]
    915     fn is_proper_subset(&self, _: &Self) -> bool {
    916         false
    917     }
    918     #[inline]
    919     fn is_subset(&self, val: &Self) -> bool {
    920         self == val
    921     }
    922 }
    923 impl SetOrd for DomainOnly<'_> {}
    924 impl<'a> Deref for DomainOnly<'a> {
    925     type Target = Domain<&'a str>;
    926     #[inline]
    927     fn deref(&self) -> &Self::Target {
    928         &self.domain
    929     }
    930 }
    931 impl<'a> ParsedDomain<'a> for DomainOnly<'a> {
    932     type Error = FirefoxDomainErr;
    933     #[expect(
    934         unsafe_code,
    935         clippy::arithmetic_side_effects,
    936         clippy::indexing_slicing,
    937         reason = "we verify all use is correct"
    938     )]
    939     #[inline]
    940     fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> {
    941         let value = val.as_bytes().trim_ascii_start();
    942         value.first().map_or_else(
    943             || Ok(Value::Blank),
    944             |byt| {
    945                 if *byt == b'#' {
    946                     // SAFETY:
    947                     // `value` came from `val` with leading ASCII whitespace removed which is still valid UTF-8
    948                     // since the first byte is '#' or '$' the remaining bytes are still valid UTF-8.
    949                     let comment = unsafe { str::from_utf8_unchecked(&value[1..]) };
    950                     Ok(Value::Comment(comment))
    951                 } else {
    952                     domain_icann_tld(
    953                         value[..value
    954                             .iter()
    955                             .try_fold(0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) })
    956                             .unwrap_or_else(convert::identity)]
    957                             .trim_ascii_end(),
    958                     )
    959                     .map(|domain| Value::Domain(Self { domain }))
    960                 }
    961             },
    962         )
    963     }
    964     #[inline]
    965     fn domain(&self) -> &Domain<&'a str> {
    966         &self.domain
    967     }
    968     #[inline]
    969     fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> {
    970         write_rpz_line(&mut writer, self.domain(), action, false)
    971     }
    972 }
    973 /// Domain constructed from a
    974 /// [`hosts(5)`-style rule](https://adguard-dns.io/kb/general/dns-filtering-syntax/#etc-hosts-syntax).
    975 ///
    976 /// Specifically the domain must conform to the following extended regex:
    977 ///
    978 /// `^<ws>*<ip><ws>+<domain><ws>*(#.*)?$`
    979 ///
    980 /// where `<domain>` conforms to a valid [`Domain`] based on [`ASCII_FIREFOX`], the TLD is either all letters
    981 /// or at least length five and begins with `xn--`, `<ws>` is any sequence of
    982 /// [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace), and `<ip>` is one of the following:
    983 ///
    984 /// `::`, `::1`, `0.0.0.0`, or `127.0.0.1`.
    985 ///
    986 /// Comments are any lines that start with `#` (ignoring whitespace). Any in-line comments after a valid domain
    987 /// are ignored and will be parsed into a [`Value::Domain`].
    988 #[derive(Clone, Debug)]
    989 pub struct Hosts<'a> {
    990     /// The `Domain`.
    991     domain: Domain<&'a str>,
    992 }
    993 impl Hosts<'_> {
    994     /// Read [`Adblock::cmp_hosts`].
    995     #[inline]
    996     #[must_use]
    997     pub fn cmp_adblock(&self, other: &Adblock<'_>) -> Ordering {
    998         other.cmp_hosts(self).reverse()
    999     }
   1000     /// Read [`DomainOnly::cmp_hosts`].
   1001     #[inline]
   1002     #[must_use]
   1003     pub fn cmp_domain_only(&self, other: &DomainOnly<'_>) -> Ordering {
   1004         other.cmp_hosts(self).reverse()
   1005     }
   1006     /// Read [`Wildcard::cmp_hosts`].
   1007     #[inline]
   1008     #[must_use]
   1009     pub fn cmp_wildcard(&self, other: &Wildcard<'_>) -> Ordering {
   1010         other.cmp_hosts(self).reverse()
   1011     }
   1012     /// Same as [`Hosts::cardinality`] except that a `NonZeroU8` is returned.
   1013     ///
   1014     /// The value is always 1.
   1015     #[inline]
   1016     #[must_use]
   1017     pub const fn domain_count(&self) -> NonZeroU8 {
   1018         ONE
   1019     }
   1020 }
   1021 impl PartialEq<Hosts<'_>> for Hosts<'_> {
   1022     #[inline]
   1023     fn eq(&self, other: &Hosts<'_>) -> bool {
   1024         self.domain == other.domain
   1025     }
   1026 }
   1027 impl PartialEq<Hosts<'_>> for &Hosts<'_> {
   1028     #[inline]
   1029     fn eq(&self, other: &Hosts<'_>) -> bool {
   1030         **self == *other
   1031     }
   1032 }
   1033 impl PartialEq<&Hosts<'_>> for Hosts<'_> {
   1034     #[inline]
   1035     fn eq(&self, other: &&Hosts<'_>) -> bool {
   1036         *self == **other
   1037     }
   1038 }
   1039 impl PartialEq<Adblock<'_>> for Hosts<'_> {
   1040     #[inline]
   1041     fn eq(&self, other: &Adblock<'_>) -> bool {
   1042         other == self
   1043     }
   1044 }
   1045 impl PartialEq<DomainOnly<'_>> for Hosts<'_> {
   1046     #[inline]
   1047     fn eq(&self, other: &DomainOnly<'_>) -> bool {
   1048         other == self
   1049     }
   1050 }
   1051 impl PartialEq<Wildcard<'_>> for Hosts<'_> {
   1052     #[inline]
   1053     fn eq(&self, other: &Wildcard<'_>) -> bool {
   1054         !other.proper_subdomains && self.domain == other.domain
   1055     }
   1056 }
   1057 impl PartialEq<&Wildcard<'_>> for Hosts<'_> {
   1058     #[inline]
   1059     fn eq(&self, other: &&Wildcard<'_>) -> bool {
   1060         *self == **other
   1061     }
   1062 }
   1063 impl PartialEq<Wildcard<'_>> for &Hosts<'_> {
   1064     #[inline]
   1065     fn eq(&self, other: &Wildcard<'_>) -> bool {
   1066         **self == *other
   1067     }
   1068 }
   1069 impl PartialEq<&Hosts<'_>> for Wildcard<'_> {
   1070     #[inline]
   1071     fn eq(&self, other: &&Hosts<'_>) -> bool {
   1072         *self == **other
   1073     }
   1074 }
   1075 impl PartialEq<Hosts<'_>> for &Wildcard<'_> {
   1076     #[inline]
   1077     fn eq(&self, other: &Hosts<'_>) -> bool {
   1078         **self == *other
   1079     }
   1080 }
   1081 impl Eq for Hosts<'_> {}
   1082 impl Hash for Hosts<'_> {
   1083     #[inline]
   1084     fn hash<H: Hasher>(&self, state: &mut H) {
   1085         self.domain.hash(state);
   1086     }
   1087 }
   1088 impl PartialOrd<Hosts<'_>> for Hosts<'_> {
   1089     #[inline]
   1090     fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> {
   1091         Some(self.cmp(other))
   1092     }
   1093 }
   1094 impl Ord for Hosts<'_> {
   1095     /// Read [`Domain::cmp`].
   1096     #[inline]
   1097     fn cmp(&self, other: &Self) -> Ordering {
   1098         self.domain.cmp(&other.domain)
   1099     }
   1100 }
   1101 impl PartialOrd<Adblock<'_>> for Hosts<'_> {
   1102     #[inline]
   1103     fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> {
   1104         Some(self.cmp_adblock(other))
   1105     }
   1106 }
   1107 impl PartialOrd<DomainOnly<'_>> for Hosts<'_> {
   1108     #[inline]
   1109     fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> {
   1110         Some(self.cmp_domain_only(other))
   1111     }
   1112 }
   1113 impl PartialOrd<Wildcard<'_>> for Hosts<'_> {
   1114     #[inline]
   1115     fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> {
   1116         Some(self.cmp_wildcard(other))
   1117     }
   1118 }
   1119 impl Display for Hosts<'_> {
   1120     #[inline]
   1121     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
   1122         self.domain.fmt(f)
   1123     }
   1124 }
   1125 impl<'a> Set for Hosts<'a> {
   1126     type Elem = Domain<&'a str>;
   1127     #[inline]
   1128     fn bounded_cardinality(&self) -> BoundedCardinality {
   1129         BoundedCardinality::from_biguint_exact(self.domain_count().get().into())
   1130     }
   1131     #[inline]
   1132     fn cardinality(&self) -> Option<Cardinality> {
   1133         Some(Cardinality::Finite(self.domain_count().get().into()))
   1134     }
   1135     #[inline]
   1136     fn contains<Q>(&self, elem: &Q) -> bool
   1137     where
   1138         Q: Borrow<Self::Elem> + Eq + ?Sized,
   1139     {
   1140         self.domain == *elem.borrow()
   1141     }
   1142     #[inline]
   1143     fn is_proper_subset(&self, _: &Self) -> bool {
   1144         false
   1145     }
   1146     #[inline]
   1147     fn is_subset(&self, val: &Self) -> bool {
   1148         self == val
   1149     }
   1150 }
   1151 impl SetOrd for Hosts<'_> {}
   1152 impl<'a> Deref for Hosts<'a> {
   1153     type Target = Domain<&'a str>;
   1154     #[inline]
   1155     fn deref(&self) -> &Self::Target {
   1156         &self.domain
   1157     }
   1158 }
   1159 impl<'a> ParsedDomain<'a> for Hosts<'a> {
   1160     type Error = FirefoxDomainErr;
   1161     #[expect(
   1162         unsafe_code,
   1163         clippy::arithmetic_side_effects,
   1164         clippy::indexing_slicing,
   1165         reason = "carefully verified use is correct"
   1166     )]
   1167     #[inline]
   1168     fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> {
   1169         let mut value = val.as_bytes().trim_ascii_start();
   1170         value.first().map_or_else(
   1171             || Ok(Value::Blank),
   1172             |byt| {
   1173                 if *byt == b'#' {
   1174                     // SAFETY:
   1175                     // `value` came from `val` with leading ASCII whitespace removed which is still valid UTF-8
   1176                     // since the first byte is '#' or '$' the remaining bytes is still valid UTF-8.
   1177                     let comment = unsafe { str::from_utf8_unchecked(&value[1..]) };
   1178                     Ok(Value::Comment(comment))
   1179                 } else {
   1180                     value = value
   1181                         .get(..3)
   1182                         .ok_or(FirefoxDomainErr::InvalidHostsIP)
   1183                         .and_then(|fst| {
   1184                             if fst == b"::1" {
   1185                                 Ok(&value[3..])
   1186                             } else if &value[..2] == b"::" {
   1187                                 Ok(&value[2..])
   1188                             } else {
   1189                                 value
   1190                                     .get(..7)
   1191                                     .ok_or(FirefoxDomainErr::InvalidHostsIP)
   1192                                     .and_then(|fst2| {
   1193                                         if fst2 == b"0.0.0.0" {
   1194                                             Ok(&value[7..])
   1195                                         } else {
   1196                                             value
   1197                                                 .get(..9)
   1198                                                 .ok_or(FirefoxDomainErr::InvalidHostsIP)
   1199                                                 .and_then(|fst3| {
   1200                                                     if fst3 == b"127.0.0.1" {
   1201                                                         Ok(&value[9..])
   1202                                                     } else {
   1203                                                         Err(FirefoxDomainErr::InvalidHostsIP)
   1204                                                     }
   1205                                                 })
   1206                                         }
   1207                                     })
   1208                             }
   1209                         })?;
   1210                     let len = value.len();
   1211                     value = value.trim_ascii_start();
   1212                     if len == value.len() {
   1213                         // There has to be at least one space or tab between the IP and domain.
   1214                         Err(FirefoxDomainErr::InvalidHostsIP)
   1215                     } else {
   1216                         domain_icann_tld(
   1217                             value[..value
   1218                                 .iter()
   1219                                 .try_fold(
   1220                                     0,
   1221                                     |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) },
   1222                                 )
   1223                                 .unwrap_or_else(convert::identity)]
   1224                                 .trim_ascii_end(),
   1225                         )
   1226                         .map(|domain| Value::Domain(Self { domain }))
   1227                     }
   1228                 }
   1229             },
   1230         )
   1231     }
   1232     #[inline]
   1233     fn domain(&self) -> &Domain<&'a str> {
   1234         &self.domain
   1235     }
   1236     #[inline]
   1237     fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> {
   1238         write_rpz_line(&mut writer, self.domain(), action, false)
   1239     }
   1240 }
   1241 /// Domain constructed from a
   1242 /// [wildcard domain rule](https://pgl.yoyo.org/adservers/serverlist.php?hostformat=adblock&showintro=0&mimetype=plaintext).
   1243 ///
   1244 /// Specifically the domain must conform to the following extended regex:
   1245 ///
   1246 /// `^<ws>*(\*\.)?<domain><ws>*(#.*)?$`
   1247 ///
   1248 /// where `<domain>` conforms to a valid [`Domain`] based on [`ASCII_FIREFOX`], the TLD is either all letters
   1249 /// or at least length five and begins with `xn--`, and `<ws>` is any sequence of
   1250 /// [ASCII whitespace](https://infra.spec.whatwg.org/#ascii-whitespace).
   1251 ///
   1252 /// If `domain` begins with `*.`, then `domain` must have length less than 252.
   1253 ///
   1254 /// Comments are any lines that start with `#` (ignoring whitespace). Any in-line comments after a valid domain
   1255 /// are ignored and will be parsed into a [`Value::Domain`].
   1256 #[derive(Clone, Debug)]
   1257 pub struct Wildcard<'a> {
   1258     /// The `Domain`.
   1259     domain: Domain<&'a str>,
   1260     /// `true` iff `domain` represents all proper subdomains. Note that this does _not_ include `domain` itself.
   1261     proper_subdomains: bool,
   1262 }
   1263 impl Wildcard<'_> {
   1264     /// Returns `true` iff the contained [`Domain`] represents all proper subdomains. Note this does _not_
   1265     /// include the `Domain` itself.
   1266     #[inline]
   1267     #[must_use]
   1268     pub const fn is_proper_subdomains(&self) -> bool {
   1269         self.proper_subdomains
   1270     }
   1271     /// Read [`Adblock::cmp_wildcard`].
   1272     #[inline]
   1273     #[must_use]
   1274     pub fn cmp_adblock(&self, other: &Adblock<'_>) -> Ordering {
   1275         other.cmp_wildcard(self).reverse()
   1276     }
   1277     /// Since `DomainOnly` and `Hosts` are treated the same, we have this helper function that can be used
   1278     /// for both.
   1279     #[must_use]
   1280     fn cmp_dom(&self, other: &Domain<&str>) -> Ordering {
   1281         match self.domain.cmp_by_domain_ordering(other) {
   1282             DomainOrdering::Less => Ordering::Less,
   1283             DomainOrdering::Shorter => {
   1284                 if self.proper_subdomains {
   1285                     Ordering::Greater
   1286                 } else {
   1287                     Ordering::Less
   1288                 }
   1289             }
   1290             DomainOrdering::Equal => {
   1291                 if self.proper_subdomains {
   1292                     Ordering::Greater
   1293                 } else {
   1294                     Ordering::Equal
   1295                 }
   1296             }
   1297             DomainOrdering::Longer | DomainOrdering::Greater => Ordering::Greater,
   1298         }
   1299     }
   1300     /// The total order that is defined follows the following hierarchy:
   1301     /// 1. Pairwise comparisons of each [`ascii_domain::dom::Label`] starting from the TLDs.
   1302     /// 2. If 1. evaluates as not equivalent, then return the result.
   1303     /// 3. If `self` represents a single `Domain` (i.e., `!self.is_proper_subdomains()`),
   1304     ///    then return the comparison of label counts.
   1305     /// 4. Return `self` is greater.
   1306     ///
   1307     /// For example, `com` `<` `example.com` `<` `*.example.com` `<` `*.com` `<` `net` `<` `example.net` `<` `*.example.net` `<` `*.net`.
   1308     #[inline]
   1309     #[must_use]
   1310     pub fn cmp_domain_only(&self, other: &DomainOnly<'_>) -> Ordering {
   1311         self.cmp_dom(&other.domain)
   1312     }
   1313     /// Read [`Wildcard::cmp_domain_only`].
   1314     #[inline]
   1315     #[must_use]
   1316     pub fn cmp_hosts(&self, other: &Hosts<'_>) -> Ordering {
   1317         self.cmp_dom(&other.domain)
   1318     }
   1319     /// Same as [`Wildcard::cardinality`] except that a `BigUint` is returned. Note the count does _not_ include
   1320     /// the `Domain` itself when `self.is_proper_subdomains()`.
   1321     ///
   1322     /// `!self.is_proper_subdomains()` ⇔ `self.domain_count() == BigUint::new(vec![1])`.
   1323     #[inline]
   1324     #[must_use]
   1325     pub fn domain_count(&self) -> BigUint {
   1326         if self.proper_subdomains {
   1327             proper_subdomain_count(&self.domain)
   1328         } else {
   1329             BigUint::new(vec![1])
   1330         }
   1331     }
   1332 }
   1333 impl PartialEq<Wildcard<'_>> for Wildcard<'_> {
   1334     #[inline]
   1335     fn eq(&self, other: &Wildcard<'_>) -> bool {
   1336         self.domain == other.domain && self.proper_subdomains == other.proper_subdomains
   1337     }
   1338 }
   1339 impl PartialEq<Wildcard<'_>> for &Wildcard<'_> {
   1340     #[inline]
   1341     fn eq(&self, other: &Wildcard<'_>) -> bool {
   1342         **self == *other
   1343     }
   1344 }
   1345 impl PartialEq<&Wildcard<'_>> for Wildcard<'_> {
   1346     #[inline]
   1347     fn eq(&self, other: &&Wildcard<'_>) -> bool {
   1348         *self == **other
   1349     }
   1350 }
   1351 impl PartialEq<Adblock<'_>> for Wildcard<'_> {
   1352     #[inline]
   1353     fn eq(&self, other: &Adblock<'_>) -> bool {
   1354         other == self
   1355     }
   1356 }
   1357 impl PartialEq<DomainOnly<'_>> for Wildcard<'_> {
   1358     #[inline]
   1359     fn eq(&self, other: &DomainOnly<'_>) -> bool {
   1360         other == self
   1361     }
   1362 }
   1363 impl PartialEq<Hosts<'_>> for Wildcard<'_> {
   1364     #[inline]
   1365     fn eq(&self, other: &Hosts<'_>) -> bool {
   1366         other == self
   1367     }
   1368 }
   1369 impl Eq for Wildcard<'_> {}
   1370 impl Hash for Wildcard<'_> {
   1371     #[inline]
   1372     fn hash<H: Hasher>(&self, state: &mut H) {
   1373         self.domain.hash(state);
   1374     }
   1375 }
   1376 impl PartialOrd<Wildcard<'_>> for Wildcard<'_> {
   1377     #[inline]
   1378     fn partial_cmp(&self, other: &Wildcard<'_>) -> Option<Ordering> {
   1379         Some(self.cmp(other))
   1380     }
   1381 }
   1382 impl Ord for Wildcard<'_> {
   1383     /// The total order that is defined follows the following hierarchy:
   1384     /// 1. Pairwise comparisons of each [`ascii_domain::dom::Label`] starting from the TLDs.
   1385     /// 2. If 1. evaluates as not equivalent, then return the result.
   1386     /// 3. If both domains represent a single `Domain`, then return the comparison
   1387     ///    of label counts.
   1388     /// 4. If one domain represents a single `Domain`, then return that that domain is less.
   1389     /// 5. Return the inverse of the comparison of label counts.
   1390     ///
   1391     /// For example, `com` `<` `example.com` `<` `*.example.com` `<` `*.com` `<` `net` `<` `example.net` `<` `*.example.net` `<` `*.net`.
   1392     #[inline]
   1393     fn cmp(&self, other: &Self) -> Ordering {
   1394         match self.domain.cmp_by_domain_ordering(&other.domain) {
   1395             DomainOrdering::Less => Ordering::Less,
   1396             DomainOrdering::Shorter => {
   1397                 if self.proper_subdomains {
   1398                     Ordering::Greater
   1399                 } else {
   1400                     Ordering::Less
   1401                 }
   1402             }
   1403             DomainOrdering::Equal => {
   1404                 if self.proper_subdomains {
   1405                     if other.proper_subdomains {
   1406                         Ordering::Equal
   1407                     } else {
   1408                         Ordering::Greater
   1409                     }
   1410                 } else if other.proper_subdomains {
   1411                     Ordering::Less
   1412                 } else {
   1413                     Ordering::Equal
   1414                 }
   1415             }
   1416             DomainOrdering::Longer => {
   1417                 if self.proper_subdomains {
   1418                     if other.proper_subdomains {
   1419                         Ordering::Less
   1420                     } else {
   1421                         Ordering::Greater
   1422                     }
   1423                 } else if other.proper_subdomains {
   1424                     Ordering::Less
   1425                 } else {
   1426                     Ordering::Greater
   1427                 }
   1428             }
   1429             DomainOrdering::Greater => Ordering::Greater,
   1430         }
   1431     }
   1432 }
   1433 impl PartialOrd<Adblock<'_>> for Wildcard<'_> {
   1434     #[inline]
   1435     fn partial_cmp(&self, other: &Adblock<'_>) -> Option<Ordering> {
   1436         Some(self.cmp_adblock(other))
   1437     }
   1438 }
   1439 impl PartialOrd<DomainOnly<'_>> for Wildcard<'_> {
   1440     #[inline]
   1441     fn partial_cmp(&self, other: &DomainOnly<'_>) -> Option<Ordering> {
   1442         Some(self.cmp_domain_only(other))
   1443     }
   1444 }
   1445 impl PartialOrd<Hosts<'_>> for Wildcard<'_> {
   1446     #[inline]
   1447     fn partial_cmp(&self, other: &Hosts<'_>) -> Option<Ordering> {
   1448         Some(self.cmp_hosts(other))
   1449     }
   1450 }
   1451 impl Display for Wildcard<'_> {
   1452     #[inline]
   1453     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
   1454         write!(
   1455             f,
   1456             "{}{}",
   1457             if self.proper_subdomains { "*." } else { "" },
   1458             self.domain
   1459         )
   1460     }
   1461 }
   1462 impl<'a> Set for Wildcard<'a> {
   1463     type Elem = Domain<&'a str>;
   1464     #[inline]
   1465     fn bounded_cardinality(&self) -> BoundedCardinality {
   1466         BoundedCardinality::from_biguint_exact(self.domain_count())
   1467     }
   1468     #[inline]
   1469     fn cardinality(&self) -> Option<Cardinality> {
   1470         Some(Cardinality::Finite(self.domain_count()))
   1471     }
   1472     #[inline]
   1473     fn contains<Q>(&self, elem: &Q) -> bool
   1474     where
   1475         Q: Borrow<Self::Elem> + Eq + ?Sized,
   1476     {
   1477         if self.proper_subdomains {
   1478             self.domain.cmp_by_domain_ordering(elem.borrow()) == DomainOrdering::Shorter
   1479         } else {
   1480             self.domain == *elem.borrow()
   1481         }
   1482     }
   1483     #[inline]
   1484     fn is_proper_subset(&self, val: &Self) -> bool {
   1485         // A single domain can never be a proper superset. Proper subdomains cannot be a proper superset if it
   1486         // has more labels or the same number of labels as another domain. In all other cases, we need to
   1487         // recursively check from the TLD that the labels are the same.
   1488         val.proper_subdomains
   1489             && val.domain.cmp_by_domain_ordering(&self.domain) == DomainOrdering::Shorter
   1490     }
   1491     #[inline]
   1492     fn is_subset(&self, val: &Self) -> bool {
   1493         self == val || self.is_proper_subset(val)
   1494     }
   1495 }
   1496 impl SetOrd for Wildcard<'_> {}
   1497 impl<'a> Deref for Wildcard<'a> {
   1498     type Target = Domain<&'a str>;
   1499     #[inline]
   1500     fn deref(&self) -> &Self::Target {
   1501         &self.domain
   1502     }
   1503 }
   1504 impl<'a> ParsedDomain<'a> for Wildcard<'a> {
   1505     type Error = FirefoxDomainErr;
   1506     #[expect(
   1507         unsafe_code,
   1508         clippy::arithmetic_side_effects,
   1509         clippy::indexing_slicing,
   1510         reason = "need them all. care has been taken."
   1511     )]
   1512     #[inline]
   1513     fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> {
   1514         let value = val.as_bytes().trim_ascii_start();
   1515         value.first().map_or_else(
   1516             || Ok(Value::Blank),
   1517             |byt| {
   1518                 if *byt == b'#' {
   1519                     // SAFETY:
   1520                     // `value` came from `val` with leading ASCII whitespace removed which is still valid UTF-8
   1521                     // since the first byte is '#' or '$' the remaining bytes is still valid UTF-8.
   1522                     let comment = unsafe { str::from_utf8_unchecked(&value[1..]) };
   1523                     Ok(Value::Comment(comment))
   1524                 } else {
   1525                     let (proper_subdomains, val2) = value.get(..2).map_or_else(
   1526                         || (false, value),
   1527                         |fst| {
   1528                             if fst == b"*." {
   1529                                 (true, &value[2..])
   1530                             } else {
   1531                                 (false, value)
   1532                             }
   1533                         },
   1534                     );
   1535                     domain_icann_tld(
   1536                         val2[..val2
   1537                             .iter()
   1538                             .try_fold(0, |i, byt2| if *byt2 == b'#' { Err(i) } else { Ok(i + 1) })
   1539                             .unwrap_or_else(convert::identity)]
   1540                             .trim_ascii_end(),
   1541                     )
   1542                     .and_then(|domain| {
   1543                         if proper_subdomains {
   1544                             if domain.len().get() > 251 {
   1545                                 Err(FirefoxDomainErr::InvalidWildcardDomain)
   1546                             } else {
   1547                                 Ok(Value::Domain(Self {
   1548                                     domain,
   1549                                     proper_subdomains: true,
   1550                                 }))
   1551                             }
   1552                         } else {
   1553                             Ok(Value::Domain(Self {
   1554                                 domain,
   1555                                 proper_subdomains,
   1556                             }))
   1557                         }
   1558                     })
   1559                 }
   1560             },
   1561         )
   1562     }
   1563     #[inline]
   1564     fn domain(&self) -> &Domain<&'a str> {
   1565         &self.domain
   1566     }
   1567     #[inline]
   1568     fn write_to_rpz<W: Write>(&self, action: RpzAction, mut writer: W) -> Result<(), Error> {
   1569         write_rpz_line(&mut writer, self.domain(), action, self.proper_subdomains)
   1570     }
   1571 }
   1572 /// A [`Domain`] in a [response policy zone (RPZ)](https://en.wikipedia.org/wiki/Response_policy_zone) file.
   1573 #[derive(Clone, Debug)]
   1574 pub enum RpzDomain<'a> {
   1575     /// An `Adblock` domain.
   1576     Adblock(Adblock<'a>),
   1577     /// A `DomainOnly` domain.
   1578     DomainOnly(DomainOnly<'a>),
   1579     /// A `Hosts` domain.
   1580     Hosts(Hosts<'a>),
   1581     /// A `Wildcard` domain.
   1582     Wildcard(Wildcard<'a>),
   1583 }
   1584 impl RpzDomain<'_> {
   1585     /// Returns `true` iff `self` represents a single [`Domain`].
   1586     #[inline]
   1587     #[must_use]
   1588     pub const fn is_domain(&self) -> bool {
   1589         match *self {
   1590             Self::Adblock(ref dom) => !dom.subdomains,
   1591             Self::DomainOnly(_) | Self::Hosts(_) => true,
   1592             Self::Wildcard(ref dom) => !dom.proper_subdomains,
   1593         }
   1594     }
   1595     /// Returns `true` iff `self` represents proper subdomains of the contained [`Domain`] (i.e.,
   1596     /// is a [`Wildcard`] such that [`Wildcard::is_proper_subdomains`]).
   1597     #[inline]
   1598     #[must_use]
   1599     pub const fn is_proper_subdomains(&self) -> bool {
   1600         match *self {
   1601             Self::Adblock(_) | Self::DomainOnly(_) | Self::Hosts(_) => false,
   1602             Self::Wildcard(ref dom) => dom.proper_subdomains,
   1603         }
   1604     }
   1605     /// Returns `true` iff `self` represents subdomains of the contained [`Domain`] (i.e., is an
   1606     /// [`Adblock`] such that [`Adblock::is_subdomains`]).
   1607     #[inline]
   1608     #[must_use]
   1609     pub const fn is_subdomains(&self) -> bool {
   1610         match *self {
   1611             Self::Adblock(ref dom) => dom.subdomains,
   1612             Self::DomainOnly(_) | Self::Hosts(_) | Self::Wildcard(_) => false,
   1613         }
   1614     }
   1615     /// Returns the count of [`Domain`]s represented by `self`. This function is the same as
   1616     /// [`RpzDomain::cardinality`] except that it returns a `BigUint`.
   1617     #[inline]
   1618     #[must_use]
   1619     pub fn domain_count(&self) -> BigUint {
   1620         match *self {
   1621             Self::Adblock(ref dom) => dom.domain_count(),
   1622             Self::DomainOnly(ref dom) => dom.domain_count().get().into(),
   1623             Self::Hosts(ref dom) => dom.domain_count().get().into(),
   1624             Self::Wildcard(ref dom) => dom.domain_count(),
   1625         }
   1626     }
   1627 }
   1628 impl PartialEq<RpzDomain<'_>> for RpzDomain<'_> {
   1629     #[inline]
   1630     fn eq(&self, other: &RpzDomain<'_>) -> bool {
   1631         match *self {
   1632             Self::Adblock(ref dom) => match *other {
   1633                 RpzDomain::Adblock(ref dom2) => dom == dom2,
   1634                 RpzDomain::DomainOnly(ref dom2) => dom == dom2,
   1635                 RpzDomain::Hosts(ref dom2) => dom == dom2,
   1636                 RpzDomain::Wildcard(ref dom2) => dom == dom2,
   1637             },
   1638             Self::DomainOnly(ref dom) => match *other {
   1639                 RpzDomain::Adblock(ref dom2) => dom == dom2,
   1640                 RpzDomain::DomainOnly(ref dom2) => dom == dom2,
   1641                 RpzDomain::Hosts(ref dom2) => dom == dom2,
   1642                 RpzDomain::Wildcard(ref dom2) => dom == dom2,
   1643             },
   1644             Self::Hosts(ref dom) => match *other {
   1645                 RpzDomain::Adblock(ref dom2) => dom == dom2,
   1646                 RpzDomain::DomainOnly(ref dom2) => dom == dom2,
   1647                 RpzDomain::Hosts(ref dom2) => dom == dom2,
   1648                 RpzDomain::Wildcard(ref dom2) => dom == dom2,
   1649             },
   1650             Self::Wildcard(ref dom) => match *other {
   1651                 RpzDomain::Adblock(ref dom2) => dom == dom2,
   1652                 RpzDomain::DomainOnly(ref dom2) => dom == dom2,
   1653                 RpzDomain::Hosts(ref dom2) => dom == dom2,
   1654                 RpzDomain::Wildcard(ref dom2) => dom == dom2,
   1655             },
   1656         }
   1657     }
   1658 }
   1659 impl PartialEq<RpzDomain<'_>> for &RpzDomain<'_> {
   1660     #[inline]
   1661     fn eq(&self, other: &RpzDomain<'_>) -> bool {
   1662         **self == *other
   1663     }
   1664 }
   1665 impl PartialEq<&RpzDomain<'_>> for RpzDomain<'_> {
   1666     #[inline]
   1667     fn eq(&self, other: &&RpzDomain<'_>) -> bool {
   1668         *self == **other
   1669     }
   1670 }
   1671 impl Eq for RpzDomain<'_> {}
   1672 impl Hash for RpzDomain<'_> {
   1673     #[inline]
   1674     fn hash<H: Hasher>(&self, state: &mut H) {
   1675         self.domain().hash(state);
   1676     }
   1677 }
   1678 impl PartialOrd<RpzDomain<'_>> for RpzDomain<'_> {
   1679     #[inline]
   1680     fn partial_cmp(&self, other: &RpzDomain<'_>) -> Option<Ordering> {
   1681         Some(self.cmp(other))
   1682     }
   1683 }
   1684 impl Ord for RpzDomain<'_> {
   1685     /// The total order that is defined follows the following hierarchy:
   1686     /// 1. Pairwise comparisons of each [`ascii_domain::dom::Label`] starting from the TLDs.
   1687     /// 2. If 1. evaluates as not equivalent, then return the result.
   1688     /// 3. If both domains represent a single `Domain`, then return the comparison
   1689     ///    of label counts.
   1690     /// 4. If one domain represents a single `Domain`, then return that that domain is less.
   1691     /// 5. If the label counts are the same and exactly one domain represents proper subdomains, the other domain is greater.
   1692     /// 6. Return the inverse of the comparison of label counts.
   1693     ///
   1694     /// For example the following is a sequence of domains in
   1695     /// ascending order:
   1696     ///
   1697     /// `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`.
   1698     #[inline]
   1699     fn cmp(&self, other: &Self) -> Ordering {
   1700         match *self {
   1701             Self::Adblock(ref dom) => match *other {
   1702                 Self::Adblock(ref dom2) => dom.cmp(dom2),
   1703                 Self::DomainOnly(ref dom2) => dom.cmp_domain_only(dom2),
   1704                 Self::Hosts(ref dom2) => dom.cmp_hosts(dom2),
   1705                 Self::Wildcard(ref dom2) => dom.cmp_wildcard(dom2),
   1706             },
   1707             Self::DomainOnly(ref dom) => match *other {
   1708                 Self::Adblock(ref dom2) => dom.cmp_adblock(dom2),
   1709                 Self::DomainOnly(ref dom2) => dom.cmp(dom2),
   1710                 Self::Hosts(ref dom2) => dom.cmp_hosts(dom2),
   1711                 Self::Wildcard(ref dom2) => dom.cmp_wildcard(dom2),
   1712             },
   1713             Self::Hosts(ref dom) => match *other {
   1714                 Self::Adblock(ref dom2) => dom.cmp_adblock(dom2),
   1715                 Self::DomainOnly(ref dom2) => dom.cmp_domain_only(dom2),
   1716                 Self::Hosts(ref dom2) => dom.cmp(dom2),
   1717                 Self::Wildcard(ref dom2) => dom.cmp_wildcard(dom2),
   1718             },
   1719             Self::Wildcard(ref dom) => match *other {
   1720                 Self::Adblock(ref dom2) => dom.cmp_adblock(dom2),
   1721                 Self::DomainOnly(ref dom2) => dom.cmp_domain_only(dom2),
   1722                 Self::Hosts(ref dom2) => dom.cmp_hosts(dom2),
   1723                 Self::Wildcard(ref dom2) => dom.cmp(dom2),
   1724             },
   1725         }
   1726     }
   1727 }
   1728 impl Display for RpzDomain<'_> {
   1729     #[inline]
   1730     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
   1731         match *self {
   1732             Self::Adblock(ref dom) => dom.fmt(f),
   1733             Self::DomainOnly(ref dom) => dom.fmt(f),
   1734             Self::Hosts(ref dom) => dom.fmt(f),
   1735             Self::Wildcard(ref dom) => dom.fmt(f),
   1736         }
   1737     }
   1738 }
   1739 impl<'a> Set for RpzDomain<'a> {
   1740     type Elem = Domain<&'a str>;
   1741     #[inline]
   1742     fn bounded_cardinality(&self) -> BoundedCardinality {
   1743         BoundedCardinality::from_biguint_exact(self.domain_count())
   1744     }
   1745     #[inline]
   1746     fn cardinality(&self) -> Option<Cardinality> {
   1747         Some(Cardinality::Finite(self.domain_count()))
   1748     }
   1749     #[inline]
   1750     fn contains<Q>(&self, elem: &Q) -> bool
   1751     where
   1752         Q: Borrow<Self::Elem> + Eq + ?Sized,
   1753     {
   1754         match *self {
   1755             Self::Adblock(ref dom) => dom.contains(elem),
   1756             Self::DomainOnly(ref dom) => dom.contains(elem),
   1757             Self::Hosts(ref dom) => dom.contains(elem),
   1758             Self::Wildcard(ref dom) => dom.contains(elem),
   1759         }
   1760     }
   1761     #[inline]
   1762     fn is_proper_subset(&self, val: &Self) -> bool {
   1763         match *val {
   1764             Self::Adblock(ref dom) => {
   1765                 dom.subdomains
   1766                     && match *self {
   1767                         Self::Adblock(ref dom2) => {
   1768                             dom.domain.cmp_by_domain_ordering(&dom2.domain)
   1769                                 == DomainOrdering::Shorter
   1770                         }
   1771                         Self::DomainOnly(ref dom2) => matches!(
   1772                             dom.domain.cmp_by_domain_ordering(&dom2.domain),
   1773                             DomainOrdering::Shorter | DomainOrdering::Equal
   1774                         ),
   1775                         Self::Hosts(ref dom2) => matches!(
   1776                             dom.domain.cmp_by_domain_ordering(&dom2.domain),
   1777                             DomainOrdering::Shorter | DomainOrdering::Equal
   1778                         ),
   1779                         Self::Wildcard(ref dom2) => matches!(
   1780                             dom.domain.cmp_by_domain_ordering(&dom2.domain),
   1781                             DomainOrdering::Shorter | DomainOrdering::Equal
   1782                         ),
   1783                     }
   1784             }
   1785             Self::DomainOnly(_) | Self::Hosts(_) => false,
   1786             Self::Wildcard(ref dom) => {
   1787                 dom.proper_subdomains
   1788                     && match *self {
   1789                         Self::Adblock(ref dom2) => {
   1790                             dom.domain.cmp_by_domain_ordering(&dom2.domain)
   1791                                 == DomainOrdering::Shorter
   1792                         }
   1793                         Self::DomainOnly(ref dom2) => {
   1794                             dom.domain.cmp_by_domain_ordering(&dom2.domain)
   1795                                 == DomainOrdering::Shorter
   1796                         }
   1797                         Self::Hosts(ref dom2) => {
   1798                             dom.domain.cmp_by_domain_ordering(&dom2.domain)
   1799                                 == DomainOrdering::Shorter
   1800                         }
   1801                         Self::Wildcard(ref dom2) => {
   1802                             dom.domain.cmp_by_domain_ordering(&dom2.domain)
   1803                                 == DomainOrdering::Shorter
   1804                         }
   1805                     }
   1806             }
   1807         }
   1808     }
   1809     #[inline]
   1810     fn is_subset(&self, val: &Self) -> bool {
   1811         self == val || self.is_proper_subset(val)
   1812     }
   1813 }
   1814 impl SetOrd for RpzDomain<'_> {}
   1815 impl<'a> Deref for RpzDomain<'a> {
   1816     type Target = Domain<&'a str>;
   1817     #[inline]
   1818     fn deref(&self) -> &Self::Target {
   1819         match *self {
   1820             Self::Adblock(ref dom) => &dom.domain,
   1821             Self::DomainOnly(ref dom) => &dom.domain,
   1822             Self::Hosts(ref dom) => &dom.domain,
   1823             Self::Wildcard(ref dom) => &dom.domain,
   1824         }
   1825     }
   1826 }
   1827 impl<'a: 'b, 'b> From<Adblock<'a>> for RpzDomain<'b> {
   1828     #[inline]
   1829     fn from(value: Adblock<'a>) -> Self {
   1830         Self::Adblock(value)
   1831     }
   1832 }
   1833 impl<'a: 'b, 'b> From<DomainOnly<'a>> for RpzDomain<'b> {
   1834     #[inline]
   1835     fn from(value: DomainOnly<'a>) -> Self {
   1836         Self::DomainOnly(value)
   1837     }
   1838 }
   1839 impl<'a: 'b, 'b> From<Hosts<'a>> for RpzDomain<'b> {
   1840     #[inline]
   1841     fn from(value: Hosts<'a>) -> Self {
   1842         Self::Hosts(value)
   1843     }
   1844 }
   1845 impl<'a: 'b, 'b> From<Wildcard<'a>> for RpzDomain<'b> {
   1846     #[inline]
   1847     fn from(value: Wildcard<'a>) -> Self {
   1848         Self::Wildcard(value)
   1849     }
   1850 }
   1851 impl<'a> ParsedDomain<'a> for RpzDomain<'a> {
   1852     type Error = FirefoxDomainErr;
   1853     #[inline]
   1854     fn parse_value<'b: 'a>(val: &'b str) -> Result<Value<'a, Self>, Self::Error> {
   1855         DomainOnly::parse_value(val).map_or_else(
   1856             |_| {
   1857                 Hosts::parse_value(val).map_or_else(
   1858                     |_| {
   1859                         Wildcard::parse_value(val).map_or_else(
   1860                             |_| {
   1861                                 Adblock::parse_value(val).map(|value| match value {
   1862                                     Value::Domain(dom) => Value::Domain(Self::Adblock(dom)),
   1863                                     Value::Comment(com) => Value::Comment(com),
   1864                                     Value::Blank => Value::Blank,
   1865                                 })
   1866                             },
   1867                             |value| {
   1868                                 Ok(match value {
   1869                                     Value::Domain(dom) => Value::Domain(Self::Wildcard(dom)),
   1870                                     Value::Comment(com) => Value::Comment(com),
   1871                                     Value::Blank => Value::Blank,
   1872                                 })
   1873                             },
   1874                         )
   1875                     },
   1876                     |value| {
   1877                         Ok(match value {
   1878                             Value::Domain(dom) => Value::Domain(Self::Hosts(dom)),
   1879                             Value::Comment(com) => Value::Comment(com),
   1880                             Value::Blank => Value::Blank,
   1881                         })
   1882                     },
   1883                 )
   1884             },
   1885             |value| {
   1886                 Ok(match value {
   1887                     Value::Domain(dom) => Value::Domain(Self::DomainOnly(dom)),
   1888                     Value::Comment(com) => Value::Comment(com),
   1889                     Value::Blank => Value::Blank,
   1890                 })
   1891             },
   1892         )
   1893     }
   1894     #[inline]
   1895     fn domain(&self) -> &Domain<&'a str> {
   1896         match *self {
   1897             Self::Adblock(ref dom) => &dom.domain,
   1898             Self::DomainOnly(ref dom) => &dom.domain,
   1899             Self::Hosts(ref dom) => &dom.domain,
   1900             Self::Wildcard(ref dom) => &dom.domain,
   1901         }
   1902     }
   1903     #[inline]
   1904     fn write_to_rpz<W: Write>(&self, action: RpzAction, writer: W) -> Result<(), Error> {
   1905         match *self {
   1906             Self::Adblock(ref dom) => dom.write_to_rpz(action, writer),
   1907             Self::DomainOnly(ref dom) => dom.write_to_rpz(action, writer),
   1908             Self::Hosts(ref dom) => dom.write_to_rpz(action, writer),
   1909             Self::Wildcard(ref dom) => dom.write_to_rpz(action, writer),
   1910         }
   1911     }
   1912 }