stem_rs/response/
mapaddress.rs

1//! MAPADDRESS response parsing.
2//!
3//! This module parses responses from the `MAPADDRESS` command, which creates
4//! address mappings in Tor. These mappings redirect connections from one
5//! address to another, useful for hostname-to-IP mappings or virtual addresses.
6//!
7//! # Response Format
8//!
9//! A successful MAPADDRESS response contains address mappings:
10//!
11//! ```text
12//! 250 1.2.3.4=tor.freehaven.net
13//! ```
14//!
15//! Responses can contain a mixture of successes and failures:
16//!
17//! ```text
18//! 512-syntax error: invalid address '@@@'
19//! 250 1.2.3.4=tor.freehaven.net
20//! ```
21//!
22//! # Example
23//!
24//! ```rust
25//! use stem_rs::response::{ControlMessage, MapAddressResponse};
26//!
27//! // Single successful mapping
28//! let msg = ControlMessage::from_str(
29//!     "250 1.2.3.4=tor.freehaven.net\r\n",
30//!     None,
31//!     false
32//! ).unwrap();
33//! let response = MapAddressResponse::from_message(&msg).unwrap();
34//!
35//! assert_eq!(
36//!     response.mapped.get("1.2.3.4"),
37//!     Some(&"tor.freehaven.net".to_string())
38//! );
39//! assert!(response.failures.is_empty());
40//! ```
41//!
42//! # Partial Failures
43//!
44//! The response can contain both successful mappings and failures. The
45//! `from_message` method only returns an error if ALL mappings fail.
46//!
47//! # See Also
48//!
49//! - [`crate::Controller::map_address`]: High-level API for address mapping
50//! - [Tor Control Protocol: MAPADDRESS](https://spec.torproject.org/control-spec/commands.html#mapaddress)
51
52use std::collections::HashMap;
53
54use super::ControlMessage;
55use crate::Error;
56
57/// Parsed response from the MAPADDRESS command.
58///
59/// Contains successful address mappings and any failure messages.
60/// Responses can contain a mixture of successes and failures.
61///
62/// # Example
63///
64/// ```rust
65/// use stem_rs::response::{ControlMessage, MapAddressResponse};
66///
67/// // Response with multiple mappings
68/// let msg = ControlMessage::from_str(
69///     "250-foo=bar\r\n\
70///      250-baz=quux\r\n\
71///      250 192.0.2.1=example.com\r\n",
72///     None,
73///     false
74/// ).unwrap();
75///
76/// let response = MapAddressResponse::from_message(&msg).unwrap();
77/// assert_eq!(response.mapped.len(), 3);
78/// assert_eq!(response.mapped.get("foo"), Some(&"bar".to_string()));
79/// ```
80#[derive(Debug, Clone)]
81pub struct MapAddressResponse {
82    /// Successful address mappings.
83    ///
84    /// Maps the original address (key) to the replacement address (value).
85    /// For example, `"1.2.3.4" => "tor.freehaven.net"` means connections
86    /// to 1.2.3.4 will be redirected to tor.freehaven.net.
87    pub mapped: HashMap<String, String>,
88
89    /// Failure messages for mappings that could not be created.
90    ///
91    /// Each string contains the error message from Tor explaining why
92    /// the mapping failed (e.g., "syntax error: invalid address '@@@'").
93    pub failures: Vec<String>,
94}
95
96impl MapAddressResponse {
97    /// Parses a MAPADDRESS response from a control message.
98    ///
99    /// Extracts successful mappings and failure messages from the response.
100    /// This method only returns an error if ALL mappings fail or if the
101    /// response format is invalid.
102    ///
103    /// # Arguments
104    ///
105    /// * `message` - The control message to parse
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if:
110    /// - [`Error::InvalidRequest`](crate::Error::InvalidRequest): All addresses
111    ///   were invalid (512 error code)
112    /// - [`Error::OperationFailed`](crate::Error::OperationFailed): Tor was
113    ///   unable to satisfy the request (451 error code)
114    /// - [`Error::Protocol`](crate::Error::Protocol): Response format was
115    ///   invalid (missing `=` separator, unexpected status code)
116    ///
117    /// # Example
118    ///
119    /// ```rust
120    /// use stem_rs::response::{ControlMessage, MapAddressResponse};
121    ///
122    /// // Successful mapping
123    /// let msg = ControlMessage::from_str(
124    ///     "250 192.0.2.1=example.com\r\n",
125    ///     None,
126    ///     false
127    /// ).unwrap();
128    /// let response = MapAddressResponse::from_message(&msg).unwrap();
129    /// assert_eq!(
130    ///     response.mapped.get("192.0.2.1"),
131    ///     Some(&"example.com".to_string())
132    /// );
133    /// ```
134    pub fn from_message(message: &ControlMessage) -> Result<Self, Error> {
135        if !message.is_ok() {
136            for (code, _, line) in message.content() {
137                if code == "512" {
138                    return Err(Error::InvalidRequest(line));
139                } else if code == "451" {
140                    return Err(Error::OperationFailed {
141                        code,
142                        message: line,
143                    });
144                } else if code != "250" {
145                    return Err(Error::Protocol(format!(
146                        "MAPADDRESS returned unexpected response code: {}",
147                        code
148                    )));
149                }
150            }
151        }
152
153        let mut mapped = HashMap::new();
154        let mut failures = Vec::new();
155
156        for (code, _, line) in message.content() {
157            if code == "250" {
158                if let Some(eq_pos) = line.find('=') {
159                    let key = line[..eq_pos].to_string();
160                    let value = line[eq_pos + 1..].to_string();
161                    mapped.insert(key, value);
162                } else {
163                    return Err(Error::Protocol(format!(
164                        "MAPADDRESS returned '{}', which isn't a mapping",
165                        line
166                    )));
167                }
168            } else {
169                failures.push(line);
170            }
171        }
172
173        Ok(Self { mapped, failures })
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    fn create_message(lines: Vec<(&str, &str)>) -> ControlMessage {
182        let parsed: Vec<(String, char, Vec<u8>)> = lines
183            .iter()
184            .enumerate()
185            .map(|(i, (code, content))| {
186                let divider = if i == lines.len() - 1 { ' ' } else { '-' };
187                (code.to_string(), divider, content.as_bytes().to_vec())
188            })
189            .collect();
190        let raw = lines
191            .iter()
192            .map(|(_, c)| *c)
193            .collect::<Vec<_>>()
194            .join("\r\n");
195        ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
196    }
197
198    #[test]
199    fn test_mapaddress_single_mapping() {
200        let msg = create_message(vec![("250", "1.2.3.4=tor.freehaven.net")]);
201        let response = MapAddressResponse::from_message(&msg).unwrap();
202        assert_eq!(
203            response.mapped.get("1.2.3.4"),
204            Some(&"tor.freehaven.net".to_string())
205        );
206        assert!(response.failures.is_empty());
207    }
208
209    #[test]
210    fn test_mapaddress_multiple_mappings() {
211        let msg = create_message(vec![
212            ("250", "1.2.3.4=example.com"),
213            ("250", "5.6.7.8=another.com"),
214        ]);
215        let response = MapAddressResponse::from_message(&msg).unwrap();
216        assert_eq!(response.mapped.len(), 2);
217        assert_eq!(
218            response.mapped.get("1.2.3.4"),
219            Some(&"example.com".to_string())
220        );
221        assert_eq!(
222            response.mapped.get("5.6.7.8"),
223            Some(&"another.com".to_string())
224        );
225    }
226
227    #[test]
228    fn test_mapaddress_mixed_success_failure() {
229        let msg = create_message(vec![
230            ("512", "syntax error: invalid address '@@@'"),
231            ("250", "1.2.3.4=tor.freehaven.net"),
232        ]);
233        let response = MapAddressResponse::from_message(&msg).unwrap();
234        assert_eq!(
235            response.mapped.get("1.2.3.4"),
236            Some(&"tor.freehaven.net".to_string())
237        );
238        assert_eq!(response.failures.len(), 1);
239        assert!(response.failures[0].contains("syntax error"));
240    }
241
242    #[test]
243    fn test_mapaddress_512_error() {
244        let msg = create_message(vec![("512", "syntax error: invalid address")]);
245        let result = MapAddressResponse::from_message(&msg);
246        assert!(matches!(result, Err(Error::InvalidRequest(_))));
247    }
248
249    #[test]
250    fn test_mapaddress_451_error() {
251        let msg = create_message(vec![("451", "Resource temporarily unavailable")]);
252        let result = MapAddressResponse::from_message(&msg);
253        assert!(matches!(result, Err(Error::OperationFailed { .. })));
254    }
255
256    #[test]
257    fn test_mapaddress_batch_response() {
258        let msg = create_message(vec![
259            ("250", "foo=bar"),
260            ("250", "baz=quux"),
261            ("250", "gzzz=bzz"),
262            ("250", "120.23.23.2=torproject.org"),
263        ]);
264        let response = MapAddressResponse::from_message(&msg).unwrap();
265        assert_eq!(response.mapped.len(), 4);
266        assert_eq!(response.mapped.get("foo"), Some(&"bar".to_string()));
267        assert_eq!(response.mapped.get("baz"), Some(&"quux".to_string()));
268        assert_eq!(response.mapped.get("gzzz"), Some(&"bzz".to_string()));
269        assert_eq!(
270            response.mapped.get("120.23.23.2"),
271            Some(&"torproject.org".to_string())
272        );
273    }
274
275    #[test]
276    fn test_mapaddress_invalid_empty_response() {
277        let msg = create_message(vec![("250", "OK")]);
278        let result = MapAddressResponse::from_message(&msg);
279        assert!(result.is_err());
280    }
281
282    #[test]
283    fn test_mapaddress_invalid_response_no_equals() {
284        let msg = create_message(vec![("250", "foo is bar")]);
285        let result = MapAddressResponse::from_message(&msg);
286        assert!(result.is_err());
287    }
288
289    #[test]
290    fn test_mapaddress_partial_failure_response() {
291        let msg = create_message(vec![
292            (
293                "512",
294                "syntax error: mapping '2389' is not of expected form 'foo=bar'",
295            ),
296            (
297                "512",
298                "syntax error: mapping '23' is not of expected form 'foo=bar'.",
299            ),
300            ("250", "23=324"),
301        ]);
302        let response = MapAddressResponse::from_message(&msg).unwrap();
303        assert_eq!(response.mapped.get("23"), Some(&"324".to_string()));
304        assert_eq!(response.failures.len(), 2);
305    }
306}