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}