stem_rs/response/
getconf.rs1use std::collections::HashMap;
64
65use super::ControlMessage;
66use crate::Error;
67
68#[derive(Debug, Clone)]
104pub struct GetConfResponse {
105 pub entries: HashMap<String, Vec<String>>,
113}
114
115impl GetConfResponse {
116 pub fn from_message(message: &ControlMessage) -> Result<Self, Error> {
155 let mut entries: HashMap<String, Vec<String>> = HashMap::new();
156
157 let content = message.content();
158
159 if content == vec![("250".to_string(), ' ', "OK".to_string())] {
160 return Ok(Self { entries });
161 }
162
163 if !message.is_ok() {
164 let mut unrecognized_keywords = Vec::new();
165
166 for (code, _, line) in &content {
167 if code == "552"
168 && line.starts_with("Unrecognized configuration key \"")
169 && line.ends_with('"')
170 {
171 let keyword = &line[32..line.len() - 1];
172 unrecognized_keywords.push(keyword.to_string());
173 }
174 }
175
176 if !unrecognized_keywords.is_empty() {
177 return Err(Error::InvalidArguments(format!(
178 "GETCONF request contained unrecognized keywords: {}",
179 unrecognized_keywords.join(", ")
180 )));
181 }
182
183 return Err(Error::Protocol(format!(
184 "GETCONF response contained a non-OK status code:\n{}",
185 message
186 )));
187 }
188
189 for line in message.iter() {
190 let line_str = line.to_string();
191
192 let (key, value) = if let Some(eq_pos) = line_str.find('=') {
193 let k = line_str[..eq_pos].to_string();
194 let v = line_str[eq_pos + 1..].to_string();
195 let v = if v.is_empty() { None } else { Some(v) };
196 (k, v)
197 } else {
198 (line_str.trim().to_string(), None)
199 };
200
201 if key.is_empty() || key == "OK" {
202 continue;
203 }
204
205 let key_clone = key.clone();
206 entries.entry(key).or_default();
207 if let Some(v) = value {
208 entries.get_mut(&key_clone).unwrap().push(v);
209 }
210 }
211
212 Ok(Self { entries })
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 fn create_message(lines: Vec<&str>) -> ControlMessage {
221 let parsed: Vec<(String, char, Vec<u8>)> = lines
222 .iter()
223 .enumerate()
224 .map(|(i, line)| {
225 let divider = if i == lines.len() - 1 { ' ' } else { '-' };
226 ("250".to_string(), divider, line.as_bytes().to_vec())
227 })
228 .collect();
229 let raw = lines.join("\r\n");
230 ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
231 }
232
233 fn create_error_message(code: &str, lines: Vec<&str>) -> ControlMessage {
234 let parsed: Vec<(String, char, Vec<u8>)> = lines
235 .iter()
236 .enumerate()
237 .map(|(i, line)| {
238 let divider = if i == lines.len() - 1 { ' ' } else { '-' };
239 (code.to_string(), divider, line.as_bytes().to_vec())
240 })
241 .collect();
242 let raw = lines.join("\r\n");
243 ControlMessage::new(parsed, raw.into_bytes(), None).unwrap()
244 }
245
246 #[test]
247 fn test_getconf_single_value() {
248 let msg = create_message(vec!["CookieAuthentication=0", "OK"]);
249 let response = GetConfResponse::from_message(&msg).unwrap();
250 assert_eq!(
251 response.entries.get("CookieAuthentication"),
252 Some(&vec!["0".to_string()])
253 );
254 }
255
256 #[test]
257 fn test_getconf_multiple_values() {
258 let msg = create_message(vec![
259 "CookieAuthentication=0",
260 "ControlPort=9100",
261 "DataDirectory=/home/user/.tor",
262 "OK",
263 ]);
264 let response = GetConfResponse::from_message(&msg).unwrap();
265 assert_eq!(
266 response.entries.get("CookieAuthentication"),
267 Some(&vec!["0".to_string()])
268 );
269 assert_eq!(
270 response.entries.get("ControlPort"),
271 Some(&vec!["9100".to_string()])
272 );
273 assert_eq!(
274 response.entries.get("DataDirectory"),
275 Some(&vec!["/home/user/.tor".to_string()])
276 );
277 }
278
279 #[test]
280 fn test_getconf_key_without_value() {
281 let msg = create_message(vec!["DirPort", "OK"]);
282 let response = GetConfResponse::from_message(&msg).unwrap();
283 assert_eq!(response.entries.get("DirPort"), Some(&vec![]));
284 }
285
286 #[test]
287 fn test_getconf_multiple_values_same_key() {
288 let msg = create_message(vec![
289 "ExitPolicy=accept *:80",
290 "ExitPolicy=accept *:443",
291 "ExitPolicy=reject *:*",
292 "OK",
293 ]);
294 let response = GetConfResponse::from_message(&msg).unwrap();
295 let policies = response.entries.get("ExitPolicy").unwrap();
296 assert_eq!(policies.len(), 3);
297 assert_eq!(policies[0], "accept *:80");
298 assert_eq!(policies[1], "accept *:443");
299 assert_eq!(policies[2], "reject *:*");
300 }
301
302 #[test]
303 fn test_getconf_empty_response() {
304 let msg = create_message(vec!["OK"]);
305 let response = GetConfResponse::from_message(&msg).unwrap();
306 assert!(response.entries.is_empty());
307 }
308
309 #[test]
310 fn test_getconf_unrecognized_key() {
311 let msg =
312 create_error_message("552", vec!["Unrecognized configuration key \"InvalidKey\""]);
313 let result = GetConfResponse::from_message(&msg);
314 assert!(result.is_err());
315 if let Err(Error::InvalidArguments(msg)) = result {
316 assert!(msg.contains("InvalidKey"));
317 } else {
318 panic!("Expected InvalidArguments error");
319 }
320 }
321
322 #[test]
323 fn test_getconf_empty_value_bug() {
324 let msg = create_message(vec!["SomeOption=", "OK"]);
325 let response = GetConfResponse::from_message(&msg).unwrap();
326 assert_eq!(response.entries.get("SomeOption"), Some(&vec![]));
327 }
328
329 #[test]
330 fn test_getconf_multiple_unrecognized_keys() {
331 let parsed = vec![
332 (
333 "552".to_string(),
334 '-',
335 "Unrecognized configuration key \"brickroad\""
336 .as_bytes()
337 .to_vec(),
338 ),
339 (
340 "552".to_string(),
341 ' ',
342 "Unrecognized configuration key \"submarine\""
343 .as_bytes()
344 .to_vec(),
345 ),
346 ];
347 let msg = ControlMessage::new(parsed, "552 error".into(), None).unwrap();
348 let result = GetConfResponse::from_message(&msg);
349 assert!(result.is_err());
350 if let Err(Error::InvalidArguments(msg)) = result {
351 assert!(msg.contains("brickroad"));
352 assert!(msg.contains("submarine"));
353 } else {
354 panic!("Expected InvalidArguments error");
355 }
356 }
357
358 #[test]
359 fn test_getconf_value_with_spaces() {
360 let msg = create_message(vec!["DataDirectory=/tmp/fake dir", "OK"]);
361 let response = GetConfResponse::from_message(&msg).unwrap();
362 assert_eq!(
363 response.entries.get("DataDirectory"),
364 Some(&vec!["/tmp/fake dir".to_string()])
365 );
366 }
367
368 #[test]
369 fn test_getconf_batch_response() {
370 let msg = create_message(vec![
371 "CookieAuthentication=0",
372 "ControlPort=9100",
373 "DataDirectory=/tmp/fake dir",
374 "DirPort",
375 "OK",
376 ]);
377 let response = GetConfResponse::from_message(&msg).unwrap();
378 assert_eq!(
379 response.entries.get("CookieAuthentication"),
380 Some(&vec!["0".to_string()])
381 );
382 assert_eq!(
383 response.entries.get("ControlPort"),
384 Some(&vec!["9100".to_string()])
385 );
386 assert_eq!(
387 response.entries.get("DataDirectory"),
388 Some(&vec!["/tmp/fake dir".to_string()])
389 );
390 assert_eq!(response.entries.get("DirPort"), Some(&vec![]));
391 }
392
393 #[test]
394 fn test_getconf_invalid_response_code() {
395 let parsed = vec![
396 ("123".to_string(), '-', "FOO".as_bytes().to_vec()),
397 ("532".to_string(), ' ', "BAR".as_bytes().to_vec()),
398 ];
399 let msg = ControlMessage::new(parsed, "invalid".into(), None).unwrap();
400 let result = GetConfResponse::from_message(&msg);
401 assert!(result.is_err());
402 }
403}