1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::AtmError;
7use crate::types::{AgentName, TeamName};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct AgentAddress {
11 pub agent: AgentName,
12 pub team: Option<TeamName>,
13}
14
15impl FromStr for AgentAddress {
16 type Err = AtmError;
17
18 fn from_str(value: &str) -> Result<Self, Self::Err> {
19 let trimmed = value.trim();
20 if trimmed.is_empty() {
21 return Err(AtmError::address_parse("agent name must not be empty"));
22 }
23
24 match trimmed.split_once('@') {
25 Some((agent, team)) => Ok(Self {
26 agent: agent.parse()?,
27 team: Some(team.parse()?),
28 }),
29 None => Ok(Self {
30 agent: trimmed.parse()?,
31 team: None,
32 }),
33 }
34 }
35}
36
37impl fmt::Display for AgentAddress {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 match &self.team {
40 Some(team) => write!(f, "{}@{}", self.agent, team),
41 None => f.write_str(&self.agent),
42 }
43 }
44}
45
46pub(crate) fn validate_path_segment(value: &str, kind: &str) -> Result<(), AtmError> {
47 if value.is_empty() {
48 return Err(AtmError::address_parse(format!(
49 "{kind} name must not be empty"
50 )));
51 }
52
53 if value.starts_with('.') {
54 return Err(AtmError::address_parse(format!(
55 "{kind} name must not start with '.'"
56 )));
57 }
58
59 if value.contains("..") {
60 return Err(AtmError::address_parse(format!(
61 "{kind} name must not contain '..'"
62 )));
63 }
64
65 if value.contains(['/', '\\']) {
66 return Err(AtmError::address_parse(format!(
67 "{kind} name must not contain path separators"
68 )));
69 }
70
71 if !value
72 .chars()
73 .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
74 {
75 return Err(AtmError::address_parse(format!(
76 "{kind} name contains invalid characters"
77 )));
78 }
79
80 Ok(())
81}
82
83#[cfg(test)]
84mod tests {
85 use std::str::FromStr;
86
87 use super::AgentAddress;
88 use crate::test_support::{TEST_SENDER, TEST_SENDER_ADDRESS, TEST_TEAM};
89 use crate::types::{AgentName, TeamName};
90
91 #[test]
92 fn parses_bare_agent_address() {
93 let parsed = AgentAddress::from_str(TEST_SENDER).expect("address");
94 assert_eq!(parsed.agent, AgentName::from_validated(TEST_SENDER));
95 assert_eq!(parsed.team, None);
96 }
97
98 #[test]
99 fn parses_agent_with_team() {
100 let parsed = AgentAddress::from_str(TEST_SENDER_ADDRESS).expect("address");
101 assert_eq!(parsed.agent, AgentName::from_validated(TEST_SENDER));
102 assert_eq!(parsed.team, Some(TeamName::from_validated(TEST_TEAM)));
103 }
104
105 #[test]
106 fn rejects_empty_agent_name() {
107 assert!(AgentAddress::from_str("").is_err());
108 assert!(AgentAddress::from_str(&format!("@{TEST_TEAM}")).is_err());
109 }
110
111 #[test]
112 fn rejects_invalid_team_segment() {
113 assert!(AgentAddress::from_str(&format!("{TEST_SENDER}@")).is_err());
114 assert!(AgentAddress::from_str(&format!("{TEST_SENDER}@atm@dev")).is_err());
115 }
116
117 #[test]
118 fn rejects_path_traversal_and_separator_segments() {
119 assert!(AgentAddress::from_str("../evil").is_err());
120 assert!(AgentAddress::from_str("../../passwd").is_err());
121 assert!(AgentAddress::from_str("team/subdir").is_err());
122 assert!(AgentAddress::from_str(r"team\\subdir").is_err());
123 assert!(AgentAddress::from_str(".hidden").is_err());
124 assert!(AgentAddress::from_str("a..b@team").is_err());
125 assert!(AgentAddress::from_str("a...b@team").is_err());
126 }
127
128 #[test]
129 fn accepts_valid_segment_characters() {
130 let parsed = AgentAddress::from_str("valid-team_name.1").expect("address");
131 assert_eq!(parsed.agent, AgentName::from_validated("valid-team_name.1"));
132 assert_eq!(parsed.team, None);
133
134 let parsed = AgentAddress::from_str(TEST_SENDER_ADDRESS).expect("address");
135 assert_eq!(parsed.agent, AgentName::from_validated(TEST_SENDER));
136 assert_eq!(parsed.team, Some(TeamName::from_validated(TEST_TEAM)));
137 }
138
139 #[test]
140 fn display_round_trips_bare_and_qualified_addresses() {
141 assert_eq!(
142 AgentAddress::from_str(TEST_SENDER)
143 .expect("address")
144 .to_string(),
145 TEST_SENDER
146 );
147 assert_eq!(
148 AgentAddress::from_str(TEST_SENDER_ADDRESS)
149 .expect("address")
150 .to_string(),
151 TEST_SENDER_ADDRESS
152 );
153 }
154}