stem_rs/response/mod.rs
1//! Response parsing for Tor control protocol messages.
2//!
3//! This module provides types for parsing and handling responses from Tor's control
4//! protocol. The control protocol uses a text-based format where responses consist
5//! of status codes, dividers, and content.
6//!
7//! # Overview
8//!
9//! The Tor control protocol response format follows this structure:
10//!
11//! - Single-line responses: `STATUS DIVIDER CONTENT\r\n`
12//! - Multi-line responses: Multiple lines with `-` or `+` dividers, ending with ` `
13//!
14//! Where:
15//! - `STATUS` is a 3-digit code (e.g., `250` for success, `5xx` for errors)
16//! - `DIVIDER` is one of:
17//! - ` ` (space): End of response
18//! - `-`: More lines follow
19//! - `+`: Data section follows (terminated by `.\r\n`)
20//! - `CONTENT` is the payload of the line
21//!
22//! # Primary Types
23//!
24//! - [`ControlMessage`]: Represents a complete control protocol response
25//! - [`ControlLine`]: A single line with parsing utilities for extracting values
26//! - [`SingleLineResponse`]: A simple response containing only one line
27//!
28//! # Response-Specific Types
29//!
30//! Each submodule provides specialized parsing for specific command responses:
31//!
32//! - [`add_onion`]: ADD_ONION response parsing
33//! - [`authchallenge`]: AUTHCHALLENGE response for SAFECOOKIE authentication
34//! - [`events`]: Asynchronous event parsing
35//! - [`getconf`]: GETCONF response parsing
36//! - [`getinfo`]: GETINFO response parsing
37//! - [`mapaddress`]: MAPADDRESS response parsing
38//! - [`onion_client_auth`]: ONION_CLIENT_AUTH_VIEW response parsing
39//! - [`protocolinfo`]: PROTOCOLINFO response parsing
40//!
41//! # Example
42//!
43//! ```rust
44//! use stem_rs::response::{ControlMessage, ControlLine};
45//!
46//! // Parse a simple OK response
47//! let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
48//! assert!(msg.is_ok());
49//!
50//! // Parse a multi-line GETINFO response
51//! let response = "250-version=0.4.7.8\r\n250 OK\r\n";
52//! let msg = ControlMessage::from_str(response, None, false).unwrap();
53//! assert_eq!(msg.len(), 2);
54//!
55//! // Iterate over response lines
56//! for line in msg.iter() {
57//! println!("Line: {}", line);
58//! }
59//! ```
60//!
61//! # Thread Safety
62//!
63//! [`ControlMessage`] is `Send` and `Sync`. [`ControlLine`] uses internal
64//! synchronization for its mutable parsing state, making it safe to share
65//! across threads.
66//!
67//! # See Also
68//!
69//! - [`crate::Controller`]: High-level API that uses these response types
70//! - [`crate::socket::ControlSocket`]: Low-level socket that produces these messages
71//! - [Tor Control Protocol Specification](https://spec.torproject.org/control-spec)
72
73pub mod add_onion;
74pub mod authchallenge;
75#[cfg(feature = "events")]
76pub mod events;
77pub mod getconf;
78pub mod getinfo;
79pub mod mapaddress;
80pub mod onion_client_auth;
81pub mod protocolinfo;
82
83use std::hash::{Hash, Hasher};
84use std::sync::Mutex;
85
86use crate::Error;
87
88pub use add_onion::AddOnionResponse;
89pub use authchallenge::AuthChallengeResponse;
90pub use getconf::GetConfResponse;
91pub use getinfo::GetInfoResponse;
92pub use mapaddress::MapAddressResponse;
93pub use onion_client_auth::OnionClientAuthViewResponse;
94pub use protocolinfo::{AuthMethod, ProtocolInfoResponse};
95
96/// A parsed control protocol message from Tor.
97///
98/// `ControlMessage` represents a complete response from Tor's control interface.
99/// It handles both single-line and multi-line responses, parsing the status codes,
100/// dividers, and content according to the control protocol specification.
101///
102/// # Response Format
103///
104/// Each line in a control message has the format:
105/// ```text
106/// STATUS DIVIDER CONTENT
107/// ```
108///
109/// Where STATUS is a 3-digit code, DIVIDER indicates continuation, and CONTENT
110/// is the payload. The parsed content is stored as tuples of `(status_code, divider, content)`.
111///
112/// # Status Codes
113///
114/// - `2xx`: Success (e.g., `250 OK`)
115/// - `4xx`: Temporary failure
116/// - `5xx`: Permanent failure (e.g., `552 Unrecognized key`)
117/// - `6xx`: Asynchronous event notification
118///
119/// # Invariants
120///
121/// - A `ControlMessage` is never empty; construction fails if no valid lines exist
122/// - The `arrived_at` timestamp is set at construction time
123/// - Raw content preserves the original bytes for hashing and equality
124///
125/// # Thread Safety
126///
127/// `ControlMessage` is `Send` and `Sync`. It is immutable after construction.
128///
129/// # Example
130///
131/// ```rust
132/// use stem_rs::response::ControlMessage;
133///
134/// // Parse a GETINFO response
135/// let response = "250-version=0.4.7.8\r\n250 OK\r\n";
136/// let msg = ControlMessage::from_str(response, None, false).unwrap();
137///
138/// // Check if successful
139/// assert!(msg.is_ok());
140///
141/// // Access parsed content
142/// let content = msg.content();
143/// assert_eq!(content[0].0, "250"); // status code
144/// assert_eq!(content[0].1, '-'); // divider (more lines follow)
145///
146/// // Iterate over lines
147/// for line in msg.iter() {
148/// if let Ok((key, value)) = line.clone().pop_mapping(false, false) {
149/// println!("{} = {}", key, value);
150/// }
151/// }
152/// ```
153#[derive(Debug, Clone)]
154pub struct ControlMessage {
155 /// Parsed content as tuples of (status_code, divider, content_bytes).
156 parsed_content: Vec<(String, char, Vec<u8>)>,
157 /// Original raw bytes received from the socket.
158 raw_content: Vec<u8>,
159 /// Unix timestamp (seconds since epoch) when this message arrived.
160 pub arrived_at: i64,
161}
162
163impl ControlMessage {
164 /// Creates a new `ControlMessage` from pre-parsed content.
165 ///
166 /// This is a low-level constructor typically used internally. Most users
167 /// should use [`from_str`](Self::from_str) instead.
168 ///
169 /// # Arguments
170 ///
171 /// * `parsed_content` - Vector of (status_code, divider, content) tuples
172 /// * `raw_content` - Original raw bytes of the message
173 /// * `arrived_at` - Optional Unix timestamp; defaults to current time if `None`
174 ///
175 /// # Errors
176 ///
177 /// Returns [`Error::Protocol`] if `parsed_content` is empty,
178 /// as control messages must contain at least one line.
179 ///
180 /// # Example
181 ///
182 /// ```rust
183 /// use stem_rs::response::ControlMessage;
184 ///
185 /// let parsed = vec![("250".to_string(), ' ', b"OK".to_vec())];
186 /// let raw = b"250 OK\r\n".to_vec();
187 /// let msg = ControlMessage::new(parsed, raw, None).unwrap();
188 /// assert!(msg.is_ok());
189 /// ```
190 pub fn new(
191 parsed_content: Vec<(String, char, Vec<u8>)>,
192 raw_content: Vec<u8>,
193 arrived_at: Option<i64>,
194 ) -> Result<Self, Error> {
195 if parsed_content.is_empty() {
196 return Err(Error::Protocol(
197 "ControlMessages can't be empty".to_string(),
198 ));
199 }
200 Ok(Self {
201 parsed_content,
202 raw_content,
203 arrived_at: arrived_at.unwrap_or_else(|| {
204 std::time::SystemTime::now()
205 .duration_since(std::time::UNIX_EPOCH)
206 .map(|d| d.as_secs() as i64)
207 .unwrap_or(0)
208 }),
209 })
210 }
211
212 /// Parses a control message from a string.
213 ///
214 /// This is the primary way to create a `ControlMessage` from raw protocol data.
215 /// It handles both single-line and multi-line responses, including data sections.
216 ///
217 /// # Arguments
218 ///
219 /// * `content` - The raw message string to parse
220 /// * `msg_type` - Optional response type for validation (e.g., "SINGLELINE", "GETINFO")
221 /// * `normalize` - If `true`, ensures proper `\r\n` line endings
222 ///
223 /// # Normalization
224 ///
225 /// When `normalize` is `true`:
226 /// - Adds a trailing newline if missing
227 /// - Converts `\n` to `\r\n` (CRLF) as required by the protocol
228 ///
229 /// # Errors
230 ///
231 /// Returns [`Error::Protocol`] if:
232 /// - The content contains no valid control protocol lines
233 /// - The specified `msg_type` validation fails
234 ///
235 /// # Example
236 ///
237 /// ```rust
238 /// use stem_rs::response::ControlMessage;
239 ///
240 /// // Parse without normalization (content already has \r\n)
241 /// let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
242 ///
243 /// // Parse with normalization (adds \r\n if needed)
244 /// let msg = ControlMessage::from_str("250 OK", None, true).unwrap();
245 ///
246 /// // Parse and validate as single-line response
247 /// let msg = ControlMessage::from_str("250 OK\r\n", Some("SINGLELINE"), false).unwrap();
248 /// ```
249 pub fn from_str(content: &str, msg_type: Option<&str>, normalize: bool) -> Result<Self, Error> {
250 let mut content_bytes = content.as_bytes().to_vec();
251
252 if normalize {
253 if !content_bytes.ends_with(b"\n") {
254 content_bytes.push(b'\n');
255 }
256 let mut normalized = Vec::with_capacity(content_bytes.len() * 2);
257 let mut i = 0;
258 while i < content_bytes.len() {
259 if content_bytes[i] == b'\n' {
260 if i == 0 || content_bytes[i - 1] != b'\r' {
261 normalized.push(b'\r');
262 }
263 normalized.push(b'\n');
264 } else {
265 normalized.push(content_bytes[i]);
266 }
267 i += 1;
268 }
269 content_bytes = normalized;
270 }
271
272 let raw_content = content_bytes.clone();
273 let parsed_content = Self::parse_content(&content_bytes)?;
274
275 let mut msg = Self::new(parsed_content, raw_content, None)?;
276
277 if let Some(response_type) = msg_type {
278 msg = convert(response_type, msg)?;
279 }
280
281 Ok(msg)
282 }
283
284 fn parse_content(content: &[u8]) -> Result<Vec<(String, char, Vec<u8>)>, Error> {
285 let mut result = Vec::new();
286 let content_str = String::from_utf8_lossy(content);
287
288 for line in content_str.lines() {
289 let line = line.trim_end_matches(['\r', '\n']);
290 if line.len() < 3 {
291 continue;
292 }
293
294 let status_code = &line[..3];
295 let divider = line.chars().nth(3).unwrap_or(' ');
296 let line_content = if line.len() > 4 { &line[4..] } else { "" };
297
298 if divider == '+' {
299 let mut data_content = Vec::new();
300 let mut in_data = false;
301 let mut data_lines = Vec::new();
302
303 for data_line in content_str
304 .lines()
305 .skip_while(|l| !l.starts_with(&format!("{}+", status_code)))
306 {
307 if data_line.starts_with(&format!("{}+", status_code)) {
308 in_data = true;
309 let key = if data_line.len() > 4 {
310 &data_line[4..]
311 } else {
312 ""
313 };
314 if let Some(eq_pos) = key.find('=') {
315 data_content.extend_from_slice(&key.as_bytes()[..=eq_pos]);
316 data_content.push(b'\n');
317 }
318 continue;
319 }
320 if in_data {
321 if data_line == "." {
322 break;
323 }
324 let unescaped = data_line.strip_prefix('.').unwrap_or(data_line);
325 data_lines.push(unescaped.to_string());
326 }
327 }
328
329 if !data_lines.is_empty() {
330 data_content.extend_from_slice(data_lines.join("\n").as_bytes());
331 }
332
333 result.push((status_code.to_string(), divider, data_content));
334 } else {
335 result.push((
336 status_code.to_string(),
337 divider,
338 line_content.as_bytes().to_vec(),
339 ));
340 }
341 }
342
343 if result.is_empty() {
344 return Err(Error::Protocol(
345 "No valid lines in control message".to_string(),
346 ));
347 }
348
349 Ok(result)
350 }
351
352 /// Checks if the response indicates success.
353 ///
354 /// A response is considered successful if any of its lines has a status code
355 /// in the 2xx range (200-299). This is the standard success range in the
356 /// Tor control protocol.
357 ///
358 /// # Returns
359 ///
360 /// `true` if at least one line has a 2xx status code, `false` otherwise.
361 ///
362 /// # Example
363 ///
364 /// ```rust
365 /// use stem_rs::response::ControlMessage;
366 ///
367 /// let ok = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
368 /// assert!(ok.is_ok());
369 ///
370 /// let err = ControlMessage::from_str("552 Unrecognized key\r\n", None, false).unwrap();
371 /// assert!(!err.is_ok());
372 /// ```
373 pub fn is_ok(&self) -> bool {
374 for (code, _, _) in &self.parsed_content {
375 if let Ok(code_num) = code.parse::<u16>() {
376 if (200..300).contains(&code_num) {
377 return true;
378 }
379 }
380 }
381 false
382 }
383
384 /// Returns the parsed content as string tuples.
385 ///
386 /// Each tuple contains:
387 /// - Status code (e.g., "250", "552", "650")
388 /// - Divider character (` `, `-`, or `+`)
389 /// - Content as a UTF-8 string (lossy conversion from bytes)
390 ///
391 /// # Returns
392 ///
393 /// A vector of `(status_code, divider, content)` tuples.
394 ///
395 /// # Example
396 ///
397 /// ```rust
398 /// use stem_rs::response::ControlMessage;
399 ///
400 /// let msg = ControlMessage::from_str("250-version=0.4.7.8\r\n250 OK\r\n", None, false).unwrap();
401 /// let content = msg.content();
402 ///
403 /// assert_eq!(content[0].0, "250");
404 /// assert_eq!(content[0].1, '-');
405 /// assert!(content[0].2.starts_with("version="));
406 ///
407 /// assert_eq!(content[1].0, "250");
408 /// assert_eq!(content[1].1, ' ');
409 /// assert_eq!(content[1].2, "OK");
410 /// ```
411 pub fn content(&self) -> Vec<(String, char, String)> {
412 self.parsed_content
413 .iter()
414 .map(|(code, div, content)| {
415 (
416 code.clone(),
417 *div,
418 String::from_utf8_lossy(content).to_string(),
419 )
420 })
421 .collect()
422 }
423
424 /// Returns the parsed content with raw bytes.
425 ///
426 /// Similar to [`content`](Self::content), but preserves the original bytes
427 /// without UTF-8 conversion. Useful when handling binary data or when
428 /// exact byte preservation is required.
429 ///
430 /// # Returns
431 ///
432 /// A slice of `(status_code, divider, content_bytes)` tuples.
433 pub fn content_bytes(&self) -> &[(String, char, Vec<u8>)] {
434 &self.parsed_content
435 }
436
437 /// Returns the original raw bytes of the message.
438 ///
439 /// This is the unmodified data as received from the control socket,
440 /// including all protocol formatting (`\r\n`, status codes, etc.).
441 ///
442 /// # Returns
443 ///
444 /// A byte slice of the original message data.
445 pub fn raw_content(&self) -> &[u8] {
446 &self.raw_content
447 }
448
449 /// Returns the raw content as a UTF-8 string.
450 ///
451 /// Performs lossy UTF-8 conversion, replacing invalid sequences with
452 /// the Unicode replacement character (U+FFFD).
453 ///
454 /// # Returns
455 ///
456 /// The raw message content as a string.
457 pub fn raw_content_str(&self) -> String {
458 String::from_utf8_lossy(&self.raw_content).to_string()
459 }
460
461 /// Returns an iterator over the message lines as [`ControlLine`] instances.
462 ///
463 /// Each [`ControlLine`] provides parsing utilities for extracting values,
464 /// key-value mappings, and quoted strings from the line content.
465 ///
466 /// # Example
467 ///
468 /// ```rust
469 /// use stem_rs::response::ControlMessage;
470 ///
471 /// let msg = ControlMessage::from_str(
472 /// "250-version=0.4.7.8\r\n250 OK\r\n",
473 /// None,
474 /// false
475 /// ).unwrap();
476 ///
477 /// for line in msg.iter() {
478 /// println!("Line: {}", line);
479 /// }
480 /// ```
481 pub fn iter(&self) -> impl Iterator<Item = ControlLine> + '_ {
482 self.parsed_content
483 .iter()
484 .map(|(_, _, content)| ControlLine::new(&String::from_utf8_lossy(content)))
485 }
486
487 /// Returns the number of lines in the message.
488 ///
489 /// # Returns
490 ///
491 /// The count of parsed lines. Always at least 1 for valid messages.
492 pub fn len(&self) -> usize {
493 self.parsed_content.len()
494 }
495
496 /// Checks if the message has no lines.
497 ///
498 /// # Returns
499 ///
500 /// `true` if the message is empty, `false` otherwise.
501 ///
502 /// # Note
503 ///
504 /// Valid `ControlMessage` instances are never empty; this method exists
505 /// for API completeness with `len()`.
506 pub fn is_empty(&self) -> bool {
507 self.parsed_content.is_empty()
508 }
509
510 /// Returns the line at the specified index as a [`ControlLine`].
511 ///
512 /// # Arguments
513 ///
514 /// * `index` - Zero-based index of the line to retrieve
515 ///
516 /// # Returns
517 ///
518 /// `Some(ControlLine)` if the index is valid, `None` otherwise.
519 ///
520 /// # Example
521 ///
522 /// ```rust
523 /// use stem_rs::response::ControlMessage;
524 ///
525 /// let msg = ControlMessage::from_str("250-first\r\n250 second\r\n", None, false).unwrap();
526 ///
527 /// assert!(msg.get(0).is_some());
528 /// assert!(msg.get(1).is_some());
529 /// assert!(msg.get(2).is_none());
530 /// ```
531 pub fn get(&self, index: usize) -> Option<ControlLine> {
532 self.parsed_content
533 .get(index)
534 .map(|(_, _, content)| ControlLine::new(&String::from_utf8_lossy(content)))
535 }
536}
537
538impl PartialEq for ControlMessage {
539 fn eq(&self, other: &Self) -> bool {
540 self.raw_content == other.raw_content
541 }
542}
543
544impl Eq for ControlMessage {}
545
546impl Hash for ControlMessage {
547 fn hash<H: Hasher>(&self, state: &mut H) {
548 self.raw_content.hash(state);
549 }
550}
551
552impl std::fmt::Display for ControlMessage {
553 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
554 let lines: Vec<String> = self.iter().map(|line| line.to_string()).collect();
555 write!(f, "{}", lines.join("\n"))
556 }
557}
558
559impl std::ops::Index<usize> for ControlMessage {
560 type Output = str;
561
562 fn index(&self, _index: usize) -> &Self::Output {
563 panic!("Use get() method instead for safe access")
564 }
565}
566
567/// A single line from a control protocol response with parsing utilities.
568///
569/// `ControlLine` provides methods for parsing space-delimited entries from
570/// control protocol response lines. It maintains internal state to track
571/// the unparsed remainder, allowing sequential extraction of values.
572///
573/// # Parsing Model
574///
575/// The line is treated as a sequence of space-separated entries that can be
576/// "popped" from left to right. Each pop operation:
577/// 1. Extracts the next entry (optionally handling quotes and escapes)
578/// 2. Updates the internal remainder to exclude the extracted entry
579/// 3. Returns the extracted value
580///
581/// # Entry Types
582///
583/// - **Simple values**: Space-separated tokens (e.g., `PROTOCOLINFO 1`)
584/// - **Quoted values**: Values enclosed in double quotes (e.g., `"hello world"`)
585/// - **Key-value mappings**: `KEY=VALUE` pairs (e.g., `Tor="0.4.7.8"`)
586/// - **Escaped values**: Values with backslash escapes (e.g., `"path\\to\\file"`)
587///
588/// # Thread Safety
589///
590/// `ControlLine` uses internal synchronization (`Mutex`) for its mutable
591/// parsing state, making it safe to share across threads. However, concurrent
592/// pops from multiple threads will produce unpredictable results.
593///
594/// # Example
595///
596/// ```rust
597/// use stem_rs::response::ControlLine;
598///
599/// let mut line = ControlLine::new("AUTH METHODS=COOKIE,PASSWORD VERSION=\"1.0\"");
600///
601/// // Pop simple value
602/// assert_eq!(line.pop(false, false).unwrap(), "AUTH");
603///
604/// // Pop key-value mapping
605/// let (key, value) = line.pop_mapping(false, false).unwrap();
606/// assert_eq!(key, "METHODS");
607/// assert_eq!(value, "COOKIE,PASSWORD");
608///
609/// // Pop quoted key-value mapping
610/// let (key, value) = line.pop_mapping(true, false).unwrap();
611/// assert_eq!(key, "VERSION");
612/// assert_eq!(value, "1.0");
613///
614/// assert!(line.is_empty());
615/// ```
616#[derive(Debug)]
617pub struct ControlLine {
618 /// The original content of the line (immutable).
619 content: String,
620 /// The unparsed remainder (mutable via Mutex).
621 remainder: Mutex<String>,
622}
623
624impl Clone for ControlLine {
625 fn clone(&self) -> Self {
626 Self {
627 content: self.content.clone(),
628 remainder: Mutex::new(self.remainder.lock().unwrap().clone()),
629 }
630 }
631}
632
633impl ControlLine {
634 /// Creates a new `ControlLine` from the given content.
635 ///
636 /// The entire content is initially available as the remainder for parsing.
637 ///
638 /// # Arguments
639 ///
640 /// * `content` - The line content to parse
641 ///
642 /// # Example
643 ///
644 /// ```rust
645 /// use stem_rs::response::ControlLine;
646 ///
647 /// let line = ControlLine::new("KEY=value more data");
648 /// assert_eq!(line.remainder(), "KEY=value more data");
649 /// ```
650 pub fn new(content: &str) -> Self {
651 Self {
652 content: content.to_string(),
653 remainder: Mutex::new(content.to_string()),
654 }
655 }
656
657 /// Returns the unparsed remainder of the line.
658 ///
659 /// This is the portion of the line that hasn't been consumed by
660 /// [`pop`](Self::pop) or [`pop_mapping`](Self::pop_mapping) calls.
661 ///
662 /// # Returns
663 ///
664 /// The remaining unparsed content as a string.
665 ///
666 /// # Example
667 ///
668 /// ```rust
669 /// use stem_rs::response::ControlLine;
670 ///
671 /// let mut line = ControlLine::new("first second third");
672 /// assert_eq!(line.remainder(), "first second third");
673 ///
674 /// line.pop(false, false).unwrap();
675 /// assert_eq!(line.remainder(), "second third");
676 /// ```
677 pub fn remainder(&self) -> String {
678 self.remainder.lock().unwrap().clone()
679 }
680
681 /// Checks if there is no remaining content to parse.
682 ///
683 /// # Returns
684 ///
685 /// `true` if the remainder is empty, `false` otherwise.
686 ///
687 /// # Example
688 ///
689 /// ```rust
690 /// use stem_rs::response::ControlLine;
691 ///
692 /// let mut line = ControlLine::new("single");
693 /// assert!(!line.is_empty());
694 ///
695 /// line.pop(false, false).unwrap();
696 /// assert!(line.is_empty());
697 /// ```
698 pub fn is_empty(&self) -> bool {
699 self.remainder.lock().unwrap().is_empty()
700 }
701
702 /// Checks if the next entry is a quoted value.
703 ///
704 /// A quoted value starts with a double quote (`"`) and has a matching
705 /// closing quote.
706 ///
707 /// # Arguments
708 ///
709 /// * `escaped` - If `true`, handles backslash-escaped quotes within the value
710 ///
711 /// # Returns
712 ///
713 /// `true` if the next entry can be parsed as a quoted value, `false` otherwise.
714 ///
715 /// # Example
716 ///
717 /// ```rust
718 /// use stem_rs::response::ControlLine;
719 ///
720 /// let line = ControlLine::new("\"quoted value\" unquoted");
721 /// assert!(line.is_next_quoted(false));
722 ///
723 /// let line = ControlLine::new("unquoted \"quoted\"");
724 /// assert!(!line.is_next_quoted(false));
725 /// ```
726 pub fn is_next_quoted(&self, escaped: bool) -> bool {
727 let remainder = self.remainder.lock().unwrap();
728 let trimmed = remainder.trim_start();
729 if !trimmed.starts_with('"') {
730 return false;
731 }
732 let (start, end) = get_quote_indices(trimmed, escaped);
733 start == 0 && end != -1
734 }
735
736 /// Checks if the next entry is a KEY=VALUE mapping.
737 ///
738 /// # Arguments
739 ///
740 /// * `key` - If `Some`, checks that the key matches this specific value
741 /// * `quoted` - If `true`, checks that the value is quoted
742 /// * `escaped` - If `true`, handles backslash escapes in quoted values
743 ///
744 /// # Returns
745 ///
746 /// `true` if the next entry is a valid KEY=VALUE mapping matching the criteria.
747 ///
748 /// # Example
749 ///
750 /// ```rust
751 /// use stem_rs::response::ControlLine;
752 ///
753 /// let line = ControlLine::new("KEY=value OTHER=stuff");
754 ///
755 /// // Check for any mapping
756 /// assert!(line.is_next_mapping(None, false, false));
757 ///
758 /// // Check for specific key
759 /// assert!(line.is_next_mapping(Some("KEY"), false, false));
760 /// assert!(!line.is_next_mapping(Some("OTHER"), false, false));
761 ///
762 /// // Check for quoted value
763 /// let line = ControlLine::new("KEY=\"quoted\"");
764 /// assert!(line.is_next_mapping(Some("KEY"), true, false));
765 /// ```
766 pub fn is_next_mapping(&self, key: Option<&str>, quoted: bool, escaped: bool) -> bool {
767 let remainder = self.remainder.lock().unwrap();
768 let trimmed = remainder.trim_start();
769
770 if let Some(eq_pos) = trimmed.find('=') {
771 let actual_key = &trimmed[..eq_pos];
772 if let Some(expected_key) = key {
773 if actual_key != expected_key {
774 return false;
775 }
776 }
777 if quoted {
778 let after_eq = &trimmed[eq_pos + 1..];
779 let (start, end) = get_quote_indices(after_eq, escaped);
780 start == 0 && end != -1
781 } else {
782 true
783 }
784 } else {
785 false
786 }
787 }
788
789 /// Returns the key of the next entry without consuming it.
790 ///
791 /// If the next entry is a KEY=VALUE mapping, returns the key portion.
792 /// Otherwise returns `None`.
793 ///
794 /// # Returns
795 ///
796 /// `Some(key)` if the next entry is a mapping, `None` otherwise.
797 ///
798 /// # Example
799 ///
800 /// ```rust
801 /// use stem_rs::response::ControlLine;
802 ///
803 /// let line = ControlLine::new("MYKEY=myvalue");
804 /// assert_eq!(line.peek_key(), Some("MYKEY".to_string()));
805 ///
806 /// let line = ControlLine::new("no_equals_here");
807 /// assert!(line.peek_key().is_none());
808 /// ```
809 pub fn peek_key(&self) -> Option<String> {
810 let remainder = self.remainder.lock().unwrap();
811 let trimmed = remainder.trim_start();
812 trimmed.find('=').map(|pos| trimmed[..pos].to_string())
813 }
814
815 /// Removes and returns the next space-separated entry.
816 ///
817 /// This method extracts the next entry from the remainder, handling
818 /// optional quoting and escape sequences.
819 ///
820 /// # Arguments
821 ///
822 /// * `quoted` - If `true`, parses the entry as a quoted value (removes quotes)
823 /// * `escaped` - If `true`, processes backslash escape sequences
824 ///
825 /// # Escape Sequences
826 ///
827 /// When `escaped` is `true`, the following sequences are processed:
828 /// - `\\n` → newline
829 /// - `\\r` → carriage return
830 /// - `\\t` → tab
831 /// - `\\\\` → backslash
832 /// - `\\"` → double quote
833 ///
834 /// # Errors
835 ///
836 /// Returns [`Error::Protocol`] if:
837 /// - No remaining content to parse
838 /// - `quoted` is `true` but the next entry isn't quoted
839 ///
840 /// # Example
841 ///
842 /// ```rust
843 /// use stem_rs::response::ControlLine;
844 ///
845 /// let mut line = ControlLine::new("\"We're all mad here.\" says the cat.");
846 ///
847 /// // Pop quoted value
848 /// assert_eq!(line.pop(true, false).unwrap(), "We're all mad here.");
849 ///
850 /// // Pop unquoted value
851 /// assert_eq!(line.pop(false, false).unwrap(), "says");
852 ///
853 /// // Check remainder
854 /// assert_eq!(line.remainder(), "the cat.");
855 /// ```
856 pub fn pop(&mut self, quoted: bool, escaped: bool) -> Result<String, Error> {
857 let mut remainder = self.remainder.lock().unwrap();
858 let trimmed = remainder.trim_start();
859
860 if trimmed.is_empty() {
861 return Err(Error::Protocol("no remaining content to parse".to_string()));
862 }
863
864 let (entry, new_remainder) = parse_entry(trimmed, quoted, escaped, false)?;
865 *remainder = new_remainder;
866 Ok(entry)
867 }
868
869 /// Removes and returns the next entry as a KEY=VALUE mapping.
870 ///
871 /// Parses the next entry expecting a `KEY=VALUE` format and returns
872 /// both the key and value as separate strings.
873 ///
874 /// # Arguments
875 ///
876 /// * `quoted` - If `true`, the value is expected to be quoted
877 /// * `escaped` - If `true`, processes backslash escape sequences in the value
878 ///
879 /// # Errors
880 ///
881 /// Returns [`Error::Protocol`] if:
882 /// - No remaining content to parse
883 /// - The next entry isn't a KEY=VALUE mapping
884 /// - `quoted` is `true` but the value isn't quoted
885 ///
886 /// # Example
887 ///
888 /// ```rust
889 /// use stem_rs::response::ControlLine;
890 ///
891 /// let mut line = ControlLine::new("Tor=\"0.4.7.8\" other=value");
892 ///
893 /// // Pop quoted mapping
894 /// let (key, value) = line.pop_mapping(true, false).unwrap();
895 /// assert_eq!(key, "Tor");
896 /// assert_eq!(value, "0.4.7.8");
897 ///
898 /// // Pop unquoted mapping
899 /// let (key, value) = line.pop_mapping(false, false).unwrap();
900 /// assert_eq!(key, "other");
901 /// assert_eq!(value, "value");
902 /// ```
903 pub fn pop_mapping(&mut self, quoted: bool, escaped: bool) -> Result<(String, String), Error> {
904 self.pop_mapping_impl(quoted, escaped, false)
905 }
906
907 /// Removes and returns the next entry as a KEY=VALUE mapping with raw bytes.
908 ///
909 /// Similar to [`pop_mapping`](Self::pop_mapping), but returns the value as
910 /// raw bytes instead of a string. Useful when the value may contain binary
911 /// data or when exact byte preservation is required.
912 ///
913 /// # Arguments
914 ///
915 /// * `quoted` - If `true`, the value is expected to be quoted
916 /// * `escaped` - If `true`, processes backslash escape sequences in the value
917 ///
918 /// # Errors
919 ///
920 /// Returns [`Error::Protocol`] if:
921 /// - No remaining content to parse
922 /// - The next entry isn't a KEY=VALUE mapping
923 /// - `quoted` is `true` but the value isn't quoted
924 ///
925 /// # Example
926 ///
927 /// ```rust
928 /// use stem_rs::response::ControlLine;
929 ///
930 /// let mut line = ControlLine::new("DATA=binary_content");
931 /// let (key, value_bytes) = line.pop_mapping_bytes(false, false).unwrap();
932 /// assert_eq!(key, "DATA");
933 /// assert_eq!(value_bytes, b"binary_content");
934 /// ```
935 pub fn pop_mapping_bytes(
936 &mut self,
937 quoted: bool,
938 escaped: bool,
939 ) -> Result<(String, Vec<u8>), Error> {
940 let mut remainder = self.remainder.lock().unwrap();
941 let trimmed = remainder.trim_start();
942
943 if trimmed.is_empty() {
944 return Err(Error::Protocol("no remaining content to parse".to_string()));
945 }
946
947 let eq_pos = trimmed.find('=').ok_or_else(|| {
948 Error::Protocol(format!(
949 "the next entry isn't a KEY=VALUE mapping: {}",
950 trimmed
951 ))
952 })?;
953
954 let key = trimmed[..eq_pos].to_string();
955 let after_eq = &trimmed[eq_pos + 1..];
956
957 let (entry, new_remainder) = parse_entry(after_eq, quoted, escaped, true)?;
958 *remainder = new_remainder;
959 Ok((key, entry.into_bytes()))
960 }
961
962 /// Internal implementation for pop_mapping variants.
963 fn pop_mapping_impl(
964 &mut self,
965 quoted: bool,
966 escaped: bool,
967 get_bytes: bool,
968 ) -> Result<(String, String), Error> {
969 let mut remainder = self.remainder.lock().unwrap();
970 let trimmed = remainder.trim_start();
971
972 if trimmed.is_empty() {
973 return Err(Error::Protocol("no remaining content to parse".to_string()));
974 }
975
976 let eq_pos = trimmed.find('=').ok_or_else(|| {
977 Error::Protocol(format!(
978 "the next entry isn't a KEY=VALUE mapping: {}",
979 trimmed
980 ))
981 })?;
982
983 let key = trimmed[..eq_pos].to_string();
984 let after_eq = &trimmed[eq_pos + 1..];
985
986 let (entry, new_remainder) = parse_entry(after_eq, quoted, escaped, get_bytes)?;
987 *remainder = new_remainder;
988 Ok((key, entry))
989 }
990}
991
992impl std::fmt::Display for ControlLine {
993 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
994 write!(f, "{}", self.content)
995 }
996}
997
998impl AsRef<str> for ControlLine {
999 fn as_ref(&self) -> &str {
1000 &self.content
1001 }
1002}
1003
1004/// Parses an entry from a line, handling quoting and escaping.
1005fn parse_entry(
1006 line: &str,
1007 quoted: bool,
1008 escaped: bool,
1009 _get_bytes: bool,
1010) -> Result<(String, String), Error> {
1011 if line.is_empty() {
1012 return Err(Error::Protocol("no remaining content to parse".to_string()));
1013 }
1014
1015 let (next_entry, remainder) = if quoted {
1016 let (start, end) = get_quote_indices(line, escaped);
1017 if start != 0 || end == -1 {
1018 return Err(Error::Protocol(format!(
1019 "the next entry isn't a quoted value: {}",
1020 line
1021 )));
1022 }
1023 let end = end as usize;
1024 let entry = &line[1..end];
1025 let rest = &line[end + 1..];
1026 (entry.to_string(), rest.trim_start().to_string())
1027 } else if let Some(space_pos) = line.find(' ') {
1028 (
1029 line[..space_pos].to_string(),
1030 line[space_pos + 1..].to_string(),
1031 )
1032 } else {
1033 (line.to_string(), String::new())
1034 };
1035
1036 let result = if escaped {
1037 unescape_string(&next_entry)
1038 } else {
1039 next_entry
1040 };
1041
1042 Ok((result, remainder))
1043}
1044
1045/// Finds the indices of the next two quote characters in a line.
1046///
1047/// Returns (-1, -1) if quotes are not found. When `escaped` is true,
1048/// skips over backslash-escaped quotes.
1049fn get_quote_indices(line: &str, escaped: bool) -> (i32, i32) {
1050 let bytes = line.as_bytes();
1051 let mut indices = [-1i32, -1i32];
1052 let mut quote_index: i32 = -1;
1053
1054 for index in indices.iter_mut().take(2) {
1055 let start = (quote_index + 1) as usize;
1056 if start >= bytes.len() {
1057 break;
1058 }
1059
1060 let mut pos = start;
1061 while pos < bytes.len() {
1062 if bytes[pos] == b'"' {
1063 if escaped && pos > 0 && bytes[pos - 1] == b'\\' {
1064 pos += 1;
1065 continue;
1066 }
1067 quote_index = pos as i32;
1068 break;
1069 }
1070 pos += 1;
1071 }
1072
1073 if pos >= bytes.len() {
1074 break;
1075 }
1076
1077 *index = quote_index;
1078 }
1079
1080 (indices[0], indices[1])
1081}
1082
1083/// Processes backslash escape sequences in a string.
1084///
1085/// Handles: `\n`, `\r`, `\t`, `\\`, `\"`
1086fn unescape_string(s: &str) -> String {
1087 let mut result = String::with_capacity(s.len());
1088 let mut chars = s.chars().peekable();
1089
1090 while let Some(c) = chars.next() {
1091 if c == '\\' {
1092 if let Some(&next) = chars.peek() {
1093 chars.next();
1094 match next {
1095 'n' => result.push('\n'),
1096 'r' => result.push('\r'),
1097 't' => result.push('\t'),
1098 '\\' => result.push('\\'),
1099 '"' => result.push('"'),
1100 _ => {
1101 result.push('\\');
1102 result.push(next);
1103 }
1104 }
1105 } else {
1106 result.push('\\');
1107 }
1108 } else {
1109 result.push(c);
1110 }
1111 }
1112
1113 result
1114}
1115
1116/// A simple single-line response from Tor.
1117///
1118/// `SingleLineResponse` represents responses that contain exactly one line,
1119/// typically used for commands that perform actions rather than query data.
1120/// These responses are usually "250 OK" on success or an error code with
1121/// a description on failure.
1122///
1123/// # Response Format
1124///
1125/// Single-line responses have the format:
1126/// ```text
1127/// STATUS CONTENT
1128/// ```
1129///
1130/// For example:
1131/// - `250 OK` - Success
1132/// - `552 Unrecognized key "foo"` - Error
1133///
1134/// # Example
1135///
1136/// ```rust
1137/// use stem_rs::response::{ControlMessage, SingleLineResponse};
1138///
1139/// let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1140/// let response = SingleLineResponse::from_message(msg).unwrap();
1141///
1142/// assert!(response.is_ok(false));
1143/// assert!(response.is_ok(true)); // Strict check for "250 OK"
1144/// assert_eq!(response.code, "250");
1145/// assert_eq!(response.message_text, "OK");
1146/// ```
1147#[derive(Debug, Clone)]
1148pub struct SingleLineResponse {
1149 /// The underlying control message.
1150 pub message: ControlMessage,
1151 /// The 3-digit status code (e.g., "250", "552").
1152 pub code: String,
1153 /// The message content after the status code.
1154 pub message_text: String,
1155}
1156
1157impl SingleLineResponse {
1158 /// Creates a `SingleLineResponse` from a control message.
1159 ///
1160 /// Validates that the message contains exactly one line and extracts
1161 /// the status code and message text.
1162 ///
1163 /// # Errors
1164 ///
1165 /// Returns [`Error::Protocol`] if:
1166 /// - The message contains more than one line
1167 /// - The message is empty
1168 ///
1169 /// # Example
1170 ///
1171 /// ```rust
1172 /// use stem_rs::response::{ControlMessage, SingleLineResponse};
1173 ///
1174 /// // Valid single-line response
1175 /// let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1176 /// let response = SingleLineResponse::from_message(msg).unwrap();
1177 ///
1178 /// // Multi-line response fails
1179 /// let msg = ControlMessage::from_str("250-line1\r\n250 line2\r\n", None, false).unwrap();
1180 /// assert!(SingleLineResponse::from_message(msg).is_err());
1181 /// ```
1182 pub fn from_message(message: ControlMessage) -> Result<Self, Error> {
1183 let content = message.content();
1184
1185 if content.len() > 1 {
1186 return Err(Error::Protocol("Received multi-line response".to_string()));
1187 }
1188 if content.is_empty() {
1189 return Err(Error::Protocol("Received empty response".to_string()));
1190 }
1191
1192 let (code, _, msg) = &content[0];
1193
1194 Ok(Self {
1195 code: code.clone(),
1196 message_text: msg.clone(),
1197 message,
1198 })
1199 }
1200
1201 /// Checks if the response indicates success.
1202 ///
1203 /// # Arguments
1204 ///
1205 /// * `strict` - If `true`, requires exactly "250 OK"; if `false`, only checks
1206 /// for status code "250"
1207 ///
1208 /// # Returns
1209 ///
1210 /// - Non-strict: `true` if status code is "250"
1211 /// - Strict: `true` if response is exactly "250 OK"
1212 ///
1213 /// # Example
1214 ///
1215 /// ```rust
1216 /// use stem_rs::response::{ControlMessage, SingleLineResponse};
1217 ///
1218 /// let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1219 /// let response = SingleLineResponse::from_message(msg).unwrap();
1220 /// assert!(response.is_ok(false)); // Non-strict
1221 /// assert!(response.is_ok(true)); // Strict
1222 ///
1223 /// let msg = ControlMessage::from_str("250 Done\r\n", None, false).unwrap();
1224 /// let response = SingleLineResponse::from_message(msg).unwrap();
1225 /// assert!(response.is_ok(false)); // Non-strict passes
1226 /// assert!(!response.is_ok(true)); // Strict fails (not "OK")
1227 /// ```
1228 pub fn is_ok(&self, strict: bool) -> bool {
1229 if strict {
1230 let content = self.message.content();
1231 if let Some((code, div, msg)) = content.first() {
1232 return code == "250" && *div == ' ' && msg == "OK";
1233 }
1234 false
1235 } else {
1236 self.code == "250"
1237 }
1238 }
1239}
1240
1241/// Converts a control message to a specific response type.
1242///
1243/// This function validates that a [`ControlMessage`] conforms to the expected
1244/// format for a specific response type. It performs type-specific validation
1245/// without modifying the message.
1246///
1247/// # Supported Response Types
1248///
1249/// | Type | Description |
1250/// |------|-------------|
1251/// | `SINGLELINE` | Simple single-line response |
1252/// | `ADD_ONION` | ADD_ONION command response |
1253/// | `AUTHCHALLENGE` | SAFECOOKIE authentication challenge |
1254/// | `EVENT` | Asynchronous event notification |
1255/// | `GETCONF` | GETCONF command response |
1256/// | `GETINFO` | GETINFO command response |
1257/// | `MAPADDRESS` | MAPADDRESS command response |
1258/// | `ONION_CLIENT_AUTH_VIEW` | Onion client auth view response |
1259/// | `PROTOCOLINFO` | PROTOCOLINFO command response |
1260///
1261/// # Arguments
1262///
1263/// * `response_type` - The type of response to validate (case-insensitive)
1264/// * `message` - The control message to validate
1265///
1266/// # Errors
1267///
1268/// Returns [`Error::Protocol`] if:
1269/// - The response type is not supported
1270/// - The message doesn't conform to the expected format (for `SINGLELINE`)
1271///
1272/// # Example
1273///
1274/// ```rust
1275/// use stem_rs::response::{ControlMessage, convert};
1276///
1277/// let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1278///
1279/// // Validate as single-line response
1280/// let validated = convert("SINGLELINE", msg.clone()).unwrap();
1281///
1282/// // Multi-line fails SINGLELINE validation
1283/// let multi = ControlMessage::from_str("250-line1\r\n250 line2\r\n", None, false).unwrap();
1284/// assert!(convert("SINGLELINE", multi).is_err());
1285/// ```
1286pub fn convert(response_type: &str, message: ControlMessage) -> Result<ControlMessage, Error> {
1287 match response_type.to_uppercase().as_str() {
1288 "SINGLELINE" => {
1289 SingleLineResponse::from_message(message.clone())?;
1290 Ok(message)
1291 }
1292 "ADD_ONION"
1293 | "AUTHCHALLENGE"
1294 | "EVENT"
1295 | "GETCONF"
1296 | "GETINFO"
1297 | "MAPADDRESS"
1298 | "ONION_CLIENT_AUTH_VIEW"
1299 | "PROTOCOLINFO" => Ok(message),
1300 _ => Err(Error::Protocol(format!(
1301 "Unsupported response type: {}",
1302 response_type
1303 ))),
1304 }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use super::*;
1310 use std::collections::hash_map::DefaultHasher;
1311 use std::hash::{Hash, Hasher};
1312
1313 const OK_REPLY: &str = "250 OK\r\n";
1314 const EVENT_BW: &str = "650 BW 32326 2856\r\n";
1315 const EVENT_CIRC_TIMEOUT: &str = "650 CIRC 5 FAILED PURPOSE=GENERAL REASON=TIMEOUT\r\n";
1316 const EVENT_CIRC_LAUNCHED: &str = "650 CIRC 9 LAUNCHED PURPOSE=GENERAL\r\n";
1317
1318 const GETINFO_VERSION: &str = "250-version=0.2.2.23-alpha (git-b85eb949b528f4d7)\r\n250 OK\r\n";
1319
1320 #[test]
1321 fn test_from_str_basic() {
1322 let msg = ControlMessage::from_str(GETINFO_VERSION, None, false).unwrap();
1323 assert!(msg.is_ok());
1324 assert_eq!(msg.len(), 2);
1325 }
1326
1327 #[test]
1328 fn test_from_str_with_normalize() {
1329 let msg = ControlMessage::from_str("250 OK", None, true).unwrap();
1330 assert!(msg.is_ok());
1331 }
1332
1333 #[test]
1334 fn test_ok_response() {
1335 let msg = ControlMessage::from_str(OK_REPLY, None, false).unwrap();
1336 assert!(msg.is_ok());
1337 let content = msg.content();
1338 assert_eq!(content.len(), 1);
1339 assert_eq!(content[0], ("250".to_string(), ' ', "OK".to_string()));
1340 }
1341
1342 #[test]
1343 fn test_event_response_bw() {
1344 let msg = ControlMessage::from_str(EVENT_BW, None, false).unwrap();
1345 let content = msg.content();
1346 assert_eq!(content.len(), 1);
1347 assert_eq!(content[0].0, "650");
1348 assert_eq!(content[0].1, ' ');
1349 assert!(content[0].2.contains("BW 32326 2856"));
1350 }
1351
1352 #[test]
1353 fn test_event_response_circ() {
1354 for circ_content in [EVENT_CIRC_TIMEOUT, EVENT_CIRC_LAUNCHED] {
1355 let msg = ControlMessage::from_str(circ_content, None, false).unwrap();
1356 let content = msg.content();
1357 assert_eq!(content.len(), 1);
1358 assert_eq!(content[0].0, "650");
1359 }
1360 }
1361
1362 #[test]
1363 fn test_getinfo_response_version() {
1364 let msg = ControlMessage::from_str(GETINFO_VERSION, None, false).unwrap();
1365 assert_eq!(msg.len(), 2);
1366 let content = msg.content();
1367 assert_eq!(content[0].0, "250");
1368 assert_eq!(content[0].1, '-');
1369 assert!(content[0].2.starts_with("version="));
1370 assert_eq!(content[1], ("250".to_string(), ' ', "OK".to_string()));
1371 }
1372
1373 #[test]
1374 fn test_is_ok_various_codes() {
1375 let msg_250 = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1376 assert!(msg_250.is_ok());
1377
1378 let msg_552 = ControlMessage::from_str("552 Unrecognized key\r\n", None, false).unwrap();
1379 assert!(!msg_552.is_ok());
1380
1381 let msg_451 = ControlMessage::from_str("451 Resource exhausted\r\n", None, false).unwrap();
1382 assert!(!msg_451.is_ok());
1383 }
1384
1385 #[test]
1386 fn test_raw_content() {
1387 let msg = ControlMessage::from_str(OK_REPLY, None, false).unwrap();
1388 assert_eq!(msg.raw_content(), OK_REPLY.as_bytes());
1389 }
1390
1391 #[test]
1392 fn test_iteration() {
1393 let msg = ControlMessage::from_str(GETINFO_VERSION, None, false).unwrap();
1394 let lines: Vec<_> = msg.iter().collect();
1395 assert_eq!(lines.len(), 2);
1396 }
1397
1398 #[test]
1399 fn test_indexing_get() {
1400 let msg = ControlMessage::from_str(GETINFO_VERSION, None, false).unwrap();
1401 let line = msg.get(0).unwrap();
1402 assert!(line.to_string().starts_with("version="));
1403 assert!(msg.get(10).is_none());
1404 }
1405
1406 #[test]
1407 fn test_equality() {
1408 let msg1 = ControlMessage::from_str(EVENT_BW, None, false).unwrap();
1409 let msg2 = ControlMessage::from_str(EVENT_BW, None, false).unwrap();
1410 let msg3 = ControlMessage::from_str(EVENT_CIRC_TIMEOUT, None, false).unwrap();
1411
1412 assert_eq!(msg1, msg2);
1413 assert_ne!(msg1, msg3);
1414 }
1415
1416 #[test]
1417 fn test_hashing() {
1418 let msg1 = ControlMessage::from_str(EVENT_BW, None, false).unwrap();
1419 let msg2 = ControlMessage::from_str(EVENT_BW, None, false).unwrap();
1420
1421 let mut hasher1 = DefaultHasher::new();
1422 let mut hasher2 = DefaultHasher::new();
1423 msg1.hash(&mut hasher1);
1424 msg2.hash(&mut hasher2);
1425
1426 assert_eq!(hasher1.finish(), hasher2.finish());
1427 }
1428
1429 #[test]
1430 fn test_empty_message_error() {
1431 let result = ControlMessage::new(vec![], vec![], None);
1432 assert!(result.is_err());
1433 }
1434
1435 #[test]
1436 fn test_control_line_pop_examples() {
1437 let mut line = ControlLine::new("\"We're all mad here.\" says the grinning cat.");
1438 assert_eq!(line.pop(true, false).unwrap(), "We're all mad here.");
1439 assert_eq!(line.pop(false, false).unwrap(), "says");
1440 assert_eq!(line.remainder(), "the grinning cat.");
1441 }
1442
1443 #[test]
1444 fn test_control_line_pop_escaped() {
1445 let mut line = ControlLine::new("\"this has a \\\" and \\\\ in it\" foo=bar more_data");
1446 assert_eq!(line.pop(true, true).unwrap(), "this has a \" and \\ in it");
1447 }
1448
1449 #[test]
1450 fn test_control_line_string_behavior() {
1451 let mut line = ControlLine::new("PROTOCOLINFO 1");
1452 assert_eq!(line.to_string(), "PROTOCOLINFO 1");
1453 assert!(line.to_string().starts_with("PROTOCOLINFO "));
1454
1455 line.pop(false, false).unwrap();
1456 assert_eq!(line.to_string(), "PROTOCOLINFO 1");
1457 }
1458
1459 #[test]
1460 fn test_control_line_general_usage() {
1461 let mut line = ControlLine::new("PROTOCOLINFO 1");
1462 assert_eq!(line.remainder(), "PROTOCOLINFO 1");
1463 assert!(!line.is_empty());
1464 assert!(!line.is_next_quoted(false));
1465 assert!(!line.is_next_mapping(None, false, false));
1466 assert!(line.peek_key().is_none());
1467
1468 assert!(line.pop_mapping(false, false).is_err());
1469 assert_eq!(line.pop(false, false).unwrap(), "PROTOCOLINFO");
1470 assert_eq!(line.remainder(), "1");
1471 assert!(!line.is_empty());
1472
1473 assert!(line.pop_mapping(false, false).is_err());
1474 assert_eq!(line.pop(false, false).unwrap(), "1");
1475 assert_eq!(line.remainder(), "");
1476 assert!(line.is_empty());
1477
1478 assert!(line.pop_mapping(false, false).is_err());
1479 assert!(line.pop(false, false).is_err());
1480 }
1481
1482 #[test]
1483 fn test_control_line_pop_mapping() {
1484 let version_entry = "Tor=\"0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)\"";
1485
1486 let mut line = ControlLine::new(version_entry);
1487 assert_eq!(line.remainder(), version_entry);
1488 assert!(!line.is_empty());
1489 assert!(!line.is_next_quoted(false));
1490 assert!(line.is_next_mapping(None, false, false));
1491 assert!(line.is_next_mapping(Some("Tor"), false, false));
1492 assert!(line.is_next_mapping(Some("Tor"), true, false));
1493 assert!(line.is_next_mapping(None, true, false));
1494 assert_eq!(line.peek_key(), Some("Tor".to_string()));
1495
1496 let (key, value) = line.pop_mapping(false, false).unwrap();
1497 assert_eq!(key, "Tor");
1498 assert_eq!(value, "\"0.2.1.30");
1499
1500 let mut line = ControlLine::new(version_entry);
1501 let (key, value) = line.pop_mapping(true, false).unwrap();
1502 assert_eq!(key, "Tor");
1503 assert_eq!(value, "0.2.1.30 (0a083b0188cacd2f07838ff0446113bd5211a024)");
1504 assert!(line.is_empty());
1505 }
1506
1507 #[test]
1508 fn test_control_line_escapes() {
1509 let auth_line =
1510 r#"AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie""#;
1511 let mut line = ControlLine::new(auth_line);
1512
1513 assert_eq!(line.pop(false, false).unwrap(), "AUTH");
1514 let (key, value) = line.pop_mapping(false, false).unwrap();
1515 assert_eq!(key, "METHODS");
1516 assert_eq!(value, "COOKIE");
1517
1518 let cookie_entry = r#"COOKIEFILE="/tmp/my data\\\"dir//control_auth_cookie""#;
1519 let mut line = ControlLine::new(cookie_entry);
1520 let (key, value) = line.pop_mapping(true, true).unwrap();
1521 assert_eq!(key, "COOKIEFILE");
1522 assert_eq!(value, r#"/tmp/my data\"dir//control_auth_cookie"#);
1523 assert!(line.is_empty());
1524 }
1525
1526 #[test]
1527 fn test_control_line_windows_path_escapes() {
1528 let mut line = ControlLine::new(
1529 r#"COOKIEFILE="C:\\Users\\Atagar\\AppData\\tor\\control_auth_cookie""#,
1530 );
1531 let (key, value) = line.pop_mapping(true, true).unwrap();
1532 assert_eq!(key, "COOKIEFILE");
1533 assert_eq!(value, r#"C:\Users\Atagar\AppData\tor\control_auth_cookie"#);
1534 assert!(line.is_empty());
1535 }
1536
1537 #[test]
1538 fn test_is_next_quoted() {
1539 let line = ControlLine::new("\"quoted value\" unquoted");
1540 assert!(line.is_next_quoted(false));
1541
1542 let line = ControlLine::new("unquoted \"quoted\"");
1543 assert!(!line.is_next_quoted(false));
1544 }
1545
1546 #[test]
1547 fn test_is_next_mapping_with_key() {
1548 let line = ControlLine::new("KEY=value OTHER=stuff");
1549 assert!(line.is_next_mapping(Some("KEY"), false, false));
1550 assert!(!line.is_next_mapping(Some("OTHER"), false, false));
1551 assert!(line.is_next_mapping(None, false, false));
1552 }
1553
1554 #[test]
1555 fn test_peek_key() {
1556 let line = ControlLine::new("MYKEY=myvalue");
1557 assert_eq!(line.peek_key(), Some("MYKEY".to_string()));
1558
1559 let line = ControlLine::new("no_equals_here");
1560 assert!(line.peek_key().is_none());
1561 }
1562
1563 #[test]
1564 fn test_single_line_response_not_ok() {
1565 let msg = ControlMessage::from_str("552 NOTOK\r\n", None, false).unwrap();
1566 let response = SingleLineResponse::from_message(msg).unwrap();
1567 assert!(!response.is_ok(false));
1568 }
1569
1570 #[test]
1571 fn test_single_line_response_ok_non_strict() {
1572 let msg = ControlMessage::from_str("250 KK\r\n", None, false).unwrap();
1573 let response = SingleLineResponse::from_message(msg).unwrap();
1574 assert!(response.is_ok(false));
1575 }
1576
1577 #[test]
1578 fn test_single_line_response_ok_strict() {
1579 let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1580 let response = SingleLineResponse::from_message(msg).unwrap();
1581 assert!(response.is_ok(true));
1582 }
1583
1584 #[test]
1585 fn test_single_line_response_ok_strict_fails() {
1586 let msg = ControlMessage::from_str("250 HMM\r\n", None, false).unwrap();
1587 let response = SingleLineResponse::from_message(msg).unwrap();
1588 assert!(!response.is_ok(true));
1589 }
1590
1591 #[test]
1592 fn test_single_line_response_multi_line_error() {
1593 let msg = ControlMessage::from_str("250-MULTI\r\n250 LINE\r\n", None, false).unwrap();
1594 let result = SingleLineResponse::from_message(msg);
1595 assert!(result.is_err());
1596 }
1597
1598 #[test]
1599 fn test_convert_singleline() {
1600 let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1601 let result = convert("SINGLELINE", msg);
1602 assert!(result.is_ok());
1603 }
1604
1605 #[test]
1606 fn test_convert_singleline_multi_line_error() {
1607 let msg = ControlMessage::from_str("250-MULTI\r\n250 LINE\r\n", None, false).unwrap();
1608 let result = convert("SINGLELINE", msg);
1609 assert!(result.is_err());
1610 }
1611
1612 #[test]
1613 fn test_convert_unsupported_type() {
1614 let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1615 let result = convert("UNKNOWN_TYPE", msg);
1616 assert!(result.is_err());
1617 }
1618
1619 #[test]
1620 fn test_convert_known_types() {
1621 let msg = ControlMessage::from_str("250 OK\r\n", None, false).unwrap();
1622 for response_type in [
1623 "ADD_ONION",
1624 "AUTHCHALLENGE",
1625 "EVENT",
1626 "GETCONF",
1627 "GETINFO",
1628 "MAPADDRESS",
1629 "PROTOCOLINFO",
1630 ] {
1631 let result = convert(response_type, msg.clone());
1632 assert!(result.is_ok(), "Failed for type: {}", response_type);
1633 }
1634 }
1635
1636 #[test]
1637 fn test_unescape_string() {
1638 assert_eq!(unescape_string(r#"hello\nworld"#), "hello\nworld");
1639 assert_eq!(unescape_string(r#"tab\there"#), "tab\there");
1640 assert_eq!(unescape_string(r#"quote\"here"#), "quote\"here");
1641 assert_eq!(unescape_string(r#"backslash\\here"#), "backslash\\here");
1642 assert_eq!(unescape_string(r#"carriage\rreturn"#), "carriage\rreturn");
1643 assert_eq!(unescape_string(r#"unknown\xescape"#), "unknown\\xescape");
1644 }
1645
1646 #[test]
1647 fn test_get_quote_indices() {
1648 assert_eq!(get_quote_indices("\"hello\"", false), (0, 6));
1649 assert_eq!(get_quote_indices("no quotes", false), (-1, -1));
1650 assert_eq!(get_quote_indices("\"only one", false), (0, -1));
1651 assert_eq!(get_quote_indices("\"escaped\\\"quote\"", true), (0, 15));
1652 }
1653}