1use std::cmp::Ordering;
17use std::fmt;
18use std::net::{Ipv4Addr, Ipv6Addr};
19use std::str::FromStr;
20
21use crate::constants::{MAX_DNS_DOMAIN_LENGTH, MAX_DNS_LABEL_LENGTH, MAX_TRUST_ROOT_LENGTH};
22use crate::error::TrustRootError;
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum Host {
27 Domain(String),
29 Ipv4(Ipv4Addr),
31 Ipv6(Ipv6Addr),
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct TrustRoot {
56 host: Host,
57 port: Option<u16>,
58 normalized: String,
60}
61
62impl TrustRoot {
63 pub fn parse(input: &str) -> Result<Self, TrustRootError> {
74 if input.is_empty() {
75 return Err(TrustRootError::Empty);
76 }
77
78 if input.len() > MAX_TRUST_ROOT_LENGTH {
79 return Err(TrustRootError::TooLong {
80 max: MAX_TRUST_ROOT_LENGTH,
81 actual: input.len(),
82 });
83 }
84
85 if input.starts_with('[') {
87 return Self::parse_ipv6_literal(input);
88 }
89
90 let (host_str, port) = Self::split_host_port(input)?;
92 let host = Self::parse_host(host_str)?;
93 let normalized = Self::normalize(&host, port);
94
95 Ok(Self {
96 host,
97 port,
98 normalized,
99 })
100 }
101
102 #[must_use]
104 pub const fn host(&self) -> &Host {
105 &self.host
106 }
107
108 #[must_use]
110 pub fn host_str(&self) -> &str {
111 match &self.host {
112 Host::Domain(d) => d,
113 Host::Ipv4(_) => {
114 self.normalized.split(':').next().unwrap_or(&self.normalized)
116 }
117 Host::Ipv6(_) => {
118 let start = self.normalized.find('[').map_or(0, |i| i + 1);
120 let end = self.normalized.find(']').unwrap_or(self.normalized.len());
121 &self.normalized[start..end]
122 }
123 }
124 }
125
126 #[must_use]
128 pub const fn port(&self) -> Option<u16> {
129 self.port
130 }
131
132 #[must_use]
134 pub fn is_localhost(&self) -> bool {
135 match &self.host {
136 Host::Domain(d) => d == "localhost",
137 Host::Ipv4(ip) => ip.is_loopback(),
138 Host::Ipv6(ip) => ip.is_loopback(),
139 }
140 }
141
142 #[must_use]
144 pub fn as_str(&self) -> &str {
145 &self.normalized
146 }
147
148 pub fn with_port(&self, port: u16) -> Result<Self, TrustRootError> {
165 let normalized = Self::normalize(&self.host, Some(port));
166 if normalized.len() > MAX_TRUST_ROOT_LENGTH {
167 return Err(TrustRootError::TooLong {
168 max: MAX_TRUST_ROOT_LENGTH,
169 actual: normalized.len(),
170 });
171 }
172 Ok(Self {
173 host: self.host.clone(),
174 port: Some(port),
175 normalized,
176 })
177 }
178
179 #[must_use]
191 pub fn without_port(&self) -> Self {
192 let normalized = Self::normalize(&self.host, None);
193 Self {
194 host: self.host.clone(),
195 port: None,
196 normalized,
197 }
198 }
199
200 fn split_host_port(input: &str) -> Result<(&str, Option<u16>), TrustRootError> {
201 if let Some(colon_idx) = input.rfind(':') {
202 let host_part = &input[..colon_idx];
203 let port_part = &input[colon_idx + 1..];
204
205 if !port_part.is_empty() && port_part.chars().all(|c| c.is_ascii_digit()) {
207 let port: u16 = port_part.parse().map_err(|_| TrustRootError::InvalidPort {
208 value: port_part.to_string(),
209 reason: "port must be 0-65535",
210 })?;
211 return Ok((host_part, Some(port)));
212 }
213 }
214 Ok((input, None))
215 }
216
217 fn parse_host(host_str: &str) -> Result<Host, TrustRootError> {
218 if let Ok(ip) = host_str.parse::<Ipv4Addr>() {
220 return Ok(Host::Ipv4(ip));
221 }
222
223 Self::validate_domain(host_str)?;
225 Ok(Host::Domain(host_str.to_lowercase()))
226 }
227
228 fn parse_ipv6_literal(input: &str) -> Result<Self, TrustRootError> {
229 let closing_bracket = input.find(']').ok_or_else(|| TrustRootError::InvalidIpAddress {
230 value: input.to_string(),
231 reason: "missing closing bracket for IPv6 literal",
232 })?;
233
234 let ipv6_str = &input[1..closing_bracket];
235 let ipv6: Ipv6Addr = ipv6_str.parse().map_err(|_| TrustRootError::InvalidIpAddress {
236 value: ipv6_str.to_string(),
237 reason: "invalid IPv6 address",
238 })?;
239
240 let port = if input.len() > closing_bracket + 1 {
241 if input.as_bytes().get(closing_bracket + 1) != Some(&b':') {
242 return Err(TrustRootError::InvalidPort {
243 value: input[closing_bracket + 1..].to_string(),
244 reason: "expected ':' after IPv6 closing bracket",
245 });
246 }
247 let port_str = &input[closing_bracket + 2..];
248 Some(port_str.parse::<u16>().map_err(|_| TrustRootError::InvalidPort {
249 value: port_str.to_string(),
250 reason: "port must be 0-65535",
251 })?)
252 } else {
253 None
254 };
255
256 let normalized = Self::normalize(&Host::Ipv6(ipv6), port);
257 Ok(Self {
258 host: Host::Ipv6(ipv6),
259 port,
260 normalized,
261 })
262 }
263
264 fn validate_domain(domain: &str) -> Result<(), TrustRootError> {
265 if domain.len() > MAX_DNS_DOMAIN_LENGTH {
266 return Err(TrustRootError::InvalidDomain {
267 domain: domain.to_string(),
268 reason: "domain exceeds 253 character limit",
269 });
270 }
271
272 for label in domain.split('.') {
273 if label.is_empty() {
274 return Err(TrustRootError::InvalidDomain {
275 domain: domain.to_string(),
276 reason: "empty label (consecutive dots or leading/trailing dot)",
277 });
278 }
279
280 if label.len() > MAX_DNS_LABEL_LENGTH {
281 return Err(TrustRootError::LabelTooLong {
282 label: label.to_string(),
283 max: MAX_DNS_LABEL_LENGTH,
284 actual: label.len(),
285 });
286 }
287
288 for (j, c) in label.chars().enumerate() {
290 if !c.is_ascii_alphanumeric() && c != '-' {
291 return Err(TrustRootError::InvalidChar {
292 char: c,
293 position: domain.find(label).unwrap_or(0) + j,
294 });
295 }
296 }
297
298 if label.starts_with('-') || label.ends_with('-') {
300 return Err(TrustRootError::InvalidDomain {
301 domain: domain.to_string(),
302 reason: "label cannot start or end with hyphen",
303 });
304 }
305 }
306
307 Ok(())
308 }
309
310 fn normalize(host: &Host, port: Option<u16>) -> String {
311 let host_str = match host {
312 Host::Domain(d) => d.clone(),
313 Host::Ipv4(ip) => ip.to_string(),
314 Host::Ipv6(ip) => format!("[{ip}]"),
315 };
316
317 match port {
318 Some(p) => format!("{host_str}:{p}"),
319 None => host_str,
320 }
321 }
322}
323
324impl fmt::Display for TrustRoot {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 write!(f, "{}", self.normalized)
327 }
328}
329
330impl FromStr for TrustRoot {
331 type Err = TrustRootError;
332
333 fn from_str(s: &str) -> Result<Self, Self::Err> {
334 Self::parse(s)
335 }
336}
337
338impl AsRef<str> for TrustRoot {
339 fn as_ref(&self) -> &str {
340 &self.normalized
341 }
342}
343
344impl TryFrom<&str> for TrustRoot {
345 type Error = TrustRootError;
346
347 fn try_from(s: &str) -> Result<Self, Self::Error> {
348 Self::parse(s)
349 }
350}
351
352impl PartialOrd for TrustRoot {
353 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
354 Some(self.cmp(other))
355 }
356}
357
358impl Ord for TrustRoot {
359 fn cmp(&self, other: &Self) -> Ordering {
360 self.normalized.cmp(&other.normalized)
361 }
362}
363
364impl PartialOrd for Host {
365 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
366 Some(self.cmp(other))
367 }
368}
369
370impl Ord for Host {
371 fn cmp(&self, other: &Self) -> Ordering {
372 match (self, other) {
373 (Self::Domain(a), Self::Domain(b)) => a.cmp(b),
374 (Self::Ipv4(a), Self::Ipv4(b)) => a.cmp(b),
375 (Self::Ipv6(a), Self::Ipv6(b)) => a.cmp(b),
376 (Self::Domain(_), _) | (Self::Ipv4(_), Self::Ipv6(_)) => Ordering::Less,
377 (_, Self::Domain(_)) | (Self::Ipv6(_), Self::Ipv4(_)) => Ordering::Greater,
378 }
379 }
380}
381
382#[cfg(feature = "serde")]
383impl serde::Serialize for TrustRoot {
384 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
385 where
386 S: serde::Serializer,
387 {
388 serializer.serialize_str(&self.normalized)
389 }
390}
391
392#[cfg(feature = "serde")]
393impl<'de> serde::Deserialize<'de> for TrustRoot {
394 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
395 where
396 D: serde::Deserializer<'de>,
397 {
398 let s = String::deserialize(deserializer)?;
399 Self::parse(&s).map_err(serde::de::Error::custom)
400 }
401}
402
403#[cfg(feature = "serde")]
404impl serde::Serialize for Host {
405 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
406 where
407 S: serde::Serializer,
408 {
409 match self {
410 Self::Domain(d) => serializer.serialize_str(d),
411 Self::Ipv4(ip) => serializer.serialize_str(&ip.to_string()),
412 Self::Ipv6(ip) => serializer.serialize_str(&format!("[{ip}]")),
413 }
414 }
415}
416
417#[cfg(feature = "serde")]
418impl<'de> serde::Deserialize<'de> for Host {
419 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
420 where
421 D: serde::Deserializer<'de>,
422 {
423 let s = String::deserialize(deserializer)?;
424 let trust_root = TrustRoot::parse(&s).map_err(serde::de::Error::custom)?;
425 Ok(trust_root.host.clone())
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn parse_domain() {
435 let root = TrustRoot::parse("anthropic.com").unwrap();
436 assert_eq!(root.host_str(), "anthropic.com");
437 assert!(root.port().is_none());
438 }
439
440 #[test]
441 fn parse_domain_with_port() {
442 let root = TrustRoot::parse("localhost:8472").unwrap();
443 assert_eq!(root.host_str(), "localhost");
444 assert_eq!(root.port(), Some(8472));
445 }
446
447 #[test]
448 fn parse_ipv4() {
449 let root = TrustRoot::parse("192.168.1.1").unwrap();
450 assert!(matches!(root.host(), Host::Ipv4(_)));
451 }
452
453 #[test]
454 fn parse_ipv4_with_port() {
455 let root = TrustRoot::parse("192.168.1.1:8080").unwrap();
456 assert!(matches!(root.host(), Host::Ipv4(_)));
457 assert_eq!(root.port(), Some(8080));
458 }
459
460 #[test]
461 fn parse_ipv6_literal() {
462 let root = TrustRoot::parse("[::1]:8472").unwrap();
463 assert!(matches!(root.host(), Host::Ipv6(_)));
464 assert_eq!(root.port(), Some(8472));
465 }
466
467 #[test]
468 fn parse_ipv6_without_port() {
469 let root = TrustRoot::parse("[::1]").unwrap();
470 assert!(matches!(root.host(), Host::Ipv6(_)));
471 assert!(root.port().is_none());
472 }
473
474 #[test]
475 fn normalizes_to_lowercase() {
476 let root = TrustRoot::parse("ANTHROPIC.COM").unwrap();
477 assert_eq!(root.as_str(), "anthropic.com");
478 }
479
480 #[test]
481 fn is_localhost_domain() {
482 let root = TrustRoot::parse("localhost").unwrap();
483 assert!(root.is_localhost());
484 }
485
486 #[test]
487 fn is_localhost_ipv4() {
488 let root = TrustRoot::parse("127.0.0.1").unwrap();
489 assert!(root.is_localhost());
490 }
491
492 #[test]
493 fn is_localhost_ipv6() {
494 let root = TrustRoot::parse("[::1]").unwrap();
495 assert!(root.is_localhost());
496 }
497
498 #[test]
499 fn is_not_localhost() {
500 let root = TrustRoot::parse("anthropic.com").unwrap();
501 assert!(!root.is_localhost());
502 }
503
504 #[test]
505 fn parse_empty_fails() {
506 let result = TrustRoot::parse("");
507 assert!(matches!(result, Err(TrustRootError::Empty)));
508 }
509
510 #[test]
511 fn parse_too_long_fails() {
512 let long = "a".repeat(129);
513 let result = TrustRoot::parse(&long);
514 assert!(matches!(result, Err(TrustRootError::TooLong { .. })));
515 }
516
517 #[test]
518 fn parse_invalid_domain_fails() {
519 let result = TrustRoot::parse("invalid..domain");
520 assert!(matches!(result, Err(TrustRootError::InvalidDomain { .. })));
521 }
522
523 #[test]
524 fn parse_label_with_hyphen_start_fails() {
525 let result = TrustRoot::parse("-invalid.com");
526 assert!(matches!(result, Err(TrustRootError::InvalidDomain { .. })));
527 }
528}