Skip to main content

agent_uri/
trust_root.rs

1//! Trust root type for agent authorities.
2//!
3//! # Grammar Reference
4//!
5//! The trust root grammar is defined in `grammar.abnf`:
6//!
7//! ```abnf
8//! trust-root = host [ ":" port ]
9//! host       = domain / ip-literal / ipv4-address
10//! domain     = label *( "." label )
11//! label      = 1*63( ALPHA / DIGIT / "-" )
12//! ```
13//!
14//! Maximum trust root length: 128 characters (including port).
15
16use 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/// The host portion of a trust root.
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub enum Host {
27    /// A domain name (e.g., "anthropic.com")
28    Domain(String),
29    /// An IPv4 address (e.g., "192.168.1.1")
30    Ipv4(Ipv4Addr),
31    /// An IPv6 address (e.g., `::1`)
32    Ipv6(Ipv6Addr),
33}
34
35/// A validated trust root (authority) from an agent URI.
36///
37/// The trust root identifies the authority that vouches for an agent's
38/// identity and capabilities. It consists of a host (domain or IP) and
39/// an optional port.
40///
41/// # Examples
42///
43/// ```
44/// use agent_uri::TrustRoot;
45///
46/// let root = TrustRoot::parse("anthropic.com").unwrap();
47/// assert_eq!(root.host_str(), "anthropic.com");
48/// assert!(root.port().is_none());
49///
50/// let root = TrustRoot::parse("localhost:8472").unwrap();
51/// assert_eq!(root.host_str(), "localhost");
52/// assert_eq!(root.port(), Some(8472));
53/// ```
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct TrustRoot {
56    host: Host,
57    port: Option<u16>,
58    /// Original string representation (normalized to lowercase)
59    normalized: String,
60}
61
62impl TrustRoot {
63    /// Parses a trust root from a string.
64    ///
65    /// # Errors
66    ///
67    /// Returns `TrustRootError` if:
68    /// - The input is empty
69    /// - The input exceeds 128 characters
70    /// - The domain contains invalid characters or labels
71    /// - The IP address is malformed
72    /// - The port is invalid (not numeric or out of range)
73    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        // Handle IPv6 literals: [::1]:port
86        if input.starts_with('[') {
87            return Self::parse_ipv6_literal(input);
88        }
89
90        // Split host and port
91        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    /// Returns the host portion.
103    #[must_use]
104    pub const fn host(&self) -> &Host {
105        &self.host
106    }
107
108    /// Returns the host as a string.
109    #[must_use]
110    pub fn host_str(&self) -> &str {
111        match &self.host {
112            Host::Domain(d) => d,
113            Host::Ipv4(_) => {
114                // Use the normalized form, split by colon
115                self.normalized.split(':').next().unwrap_or(&self.normalized)
116            }
117            Host::Ipv6(_) => {
118                // Extract from [addr] format
119                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    /// Returns the port, if specified.
127    #[must_use]
128    pub const fn port(&self) -> Option<u16> {
129        self.port
130    }
131
132    /// Returns true if this is a localhost address.
133    #[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    /// Returns the normalized string representation.
143    #[must_use]
144    pub fn as_str(&self) -> &str {
145        &self.normalized
146    }
147
148    /// Returns a new trust root with the given port.
149    ///
150    /// # Errors
151    ///
152    /// Returns `TrustRootError` if the resulting trust root would exceed
153    /// the maximum length.
154    ///
155    /// # Examples
156    ///
157    /// ```
158    /// use agent_uri::TrustRoot;
159    ///
160    /// let root = TrustRoot::parse("localhost").unwrap();
161    /// let with_port = root.with_port(8472).unwrap();
162    /// assert_eq!(with_port.port(), Some(8472));
163    /// ```
164    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    /// Returns a new trust root without a port.
180    ///
181    /// # Examples
182    ///
183    /// ```
184    /// use agent_uri::TrustRoot;
185    ///
186    /// let root = TrustRoot::parse("localhost:8472").unwrap();
187    /// let without_port = root.without_port();
188    /// assert!(without_port.port().is_none());
189    /// ```
190    #[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            // Check if this looks like a port (all digits)
206            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        // Try IPv4 first
219        if let Ok(ip) = host_str.parse::<Ipv4Addr>() {
220            return Ok(Host::Ipv4(ip));
221        }
222
223        // Must be a domain name
224        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            // Check characters (alphanumeric and hyphens)
289            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            // Labels cannot start or end with hyphen
299            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}