ascii_domain

Domains whose labels are only ASCII.
git clone https://git.philomathiclife.com/repos/ascii_domain
Log | Files | Refs | README

char_set.rs (11269B)


      1 #[cfg(test)]
      2 /// Unit tests.
      3 mod tests;
      4 use core::{
      5     error::Error,
      6     fmt::{self, Display, Formatter},
      7     str,
      8 };
      9 /// Error returned from [`AllowedAscii::try_from_unique_ascii`].
     10 #[expect(variant_size_differences, reason = "usize is fine in size")]
     11 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
     12 pub enum AsciiErr {
     13     /// Since `AllowedAscii` only allows unique ASCII characters and doesn't allow `b'.'`, the maximum count is
     14     /// 127. This variant is returned when the count exceeds that.
     15     CountTooLarge(usize),
     16     /// The contained `u8` is not valid ASCII (i.e., it is strictly greater than 127).
     17     InvalidByte(u8),
     18     /// `b'.'` was in the allowed ASCII. It is the only ASCII value not allowed since it is always used
     19     /// as a [`crate::dom::Label`] separator.
     20     Contains46,
     21     /// The contained ASCII appeared more than once.
     22     Duplicate(u8),
     23 }
     24 impl Display for AsciiErr {
     25     #[inline]
     26     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
     27         match *self {
     28             Self::CountTooLarge(byt) => {
     29                 write!(f, "the allowed ASCII had {byt} values, but 127 is the max")
     30             }
     31             Self::InvalidByte(byt) => {
     32                 write!(f, "allowed ASCII was passed the invalid byte value {byt}")
     33             }
     34             Self::Contains46 => f.write_str("allowed ASCII contains '.'"),
     35             Self::Duplicate(byt) => {
     36                 let input = [byt];
     37                 if let Ok(val) = str::from_utf8(input.as_slice()) {
     38                     write!(f, "allowed ASCII has the duplicate value '{val}'")
     39                 } else {
     40                     write!(f, "allowed ASCII has the invalid value '{byt}'")
     41                 }
     42             }
     43         }
     44     }
     45 }
     46 impl Error for AsciiErr {}
     47 /// Container of the ASCII `u8`s that are allowed to appear in a [`crate::dom::Label`].
     48 ///
     49 /// Note that while
     50 /// [`crate::dom::Domain`] treats ASCII uppercase letters as lowercase, it still depends on such `u8`s being
     51 /// included. For example if `b'A'` is not included, then `b'A'` is not allowed even if `b'a'` is included.
     52 ///
     53 /// It is _highly_ unlikely that non-printable ASCII nor `b'\\'` should be used since such ASCII would almost
     54 /// certainly require being escaped.
     55 #[cfg_attr(test, derive(Eq, PartialEq))]
     56 #[derive(Debug)]
     57 pub struct AllowedAscii<T> {
     58     /// The allowed ASCII `u8`s.
     59     allowed: T,
     60 }
     61 impl<T> AllowedAscii<T> {
     62     /// Returns a reference to the contained value.
     63     ///
     64     /// # Example
     65     ///
     66     /// ```
     67     /// use ascii_domain::char_set;
     68     /// assert!(char_set::ASCII_LETTERS.as_inner().len() == 52);
     69     /// ```
     70     #[inline]
     71     pub const fn as_inner(&self) -> &T {
     72         &self.allowed
     73     }
     74     /// Returns the contained value consuming `self`.
     75     ///
     76     /// # Example
     77     ///
     78     /// ```
     79     /// use ascii_domain::char_set;
     80     /// assert!(char_set::ASCII_LETTERS.into_inner().len() == 52);
     81     /// ```
     82     #[inline]
     83     pub fn into_inner(self) -> T {
     84         self.allowed
     85     }
     86 }
     87 impl<T: AsRef<[u8]>> AllowedAscii<T> {
     88     /// Returns `true` iff `val` is an allowed ASCII value in a [`crate::dom::Label`].
     89     ///
     90     /// # Example
     91     ///
     92     /// ```
     93     /// use ascii_domain::char_set;
     94     /// assert!(char_set::ASCII_LETTERS.contains(b'a'));
     95     /// ```
     96     #[inline]
     97     #[must_use]
     98     pub fn contains(&self, val: u8) -> bool {
     99         // We sort `allowed` in `try_from_unique_ascii`, so `binary_search` is fine.
    100         self.allowed.as_ref().binary_search(&val).is_ok()
    101     }
    102     /// Returns the number of allowed ASCII characters.
    103     ///
    104     /// # Example
    105     ///
    106     /// ```
    107     /// use ascii_domain::char_set;
    108     /// assert!(char_set::ASCII_LETTERS.len() == 52);
    109     /// ```
    110     #[expect(
    111         clippy::as_conversions,
    112         clippy::cast_possible_truncation,
    113         reason = "comment justifies its correctness"
    114     )]
    115     #[inline]
    116     #[must_use]
    117     pub fn len(&self) -> u8 {
    118         // We enforce only unique non `b'.'` ASCII in `try_from_unique_ascii` which among other things means
    119         // the max count is 127 so truncation will not occur
    120         self.allowed.as_ref().len() as u8
    121     }
    122     /// Returns `true` iff `self` does not contain any ASCII.
    123     ///
    124     /// # Examples
    125     ///
    126     /// ```
    127     /// use ascii_domain::char_set::{self, AllowedAscii};
    128     /// assert!(!char_set::ASCII_LETTERS.is_empty());
    129     /// assert!(AllowedAscii::try_from_unique_ascii([]).unwrap().is_empty());
    130     /// ```
    131     #[inline]
    132     #[must_use]
    133     pub fn is_empty(&self) -> bool {
    134         self.allowed.as_ref().is_empty()
    135     }
    136 }
    137 impl<T: AsMut<[u8]>> AllowedAscii<T> {
    138     /// `allowed` must contain unique ASCII `u8`s. Note it is likely `allowed` should be a subset of
    139     /// [`PRINTABLE_ASCII`] since any other ASCII would likely require some form of escape character logic.
    140     /// Additionally, it is likely `allowed.as_mut().len()` should be greater than 0; otherwise the returned
    141     /// `AllowedAscii` will always cause [`crate::dom::Domain::try_from_bytes`] to error since `Domain` requires
    142     /// at least one non-root [`crate::dom::Label`].
    143     ///
    144     /// `allowed` is mutated such that `allowed.as_mut()` is sorted in order.
    145     ///
    146     /// # Errors
    147     ///
    148     /// Returns `AsciiError` iff `allowed` does not contain a set of unique ASCII `u8`s or contains `b'.'`.
    149     ///
    150     /// # Examples
    151     ///
    152     /// ```
    153     /// use ascii_domain::char_set::{AllowedAscii, AsciiErr};
    154     /// assert!(AllowedAscii::try_from_unique_ascii(b"asdfghjkl".to_owned()).map_or(false, |ascii| ascii.contains(b'a') && !ascii.contains(b'A')));
    155     /// assert!(AllowedAscii::try_from_unique_ascii(b"aa".to_owned()).map_or_else(|err| err == AsciiErr::Duplicate(b'a'), |_| false));
    156     /// assert!(AllowedAscii::try_from_unique_ascii([255]).map_or_else(|err| err == AsciiErr::InvalidByte(255), |_| false));
    157     /// assert!(AllowedAscii::try_from_unique_ascii([0; 128]).map_or_else(|err| err == AsciiErr::CountTooLarge(128), |_| false));
    158     /// assert!(AllowedAscii::try_from_unique_ascii([b'.']).map_or_else(|err| err == AsciiErr::Contains46, |_| false));
    159     /// ```
    160     #[inline]
    161     pub fn try_from_unique_ascii(mut allowed: T) -> Result<Self, AsciiErr> {
    162         let bytes = allowed.as_mut();
    163         if bytes.len() > 127 {
    164             Err(AsciiErr::CountTooLarge(bytes.len()))
    165         } else {
    166             bytes.sort_unstable();
    167             // Since `bytes` is sorted, we simply have to check the last value to determine if valid ASCII was
    168             // provided.
    169             if let Some(byt) = bytes.last() {
    170                 let b = *byt;
    171                 if b > 127 {
    172                     return Err(AsciiErr::InvalidByte(b));
    173                 }
    174             }
    175             bytes
    176                 .iter()
    177                 // 255 is not valid ASCII, so we can use it as an initializer.
    178                 .try_fold(255, |prev, b| {
    179                     let byt = *b;
    180                     if byt == b'.' {
    181                         Err(AsciiErr::Contains46)
    182                     } else if prev == byt {
    183                         Err(AsciiErr::Duplicate(prev))
    184                     } else {
    185                         Ok(byt)
    186                     }
    187                 })
    188                 .map(|_| Self { allowed })
    189         }
    190     }
    191 }
    192 /// Printable ASCII that should not need to be "escaped".
    193 ///
    194 /// That is to say printable ASCII excluding space (i.e., 32), dot (i.e. 46), and backslash (i.e., 92).
    195 /// This returns all `u8`s inclusively between 33 and 126 except 46 and 92.
    196 pub const PRINTABLE_ASCII: AllowedAscii<[u8; 92]> = AllowedAscii {
    197     allowed: *b"!\"#$%&'()*+,-/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~",
    198 };
    199 /// ASCII allowed in [RFC 5322 `atext`](https://www.rfc-editor.org/rfc/rfc5322#section-3.2.3).
    200 /// This contains the following `u8`s:
    201 ///
    202 /// 33, 35–39, 42–43, 45, 47–57, 61, 63, 65–90, and 94–126.
    203 pub const RFC5322_ATEXT: AllowedAscii<[u8; 81]> = AllowedAscii {
    204     allowed: *b"!#$%&'*+-/0123456789=?ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz{|}~",
    205 };
    206 /// ASCII allowed in a domain by [Firefox](https://www.mozilla.org/en-US/firefox/)
    207 /// as of 2023-09-03T20:50+00:00.
    208 /// This contains the following `u8`s:
    209 ///
    210 /// 33, 36, 38–41, 43–45, 48–57, 59, 61, 65–90, 95–123, and 125–126.
    211 pub const ASCII_FIREFOX: AllowedAscii<[u8; 78]> = AllowedAscii {
    212     allowed: *b"!$&'()+,-0123456789;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{}~",
    213 };
    214 /// ASCII hyphen, digits, and letters.
    215 /// This contains 45 and all `u8`s inclusively between 48 and 57, 65 and 90, and 97 and 122.
    216 pub const ASCII_HYPHEN_DIGITS_LETTERS: AllowedAscii<[u8; 63]> = AllowedAscii {
    217     allowed: *b"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
    218 };
    219 /// ASCII digits and letters.
    220 /// This contains all `u8`s inclusively between 48 and 57, 65 and 90, and 97 and 122.
    221 pub const ASCII_DIGITS_LETTERS: AllowedAscii<[u8; 62]> = AllowedAscii {
    222     allowed: *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
    223 };
    224 /// ASCII letters.
    225 /// This contains all `u8`s inclusively between 65 and 90 and 97 and 122.
    226 pub const ASCII_LETTERS: AllowedAscii<[u8; 52]> = AllowedAscii {
    227     allowed: *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
    228 };
    229 /// ASCII hyphen, digits, and uppercase letters.
    230 /// This contains 45 and all `u8`s inclusively between 48 and 57 and 65 and 90.
    231 pub const ASCII_HYPHEN_DIGITS_UPPERCASE: AllowedAscii<[u8; 37]> = AllowedAscii {
    232     allowed: *b"-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    233 };
    234 /// ASCII hyphen, digits, and lowercase letters.
    235 /// This contains 45 and all `u8`s inclusively between 48 and 57 and 97 and 122.
    236 pub const ASCII_HYPHEN_DIGITS_LOWERCASE: AllowedAscii<[u8; 37]> = AllowedAscii {
    237     allowed: *b"-0123456789abcdefghijklmnopqrstuvwxyz",
    238 };
    239 /// ASCII digits and uppercase letters.
    240 /// This contains all `u8`s inclusively between 48 and 57 and 65 and 90.
    241 pub const ASCII_DIGITS_UPPERCASE: AllowedAscii<[u8; 36]> = AllowedAscii {
    242     allowed: *b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    243 };
    244 /// ASCII digits and lowercase letters.
    245 /// This contains all `u8`s inclusively between 48 and 57 and 97 and 122.
    246 pub const ASCII_DIGITS_LOWERCASE: AllowedAscii<[u8; 36]> = AllowedAscii {
    247     allowed: *b"0123456789abcdefghijklmnopqrstuvwxyz",
    248 };
    249 /// ASCII uppercase letters.
    250 /// This contains all `u8`s inclusively between 65 and 90.
    251 pub const ASCII_UPPERCASE: AllowedAscii<[u8; 26]> = AllowedAscii {
    252     allowed: *b"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
    253 };
    254 /// ASCII lowercase letters.
    255 /// This contains all `u8`s inclusively between 97 and 122.
    256 pub const ASCII_LOWERCASE: AllowedAscii<[u8; 26]> = AllowedAscii {
    257     allowed: *b"abcdefghijklmnopqrstuvwxyz",
    258 };
    259 /// ASCII digits.
    260 /// This contains all `u8`s inclusively between 48 and 57.
    261 pub const ASCII_DIGITS: AllowedAscii<[u8; 10]> = AllowedAscii {
    262     allowed: *b"0123456789",
    263 };
    264 /// ASCII that is not a [forbidden domain code point](https://url.spec.whatwg.org/#forbidden-domain-code-point).
    265 ///
    266 /// This contains the following `u8`s:
    267 ///
    268 /// 33, 34, 36, 38–45, 48–57, 59, 61, 65–90, 95–123, and 125–126.
    269 pub const WHATWG_VALID_DOMAIN_CODE_POINTS: AllowedAscii<[u8; 80]> = AllowedAscii {
    270     allowed: *b"!\"$&'()*+,-0123456789;=ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz{}~",
    271 };