stem_rs/response/
getinfo.rs

1//! GETINFO response parsing.
2//!
3//! This module parses responses from the `GETINFO` command, which retrieves
4//! runtime information from Tor. Unlike GETCONF (which gets configuration),
5//! GETINFO retrieves dynamic state like version, address, and descriptors.
6//!
7//! # Response Format
8//!
9//! A successful GETINFO response contains key-value pairs:
10//!
11//! ```text
12//! 250-version=0.4.7.1
13//! 250-address=192.0.2.1
14//! 250 OK
15//! ```
16//!
17//! Multi-line values use the `+` divider:
18//!
19//! ```text
20//! 250+config-text=
21//! ControlPort 9051
22//! DataDirectory /home/user/.tor
23//! .
24//! 250 OK
25//! ```
26//!
27//! # Example
28//!
29//! ```rust
30//! use stem_rs::response::{ControlMessage, GetInfoResponse};
31//!
32//! let response_text = "250-version=0.4.7.1\r\n\
33//!                      250-address=192.0.2.1\r\n\
34//!                      250 OK\r\n";
35//! let msg = ControlMessage::from_str(response_text, None, false).unwrap();
36//! let response = GetInfoResponse::from_message(&msg).unwrap();
37//!
38//! assert_eq!(response.get_str("version"), Some("0.4.7.1".to_string()));
39//! assert_eq!(response.get_str("address"), Some("192.0.2.1".to_string()));
40//! ```
41//!
42//! # Binary Data
43//!
44//! Values are stored as raw bytes to support binary data (like descriptors).
45//! Use [`get_str`](GetInfoResponse::get_str) for string values or access
46//! [`entries`](GetInfoResponse::entries) directly for binary data.
47//!
48//! # See Also
49//!
50//! - [`crate::Controller::get_info`]: High-level API for getting information
51//! - [`GetConfResponse`](super::GetConfResponse): For querying configuration
52//! - [Tor Control Protocol: GETINFO](https://spec.torproject.org/control-spec/commands.html#getinfo)
53
54use std::collections::HashMap;
55use std::collections::HashSet;
56
57use super::ControlMessage;
58use crate::Error;
59
60/// Parsed response from the GETINFO command.
61///
62/// Contains a mapping of information keys to their byte values. Values are
63/// stored as bytes to support binary data like descriptors.
64///
65/// # Example
66///
67/// ```rust
68/// use stem_rs::response::{ControlMessage, GetInfoResponse};
69///
70/// let msg = ControlMessage::from_str(
71///     "250-version=0.4.7.1\r\n\
72///      250-fingerprint=ABCD1234\r\n\
73///      250 OK\r\n",
74///     None,
75///     false
76/// ).unwrap();
77///
78/// let response = GetInfoResponse::from_message(&msg).unwrap();
79///
80/// // Use get_str for string values
81/// assert_eq!(response.get_str("version"), Some("0.4.7.1".to_string()));
82///
83/// // Or access raw bytes directly
84/// assert_eq!(response.entries.get("fingerprint"), Some(&b"ABCD1234".to_vec()));
85/// ```
86#[derive(Debug, Clone)]
87pub struct GetInfoResponse {
88    /// Mapping of information keys to their byte values.
89    ///
90    /// Values are stored as raw bytes to support binary data. Use
91    /// [`get_str`](Self::get_str) for convenient string access.
92    pub entries: HashMap<String, Vec<u8>>,
93}
94
95impl GetInfoResponse {
96    /// Parses a GETINFO response from a control message.
97    ///
98    /// Extracts information keys and their values from the response.
99    /// Multi-line values (indicated by `+` divider) are handled automatically.
100    ///
101    /// # Arguments
102    ///
103    /// * `message` - The control message to parse
104    ///
105    /// # Errors
106    ///
107    /// Returns an error if:
108    /// - [`Error::InvalidArguments`](crate::Error::InvalidArguments): One or more
109    ///   requested keys were not recognized by Tor
110    /// - [`Error::OperationFailed`](crate::Error::OperationFailed): Tor returned
111    ///   an error code
112    /// - [`Error::Protocol`](crate::Error::Protocol): The response format was
113    ///   invalid (missing `=` separator, malformed multi-line value, etc.)
114    ///
115    /// # Example
116    ///
117    /// ```rust
118    /// use stem_rs::response::{ControlMessage, GetInfoResponse};
119    ///
120    /// let msg = ControlMessage::from_str(
121    ///     "250-version=0.4.7.1\r\n250 OK\r\n",
122    ///     None,
123    ///     false
124    /// ).unwrap();
125    /// let response = GetInfoResponse::from_message(&msg).unwrap();
126    /// assert_eq!(response.get_str("version"), Some("0.4.7.1".to_string()));
127    /// ```
128    pub fn from_message(message: &ControlMessage) -> Result<Self, Error> {
129        let mut entries: HashMap<String, Vec<u8>> = HashMap::new();
130
131        let content_bytes = message.content_bytes();
132        let mut remaining: Vec<&(String, char, Vec<u8>)> = content_bytes.iter().collect();
133
134        if !message.is_ok() {
135            let mut unrecognized_keywords = Vec::new();
136            let mut error_code = None;
137            let mut error_msg = None;
138
139            for (code, _, line) in message.content() {
140                if code != "250" {
141                    error_code = Some(code.clone());
142                    error_msg = Some(line.clone());
143                }
144
145                if code == "552" && line.starts_with("Unrecognized key \"") && line.ends_with('"') {
146                    let keyword = &line[18..line.len() - 1];
147                    unrecognized_keywords.push(keyword.to_string());
148                }
149            }
150
151            if !unrecognized_keywords.is_empty() {
152                return Err(Error::InvalidArguments(format!(
153                    "GETINFO request contained unrecognized keywords: {}",
154                    unrecognized_keywords.join(", ")
155                )));
156            }
157
158            if let (Some(code), Some(msg)) = (error_code, error_msg) {
159                return Err(Error::OperationFailed { code, message: msg });
160            }
161
162            return Err(Error::Protocol(format!(
163                "GETINFO response didn't have an OK status:\n{}",
164                message
165            )));
166        }
167
168        if let Some(last) = remaining.last() {
169            let last_content = String::from_utf8_lossy(&last.2);
170            if last_content == "OK" {
171                remaining.pop();
172            }
173        }
174
175        for (_, divider, content) in remaining {
176            let content_str = String::from_utf8_lossy(content);
177
178            let eq_pos = content_str.find('=').ok_or_else(|| {
179                Error::Protocol(format!(
180                    "GETINFO replies should only contain parameter=value mappings:\n{}",
181                    message
182                ))
183            })?;
184
185            let key = content_str[..eq_pos].to_string();
186            let mut value = content[eq_pos + 1..].to_vec();
187
188            if *divider == '+' {
189                if !value.starts_with(b"\n") && !value.is_empty() {
190                    return Err(Error::Protocol(format!(
191                        "GETINFO response contained a multi-line value that didn't start with a newline:\n{}",
192                        message
193                    )));
194                }
195                if value.starts_with(b"\n") {
196                    value = value[1..].to_vec();
197                }
198            }
199
200            entries.insert(key, value);
201        }
202
203        Ok(Self { entries })
204    }
205
206    /// Verifies that the response contains exactly the requested parameters.
207    ///
208    /// This is useful for ensuring the response matches what was requested,
209    /// catching protocol errors where Tor returns different keys than expected.
210    ///
211    /// # Arguments
212    ///
213    /// * `params` - Set of parameter names that were requested
214    ///
215    /// # Errors
216    ///
217    /// Returns [`Error::Protocol`](crate::Error::Protocol) if the response
218    /// keys don't exactly match the requested parameters.
219    ///
220    /// # Example
221    ///
222    /// ```rust
223    /// use std::collections::HashSet;
224    /// use stem_rs::response::{ControlMessage, GetInfoResponse};
225    ///
226    /// let msg = ControlMessage::from_str(
227    ///     "250-version=0.4.7.1\r\n250 OK\r\n",
228    ///     None,
229    ///     false
230    /// ).unwrap();
231    /// let response = GetInfoResponse::from_message(&msg).unwrap();
232    ///
233    /// // Matches what we requested
234    /// let mut expected = HashSet::new();
235    /// expected.insert("version".to_string());
236    /// assert!(response.assert_matches(&expected).is_ok());
237    ///
238    /// // Doesn't match
239    /// let mut wrong = HashSet::new();
240    /// wrong.insert("address".to_string());
241    /// assert!(response.assert_matches(&wrong).is_err());
242    /// ```
243    pub fn assert_matches(&self, params: &HashSet<String>) -> Result<(), Error> {
244        let reply_params: HashSet<String> = self.entries.keys().cloned().collect();
245
246        if params != &reply_params {
247            let requested_label = params.iter().cloned().collect::<Vec<_>>().join(", ");
248            let reply_label = reply_params.iter().cloned().collect::<Vec<_>>().join(", ");
249
250            return Err(Error::Protocol(format!(
251                "GETINFO reply doesn't match the parameters that we requested. Queried '{}' but got '{}'.",
252                requested_label, reply_label
253            )));
254        }
255
256        Ok(())
257    }
258
259    /// Gets a value as a UTF-8 string.
260    ///
261    /// Convenience method for accessing string values. Invalid UTF-8 sequences
262    /// are replaced with the Unicode replacement character (U+FFFD).
263    ///
264    /// # Arguments
265    ///
266    /// * `key` - The information key to retrieve
267    ///
268    /// # Returns
269    ///
270    /// `Some(String)` if the key exists, `None` otherwise.
271    ///
272    /// # Example
273    ///
274    /// ```rust
275    /// use stem_rs::response::{ControlMessage, GetInfoResponse};
276    ///
277    /// let msg = ControlMessage::from_str(
278    ///     "250-version=0.4.7.1\r\n250 OK\r\n",
279    ///     None,
280    ///     false
281    /// ).unwrap();
282    /// let response = GetInfoResponse::from_message(&msg).unwrap();
283    ///
284    /// assert_eq!(response.get_str("version"), Some("0.4.7.1".to_string()));
285    /// assert_eq!(response.get_str("nonexistent"), None);
286    /// ```
287    pub fn get_str(&self, key: &str) -> Option<String> {
288        self.entries
289            .get(key)
290            .map(|v| String::from_utf8_lossy(v).to_string())
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    fn create_message(lines: Vec<(&str, char, &str)>) -> ControlMessage {
299        let parsed: Vec<(String, char, Vec<u8>)> = lines
300            .iter()
301            .map(|(code, div, content)| (code.to_string(), *div, content.as_bytes().to_vec()))
302            .collect();
303        let raw = lines
304            .iter()
305            .map(|(_, _, c)| *c)
306            .collect::<Vec<_>>()
307            .join("\r\n");
308        ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
309    }
310
311    fn create_simple_message(lines: Vec<&str>) -> ControlMessage {
312        let parsed: Vec<(String, char, Vec<u8>)> = lines
313            .iter()
314            .enumerate()
315            .map(|(i, line)| {
316                let divider = if i == lines.len() - 1 { ' ' } else { '-' };
317                ("250".to_string(), divider, line.as_bytes().to_vec())
318            })
319            .collect();
320        let raw = lines.join("\r\n");
321        ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
322    }
323
324    #[test]
325    fn test_getinfo_single_value() {
326        let msg = create_simple_message(vec!["version=0.4.7.1", "OK"]);
327        let response = GetInfoResponse::from_message(&msg).unwrap();
328        assert_eq!(response.get_str("version"), Some("0.4.7.1".to_string()));
329    }
330
331    #[test]
332    fn test_getinfo_multiple_values() {
333        let msg =
334            create_simple_message(vec!["version=0.4.7.1", "config-file=/etc/tor/torrc", "OK"]);
335        let response = GetInfoResponse::from_message(&msg).unwrap();
336        assert_eq!(response.get_str("version"), Some("0.4.7.1".to_string()));
337        assert_eq!(
338            response.get_str("config-file"),
339            Some("/etc/tor/torrc".to_string())
340        );
341    }
342
343    #[test]
344    fn test_getinfo_multiline_value() {
345        let msg = create_message(vec![
346            (
347                "250",
348                '+',
349                "config-text=\nControlPort 9051\nDataDirectory /home/.tor",
350            ),
351            ("250", ' ', "OK"),
352        ]);
353        let response = GetInfoResponse::from_message(&msg).unwrap();
354        let config = response.get_str("config-text").unwrap();
355        assert!(config.contains("ControlPort 9051"));
356        assert!(config.contains("DataDirectory /home/.tor"));
357    }
358
359    #[test]
360    fn test_getinfo_assert_matches() {
361        let msg = create_simple_message(vec!["version=0.4.7.1", "OK"]);
362        let response = GetInfoResponse::from_message(&msg).unwrap();
363
364        let mut expected = HashSet::new();
365        expected.insert("version".to_string());
366        assert!(response.assert_matches(&expected).is_ok());
367
368        let mut wrong = HashSet::new();
369        wrong.insert("other".to_string());
370        assert!(response.assert_matches(&wrong).is_err());
371    }
372
373    #[test]
374    fn test_getinfo_unrecognized_key() {
375        let msg = create_message(vec![("552", ' ', "Unrecognized key \"invalid-key\"")]);
376        let result = GetInfoResponse::from_message(&msg);
377        assert!(result.is_err());
378        if let Err(Error::InvalidArguments(msg)) = result {
379            assert!(msg.contains("invalid-key"));
380        } else {
381            panic!("Expected InvalidArguments error");
382        }
383    }
384
385    #[test]
386    fn test_getinfo_empty_value() {
387        let msg = create_simple_message(vec!["some-key=", "OK"]);
388        let response = GetInfoResponse::from_message(&msg).unwrap();
389        assert_eq!(response.get_str("some-key"), Some("".to_string()));
390    }
391
392    #[test]
393    fn test_getinfo_empty_response() {
394        let msg = create_simple_message(vec!["OK"]);
395        let response = GetInfoResponse::from_message(&msg).unwrap();
396        assert!(response.entries.is_empty());
397    }
398
399    #[test]
400    fn test_getinfo_batch_response() {
401        let msg = create_simple_message(vec![
402            "version=0.2.3.11-alpha-dev",
403            "address=67.137.76.214",
404            "fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0",
405            "OK",
406        ]);
407        let response = GetInfoResponse::from_message(&msg).unwrap();
408        assert_eq!(
409            response.get_str("version"),
410            Some("0.2.3.11-alpha-dev".to_string())
411        );
412        assert_eq!(
413            response.get_str("address"),
414            Some("67.137.76.214".to_string())
415        );
416        assert_eq!(
417            response.get_str("fingerprint"),
418            Some("5FDE0422045DF0E1879A3738D09099EB4A0C5BA0".to_string())
419        );
420    }
421
422    #[test]
423    fn test_getinfo_non_mapping_content() {
424        let msg = create_simple_message(vec![
425            "version=0.2.3.11-alpha-dev",
426            "address 67.137.76.214",
427            "OK",
428        ]);
429        let result = GetInfoResponse::from_message(&msg);
430        assert!(result.is_err());
431    }
432
433    #[test]
434    fn test_getinfo_multiline_missing_newline() {
435        let msg = create_message(vec![
436            ("250", '+', "config-text=ControlPort 9051"),
437            ("250", ' ', "OK"),
438        ]);
439        let result = GetInfoResponse::from_message(&msg);
440        assert!(result.is_err());
441    }
442
443    #[test]
444    fn test_getinfo_bytes_access() {
445        let msg = create_simple_message(vec!["version=0.4.7.1", "OK"]);
446        let response = GetInfoResponse::from_message(&msg).unwrap();
447        let bytes = response.entries.get("version").unwrap();
448        assert_eq!(bytes, b"0.4.7.1");
449    }
450}