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 };