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