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}