Skip to main content

digit/
query.rs

1/// A parsed finger query.
2///
3/// Represents the structured result of parsing a finger query string
4/// like `"user@host"`, `"@host"`, or `"user@host1@host2"`.
5#[derive(Debug, Clone, PartialEq)]
6pub struct Query {
7    /// The user to query. `None` means list all users.
8    pub user: Option<String>,
9    /// The host(s) to query. The last host is the connection target.
10    /// Multiple hosts indicate a forwarding chain (RFC 1288).
11    pub hosts: Vec<String>,
12    /// Whether to request verbose output (sends `/W` prefix).
13    pub long: bool,
14    /// TCP port to connect on. Default is 79.
15    pub port: u16,
16}
17
18/// The default finger protocol port.
19pub const DEFAULT_PORT: u16 = 79;
20
21impl Query {
22    /// Parse a query string into a `Query`.
23    ///
24    /// # Examples
25    ///
26    /// - `"user@host"` -> user=Some("user"), hosts=["host"]
27    /// - `"@host"` -> user=None, hosts=["host"]
28    /// - `"user@host1@host2"` -> user=Some("user"), hosts=["host1", "host2"]
29    /// - `"user"` -> user=Some("user"), hosts=["localhost"]
30    /// - `""` or `None` -> user=None, hosts=["localhost"]
31    pub fn parse(input: Option<&str>, long: bool, port: u16) -> Query {
32        let input = input.unwrap_or("");
33
34        if input.is_empty() {
35            return Query {
36                user: None,
37                hosts: vec!["localhost".to_string()],
38                long,
39                port,
40            };
41        }
42
43        // Split on '@'. First part is the user (if non-empty), rest are hosts.
44        let parts: Vec<&str> = input.splitn(2, '@').collect();
45
46        if parts.len() == 1 {
47            // No '@' found -- user only, default to localhost.
48            return Query {
49                user: Some(parts[0].to_string()),
50                hosts: vec!["localhost".to_string()],
51                long,
52                port,
53            };
54        }
55
56        // Has at least one '@'.
57        let user = if parts[0].is_empty() {
58            None
59        } else {
60            Some(parts[0].to_string())
61        };
62
63        let hosts: Vec<String> = parts[1].split('@').map(|s| s.to_string()).collect();
64
65        Query {
66            user,
67            hosts,
68            long,
69            port,
70        }
71    }
72
73    /// Returns the host to connect to (the last host in the chain).
74    pub fn target_host(&self) -> &str {
75        self.hosts.last().expect("hosts must not be empty")
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn parse_user_at_host() {
85        let q = Query::parse(Some("user@host"), false, 79);
86        assert_eq!(q.user, Some("user".to_string()));
87        assert_eq!(q.hosts, vec!["host".to_string()]);
88        assert!(!q.long);
89        assert_eq!(q.port, 79);
90    }
91
92    #[test]
93    fn parse_at_host_lists_users() {
94        let q = Query::parse(Some("@host"), false, 79);
95        assert_eq!(q.user, None);
96        assert_eq!(q.hosts, vec!["host".to_string()]);
97    }
98
99    #[test]
100    fn parse_user_only_defaults_to_localhost() {
101        let q = Query::parse(Some("user"), false, 79);
102        assert_eq!(q.user, Some("user".to_string()));
103        assert_eq!(q.hosts, vec!["localhost".to_string()]);
104    }
105
106    #[test]
107    fn parse_empty_string_defaults_to_localhost() {
108        let q = Query::parse(Some(""), false, 79);
109        assert_eq!(q.user, None);
110        assert_eq!(q.hosts, vec!["localhost".to_string()]);
111    }
112
113    #[test]
114    fn parse_none_defaults_to_localhost() {
115        let q = Query::parse(None, false, 79);
116        assert_eq!(q.user, None);
117        assert_eq!(q.hosts, vec!["localhost".to_string()]);
118    }
119
120    #[test]
121    fn parse_forwarding_chain() {
122        let q = Query::parse(Some("user@host1@host2"), false, 79);
123        assert_eq!(q.user, Some("user".to_string()));
124        assert_eq!(q.hosts, vec!["host1".to_string(), "host2".to_string()]);
125    }
126
127    #[test]
128    fn parse_forwarding_chain_no_user() {
129        let q = Query::parse(Some("@host1@host2"), false, 79);
130        assert_eq!(q.user, None);
131        assert_eq!(q.hosts, vec!["host1".to_string(), "host2".to_string()]);
132    }
133
134    #[test]
135    fn parse_long_flag_preserved() {
136        let q = Query::parse(Some("user@host"), true, 79);
137        assert!(q.long);
138    }
139
140    #[test]
141    fn parse_custom_port_preserved() {
142        let q = Query::parse(Some("user@host"), false, 7979);
143        assert_eq!(q.port, 7979);
144    }
145
146    #[test]
147    fn target_host_returns_last_host() {
148        let q = Query::parse(Some("user@host1@host2"), false, 79);
149        assert_eq!(q.target_host(), "host2");
150    }
151
152    #[test]
153    fn target_host_single_host() {
154        let q = Query::parse(Some("user@host"), false, 79);
155        assert_eq!(q.target_host(), "host");
156    }
157
158    #[test]
159    fn parse_three_host_chain() {
160        let q = Query::parse(Some("user@a@b@c"), false, 79);
161        assert_eq!(q.user, Some("user".to_string()));
162        assert_eq!(
163            q.hosts,
164            vec!["a".to_string(), "b".to_string(), "c".to_string()]
165        );
166        assert_eq!(q.target_host(), "c");
167    }
168}