dom.rs (57813B)
1 extern crate alloc; 2 #[cfg(test)] 3 /// Unit tests. 4 mod tests; 5 use crate::char_set::{ASCII_HYPHEN_DIGITS_LETTERS, AllowedAscii}; 6 use alloc::{string::String, vec::Vec}; 7 use core::{ 8 borrow::Borrow, 9 cmp::Ordering, 10 convert, 11 error::Error, 12 fmt::{self, Display, Formatter}, 13 hash::{Hash, Hasher}, 14 iter::FusedIterator, 15 num::NonZeroU8, 16 ops::Deref, 17 str, 18 }; 19 /// The `AllowedAscii` used by `Rfc1123Domain`. 20 static RFC_CHARS: &AllowedAscii<[u8; 63]> = &ASCII_HYPHEN_DIGITS_LETTERS; 21 /// Returned by [`Domain::cmp_by_domain_ordering`]. 22 /// 23 /// It is more informative than [`Ordering`] in that it 24 /// distinguishes between a `Domain` that is greater than another `Domain` due to a [`Label`] being greater 25 /// from a `Domain` that has the same `Label`s as another but simply more. 26 /// 27 /// Another way to view this is that [`Self::Shorter`] is "closer" to being [`Self::Equal`] than [`Self::Less`] 28 /// since the `Domain`s are still part of the same branch in the DNS hierarchy. Ditto for [`Self::Longer`]. 29 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 30 pub enum DomainOrdering { 31 /// The `Domain` is less than another since a `Label` was less. 32 Less, 33 /// The `Domain` is less than other but only because it had fewer `Label`s. 34 Shorter, 35 /// The `Domain` is equal to another. 36 Equal, 37 /// The `Domain` is greater than another but only because it had more `Label`s. 38 Longer, 39 /// The `Domain` is greater than another since a `Label` was greater. 40 Greater, 41 } 42 impl Display for DomainOrdering { 43 #[inline] 44 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 45 match *self { 46 Self::Less => f.write_str("less since a label was less"), 47 Self::Shorter => f.write_str("less since there were fewer labels"), 48 Self::Equal => f.write_str("equal"), 49 Self::Longer => f.write_str("greater since there were more labels"), 50 Self::Greater => f.write_str("greater since a label was greater"), 51 } 52 } 53 } 54 impl From<DomainOrdering> for Ordering { 55 #[inline] 56 fn from(value: DomainOrdering) -> Self { 57 match value { 58 DomainOrdering::Less | DomainOrdering::Shorter => Self::Less, 59 DomainOrdering::Equal => Self::Equal, 60 DomainOrdering::Longer | DomainOrdering::Greater => Self::Greater, 61 } 62 } 63 } 64 /// A domain that consists of at least one [`Label`] with each `Label` only containing the ASCII `u8`s in 65 /// the [`AllowedAscii`] passed to [`Self::try_from_bytes`]. 66 /// 67 /// The total length of a `Domain` is at most 68 /// 253 bytes[^note] in length including the `b'.'` separator. The trailing `b'.'`, if one exists, is always 69 /// ignored. 70 /// 71 /// This is more restrictive than what a domain is allowed to be per the 72 /// [Domain Name System (DNS)](https://www.rfc-editor.org/rfc/rfc2181) since all octets/`u8`s are allowed in a 73 /// label. Additionally there is no way to represent the root domain. 74 /// 75 /// Last, ASCII uppercase letters are treated as lowercase; however for better comparison performance 76 /// that doesn't lead to intermediate memory allocations, two `Domain`s should consist entirely of the same 77 /// case. 78 /// 79 /// [^note]: It is a common misconception that the max length of a domain is 255, but that is only true for 80 /// domains in _wire_ format. In representation format, which `Domain` can be thought of when only visible 81 /// ASCII bytes are used, the max length is 253 when the last byte is not `b'.'`; otherwise the max length is 82 /// 254. This is due to the fact that there is no way to explicitly represent the root label which in wire format 83 /// contributes one byte due to each label being preceded by the octet that represents its length. 84 /// 85 /// Note this only contains `T`, so this is allocation-free and the same size as `T`. 86 #[derive(Clone, Copy, Debug)] 87 pub struct Domain<T> { 88 /// The domain value. `value.as_ref().len()` is guaranteed to have length between 1 and 253 when the last `u8` 89 /// is not `b'.'`; otherwise the length is between 2 and 254. 90 /// Guaranteed to only contain `b'.'` and the ASCII `u8`s in `allowed_ascii`. 91 value: T, 92 } 93 impl<T> Domain<T> { 94 /// A reference to the contained `T`. 95 /// 96 /// # Example 97 /// 98 /// ``` 99 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 100 /// assert!(*Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().as_inner() == "example.com."); 101 /// ``` 102 #[inline] 103 pub const fn as_inner(&self) -> &T { 104 &self.value 105 } 106 /// Same as [`Self::as_inner`] except `self` is consumed. 107 /// 108 /// # Example 109 /// 110 /// ``` 111 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 112 /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().into_inner() == "example.com."); 113 /// ``` 114 #[inline] 115 pub fn into_inner(self) -> T { 116 self.value 117 } 118 } 119 impl<T: AsRef<[u8]>> Domain<T> { 120 /// Returns `true` iff the domain contains a trailing `b'.'`. 121 /// 122 /// # Example 123 /// 124 /// ``` 125 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 126 /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().contains_trailing_dot()); 127 /// ``` 128 #[expect( 129 clippy::arithmetic_side_effects, 130 clippy::indexing_slicing, 131 reason = "comments explain their correctness" 132 )] 133 #[inline] 134 pub fn contains_trailing_dot(&self) -> bool { 135 let bytes = self.value.as_ref(); 136 // This won't underflow or `panic` since `Domain`s are not empty. 137 bytes[bytes.len() - 1] == b'.' 138 } 139 /// The domain without a trailing `b'.'` if there was one. 140 /// 141 /// # Example 142 /// 143 /// ``` 144 /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS}; 145 /// assert!(Domain::try_from_bytes("Example.com.", &ASCII_LETTERS).unwrap().as_str() == "Example.com"); 146 /// ``` 147 #[inline] 148 pub fn as_str(&self) -> &str { 149 <&str>::from(Domain::<&str>::from(Domain::<&[u8]>::from(self))) 150 } 151 /// The domain without a trailing `b'.'` if there was one. 152 /// 153 /// # Example 154 /// 155 /// ``` 156 /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS}; 157 /// assert!(Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap().as_bytes() == b"Example.com"); 158 /// ``` 159 #[inline] 160 pub fn as_bytes(&self) -> &[u8] { 161 <&[u8]>::from(Domain::<&[u8]>::from(self)) 162 } 163 /// The length of the `Domain`. This does _not_ include the trailing `b'.'` if there was one. 164 /// 165 /// # Example 166 /// 167 /// ``` 168 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 169 /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().len().get() == 11); 170 /// ``` 171 #[expect( 172 unsafe_code, 173 reason = "we enforce nonzero lengths, so NonZeroU8::new_unchecked is fine" 174 )] 175 #[expect( 176 clippy::arithmetic_side_effects, 177 clippy::as_conversions, 178 clippy::cast_possible_truncation, 179 reason = "comments justify their correctness" 180 )] 181 #[inline] 182 pub fn len(&self) -> NonZeroU8 { 183 // No fear of underflow since the length of `value` is at least 1 _not including_ the 184 // trailing `b'.'` if there was one. 185 // `true as usize` is guaranteed to be 1 and `false as usize` is guaranteed to be 0. 186 // No fear of truncation either since the length is guaranteed to be less than 255. 187 // `Domain` is immutable ensuring such invariants are kept. 188 let len = (self.value.as_ref().len() - usize::from(self.contains_trailing_dot())) as u8; 189 // SAFETY: 190 // The only way to construct a `Domain` is via `try_from_bytes` which ensures `len` is 191 // is at least 1. 192 unsafe { NonZeroU8::new_unchecked(len) } 193 } 194 /// Function that transforms `v` into a `Domain` by only allowing [`Label`]s to contain the ASCII `u8`s in 195 /// `allowed_ascii`. A trailing `b'.'` is ignored. 196 /// 197 /// Note that while ASCII uppercase is treated as ASCII lowercase, `allowed_ascii` MUST still contain 198 /// each ASCII `u8` (e.g., if `!allowed_ascii.contains(b'A')`, then `b'A'` is not allowed even if 199 /// `allowed_ascii.contains(b'a')`). 200 /// 201 /// # Examples 202 /// 203 /// ``` 204 /// use ascii_domain::{dom::{Domain, DomainErr}, char_set::ASCII_LOWERCASE}; 205 /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).is_ok()); 206 /// assert!(Domain::try_from_bytes("exam2ple.com", &ASCII_LOWERCASE).map_or_else(|err| err == DomainErr::InvalidByte(b'2'), |_| false)); 207 /// ``` 208 /// 209 /// # Errors 210 /// 211 /// Returns [`DomainErr`] iff `v.as_ref()` is an invalid `Domain`. 212 #[expect( 213 clippy::arithmetic_side_effects, 214 reason = "comment justifies its correctness" 215 )] 216 #[inline] 217 pub fn try_from_bytes<T2: AsRef<[u8]>>( 218 v: T, 219 allowed_ascii: &AllowedAscii<T2>, 220 ) -> Result<Self, DomainErr> { 221 let val = v.as_ref(); 222 let value = val 223 .split_last() 224 .ok_or(DomainErr::Empty) 225 .and_then(|(lst, rem)| { 226 if *lst == b'.' { 227 rem.split_last() 228 .ok_or(DomainErr::RootDomain) 229 .and_then(|(lst_2, _)| { 230 if *lst_2 == b'.' { 231 Err(DomainErr::EmptyLabel) 232 } else { 233 Ok(rem) 234 } 235 }) 236 } else { 237 Ok(val) 238 } 239 })?; 240 if value.len() > 253 { 241 Err(DomainErr::LenExceeds253(value.len())) 242 } else { 243 value 244 .iter() 245 .try_fold(0, |label_len, byt| { 246 let b = *byt; 247 if b == b'.' { 248 NonZeroU8::new(label_len).map_or(Err(DomainErr::EmptyLabel), |_| Ok(0)) 249 } else if !allowed_ascii.contains(b) { 250 Err(DomainErr::InvalidByte(b)) 251 } else if label_len == 63 { 252 Err(DomainErr::LabelLenExceeds63) 253 } else { 254 // This is less than 63 due to the above check, so this won't overflow; 255 Ok(label_len + 1) 256 } 257 }) 258 .map(|_| Self { value: v }) 259 } 260 } 261 /// Returns an [`Iterator`] of [`Label`]s without consuming the `Domain`. 262 /// # Example 263 /// 264 /// ``` 265 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 266 /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().into_iter().next().unwrap().as_str() == "com"); 267 /// ``` 268 #[inline] 269 pub fn iter(&self) -> LabelIter<'_> { 270 LabelIter { 271 domain: self.as_bytes(), 272 } 273 } 274 /// Returns `true` iff `self` and `right` are part of the same branch in the DNS hierarchy. 275 /// 276 /// For example `www.example.com` and `example.com` are in the `same_branch`, but `example.com` and 277 /// `foo.com` are not. 278 /// 279 /// Note that trailing `b'.'`s are ignored and ASCII uppercase and lowercase are treated the same. 280 /// 281 /// # Examples 282 /// 283 /// ``` 284 /// use ascii_domain::{dom::Domain, char_set::{ASCII_LETTERS, ASCII_LOWERCASE}}; 285 /// let dom1 = Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap(); 286 /// let dom2 = Domain::try_from_bytes("www.example.com", &ASCII_LOWERCASE).unwrap(); 287 /// assert!(dom1.same_branch(&dom2)); 288 /// let dom3 = Domain::try_from_bytes("foo.com", &ASCII_LOWERCASE).unwrap(); 289 /// assert!(!dom1.same_branch(&dom3)); 290 /// ``` 291 #[inline] 292 pub fn same_branch<T2: AsRef<[u8]>>(&self, right: &Domain<T2>) -> bool { 293 // Faster to check the values as bytes and not iterate each `Label`. 294 if self == right { 295 true 296 } else { 297 self.iter() 298 .zip(right) 299 .try_fold( 300 (), 301 |(), (label, label2)| if label == label2 { Ok(()) } else { Err(()) }, 302 ) 303 .is_ok_and(|()| true) 304 } 305 } 306 /// Same as [`Self::cmp_doms`] except returns [`DomainOrdering::Longer`] iff `self > right` due solely 307 /// to having more [`Label`]s and [`DomainOrdering::Shorter`] iff `self < right` due solely to having 308 /// fewer `Label`s. 309 /// 310 /// For example `example.com` < `www.example.com` and `bar.com` < `www.example.com`; but with this function, 311 /// `example.com` is [`DomainOrdering::Shorter`] than `www.example.com` and `www.example.com` is 312 /// [`DomainOrdering::Longer`] than `example.com`; while `bar.com` is [`DomainOrdering::Less`] than 313 /// `www.example.com` and `www.example.com` is [`DomainOrdering::Greater`] than `bar.com`. 314 /// 315 /// In other words `DomainOrdering::Shorter` implies `Ordering::Less` and `DomainOrdering::Longer` implies 316 /// `Ordering::Greater` with additional information pertaining to the quantity of `Label`s. 317 /// 318 /// # Examples 319 /// 320 /// ``` 321 /// use ascii_domain::{dom::{Domain, DomainOrdering}, char_set::{ASCII_LETTERS, ASCII_LOWERCASE}}; 322 /// let dom1 = Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap(); 323 /// assert!(matches!(dom1.cmp_by_domain_ordering(&dom1), DomainOrdering::Equal)); 324 /// let dom2 = Domain::try_from_bytes("www.example.com", &ASCII_LOWERCASE).unwrap(); 325 /// assert!(matches!(dom1.cmp_by_domain_ordering(&dom2), DomainOrdering::Shorter)); 326 /// assert!(matches!(dom2.cmp_by_domain_ordering(&dom1), DomainOrdering::Longer)); 327 /// let dom3 = Domain::try_from_bytes("foo.com", &ASCII_LOWERCASE).unwrap(); 328 /// assert!(matches!(dom1.cmp_by_domain_ordering(&dom3), DomainOrdering::Less)); 329 /// assert!(matches!(dom3.cmp_by_domain_ordering(&dom1), DomainOrdering::Greater)); 330 /// ``` 331 #[inline] 332 pub fn cmp_by_domain_ordering<T2: AsRef<[u8]>>(&self, right: &Domain<T2>) -> DomainOrdering { 333 // Faster to compare the entire value when we can instead each `Label`. 334 if self == right { 335 DomainOrdering::Equal 336 } else { 337 let mut right_iter = right.iter(); 338 self.iter() 339 .try_fold(false, |_, label| { 340 right_iter 341 .next() 342 .map_or(Ok(true), |label2| match label.cmp(&label2) { 343 Ordering::Less => Err(DomainOrdering::Less), 344 Ordering::Equal => Ok(false), 345 Ordering::Greater => Err(DomainOrdering::Greater), 346 }) 347 }) 348 .map_or_else(convert::identity, |flag| { 349 // We iterate `self` before `right`, so `flag` is `true` iff `right` 350 // has fewer `Label`s than `self`. 351 if flag { 352 DomainOrdering::Longer 353 } else { 354 // `self` has as many or fewer `Label`s than `right`; however if it had as many 355 // `Label`s as `right`, then all `Label`s are the same which is impossible since 356 // we already checked if `self == right`. 357 DomainOrdering::Shorter 358 } 359 }) 360 } 361 } 362 /// The total order that is defined follows the following hierarchy: 363 /// 1. Pairwise comparisons of each [`Label`] starting from the TLDs. 364 /// 2. If 1. evaluates as not equivalent, then return the result. 365 /// 3. Return the comparison of `Label` counts. 366 /// 367 /// For example, `com` < `example.com` < `net` < `example.net`. 368 /// 369 /// This is the same as the [canonical DNS name order](https://datatracker.ietf.org/doc/html/rfc4034#section-6.1). 370 /// ASCII uppercase is treated as ASCII lowercase and trailing `b'.'`s are ignored. 371 /// The [`AllowedAscii`]s in the `Domain`s are ignored. 372 /// 373 /// # Examples 374 /// 375 /// ``` 376 /// use core::cmp::Ordering; 377 /// use ascii_domain::{dom::Domain, char_set::{ASCII_LETTERS, ASCII_LOWERCASE}}; 378 /// let dom1 = Domain::try_from_bytes("Example.com", &ASCII_LETTERS).unwrap(); 379 /// assert!(matches!(dom1.cmp_doms(&dom1), Ordering::Equal)); 380 /// let dom2 = Domain::try_from_bytes("www.example.com", &ASCII_LOWERCASE).unwrap(); 381 /// assert!(matches!(dom1.cmp_doms(&dom2), Ordering::Less)); 382 /// assert!(matches!(dom2.cmp_doms(&dom1), Ordering::Greater)); 383 /// let dom3 = Domain::try_from_bytes("foo.com", &ASCII_LOWERCASE).unwrap(); 384 /// assert!(matches!(dom1.cmp_doms(&dom3), Ordering::Less)); 385 /// assert!(matches!(dom3.cmp_doms(&dom1), Ordering::Greater)); 386 /// ``` 387 #[inline] 388 pub fn cmp_doms<T2: AsRef<[u8]>>(&self, right: &Domain<T2>) -> Ordering { 389 self.cmp_by_domain_ordering(right).into() 390 } 391 /// Returns the first `Label`. 392 /// 393 /// # Example 394 /// 395 /// ``` 396 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 397 /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().first_label().as_str() == "example"); 398 /// ``` 399 #[expect(clippy::unreachable, reason = "bug in code, so we want to crash")] 400 #[inline] 401 pub fn first_label(&self) -> Label<'_> { 402 self.iter() 403 .next_back() 404 .unwrap_or_else(|| unreachable!("there is a bug in Domain::try_from_bytes")) 405 } 406 /// Returns the last `Label` (i.e., the TLD). 407 /// 408 /// # Example 409 /// 410 /// ``` 411 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 412 /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().tld().as_str() == "com"); 413 /// ``` 414 #[expect(clippy::unreachable, reason = "bug in code, so we want to crash")] 415 #[inline] 416 pub fn tld(&self) -> Label<'_> { 417 self.iter() 418 .next() 419 .unwrap_or_else(|| unreachable!("there is a bug in Domain::try_from_bytes")) 420 } 421 } 422 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Domain<T>> for Domain<T2> { 423 /// Ignores the provided [`AllowedAscii`] and simply compares the two `Domain`s as [`Label`]s 424 /// of bytes. Note uppercase ASCII is treated as lowercase ASCII and trailing `b'.'`s are ignored. 425 #[inline] 426 fn eq(&self, other: &Domain<T>) -> bool { 427 self.as_bytes().eq_ignore_ascii_case(other.as_bytes()) 428 } 429 } 430 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<&Domain<T>> for Domain<T2> { 431 #[inline] 432 fn eq(&self, other: &&Domain<T>) -> bool { 433 *self == **other 434 } 435 } 436 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Domain<T>> for &Domain<T2> { 437 #[inline] 438 fn eq(&self, other: &Domain<T>) -> bool { 439 **self == *other 440 } 441 } 442 impl<T: AsRef<[u8]>> Eq for Domain<T> {} 443 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Domain<T>> for Domain<T2> { 444 /// Consult [`Self::cmp_doms`]. 445 #[inline] 446 fn partial_cmp(&self, other: &Domain<T>) -> Option<Ordering> { 447 Some(self.cmp_doms(other)) 448 } 449 } 450 impl<T: AsRef<[u8]>> Ord for Domain<T> { 451 /// Consult [`Self::cmp_doms`]. 452 #[inline] 453 fn cmp(&self, other: &Self) -> Ordering { 454 self.cmp_doms(other) 455 } 456 } 457 impl<T: AsRef<[u8]>> Hash for Domain<T> { 458 #[inline] 459 fn hash<H: Hasher>(&self, state: &mut H) { 460 self.as_bytes().to_ascii_lowercase().hash(state); 461 } 462 } 463 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> TryFrom<(T, &AllowedAscii<T2>)> for Domain<T> { 464 type Error = DomainErr; 465 #[inline] 466 fn try_from(value: (T, &AllowedAscii<T2>)) -> Result<Self, Self::Error> { 467 Self::try_from_bytes(value.0, value.1) 468 } 469 } 470 impl<T: AsRef<[u8]>> Display for Domain<T> { 471 #[inline] 472 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 473 f.write_str(self) 474 } 475 } 476 impl<T: AsRef<[u8]>> AsRef<str> for Domain<T> { 477 #[inline] 478 fn as_ref(&self) -> &str { 479 self.as_str() 480 } 481 } 482 impl<T: AsRef<[u8]>> AsRef<[u8]> for Domain<T> { 483 #[inline] 484 fn as_ref(&self) -> &[u8] { 485 self.as_bytes() 486 } 487 } 488 impl<T: AsRef<[u8]>> Deref for Domain<T> { 489 type Target = str; 490 #[inline] 491 fn deref(&self) -> &Self::Target { 492 self.as_str() 493 } 494 } 495 impl From<Domain<Vec<u8>>> for Domain<String> { 496 #[expect( 497 unsafe_code, 498 reason = "we enforce ASCII, so String::from_utf8_unchecked is fine" 499 )] 500 #[inline] 501 fn from(value: Domain<Vec<u8>>) -> Self { 502 // SAFETY: 503 // We only allow ASCII, so this is fine. 504 let val = unsafe { String::from_utf8_unchecked(value.value) }; 505 Self { value: val } 506 } 507 } 508 impl<'a: 'b, 'b, T: AsRef<[u8]>> From<&'a Domain<T>> for Domain<&'b [u8]> { 509 #[inline] 510 fn from(value: &'a Domain<T>) -> Self { 511 Self { 512 value: value.value.as_ref(), 513 } 514 } 515 } 516 impl<'a: 'b, 'b, T: AsRef<str>> From<&'a Domain<T>> for Domain<&'b str> { 517 #[inline] 518 fn from(value: &'a Domain<T>) -> Self { 519 Self { 520 value: value.value.as_ref(), 521 } 522 } 523 } 524 impl From<Domain<String>> for Domain<Vec<u8>> { 525 #[inline] 526 fn from(value: Domain<String>) -> Self { 527 Self { 528 value: value.value.into_bytes(), 529 } 530 } 531 } 532 impl<'a: 'b, 'b> From<Domain<&'a [u8]>> for Domain<&'b str> { 533 #[expect( 534 unsafe_code, 535 reason = "we enforce ASCII, so str::from_utf8_unchecked is fine" 536 )] 537 #[inline] 538 fn from(value: Domain<&'a [u8]>) -> Self { 539 // SAFETY: 540 // We only allow ASCII, so this is fine. 541 let val = unsafe { str::from_utf8_unchecked(value.value) }; 542 Self { value: val } 543 } 544 } 545 impl<'a: 'b, 'b> From<Domain<&'a str>> for Domain<&'b [u8]> { 546 #[inline] 547 fn from(value: Domain<&'a str>) -> Self { 548 Self { 549 value: value.value.as_bytes(), 550 } 551 } 552 } 553 impl From<Domain<Self>> for String { 554 /// Returns the contained `String` _without_ a trailing `'.'` if there was one. 555 /// 556 /// # Example 557 /// 558 /// ``` 559 /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS}; 560 /// assert!(String::from(Domain::try_from_bytes(String::from("Example.com."), &ASCII_LETTERS).unwrap()).as_str() == "Example.com"); 561 /// ``` 562 #[inline] 563 fn from(value: Domain<Self>) -> Self { 564 if value.contains_trailing_dot() { 565 let mut val = value.value; 566 _ = val.pop(); 567 val 568 } else { 569 value.value 570 } 571 } 572 } 573 impl<'a: 'b, 'b> From<Domain<&'a str>> for &'b str { 574 /// Returns the contained `str` _without_ a trailing `'.'` if there was one. 575 /// 576 /// # Example 577 /// 578 /// ``` 579 /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS}; 580 /// assert!(<&str>::from(Domain::try_from_bytes("Example.com.", &ASCII_LETTERS).unwrap()) == "Example.com"); 581 /// ``` 582 #[expect( 583 unsafe_code, 584 reason = "we enforce ASCII, so str::from_utf8_unchecked is fine" 585 )] 586 #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")] 587 #[inline] 588 fn from(value: Domain<&'a str>) -> Self { 589 // Indexing won't `panic` since `value.len()` is at most as long as `value.value`. 590 let utf8 = &value.value.as_bytes()[..value.len().get().into()]; 591 // SAFETY: 592 // Only ASCII is allowed, so this is fine. 593 unsafe { str::from_utf8_unchecked(utf8) } 594 } 595 } 596 impl From<Domain<Self>> for Vec<u8> { 597 /// Returns the contained `Vec` _without_ a trailing `b'.'` if there was one. 598 /// 599 /// # Example 600 /// 601 /// ``` 602 /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS}; 603 /// assert!(Vec::from(Domain::try_from_bytes(vec![b'F', b'o', b'o', b'.', b'c', b'o', b'm'], &ASCII_LETTERS).unwrap()).as_slice() == b"Foo.com"); 604 /// ``` 605 #[inline] 606 fn from(value: Domain<Self>) -> Self { 607 if value.contains_trailing_dot() { 608 let mut val = value.value; 609 _ = val.pop(); 610 val 611 } else { 612 value.value 613 } 614 } 615 } 616 impl<'a: 'b, 'b> From<Domain<&'a [u8]>> for &'b [u8] { 617 /// Returns the contained slice _without_ a trailing `b'.'` if there was one. 618 /// 619 /// # Example 620 /// 621 /// ``` 622 /// use ascii_domain::{dom::Domain, char_set::ASCII_LETTERS}; 623 /// assert!(<&[u8]>::from(Domain::try_from_bytes(b"Example.com.".as_slice(), &ASCII_LETTERS).unwrap()) == b"Example.com"); 624 /// ``` 625 #[expect(clippy::indexing_slicing, reason = "comment justifies its correctness")] 626 #[inline] 627 fn from(value: Domain<&'a [u8]>) -> Self { 628 // Indexing won't `panic` since `value.len()` is at most as long as `value.value`. 629 &value.value[..value.len().get().into()] 630 } 631 } 632 /// Error returned from [`Domain::try_from_bytes`]. 633 #[expect(variant_size_differences, reason = "usize is fine in size")] 634 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 635 pub enum DomainErr { 636 /// The domain was empty. 637 Empty, 638 /// The domain was the root domain that is to say it was the domain that only contained the root 639 /// zone (i.e., `b'.'`). 640 RootDomain, 641 /// The length of the domain was greater than 253 not counting a terminating `b'.'` if there was one. 642 LenExceeds253(usize), 643 /// The domain contained at least one empty label. 644 EmptyLabel, 645 /// The domain contained at least one label whose length exceeded 63. 646 LabelLenExceeds63, 647 /// The domain contained an invalid byte value. 648 InvalidByte(u8), 649 } 650 impl Display for DomainErr { 651 #[inline] 652 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 653 match *self { 654 Self::Empty => f.write_str("domain is empty"), 655 Self::RootDomain => f.write_str("domain is the root domain"), 656 Self::LenExceeds253(len) => write!( 657 f, 658 "domain has length {len} which is greater than the max length of 253" 659 ), 660 Self::EmptyLabel => f.write_str("domain has an empty label"), 661 Self::LabelLenExceeds63 => { 662 f.write_str("domain has a label that exceeds the max length of 63") 663 } 664 Self::InvalidByte(byt) => { 665 write!(f, "domain has a label with the invalid byte value {byt}") 666 } 667 } 668 } 669 } 670 impl Error for DomainErr {} 671 /// A label of a [`Domain`]. The total length of a `Label` is inclusively between 1 and 63. 672 #[derive(Clone, Copy, Debug)] 673 pub struct Label<'a> { 674 /// The label value. 675 value: &'a str, 676 } 677 impl<'a> Label<'a> { 678 /// The label. 679 /// 680 /// # Example 681 /// 682 /// ``` 683 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 684 /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.as_str() == "com")); 685 /// ``` 686 #[inline] 687 #[must_use] 688 pub const fn as_str(self) -> &'a str { 689 self.value 690 } 691 /// Returns `true` iff the label only contains ASCII letters. 692 /// 693 /// # Example 694 /// 695 /// ``` 696 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 697 /// assert!(Domain::try_from_bytes("example.com", &ASCII_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_alphabetic())); 698 /// ``` 699 #[inline] 700 #[must_use] 701 pub fn is_alphabetic(self) -> bool { 702 self.value 703 .as_bytes() 704 .iter() 705 .try_fold((), |(), byt| { 706 if byt.is_ascii_alphabetic() { 707 Ok(()) 708 } else { 709 Err(()) 710 } 711 }) 712 .is_ok() 713 } 714 /// Returns `true` iff the label only contains ASCII digits. 715 /// 716 /// # Example 717 /// 718 /// ``` 719 /// use ascii_domain::{dom::Domain, char_set::ASCII_DIGITS_LOWERCASE}; 720 /// assert!(Domain::try_from_bytes("example.123", &ASCII_DIGITS_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_digits())); 721 /// ``` 722 #[inline] 723 #[must_use] 724 pub fn is_digits(self) -> bool { 725 self.value 726 .as_bytes() 727 .iter() 728 .try_fold((), |(), byt| { 729 if byt.is_ascii_digit() { 730 Ok(()) 731 } else { 732 Err(()) 733 } 734 }) 735 .is_ok() 736 } 737 /// Returns `true` iff the label only contains ASCII digits or letters. 738 /// 739 /// # Example 740 /// 741 /// ``` 742 /// use ascii_domain::{dom::Domain, char_set::ASCII_DIGITS_LOWERCASE}; 743 /// assert!(Domain::try_from_bytes("example.1com", &ASCII_DIGITS_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_alphanumeric())); 744 /// ``` 745 #[inline] 746 #[must_use] 747 pub fn is_alphanumeric(self) -> bool { 748 self.value 749 .as_bytes() 750 .iter() 751 .try_fold((), |(), byt| { 752 if byt.is_ascii_alphanumeric() { 753 Ok(()) 754 } else { 755 Err(()) 756 } 757 }) 758 .is_ok() 759 } 760 /// Returns `true` iff the label only contains ASCII hyphen, digits, or letters. 761 /// 762 /// # Example 763 /// 764 /// ``` 765 /// use ascii_domain::{dom::Domain, char_set::ASCII_HYPHEN_DIGITS_LOWERCASE}; 766 /// assert!(Domain::try_from_bytes("example.1-com", &ASCII_HYPHEN_DIGITS_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.is_hyphen_or_alphanumeric())); 767 /// ``` 768 #[inline] 769 #[must_use] 770 pub fn is_hyphen_or_alphanumeric(self) -> bool { 771 self.value 772 .as_bytes() 773 .iter() 774 .try_fold((), |(), byt| { 775 if *byt == b'-' || byt.is_ascii_alphanumeric() { 776 Ok(()) 777 } else { 778 Err(()) 779 } 780 }) 781 .is_ok() 782 } 783 /// The length of the `Label`. This is inclusively between 1 and 63. 784 /// 785 /// # Example 786 /// 787 /// ``` 788 /// use ascii_domain::{dom::Domain, char_set::ASCII_LOWERCASE}; 789 /// assert!(Domain::try_from_bytes("example.com.", &ASCII_LOWERCASE).unwrap().into_iter().next().map_or(false, |label| label.len().get() == 3)); 790 /// ``` 791 #[expect( 792 unsafe_code, 793 reason = "we enforce label lengths, so NonZeroU8::new_unchecked is fine" 794 )] 795 #[expect( 796 clippy::as_conversions, 797 clippy::cast_possible_truncation, 798 reason = "comments justify their correctness" 799 )] 800 #[inline] 801 #[must_use] 802 pub const fn len(self) -> NonZeroU8 { 803 // The max length of a `Label` is 63. 804 let len = self.value.len() as u8; 805 // SAFETY: 806 // `Label`s are never empty. 807 unsafe { NonZeroU8::new_unchecked(len) } 808 } 809 } 810 impl PartialEq<Label<'_>> for Label<'_> { 811 #[inline] 812 fn eq(&self, other: &Label<'_>) -> bool { 813 self.value.eq_ignore_ascii_case(other.value) 814 } 815 } 816 impl PartialEq<&Label<'_>> for Label<'_> { 817 #[inline] 818 fn eq(&self, other: &&Label<'_>) -> bool { 819 *self == **other 820 } 821 } 822 impl PartialEq<Label<'_>> for &Label<'_> { 823 #[inline] 824 fn eq(&self, other: &Label<'_>) -> bool { 825 **self == *other 826 } 827 } 828 impl Eq for Label<'_> {} 829 impl PartialOrd<Label<'_>> for Label<'_> { 830 #[inline] 831 fn partial_cmp(&self, other: &Label<'_>) -> Option<Ordering> { 832 Some(self.cmp(other)) 833 } 834 } 835 impl Ord for Label<'_> { 836 #[inline] 837 fn cmp(&self, other: &Self) -> Ordering { 838 self.value 839 .to_ascii_lowercase() 840 .cmp(&other.value.to_ascii_lowercase()) 841 } 842 } 843 impl Hash for Label<'_> { 844 #[inline] 845 fn hash<H: Hasher>(&self, state: &mut H) { 846 self.value.to_ascii_lowercase().hash(state); 847 } 848 } 849 impl Display for Label<'_> { 850 #[inline] 851 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 852 f.write_str(self.value) 853 } 854 } 855 impl<'a> AsRef<[u8]> for Label<'a> { 856 #[inline] 857 fn as_ref(&self) -> &'a [u8] { 858 self.value.as_bytes() 859 } 860 } 861 impl<'a> AsRef<str> for Label<'a> { 862 #[inline] 863 fn as_ref(&self) -> &'a str { 864 self.value 865 } 866 } 867 impl<'a> Deref for Label<'a> { 868 type Target = str; 869 #[inline] 870 fn deref(&self) -> &'a Self::Target { 871 self.value 872 } 873 } 874 /// [`Iterator`] that iterates [`Label`]s from a [`Domain`] or [`Rfc1123Domain`] starting from the TLD down. 875 /// 876 /// This iterates `Label`s on demand; so if repeated iteration is desired, it may be better to collect the `Label`s 877 /// in a collection (e.g., [`Vec`]) than create the iterator again. This is also why [`ExactSizeIterator`] is not 878 /// implemented. 879 #[derive(Clone, Debug)] 880 pub struct LabelIter<'a> { 881 /// Domain as ASCII. 882 domain: &'a [u8], 883 } 884 impl<'a> Iterator for LabelIter<'a> { 885 type Item = Label<'a>; 886 #[expect( 887 unsafe_code, 888 reason = "we only allow ASCII, so str::from_utf8_unchecked is fine" 889 )] 890 #[expect( 891 clippy::arithmetic_side_effects, 892 clippy::indexing_slicing, 893 reason = "comments justify their correctness" 894 )] 895 #[inline] 896 fn next(&mut self) -> Option<Self::Item> { 897 (!self.domain.is_empty()).then(|| { 898 self.domain 899 .iter() 900 .rev() 901 .try_fold(1, |count, byt| { 902 if *byt == b'.' { 903 let len = self.domain.len(); 904 // `count` < `len` since there is at least one more `u8` before `b'.'`. 905 let idx = len - count; 906 // `idx + 1` < `len` since `count` is > 1 since `Label`s are never empty. 907 let ascii = &self.domain[idx + 1..len]; 908 // SAFETY: 909 // We only allow ASCII, so this is safe. 910 let value = unsafe { str::from_utf8_unchecked(ascii) }; 911 self.domain = &self.domain[..idx]; 912 Err(Label { value }) 913 } else { 914 Ok(count + 1) 915 } 916 }) 917 .map_or_else(convert::identity, |_| { 918 // SAFETY: 919 // We only allow ASCII, so this is safe. 920 let value = unsafe { str::from_utf8_unchecked(self.domain) }; 921 self.domain = &[]; 922 Label { value } 923 }) 924 }) 925 } 926 #[inline] 927 fn last(mut self) -> Option<Self::Item> 928 where 929 Self: Sized, 930 { 931 self.next_back() 932 } 933 #[inline] 934 fn size_hint(&self) -> (usize, Option<usize>) { 935 if self.domain.is_empty() { 936 (0, Some(0)) 937 } else { 938 // The max size of a `Label` is 63; and all but the last have a `b'.'` that follow it. 939 // This means the fewest `Label`s possible is the floor of the length divided by 64 with 940 // the added requirement that it's at least one since we know the domain is not empty. 941 // The min size of a `Label` is 1; and all but the last have a `b'.'` that follow it. 942 // This means the max number of `Label`s is the ceiling of the length divided by 2. 943 ( 944 (self.domain.len() >> 6).max(1), 945 Some(self.domain.len().div_ceil(2)), 946 ) 947 } 948 } 949 } 950 impl FusedIterator for LabelIter<'_> {} 951 impl DoubleEndedIterator for LabelIter<'_> { 952 #[expect( 953 unsafe_code, 954 reason = "we only allow ASCII, so str::from_utf8_unchecked is fine" 955 )] 956 #[expect( 957 clippy::arithmetic_side_effects, 958 clippy::indexing_slicing, 959 reason = "comments justify their correctness" 960 )] 961 #[inline] 962 fn next_back(&mut self) -> Option<Self::Item> { 963 (!self.domain.is_empty()).then(|| { 964 self.domain 965 .iter() 966 .try_fold(0, |count, byt| { 967 if *byt == b'.' { 968 // `count + 1` < `self.domain.len()` since there is at least one more `Label` and `Label`s 969 // are not empty. 970 let ascii = &self.domain[..count]; 971 // SAFETY: 972 // We only allow ASCII, so this is safe. 973 let value = unsafe { str::from_utf8_unchecked(ascii) }; 974 // `count + 1` < `self.domain.len()` since there is at least one more `Label` and `Label`s 975 // are not empty. 976 self.domain = &self.domain[count + 1..]; 977 Err(Label { value }) 978 } else { 979 Ok(count + 1) 980 } 981 }) 982 .map_or_else(convert::identity, |_| { 983 // SAFETY: 984 // We only allow ASCII, so this is safe. 985 let value = unsafe { str::from_utf8_unchecked(self.domain) }; 986 self.domain = &[]; 987 Label { value } 988 }) 989 }) 990 } 991 } 992 impl<'a, T: AsRef<[u8]>> IntoIterator for &'a Domain<T> { 993 type Item = Label<'a>; 994 type IntoIter = LabelIter<'a>; 995 #[inline] 996 fn into_iter(self) -> Self::IntoIter { 997 LabelIter { 998 domain: self.as_bytes(), 999 } 1000 } 1001 } 1002 impl<'a> IntoIterator for Domain<&'a str> { 1003 type Item = Label<'a>; 1004 type IntoIter = LabelIter<'a>; 1005 #[inline] 1006 fn into_iter(self) -> Self::IntoIter { 1007 LabelIter { 1008 domain: <&str>::from(self).as_bytes(), 1009 } 1010 } 1011 } 1012 impl<'a> IntoIterator for Domain<&'a [u8]> { 1013 type Item = Label<'a>; 1014 type IntoIter = LabelIter<'a>; 1015 #[inline] 1016 fn into_iter(self) -> Self::IntoIter { 1017 LabelIter { 1018 domain: <&[u8]>::from(self), 1019 } 1020 } 1021 } 1022 /// Error returned from [`Rfc1123Domain::try_from`] and [`Rfc1123Domain::try_from_bytes`]. 1023 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 1024 pub enum Rfc1123Err { 1025 /// The inputs was not a valid [`Domain`]. 1026 DomainErr(DomainErr), 1027 /// A [`Label`] of [`Domain`] starts with an ASCII hyphen. 1028 LabelStartsWithAHyphen, 1029 /// A [`Label`] of [`Domain`] ends with an ASCII hyphen. 1030 LabelEndsWithAHyphen, 1031 /// The last [`Label`] (i.e., TLD) was invalid which means it was not all ASCII letters nor 1032 /// had length of at least five with the first 4 characters being `xn--`. 1033 InvalidTld, 1034 } 1035 impl Display for Rfc1123Err { 1036 #[inline] 1037 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 1038 match *self { 1039 Self::DomainErr(err) => err.fmt(f), 1040 Self::LabelStartsWithAHyphen => { 1041 f.write_str("a label in the domain starts with a hyphen") 1042 } 1043 Self::LabelEndsWithAHyphen => f.write_str("a label in the domain ends with a hyphen"), 1044 Self::InvalidTld => f.write_str("the TLD in the domain was not all letters nor had length of at least five with the first 4 characters being 'xn--'") 1045 } 1046 } 1047 } 1048 impl Error for Rfc1123Err {} 1049 /// **TL;DR** Wrapper type around a [`Domain`] that enforces conformance to 1050 /// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#page-13). 1051 /// 1052 /// * Each [`Label`] must only contain ASCII digits, letters, or hyphen. 1053 /// * Each `Label` must not begin or end with a hyphen. 1054 /// * The last `Label` (i.e., TLD) must either contain only ASCII letters or have length of at least five and 1055 /// begin with `xn--`. 1056 /// --- 1057 /// Unsurprisingly, RFC 1123 is not super precise as it uses "host name" to mean label and also domain: 1058 /// "Host software MUST handle host names \[labels\] of up to 63 characters and SHOULD handle host 1059 /// names \[domains\] of up to 255 characters". It also states that only "one aspect of host name \[label\] 1060 /// syntax is hereby changed" from [RFC 952](https://www.rfc-editor.org/rfc/rfc952): "the restriction on the 1061 /// first character is relaxed to allow either a letter or a digit". Despite that, it goes on to mention other 1062 /// restrictions not mentioned in RFC 952: "the highest-level component label will be alphabetic". It is therefore 1063 /// important to understand how this type interprets that RFC and why it does so. 1064 /// 1065 /// The primary issue with RFC 1123 is the unjustified comment about the TLD being alphabetic. It is given 1066 /// as if it is common knowledge. As explained by (the rejected) 1067 /// [Errata 1353](https://www.rfc-editor.org/errata/eid1353), there seemed to be the assumption that the TLDs 1068 /// at the time would be the only ones that would ever exist or at least that the format of them would always be 1069 /// true. This leads to several possible interpretations: 1070 /// 1071 /// * Strictest: enforce the TLD is one of the TLDs that existed at the time of the RFC. 1072 /// * Strict: enforce the TLD has the same format as the TLDs at the time (i.e., two or three letters long). 1073 /// * Literal: enforce the TLD is alphabetic regardless of the lack of justification. 1074 /// * Relaxed: enforce the "spirit" that the TLD must exist. 1075 /// * More relaxed: enforce the "spirit" that the TLD must have the same format of a valid TLD. 1076 /// * Much more relaxed: enforce the "spirit" that the domain cannot have the form of an IPv4 address. 1077 /// * Most relaxed: treat TLDs no differently than other labels (i.e., don't make assumptions about what will be 1078 /// a valid TLD in the future). 1079 /// 1080 /// RFC 1123 is not obsolete, and it is clear from more recent RFCs like 1081 /// [RFC 5891](https://www.rfc-editor.org/rfc/rfc5891) that it is designed to be a foundation (i.e., domains that 1082 /// are valid per newer RFCs are valid per RFC 1123). Clearly due to RFCs like RFC 5891, requiring the TLD 1083 /// to be alphabetic or exactly two or three characters long would violate that. For those reasons the strictest, 1084 /// strict, and literal interpretations are rejected. 1085 /// 1086 /// Assuming TLDs are static is absurd, and relying on some dynamic list of TLDs is undesirable. For that reason 1087 /// the relaxed interpretation is rejected. 1088 /// 1089 /// Enforcing that domains do not have the form of an IPv4 address opens up the question of what is an IPv4 1090 /// address? Should leading 0s be allowed? What about hexadecimal? Should there be length limits for each octet? 1091 /// It also has the undesirable effect where subdomains that are all numeric exist but their parent domain does 1092 /// not which goes against the hierarchical nature of DNS. For those reasons the much more relaxed interpretation 1093 /// is rejected. 1094 /// 1095 /// Treating TLDs no differently than other labels is nice from a consistency perspective, but it suffers from 1096 /// the fact that domains that have the form of an IPv4 address are now allowed. For that reason the most 1097 /// relaxed interpretation is rejected. 1098 /// 1099 /// [ICANN](https://newgtlds.icann.org/sites/default/files/guidebook-full-04jun12-en.pdf) requires TLDs to either 1100 /// be alphabetic or a valid A-label per RFC 5891. Verifying a label is a valid A-label is not a cheap operation 1101 /// though. For that reason the more relaxed interpretation is accepted but with a twist: fake and valid A-labels 1102 /// are allowed in addition to entirely alphabetic labels. More specifically the TLD must either contain only 1103 /// letters or must be at least five characters long with the first 4 characters being `xn--`. 1104 /// 1105 /// If one wants to enforce the literal interpretation, one can use [`Self::is_literal_interpretation`]. Similarly, 1106 /// if one wants to enforce the strict interpretation, one can use [`Self::is_strict_interpretation`]. 1107 #[derive(Clone, Copy, Debug)] 1108 pub struct Rfc1123Domain<T> { 1109 /// The domain. 1110 dom: Domain<T>, 1111 } 1112 impl<T> Rfc1123Domain<T> { 1113 /// Returns a reference to the contained [`Domain`]. 1114 /// 1115 /// # Example 1116 /// 1117 /// ``` 1118 /// use ascii_domain::dom::Rfc1123Domain; 1119 /// assert!(Rfc1123Domain::try_from_bytes("example.com").unwrap().domain().len().get() == 11); 1120 /// ``` 1121 #[inline] 1122 pub const fn domain(&self) -> &Domain<T> { 1123 &self.dom 1124 } 1125 /// Returns the contained [`Domain`] consuming `self`. 1126 /// 1127 /// # Example 1128 /// 1129 /// ``` 1130 /// use ascii_domain::dom::Rfc1123Domain; 1131 /// assert!(Rfc1123Domain::try_from_bytes("example.com").unwrap().into_domain().len().get() == 11); 1132 /// ``` 1133 #[inline] 1134 pub fn into_domain(self) -> Domain<T> { 1135 self.dom 1136 } 1137 } 1138 impl<T: AsRef<[u8]>> Rfc1123Domain<T> { 1139 /// Function that transforms `v` into an `Rfc1123Domain` by only allowing [`Label`]s to contain the ASCII `u8`s 1140 /// in [`ASCII_HYPHEN_DIGITS_LETTERS`] with each `Label` not starting or ending with a `b'-'`. A trailing `b'.'` 1141 /// is ignored. The last `Label` (i.e., TLD) must either only contain ASCII letters or must have length of at 1142 /// least five with the first 4 bytes being `b"xn--"`. 1143 /// 1144 /// Unliked calling [`Domain::try_from_bytes`] then [`Rfc1123Domain::try_from`] which performs two traversals 1145 /// of `v`, this performs a single traversal of `v`. 1146 /// 1147 /// # Examples 1148 /// 1149 /// ``` 1150 /// use ascii_domain::dom::{Rfc1123Domain, Rfc1123Err}; 1151 /// assert!(Rfc1123Domain::try_from_bytes("example.com").is_ok()); 1152 /// assert!(Rfc1123Domain::try_from_bytes("example.xn--abc").is_ok()); 1153 /// assert!(Rfc1123Domain::try_from_bytes("a-.com").map_or_else(|err| err == Rfc1123Err::LabelEndsWithAHyphen, |_| false)); 1154 /// ``` 1155 /// 1156 /// # Errors 1157 /// 1158 /// Returns [`Rfc1123Err`] iff `v.as_ref()` is an invalid `Rfc1123Domain`. 1159 #[expect( 1160 clippy::arithmetic_side_effects, 1161 clippy::indexing_slicing, 1162 reason = "comments justify their correctness" 1163 )] 1164 #[expect(clippy::redundant_else, reason = "prefer else with else-if")] 1165 #[inline] 1166 pub fn try_from_bytes(v: T) -> Result<Self, Rfc1123Err> { 1167 // The easiest implementation would be redirecting to `Domain::try_from_bytes`; and upon success, 1168 // verify each `Label` doesn't begin or end with a hyphen. That requires traversing `v` twice though. 1169 // We opt to traverse just once. 1170 let val = v.as_ref(); 1171 let value = match val.last() { 1172 None => return Err(Rfc1123Err::DomainErr(DomainErr::Empty)), 1173 Some(byt) => { 1174 let b = *byt; 1175 if b == b'.' { 1176 if val.len() == 1 { 1177 return Err(Rfc1123Err::DomainErr(DomainErr::RootDomain)); 1178 } 1179 // We know `val.len` is at least 2. 1180 let len = val.len() - 1; 1181 let lst = val[len - 1]; 1182 if lst == b'.' { 1183 return Err(Rfc1123Err::DomainErr(DomainErr::EmptyLabel)); 1184 } else if lst == b'-' { 1185 return Err(Rfc1123Err::LabelEndsWithAHyphen); 1186 } else { 1187 &val[..len] 1188 } 1189 } else if b == b'-' { 1190 return Err(Rfc1123Err::LabelEndsWithAHyphen); 1191 } else { 1192 val 1193 } 1194 } 1195 }; 1196 if value.len() > 253 { 1197 Err(Rfc1123Err::DomainErr(DomainErr::LenExceeds253(value.len()))) 1198 } else { 1199 let mut count = 0; 1200 value 1201 .iter() 1202 .try_fold(0, |label_len, byt| { 1203 let b = *byt; 1204 if b == b'.' { 1205 NonZeroU8::new(label_len).map_or( 1206 Err(Rfc1123Err::DomainErr(DomainErr::EmptyLabel)), 1207 |_| { 1208 // We verify the last character in the `Label` is not a hyphen. 1209 // `count` > 0 since `label_len` > 0 and `count` < `value.len()` since 1210 // it's the index of the `b'.'`. 1211 if value[count - 1] == b'-' { 1212 Err(Rfc1123Err::LabelEndsWithAHyphen) 1213 } else { 1214 Ok(0) 1215 } 1216 }, 1217 ) 1218 } else if !RFC_CHARS.contains(b) { 1219 Err(Rfc1123Err::DomainErr(DomainErr::InvalidByte(b))) 1220 } else if b == b'-' && label_len == 0 { 1221 Err(Rfc1123Err::LabelStartsWithAHyphen) 1222 } else if label_len == 63 { 1223 Err(Rfc1123Err::DomainErr(DomainErr::LabelLenExceeds63)) 1224 } else { 1225 // This caps at 253, so no overflow. 1226 count += 1; 1227 // This is less than 64 due to the above check, so this won't overflow; 1228 Ok(label_len + 1) 1229 } 1230 }) 1231 .and_then(|tld_len| { 1232 // `tld_len <= value.len()`. 1233 let tld = &value[value.len() - usize::from(tld_len)..]; 1234 if (tld 1235 .split_at_checked(4) 1236 .is_some_and(|(fst, rem)| !rem.is_empty() && fst == b"xn--")) 1237 || tld 1238 .iter() 1239 .try_fold((), |(), byt| { 1240 if byt.is_ascii_alphabetic() { 1241 Ok(()) 1242 } else { 1243 Err(()) 1244 } 1245 }) 1246 .is_ok() 1247 { 1248 Ok(()) 1249 } else { 1250 Err(Rfc1123Err::InvalidTld) 1251 } 1252 }) 1253 .map(|()| Self { 1254 dom: Domain { value: v }, 1255 }) 1256 } 1257 } 1258 /// Returns `true` iff the domain adheres to the literal interpretation of RFC 1123. For more information 1259 /// read the description of [`Rfc1123Domain`]. 1260 /// 1261 /// # Examples 1262 /// 1263 /// ``` 1264 /// use ascii_domain::dom::Rfc1123Domain; 1265 /// assert!(Rfc1123Domain::try_from_bytes("example.commmm").unwrap().is_literal_interpretation()); 1266 /// assert!(!Rfc1123Domain::try_from_bytes("example.xn--abc").unwrap().is_literal_interpretation()); 1267 /// ``` 1268 #[inline] 1269 pub fn is_literal_interpretation(&self) -> bool { 1270 self.dom.tld().is_alphabetic() 1271 } 1272 /// Returns `true` iff the domain adheres to the strict interpretation of RFC 1123. For more information 1273 /// read the description of [`Rfc1123Domain`]. 1274 /// 1275 /// # Examples 1276 /// 1277 /// ``` 1278 /// use ascii_domain::dom::Rfc1123Domain; 1279 /// assert!(Rfc1123Domain::try_from_bytes("example.Com").unwrap().is_strict_interpretation()); 1280 /// assert!(!Rfc1123Domain::try_from_bytes("example.comm").unwrap().is_strict_interpretation()); 1281 /// ``` 1282 #[inline] 1283 pub fn is_strict_interpretation(&self) -> bool { 1284 let tld = self.dom.tld(); 1285 (2..4).contains(&tld.len().get()) && tld.is_alphabetic() 1286 } 1287 } 1288 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for Rfc1123Domain<T2> { 1289 #[inline] 1290 fn eq(&self, other: &Rfc1123Domain<T>) -> bool { 1291 self.dom == other.dom 1292 } 1293 } 1294 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<&Rfc1123Domain<T>> for Rfc1123Domain<T2> { 1295 #[inline] 1296 fn eq(&self, other: &&Rfc1123Domain<T>) -> bool { 1297 self.dom == other.dom 1298 } 1299 } 1300 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for &Rfc1123Domain<T2> { 1301 #[inline] 1302 fn eq(&self, other: &Rfc1123Domain<T>) -> bool { 1303 self.dom == other.dom 1304 } 1305 } 1306 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for Domain<T2> { 1307 #[inline] 1308 fn eq(&self, other: &Rfc1123Domain<T>) -> bool { 1309 *self == other.dom 1310 } 1311 } 1312 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Rfc1123Domain<T>> for &Domain<T2> { 1313 #[inline] 1314 fn eq(&self, other: &Rfc1123Domain<T>) -> bool { 1315 **self == other.dom 1316 } 1317 } 1318 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<&Rfc1123Domain<T>> for Domain<T2> { 1319 #[inline] 1320 fn eq(&self, other: &&Rfc1123Domain<T>) -> bool { 1321 *self == other.dom 1322 } 1323 } 1324 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialEq<Domain<T>> for Rfc1123Domain<T2> { 1325 #[inline] 1326 fn eq(&self, other: &Domain<T>) -> bool { 1327 self.dom == *other 1328 } 1329 } 1330 impl<T: AsRef<[u8]>> Eq for Rfc1123Domain<T> {} 1331 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Rfc1123Domain<T>> for Rfc1123Domain<T2> { 1332 #[inline] 1333 fn partial_cmp(&self, other: &Rfc1123Domain<T>) -> Option<Ordering> { 1334 self.dom.partial_cmp(&other.dom) 1335 } 1336 } 1337 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Rfc1123Domain<T>> for Domain<T2> { 1338 #[inline] 1339 fn partial_cmp(&self, other: &Rfc1123Domain<T>) -> Option<Ordering> { 1340 self.partial_cmp(&other.dom) 1341 } 1342 } 1343 impl<T: AsRef<[u8]>, T2: AsRef<[u8]>> PartialOrd<Domain<T>> for Rfc1123Domain<T2> { 1344 #[inline] 1345 fn partial_cmp(&self, other: &Domain<T>) -> Option<Ordering> { 1346 self.dom.partial_cmp(other) 1347 } 1348 } 1349 impl<T: AsRef<[u8]>> Ord for Rfc1123Domain<T> { 1350 #[inline] 1351 fn cmp(&self, other: &Self) -> Ordering { 1352 self.dom.cmp(&other.dom) 1353 } 1354 } 1355 impl<T: AsRef<[u8]>> Hash for Rfc1123Domain<T> { 1356 #[inline] 1357 fn hash<H: Hasher>(&self, state: &mut H) { 1358 self.dom.hash(state); 1359 } 1360 } 1361 impl<T> AsRef<Domain<T>> for Rfc1123Domain<T> { 1362 #[inline] 1363 fn as_ref(&self) -> &Domain<T> { 1364 &self.dom 1365 } 1366 } 1367 impl<T> Borrow<Domain<T>> for Rfc1123Domain<T> { 1368 #[inline] 1369 fn borrow(&self) -> &Domain<T> { 1370 &self.dom 1371 } 1372 } 1373 impl<T> Deref for Rfc1123Domain<T> { 1374 type Target = Domain<T>; 1375 #[inline] 1376 fn deref(&self) -> &Self::Target { 1377 &self.dom 1378 } 1379 } 1380 impl<T> From<Rfc1123Domain<T>> for Domain<T> { 1381 #[inline] 1382 fn from(value: Rfc1123Domain<T>) -> Self { 1383 value.dom 1384 } 1385 } 1386 impl From<Rfc1123Domain<Vec<u8>>> for Rfc1123Domain<String> { 1387 #[inline] 1388 fn from(value: Rfc1123Domain<Vec<u8>>) -> Self { 1389 Self { 1390 dom: Domain::<String>::from(value.dom), 1391 } 1392 } 1393 } 1394 impl<'a: 'b, 'b, T: AsRef<[u8]>> From<&'a Rfc1123Domain<T>> for Rfc1123Domain<&'b [u8]> { 1395 #[inline] 1396 fn from(value: &'a Rfc1123Domain<T>) -> Self { 1397 Self { 1398 dom: Domain::<&'b [u8]>::from(&value.dom), 1399 } 1400 } 1401 } 1402 impl<'a: 'b, 'b, T: AsRef<str>> From<&'a Rfc1123Domain<T>> for Rfc1123Domain<&'b str> { 1403 #[inline] 1404 fn from(value: &'a Rfc1123Domain<T>) -> Self { 1405 Self { 1406 dom: Domain::<&'b str>::from(&value.dom), 1407 } 1408 } 1409 } 1410 impl From<Rfc1123Domain<String>> for Rfc1123Domain<Vec<u8>> { 1411 #[inline] 1412 fn from(value: Rfc1123Domain<String>) -> Self { 1413 Self { 1414 dom: Domain::<Vec<u8>>::from(value.dom), 1415 } 1416 } 1417 } 1418 impl<'a: 'b, 'b> From<Rfc1123Domain<&'a [u8]>> for Rfc1123Domain<&'b str> { 1419 #[inline] 1420 fn from(value: Rfc1123Domain<&'a [u8]>) -> Self { 1421 Self { 1422 dom: Domain::<&'b str>::from(value.dom), 1423 } 1424 } 1425 } 1426 impl<'a: 'b, 'b> From<Rfc1123Domain<&'a str>> for Rfc1123Domain<&'b [u8]> { 1427 #[inline] 1428 fn from(value: Rfc1123Domain<&'a str>) -> Self { 1429 Self { 1430 dom: Domain::<&'b [u8]>::from(value.dom), 1431 } 1432 } 1433 } 1434 impl<T: AsRef<[u8]>> TryFrom<Domain<T>> for Rfc1123Domain<T> { 1435 type Error = Rfc1123Err; 1436 #[expect( 1437 clippy::arithmetic_side_effects, 1438 clippy::indexing_slicing, 1439 clippy::unreachable, 1440 reason = "comments explain their correctness" 1441 )] 1442 #[inline] 1443 fn try_from(value: Domain<T>) -> Result<Self, Self::Error> { 1444 let mut labels = value.iter(); 1445 let tld = labels 1446 .next() 1447 .unwrap_or_else(|| unreachable!("there is a bug in Domain::try_from_bytes")); 1448 if tld.is_alphabetic() 1449 || tld 1450 .split_at_checked(4) 1451 .is_some_and(|(fst, rem)| !rem.is_empty() && fst == "xn--") 1452 { 1453 labels 1454 .try_fold((), |(), label| { 1455 let bytes = label.value.as_bytes(); 1456 // `Label`s are never empty, so the below indexing is fine. 1457 // Underflow won't occur for the same reason. 1458 if bytes[0] == b'-' { 1459 Err(Rfc1123Err::LabelStartsWithAHyphen) 1460 } else if bytes[bytes.len() - 1] == b'-' { 1461 Err(Rfc1123Err::LabelEndsWithAHyphen) 1462 } else { 1463 bytes.iter().try_fold((), |(), byt| match *byt { 1464 b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' => Ok(()), 1465 val => Err(Rfc1123Err::DomainErr(DomainErr::InvalidByte(val))), 1466 }) 1467 } 1468 }) 1469 .map(|()| Self { dom: value }) 1470 } else { 1471 Err(Rfc1123Err::InvalidTld) 1472 } 1473 } 1474 } 1475 impl<T: AsRef<[u8]>> Display for Rfc1123Domain<T> { 1476 #[inline] 1477 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 1478 self.dom.fmt(f) 1479 } 1480 } 1481 impl<'a, T: AsRef<[u8]>> IntoIterator for &'a Rfc1123Domain<T> { 1482 type Item = Label<'a>; 1483 type IntoIter = LabelIter<'a>; 1484 #[inline] 1485 fn into_iter(self) -> Self::IntoIter { 1486 LabelIter { 1487 domain: self.dom.as_bytes(), 1488 } 1489 } 1490 } 1491 impl<'a> IntoIterator for Rfc1123Domain<&'a str> { 1492 type Item = Label<'a>; 1493 type IntoIter = LabelIter<'a>; 1494 #[inline] 1495 fn into_iter(self) -> Self::IntoIter { 1496 LabelIter { 1497 domain: <&str>::from(self.dom).as_bytes(), 1498 } 1499 } 1500 } 1501 impl<'a> IntoIterator for Rfc1123Domain<&'a [u8]> { 1502 type Item = Label<'a>; 1503 type IntoIter = LabelIter<'a>; 1504 #[inline] 1505 fn into_iter(self) -> Self::IntoIter { 1506 LabelIter { 1507 domain: <&[u8]>::from(self.dom), 1508 } 1509 } 1510 }