Skip to main content

atm_core/
address.rs

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}