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