1use std::cmp::Ordering;
20use std::fmt;
21use std::net::{Ipv4Addr, Ipv6Addr};
22use std::str::FromStr;
23
24use crate::constants::{MAX_DNS_DOMAIN_LENGTH, MAX_DNS_LABEL_LENGTH, MAX_TRUST_ROOT_LENGTH};
25use crate::error::TrustRootError;
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub enum Host {
30 Domain(String),
32 Ipv4(Ipv4Addr),
34 Ipv6(Ipv6Addr),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash)]
107pub struct TrustRoot {
108 host: Host,
109 port: Option<u16>,
110 normalized: String,
112}
113
114impl TrustRoot {
115 pub fn parse(input: &str) -> Result<Self, TrustRootError> {
128 if input.is_empty() {
129 return Err(TrustRootError::Empty);
130 }
131
132 if input.len() > MAX_TRUST_ROOT_LENGTH {
133 return Err(TrustRootError::TooLong {
134 max: MAX_TRUST_ROOT_LENGTH,
135 actual: input.len(),
136 });
137 }
138
139 if input.starts_with('[') {
141 return Self::parse_ipv6_literal(input);
142 }
143
144 let (host_str, port) = Self::split_host_port(input)?;
146 let host = Self::parse_host(host_str)?;
147 let normalized = Self::normalize(&host, port);
148
149 Ok(Self {
150 host,
151 port,
152 normalized,
153 })
154 }
155
156 #[must_use]
158 pub const fn host(&self) -> &Host {
159 &self.host
160 }
161
162 #[must_use]
164 pub fn host_str(&self) -> &str {
165 match &self.host {
166 Host::Domain(d) => d,
167 Host::Ipv4(_) => {
168 self.normalized
170 .split(':')
171 .next()
172 .unwrap_or(&self.normalized)
173 }
174 Host::Ipv6(_) => {
175 let start = self.normalized.find('[').map_or(0, |i| i + 1);
177 let end = self.normalized.find(']').unwrap_or(self.normalized.len());
178 &self.normalized[start..end]
179 }
180 }
181 }
182
183 #[must_use]
185 pub const fn port(&self) -> Option<u16> {
186 self.port
187 }
188
189 #[must_use]
191 pub fn is_localhost(&self) -> bool {
192 match &self.host {
193 Host::Domain(d) => d == "localhost",
194 Host::Ipv4(ip) => ip.is_loopback(),
195 Host::Ipv6(ip) => ip.is_loopback(),
196 }
197 }
198
199 #[must_use]
201 pub fn as_str(&self) -> &str {
202 &self.normalized
203 }
204
205 pub fn with_port(&self, port: u16) -> Result<Self, TrustRootError> {
222 let normalized = Self::normalize(&self.host, Some(port));
223 if normalized.len() > MAX_TRUST_ROOT_LENGTH {
224 return Err(TrustRootError::TooLong {
225 max: MAX_TRUST_ROOT_LENGTH,
226 actual: normalized.len(),
227 });
228 }
229 Ok(Self {
230 host: self.host.clone(),
231 port: Some(port),
232 normalized,
233 })
234 }
235
236 #[must_use]
248 pub fn without_port(&self) -> Self {
249 let normalized = Self::normalize(&self.host, None);
250 Self {
251 host: self.host.clone(),
252 port: None,
253 normalized,
254 }
255 }
256
257 fn split_host_port(input: &str) -> Result<(&str, Option<u16>), TrustRootError> {
258 if let Some(colon_idx) = input.rfind(':') {
259 let host_part = &input[..colon_idx];
260 let port_part = &input[colon_idx + 1..];
261
262 if !port_part.is_empty() && port_part.chars().all(|c| c.is_ascii_digit()) {
264 let port: u16 = port_part.parse().map_err(|_| TrustRootError::InvalidPort {
265 value: port_part.to_string(),
266 reason: "port must be 0-65535",
267 })?;
268 return Ok((host_part, Some(port)));
269 }
270 }
271 Ok((input, None))
272 }
273
274 fn parse_host(host_str: &str) -> Result<Host, TrustRootError> {
275 if let Ok(ip) = host_str.parse::<Ipv4Addr>() {
276 return Ok(Host::Ipv4(ip));
277 }
278
279 if Self::is_ipv4_shaped(host_str) {
282 return Err(TrustRootError::InvalidIpAddress {
283 value: host_str.to_string(),
284 reason: Self::ipv4_rejection_reason(host_str),
285 });
286 }
287
288 Self::validate_domain(host_str)?;
289 Ok(Host::Domain(host_str.to_lowercase()))
290 }
291
292 fn is_ipv4_shaped(host: &str) -> bool {
296 let mut labels = 0usize;
297 for label in host.split('.') {
298 labels += 1;
299 if labels > 4 || label.is_empty() || !label.bytes().all(|b| b.is_ascii_digit()) {
300 return false;
301 }
302 }
303 labels == 4
304 }
305
306 fn ipv4_rejection_reason(host: &str) -> &'static str {
312 for label in host.split('.') {
313 match label.parse::<u64>() {
314 Ok(value) if value <= 255 => {}
315 _ => return "IPv4 octets must be 0-255",
316 }
317 }
318 "IPv4 octets must not have leading zeros"
319 }
320
321 fn parse_ipv6_literal(input: &str) -> Result<Self, TrustRootError> {
322 let closing_bracket = input
323 .find(']')
324 .ok_or_else(|| TrustRootError::InvalidIpAddress {
325 value: input.to_string(),
326 reason: "missing closing bracket for IPv6 literal",
327 })?;
328
329 let ipv6_str = &input[1..closing_bracket];
330 let ipv6: Ipv6Addr = ipv6_str
331 .parse()
332 .map_err(|_| TrustRootError::InvalidIpAddress {
333 value: ipv6_str.to_string(),
334 reason: "invalid IPv6 address",
335 })?;
336
337 let port = if input.len() > closing_bracket + 1 {
338 if input.as_bytes().get(closing_bracket + 1) != Some(&b':') {
339 return Err(TrustRootError::InvalidPort {
340 value: input[closing_bracket + 1..].to_string(),
341 reason: "expected ':' after IPv6 closing bracket",
342 });
343 }
344 let port_str = &input[closing_bracket + 2..];
345 Some(
346 port_str
347 .parse::<u16>()
348 .map_err(|_| TrustRootError::InvalidPort {
349 value: port_str.to_string(),
350 reason: "port must be 0-65535",
351 })?,
352 )
353 } else {
354 None
355 };
356
357 let normalized = Self::normalize(&Host::Ipv6(ipv6), port);
358 Ok(Self {
359 host: Host::Ipv6(ipv6),
360 port,
361 normalized,
362 })
363 }
364
365 fn validate_domain(domain: &str) -> Result<(), TrustRootError> {
366 if domain.len() > MAX_DNS_DOMAIN_LENGTH {
367 return Err(TrustRootError::InvalidDomain {
368 domain: domain.to_string(),
369 reason: "domain exceeds 253 character limit",
370 });
371 }
372
373 for label in domain.split('.') {
374 if label.is_empty() {
375 return Err(TrustRootError::InvalidDomain {
376 domain: domain.to_string(),
377 reason: "empty label (consecutive dots or leading/trailing dot)",
378 });
379 }
380
381 if label.len() > MAX_DNS_LABEL_LENGTH {
382 return Err(TrustRootError::LabelTooLong {
383 label: label.to_string(),
384 max: MAX_DNS_LABEL_LENGTH,
385 actual: label.len(),
386 });
387 }
388
389 for (j, c) in label.chars().enumerate() {
391 if !c.is_ascii_alphanumeric() && c != '-' {
392 return Err(TrustRootError::InvalidChar {
393 char: c,
394 position: domain.find(label).unwrap_or(0) + j,
395 });
396 }
397 }
398
399 if label.starts_with('-') || label.ends_with('-') {
401 return Err(TrustRootError::InvalidDomain {
402 domain: domain.to_string(),
403 reason: "label cannot start or end with hyphen",
404 });
405 }
406 }
407
408 Ok(())
409 }
410
411 fn normalize(host: &Host, port: Option<u16>) -> String {
412 let host_str = match host {
413 Host::Domain(d) => d.clone(),
414 Host::Ipv4(ip) => ip.to_string(),
415 Host::Ipv6(ip) => format!("[{ip}]"),
416 };
417
418 match port {
419 Some(p) => format!("{host_str}:{p}"),
420 None => host_str,
421 }
422 }
423}
424
425impl fmt::Display for TrustRoot {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 write!(f, "{}", self.normalized)
428 }
429}
430
431impl FromStr for TrustRoot {
432 type Err = TrustRootError;
433
434 fn from_str(s: &str) -> Result<Self, Self::Err> {
435 Self::parse(s)
436 }
437}
438
439impl AsRef<str> for TrustRoot {
440 fn as_ref(&self) -> &str {
441 &self.normalized
442 }
443}
444
445impl TryFrom<&str> for TrustRoot {
446 type Error = TrustRootError;
447
448 fn try_from(s: &str) -> Result<Self, Self::Error> {
449 Self::parse(s)
450 }
451}
452
453impl PartialOrd for TrustRoot {
454 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
455 Some(self.cmp(other))
456 }
457}
458
459impl Ord for TrustRoot {
460 fn cmp(&self, other: &Self) -> Ordering {
461 self.normalized.cmp(&other.normalized)
462 }
463}
464
465impl PartialOrd for Host {
466 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
467 Some(self.cmp(other))
468 }
469}
470
471impl Ord for Host {
472 fn cmp(&self, other: &Self) -> Ordering {
473 match (self, other) {
474 (Self::Domain(a), Self::Domain(b)) => a.cmp(b),
475 (Self::Ipv4(a), Self::Ipv4(b)) => a.cmp(b),
476 (Self::Ipv6(a), Self::Ipv6(b)) => a.cmp(b),
477 (Self::Domain(_), _) | (Self::Ipv4(_), Self::Ipv6(_)) => Ordering::Less,
478 (_, Self::Domain(_)) | (Self::Ipv6(_), Self::Ipv4(_)) => Ordering::Greater,
479 }
480 }
481}
482
483#[cfg(feature = "serde")]
484impl serde::Serialize for TrustRoot {
485 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
486 where
487 S: serde::Serializer,
488 {
489 serializer.serialize_str(&self.normalized)
490 }
491}
492
493#[cfg(feature = "serde")]
494impl<'de> serde::Deserialize<'de> for TrustRoot {
495 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
496 where
497 D: serde::Deserializer<'de>,
498 {
499 let s = String::deserialize(deserializer)?;
500 Self::parse(&s).map_err(serde::de::Error::custom)
501 }
502}
503
504#[cfg(feature = "serde")]
505impl serde::Serialize for Host {
506 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
507 where
508 S: serde::Serializer,
509 {
510 match self {
511 Self::Domain(d) => serializer.serialize_str(d),
512 Self::Ipv4(ip) => serializer.serialize_str(&ip.to_string()),
513 Self::Ipv6(ip) => serializer.serialize_str(&format!("[{ip}]")),
514 }
515 }
516}
517
518#[cfg(feature = "serde")]
519impl<'de> serde::Deserialize<'de> for Host {
520 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
521 where
522 D: serde::Deserializer<'de>,
523 {
524 let s = String::deserialize(deserializer)?;
525 let trust_root = TrustRoot::parse(&s).map_err(serde::de::Error::custom)?;
526 Ok(trust_root.host.clone())
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn parse_domain() {
536 let root = TrustRoot::parse("anthropic.com").unwrap();
537 assert_eq!(root.host_str(), "anthropic.com");
538 assert!(root.port().is_none());
539 }
540
541 #[test]
542 fn parse_domain_with_port() {
543 let root = TrustRoot::parse("localhost:8472").unwrap();
544 assert_eq!(root.host_str(), "localhost");
545 assert_eq!(root.port(), Some(8472));
546 }
547
548 #[test]
549 fn parse_ipv4() {
550 let root = TrustRoot::parse("192.168.1.1").unwrap();
551 assert!(matches!(root.host(), Host::Ipv4(_)));
552 }
553
554 #[test]
555 fn parse_ipv4_with_port() {
556 let root = TrustRoot::parse("192.168.1.1:8080").unwrap();
557 assert!(matches!(root.host(), Host::Ipv4(_)));
558 assert_eq!(root.port(), Some(8080));
559 }
560
561 #[test]
562 fn parse_ipv6_literal() {
563 let root = TrustRoot::parse("[::1]:8472").unwrap();
564 assert!(matches!(root.host(), Host::Ipv6(_)));
565 assert_eq!(root.port(), Some(8472));
566 }
567
568 #[test]
569 fn parse_ipv6_without_port() {
570 let root = TrustRoot::parse("[::1]").unwrap();
571 assert!(matches!(root.host(), Host::Ipv6(_)));
572 assert!(root.port().is_none());
573 }
574
575 #[test]
576 fn normalizes_to_lowercase() {
577 let root = TrustRoot::parse("ANTHROPIC.COM").unwrap();
578 assert_eq!(root.as_str(), "anthropic.com");
579 }
580
581 #[test]
582 fn is_localhost_domain() {
583 let root = TrustRoot::parse("localhost").unwrap();
584 assert!(root.is_localhost());
585 }
586
587 #[test]
588 fn is_localhost_ipv4() {
589 let root = TrustRoot::parse("127.0.0.1").unwrap();
590 assert!(root.is_localhost());
591 }
592
593 #[test]
594 fn is_localhost_ipv6() {
595 let root = TrustRoot::parse("[::1]").unwrap();
596 assert!(root.is_localhost());
597 }
598
599 #[test]
600 fn is_not_localhost() {
601 let root = TrustRoot::parse("anthropic.com").unwrap();
602 assert!(!root.is_localhost());
603 }
604
605 #[test]
606 fn parse_empty_fails() {
607 let result = TrustRoot::parse("");
608 assert!(matches!(result, Err(TrustRootError::Empty)));
609 }
610
611 #[test]
612 fn parse_too_long_fails() {
613 let long = "a".repeat(129);
614 let result = TrustRoot::parse(&long);
615 assert!(matches!(result, Err(TrustRootError::TooLong { .. })));
616 }
617
618 #[test]
619 fn parse_invalid_domain_fails() {
620 let result = TrustRoot::parse("invalid..domain");
621 assert!(matches!(result, Err(TrustRootError::InvalidDomain { .. })));
622 }
623
624 #[test]
625 fn parse_label_with_hyphen_start_fails() {
626 let result = TrustRoot::parse("-invalid.com");
627 assert!(matches!(result, Err(TrustRootError::InvalidDomain { .. })));
628 }
629
630 #[test]
631 fn ipv4_shaped_host_with_out_of_range_octet_is_rejected() {
632 let result = TrustRoot::parse("256.1.1.1");
633
634 assert!(matches!(
635 result,
636 Err(TrustRootError::InvalidIpAddress {
637 reason: "IPv4 octets must be 0-255",
638 ..
639 })
640 ));
641 }
642
643 #[test]
644 fn ipv4_shaped_host_with_port_and_out_of_range_octet_is_rejected() {
645 let result = TrustRoot::parse("256.1.1.1:8080");
646
647 assert!(matches!(
648 result,
649 Err(TrustRootError::InvalidIpAddress {
650 reason: "IPv4 octets must be 0-255",
651 ..
652 })
653 ));
654 }
655
656 #[test]
657 fn ipv4_shaped_host_with_leading_zero_is_rejected() {
658 let result = TrustRoot::parse("192.168.001.1");
659
660 assert!(matches!(
661 result,
662 Err(TrustRootError::InvalidIpAddress { .. })
663 ));
664 }
665
666 #[test]
667 fn ipv4_shaped_host_with_large_octet_is_rejected() {
668 let result = TrustRoot::parse("1.2.3.99999");
669
670 assert!(matches!(
671 result,
672 Err(TrustRootError::InvalidIpAddress { .. })
673 ));
674 }
675
676 #[test]
677 fn ipv4_shaped_host_with_all_octets_out_of_range_is_rejected() {
678 let result = TrustRoot::parse("999.999.999.999");
679
680 assert!(matches!(
681 result,
682 Err(TrustRootError::InvalidIpAddress { .. })
683 ));
684 }
685
686 #[test]
687 fn valid_ipv4_hosts_parse_as_ipv4() {
688 for input in ["192.168.1.1", "127.0.0.1", "0.0.0.0", "255.255.255.255"] {
689 let root = TrustRoot::parse(input).unwrap();
690
691 assert!(matches!(root.host(), Host::Ipv4(_)));
692 }
693 }
694
695 #[test]
696 fn valid_ipv4_host_with_port_parses_as_ipv4() {
697 let root = TrustRoot::parse("192.168.1.1:8080").unwrap();
698
699 assert!(matches!(root.host(), Host::Ipv4(_)));
700 assert_eq!(root.port(), Some(8080));
701 }
702
703 #[test]
704 fn hosts_without_exactly_four_numeric_labels_parse_as_domains() {
705 for input in ["1.2.3", "1.2.3.4.5", "1a.2.3.4", "256.1.1.1.example.com"] {
706 let root = TrustRoot::parse(input).unwrap();
707
708 assert!(matches!(root.host(), Host::Domain(domain) if domain == input));
709 }
710 }
711
712 #[test]
713 fn a_non_ascii_host_is_refused_and_not_converted() {
714 for input in [
718 "münchen.de",
719 "\u{43f}\u{440}\u{438}\u{43c}\u{435}\u{440}.com",
720 "café.fr",
721 ] {
722 let result = TrustRoot::parse(input);
723 assert!(
724 matches!(result, Err(TrustRootError::InvalidChar { .. })),
725 "{input} was not refused as a non-ASCII host: {result:?}"
726 );
727 }
728 }
729
730 #[test]
731 fn an_a_label_is_an_opaque_dns_label() {
732 let converted = TrustRoot::parse("xn--mnchen-3ya.de").expect("an A-label is ASCII");
737 assert_eq!(converted.host_str(), "xn--mnchen-3ya.de");
738
739 let nonsense = TrustRoot::parse("xn--zzzzzz").expect("not decoded, so not refused");
740 assert_eq!(nonsense.host_str(), "xn--zzzzzz");
741 }
742
743 #[test]
744 fn a_homograph_is_a_different_trust_root_and_nothing_here_says_so() {
745 let latin = TrustRoot::parse("apple.com").expect("valid");
748 let cyrillic = TrustRoot::parse("xn--pple-43d.com").expect("valid");
749
750 assert_ne!(latin, cyrillic);
751 assert_ne!(latin.host_str(), cyrillic.host_str());
752 }
753
754 #[test]
755 fn a_root_anchored_name_is_not_a_second_spelling() {
756 assert!(TrustRoot::parse("example.com.").is_err());
759 assert!(TrustRoot::parse("example.com").is_ok());
760 }
761}