stem_rs/
protocol.rs

1//! Control protocol message parsing for Tor control protocol.
2//!
3//! This module provides parsing utilities for control protocol responses,
4//! including single-line, multi-line, and data responses. It implements
5//! the message format defined in section 2.3 of the Tor control protocol
6//! specification.
7//!
8//! # Conceptual Role
9//!
10//! The protocol module handles the low-level parsing of control protocol
11//! messages. It sits between the raw socket communication ([`crate::socket`])
12//! and the high-level response types ([`crate::response`]).
13//!
14//! # Protocol Format
15//!
16//! The Tor control protocol uses a text-based format with the following structure:
17//!
18//! ## Request Format
19//!
20//! ```text
21//! COMMAND [ARGS]\r\n
22//! ```
23//!
24//! ## Response Format
25//!
26//! Each response line has the format:
27//!
28//! ```text
29//! STATUS DIVIDER MESSAGE\r\n
30//! ```
31//!
32//! Where:
33//! - `STATUS` is a 3-digit status code (e.g., 250 for success, 5xx for errors)
34//! - `DIVIDER` indicates the line type:
35//!   - ` ` (space): Final line of the response
36//!   - `-`: Continuation line (more lines follow)
37//!   - `+`: Data line (multi-line data block follows)
38//! - `MESSAGE` is the response content
39//!
40//! ## Status Codes
41//!
42//! | Code | Meaning |
43//! |------|---------|
44//! | 250  | Success |
45//! | 251  | Operation unnecessary |
46//! | 5xx  | Error (various types) |
47//! | 650  | Asynchronous event |
48//!
49//! # Single-Line Response Example
50//!
51//! ```text
52//! 250 OK
53//! ```
54//!
55//! # Multi-Line Response Example
56//!
57//! ```text
58//! 250-version=0.4.7.1
59//! 250-config-file=/etc/tor/torrc
60//! 250 OK
61//! ```
62//!
63//! # Data Response Example
64//!
65//! ```text
66//! 250+info/names=
67//! desc/id/* -- Router descriptors by ID.
68//! desc/name/* -- Router descriptors by nickname.
69//! .
70//! 250 OK
71//! ```
72//!
73//! # Example
74//!
75//! ```rust
76//! use stem_rs::protocol::{ParsedLine, ControlLine, format_command};
77//!
78//! // Parse a response line
79//! let line = ParsedLine::parse("250 OK").unwrap();
80//! assert_eq!(line.status_code, 250);
81//! assert!(line.is_final());
82//!
83//! // Parse key=value content
84//! let mut ctrl = ControlLine::new("key1=value1 key2=\"quoted value\"");
85//! let (k, v) = ctrl.pop_mapping(false, false).unwrap();
86//! assert_eq!(k, "key1");
87//! assert_eq!(v, "value1");
88//!
89//! // Format a command
90//! let cmd = format_command("GETINFO", &["version"]);
91//! assert_eq!(cmd, "GETINFO version\r\n");
92//! ```
93//!
94//! # Thread Safety
95//!
96//! [`ParsedLine`] is `Send` and `Sync` as it contains only owned data.
97//! [`ControlLine`] is `Send` but not `Sync` due to internal mutable state
98//! for tracking parse position.
99//!
100//! # See Also
101//!
102//! - [`crate::socket`]: Low-level socket communication
103//! - [`crate::response`]: High-level response parsing
104//! - [`crate::controller`]: High-level controller API
105
106use crate::Error;
107
108/// A parsed line from a Tor control protocol response.
109///
110/// Represents a single line of a control protocol response, broken down into
111/// its component parts: status code, divider character, and content.
112///
113/// # Protocol Format
114///
115/// Each response line has the format: `STATUS DIVIDER CONTENT`
116///
117/// - `status_code`: 3-digit numeric code indicating success/failure
118/// - `divider`: Single character indicating line type
119/// - `content`: The actual message content
120///
121/// # Divider Types
122///
123/// | Divider | Method | Meaning |
124/// |---------|--------|---------|
125/// | ` ` (space) | [`is_final()`](Self::is_final) | Final line of response |
126/// | `-` | [`is_continuation()`](Self::is_continuation) | More lines follow |
127/// | `+` | [`is_data()`](Self::is_data) | Multi-line data block follows |
128///
129/// # Example
130///
131/// ```rust
132/// use stem_rs::protocol::ParsedLine;
133///
134/// // Parse a success response
135/// let line = ParsedLine::parse("250 OK").unwrap();
136/// assert_eq!(line.status_code, 250);
137/// assert_eq!(line.divider, ' ');
138/// assert_eq!(line.content, "OK");
139/// assert!(line.is_final());
140///
141/// // Parse a continuation line
142/// let cont = ParsedLine::parse("250-version=0.4.7.1").unwrap();
143/// assert!(cont.is_continuation());
144///
145/// // Parse a data line
146/// let data = ParsedLine::parse("250+getinfo").unwrap();
147/// assert!(data.is_data());
148///
149/// // Parse an error response
150/// let err = ParsedLine::parse("515 Authentication failed").unwrap();
151/// assert_eq!(err.status_code, 515);
152/// ```
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct ParsedLine {
155    /// The 3-digit status code from the response.
156    ///
157    /// Common codes:
158    /// - `250`: Success
159    /// - `251`: Operation unnecessary
160    /// - `5xx`: Various error conditions
161    /// - `650`: Asynchronous event notification
162    pub status_code: u16,
163
164    /// The divider character indicating the line type.
165    ///
166    /// - `' '` (space): Final line of the response
167    /// - `'-'`: Continuation line (more lines follow)
168    /// - `'+'`: Data line (multi-line data block follows)
169    pub divider: char,
170
171    /// The content of the response line after the status code and divider.
172    ///
173    /// For success responses, this is typically "OK".
174    /// For error responses, this contains the error description.
175    /// For data responses, this may contain key=value pairs or other data.
176    pub content: String,
177}
178
179impl ParsedLine {
180    /// Parses a raw control protocol response line into its components.
181    ///
182    /// Takes a line from the control socket and extracts the status code,
183    /// divider character, and content. The line may optionally include
184    /// trailing `\r\n` characters which are stripped.
185    ///
186    /// # Arguments
187    ///
188    /// * `line` - The raw line to parse, with or without trailing CRLF
189    ///
190    /// # Returns
191    ///
192    /// A `ParsedLine` containing the extracted components.
193    ///
194    /// # Errors
195    ///
196    /// Returns [`Error::Protocol`] if:
197    /// - The line is shorter than 3 characters
198    /// - The first 3 characters are not a valid numeric status code
199    ///
200    /// # Example
201    ///
202    /// ```rust
203    /// use stem_rs::protocol::ParsedLine;
204    ///
205    /// // Parse with CRLF (as received from socket)
206    /// let line = ParsedLine::parse("250 OK\r\n").unwrap();
207    /// assert_eq!(line.status_code, 250);
208    /// assert_eq!(line.content, "OK");
209    ///
210    /// // Parse without CRLF
211    /// let line = ParsedLine::parse("250-version=0.4.7.1").unwrap();
212    /// assert_eq!(line.content, "version=0.4.7.1");
213    ///
214    /// // Error: line too short
215    /// assert!(ParsedLine::parse("25").is_err());
216    ///
217    /// // Error: invalid status code
218    /// assert!(ParsedLine::parse("abc OK").is_err());
219    /// ```
220    pub fn parse(line: &str) -> Result<Self, Error> {
221        let line = line.trim_end_matches(['\r', '\n']);
222        if line.len() < 3 {
223            return Err(Error::Protocol(format!("line too short: {}", line)));
224        }
225
226        let status_code: u16 = line[..3]
227            .parse()
228            .map_err(|_| Error::Protocol(format!("invalid status code: {}", &line[..3])))?;
229
230        let divider = if line.len() > 3 {
231            line.chars().nth(3).unwrap_or(' ')
232        } else {
233            ' '
234        };
235
236        let content = if line.len() > 4 {
237            line[4..].to_string()
238        } else {
239            String::new()
240        };
241
242        Ok(Self {
243            status_code,
244            divider,
245            content,
246        })
247    }
248
249    /// Returns `true` if this is the final line of a response.
250    ///
251    /// A final line has a space character as its divider, indicating
252    /// no more lines follow in this response.
253    ///
254    /// # Example
255    ///
256    /// ```rust
257    /// use stem_rs::protocol::ParsedLine;
258    ///
259    /// let final_line = ParsedLine::parse("250 OK").unwrap();
260    /// assert!(final_line.is_final());
261    ///
262    /// let cont_line = ParsedLine::parse("250-more data").unwrap();
263    /// assert!(!cont_line.is_final());
264    /// ```
265    pub fn is_final(&self) -> bool {
266        self.divider == ' '
267    }
268
269    /// Returns `true` if this is a continuation line.
270    ///
271    /// A continuation line has a `-` character as its divider, indicating
272    /// more lines follow in this response.
273    ///
274    /// # Example
275    ///
276    /// ```rust
277    /// use stem_rs::protocol::ParsedLine;
278    ///
279    /// let cont_line = ParsedLine::parse("250-version=0.4.7.1").unwrap();
280    /// assert!(cont_line.is_continuation());
281    ///
282    /// let final_line = ParsedLine::parse("250 OK").unwrap();
283    /// assert!(!final_line.is_continuation());
284    /// ```
285    pub fn is_continuation(&self) -> bool {
286        self.divider == '-'
287    }
288
289    /// Returns `true` if this is a data line.
290    ///
291    /// A data line has a `+` character as its divider, indicating a
292    /// multi-line data block follows. The data block is terminated
293    /// by a line containing only a period (`.`).
294    ///
295    /// # Example
296    ///
297    /// ```rust
298    /// use stem_rs::protocol::ParsedLine;
299    ///
300    /// let data_line = ParsedLine::parse("250+getinfo").unwrap();
301    /// assert!(data_line.is_data());
302    ///
303    /// let final_line = ParsedLine::parse("250 OK").unwrap();
304    /// assert!(!data_line.is_final());
305    /// ```
306    pub fn is_data(&self) -> bool {
307        self.divider == '+'
308    }
309}
310
311/// A parser for space-delimited control protocol response content.
312///
313/// `ControlLine` provides methods for parsing the content portion of control
314/// protocol responses, which often contain space-separated values and
315/// `KEY=VALUE` mappings. It maintains an internal position to track parsing
316/// progress.
317///
318/// # Conceptual Role
319///
320/// After extracting the content from a [`ParsedLine`], `ControlLine` is used
321/// to parse individual entries from that content. It supports:
322///
323/// - Unquoted values: `value1 value2 value3`
324/// - Quoted values: `"value with spaces"`
325/// - Key-value mappings: `key=value` or `key="quoted value"`
326/// - Escaped strings: `"value with \"quotes\" and \\backslashes"`
327///
328/// # What This Type Does NOT Do
329///
330/// - Parse the status code or divider (use [`ParsedLine`] for that)
331/// - Handle multi-line data blocks
332/// - Validate semantic correctness of values
333///
334/// # Thread Safety
335///
336/// `ControlLine` is `Send` but not `Sync` due to internal mutable state
337/// for tracking the parse position. For concurrent access, create separate
338/// `ControlLine` instances.
339///
340/// # Example
341///
342/// ```rust
343/// use stem_rs::protocol::ControlLine;
344///
345/// // Parse space-separated values
346/// let mut line = ControlLine::new("hello world test");
347/// assert_eq!(line.pop(false, false).unwrap(), "hello");
348/// assert_eq!(line.pop(false, false).unwrap(), "world");
349/// assert_eq!(line.pop(false, false).unwrap(), "test");
350/// assert!(line.is_empty());
351///
352/// // Parse quoted values
353/// let mut line = ControlLine::new("\"hello world\" test");
354/// assert_eq!(line.pop(true, false).unwrap(), "hello world");
355/// assert_eq!(line.pop(false, false).unwrap(), "test");
356///
357/// // Parse key=value mappings
358/// let mut line = ControlLine::new("key=value other=\"quoted\"");
359/// let (k, v) = line.pop_mapping(false, false).unwrap();
360/// assert_eq!(k, "key");
361/// assert_eq!(v, "value");
362/// let (k2, v2) = line.pop_mapping(true, false).unwrap();
363/// assert_eq!(k2, "other");
364/// assert_eq!(v2, "quoted");
365/// ```
366pub struct ControlLine {
367    /// The full content string being parsed.
368    content: String,
369    /// Current position in the content string.
370    position: usize,
371}
372
373impl ControlLine {
374    /// Creates a new `ControlLine` parser for the given content.
375    ///
376    /// The parser starts at the beginning of the content string.
377    ///
378    /// # Arguments
379    ///
380    /// * `content` - The content string to parse
381    ///
382    /// # Example
383    ///
384    /// ```rust
385    /// use stem_rs::protocol::ControlLine;
386    ///
387    /// let line = ControlLine::new("key=value other=data");
388    /// assert!(!line.is_empty());
389    /// ```
390    pub fn new(content: &str) -> Self {
391        Self {
392            content: content.to_string(),
393            position: 0,
394        }
395    }
396
397    /// Returns the unparsed remainder of the content.
398    ///
399    /// Leading whitespace is trimmed from the returned string.
400    ///
401    /// # Example
402    ///
403    /// ```rust
404    /// use stem_rs::protocol::ControlLine;
405    ///
406    /// let mut line = ControlLine::new("hello world");
407    /// assert_eq!(line.remainder(), "hello world");
408    /// line.pop(false, false).unwrap();
409    /// assert_eq!(line.remainder(), "world");
410    /// ```
411    pub fn remainder(&self) -> &str {
412        self.content[self.position..].trim_start()
413    }
414
415    /// Returns `true` if there is no more content to parse.
416    ///
417    /// # Example
418    ///
419    /// ```rust
420    /// use stem_rs::protocol::ControlLine;
421    ///
422    /// let mut line = ControlLine::new("hello");
423    /// assert!(!line.is_empty());
424    /// line.pop(false, false).unwrap();
425    /// assert!(line.is_empty());
426    ///
427    /// let empty = ControlLine::new("");
428    /// assert!(empty.is_empty());
429    /// ```
430    pub fn is_empty(&self) -> bool {
431        self.remainder().is_empty()
432    }
433
434    /// Returns `true` if the next entry is a quoted value.
435    ///
436    /// Checks if the next non-whitespace character is a double quote (`"`).
437    ///
438    /// # Example
439    ///
440    /// ```rust
441    /// use stem_rs::protocol::ControlLine;
442    ///
443    /// let line = ControlLine::new("\"quoted\" unquoted");
444    /// assert!(line.is_next_quoted());
445    ///
446    /// let line2 = ControlLine::new("unquoted \"quoted\"");
447    /// assert!(!line2.is_next_quoted());
448    /// ```
449    pub fn is_next_quoted(&self) -> bool {
450        self.remainder().starts_with('"')
451    }
452
453    /// Returns `true` if the next entry is a `KEY=VALUE` mapping.
454    ///
455    /// Optionally checks that the key matches a specific value and/or
456    /// that the value is quoted.
457    ///
458    /// # Arguments
459    ///
460    /// * `key` - If `Some`, checks that the key matches this value
461    /// * `quoted` - If `true`, checks that the value is quoted
462    ///
463    /// # Example
464    ///
465    /// ```rust
466    /// use stem_rs::protocol::ControlLine;
467    ///
468    /// let line = ControlLine::new("key=value");
469    /// assert!(line.is_next_mapping(None, false));
470    /// assert!(line.is_next_mapping(Some("key"), false));
471    /// assert!(!line.is_next_mapping(Some("other"), false));
472    ///
473    /// let quoted = ControlLine::new("key=\"value\"");
474    /// assert!(quoted.is_next_mapping(None, true));
475    /// assert!(quoted.is_next_mapping(Some("key"), true));
476    /// ```
477    pub fn is_next_mapping(&self, key: Option<&str>, quoted: bool) -> bool {
478        let rest = self.remainder();
479        if let Some(eq_pos) = rest.find('=') {
480            if let Some(expected_key) = key {
481                let actual_key = &rest[..eq_pos];
482                if actual_key != expected_key {
483                    return false;
484                }
485            }
486            if quoted {
487                rest.get(eq_pos + 1..).is_some_and(|s| s.starts_with('"'))
488            } else {
489                true
490            }
491        } else {
492            false
493        }
494    }
495
496    /// Returns the key of the next entry if it's a `KEY=VALUE` mapping.
497    ///
498    /// Returns `None` if the next entry is not a mapping.
499    ///
500    /// # Example
501    ///
502    /// ```rust
503    /// use stem_rs::protocol::ControlLine;
504    ///
505    /// let line = ControlLine::new("mykey=myvalue");
506    /// assert_eq!(line.peek_key(), Some("mykey"));
507    ///
508    /// let no_mapping = ControlLine::new("just a value");
509    /// assert_eq!(no_mapping.peek_key(), None);
510    /// ```
511    pub fn peek_key(&self) -> Option<&str> {
512        let rest = self.remainder();
513        rest.find('=').map(|pos| &rest[..pos])
514    }
515
516    /// Removes and returns the next space-separated entry.
517    ///
518    /// Advances the internal position past the extracted entry.
519    ///
520    /// # Arguments
521    ///
522    /// * `quoted` - If `true`, expects and removes surrounding quotes
523    /// * `escaped` - If `true`, processes escape sequences in the value
524    ///
525    /// # Escape Sequences
526    ///
527    /// When `escaped` is `true`, the following sequences are processed:
528    /// - `\\n` → newline
529    /// - `\\r` → carriage return
530    /// - `\\t` → tab
531    /// - `\\\\` → backslash
532    /// - `\\"` → double quote
533    ///
534    /// # Errors
535    ///
536    /// Returns [`Error::Protocol`] if:
537    /// - No more content to parse
538    /// - `quoted` is `true` but the next entry doesn't start with a quote
539    /// - A quoted string is not properly terminated
540    ///
541    /// # Example
542    ///
543    /// ```rust
544    /// use stem_rs::protocol::ControlLine;
545    ///
546    /// // Unquoted values
547    /// let mut line = ControlLine::new("hello world");
548    /// assert_eq!(line.pop(false, false).unwrap(), "hello");
549    /// assert_eq!(line.pop(false, false).unwrap(), "world");
550    ///
551    /// // Quoted values
552    /// let mut line = ControlLine::new("\"hello world\" test");
553    /// assert_eq!(line.pop(true, false).unwrap(), "hello world");
554    ///
555    /// // Escaped values
556    /// let mut line = ControlLine::new("\"hello\\nworld\"");
557    /// assert_eq!(line.pop(true, true).unwrap(), "hello\nworld");
558    /// ```
559    pub fn pop(&mut self, quoted: bool, escaped: bool) -> Result<String, Error> {
560        let rest = self.remainder();
561        if rest.is_empty() {
562            return Err(Error::Protocol("no more content to pop".to_string()));
563        }
564
565        if quoted {
566            if !rest.starts_with('"') {
567                return Err(Error::Protocol("expected quoted string".to_string()));
568            }
569            let after_quote = &rest[1..];
570            let end_pos = find_closing_quote(after_quote, escaped)?;
571            let value = if escaped {
572                unescape_string(&after_quote[..end_pos])
573            } else {
574                after_quote[..end_pos].to_string()
575            };
576            self.position = self.content.len() - rest.len() + 2 + end_pos;
577            if self.position < self.content.len() && self.content.as_bytes()[self.position] == b' '
578            {
579                self.position += 1;
580            }
581            Ok(value)
582        } else {
583            let end_pos = rest.find(' ').unwrap_or(rest.len());
584            let value = rest[..end_pos].to_string();
585            self.position = self.content.len() - rest.len() + end_pos;
586            if self.position < self.content.len() {
587                self.position += 1;
588            }
589            Ok(value)
590        }
591    }
592
593    /// Removes and returns the next `KEY=VALUE` mapping.
594    ///
595    /// Parses the next entry as a key-value pair separated by `=`.
596    /// Advances the internal position past the extracted mapping.
597    ///
598    /// # Arguments
599    ///
600    /// * `quoted` - If `true`, expects the value to be quoted
601    /// * `escaped` - If `true`, processes escape sequences in the value
602    ///
603    /// # Errors
604    ///
605    /// Returns [`Error::Protocol`] if:
606    /// - The next entry is not a `KEY=VALUE` mapping
607    /// - `quoted` is `true` but the value is not quoted
608    /// - A quoted string is not properly terminated
609    ///
610    /// # Example
611    ///
612    /// ```rust
613    /// use stem_rs::protocol::ControlLine;
614    ///
615    /// // Simple mapping
616    /// let mut line = ControlLine::new("key=value other=data");
617    /// let (k, v) = line.pop_mapping(false, false).unwrap();
618    /// assert_eq!(k, "key");
619    /// assert_eq!(v, "value");
620    ///
621    /// // Quoted mapping
622    /// let mut line = ControlLine::new("key=\"hello world\"");
623    /// let (k, v) = line.pop_mapping(true, false).unwrap();
624    /// assert_eq!(k, "key");
625    /// assert_eq!(v, "hello world");
626    ///
627    /// // Error: not a mapping
628    /// let mut line = ControlLine::new("not_a_mapping");
629    /// assert!(line.pop_mapping(false, false).is_err());
630    /// ```
631    pub fn pop_mapping(&mut self, quoted: bool, escaped: bool) -> Result<(String, String), Error> {
632        let rest = self.remainder();
633        let eq_pos = rest
634            .find('=')
635            .ok_or_else(|| Error::Protocol(format!("expected key=value mapping in: {}", rest)))?;
636
637        let key = rest[..eq_pos].to_string();
638        self.position = self.content.len() - rest.len() + eq_pos + 1;
639
640        let value = self.pop(quoted, escaped)?;
641        Ok((key, value))
642    }
643}
644
645/// Finds the position of the closing quote in a string.
646///
647/// Searches for the closing `"` character, optionally handling escape
648/// sequences where `\"` does not count as a closing quote.
649///
650/// # Arguments
651///
652/// * `s` - The string to search (should not include the opening quote)
653/// * `escaped` - If `true`, `\"` sequences are skipped
654///
655/// # Errors
656///
657/// Returns [`Error::Protocol`] if no closing quote is found.
658fn find_closing_quote(s: &str, escaped: bool) -> Result<usize, Error> {
659    let mut pos = 0;
660    let bytes = s.as_bytes();
661    while pos < bytes.len() {
662        if bytes[pos] == b'"' {
663            return Ok(pos);
664        }
665        if escaped && bytes[pos] == b'\\' && pos + 1 < bytes.len() {
666            pos += 2;
667        } else {
668            pos += 1;
669        }
670    }
671    Err(Error::Protocol("unterminated quoted string".to_string()))
672}
673
674/// Processes escape sequences in a string.
675///
676/// Converts escape sequences to their actual characters:
677/// - `\n` → newline
678/// - `\r` → carriage return
679/// - `\t` → tab
680/// - `\\` → backslash
681/// - `\"` → double quote
682///
683/// Unknown escape sequences are preserved as-is (e.g., `\x` → `\x`).
684fn unescape_string(s: &str) -> String {
685    let mut result = String::with_capacity(s.len());
686    let mut chars = s.chars().peekable();
687    while let Some(c) = chars.next() {
688        if c == '\\' {
689            if let Some(&next) = chars.peek() {
690                chars.next();
691                match next {
692                    'n' => result.push('\n'),
693                    'r' => result.push('\r'),
694                    't' => result.push('\t'),
695                    '\\' => result.push('\\'),
696                    '"' => result.push('"'),
697                    _ => {
698                        result.push('\\');
699                        result.push(next);
700                    }
701                }
702            } else {
703                result.push('\\');
704            }
705        } else {
706            result.push(c);
707        }
708    }
709    result
710}
711
712/// Formats a control protocol command with arguments.
713///
714/// Creates a properly formatted command string ready to send to the
715/// control socket. The command is terminated with `\r\n` as required
716/// by the protocol.
717///
718/// # Arguments
719///
720/// * `command` - The command name (e.g., "GETINFO", "SETCONF")
721/// * `args` - Slice of argument strings to append to the command
722///
723/// # Returns
724///
725/// A formatted command string ending with `\r\n`.
726///
727/// # Example
728///
729/// ```rust
730/// use stem_rs::protocol::format_command;
731///
732/// // Command without arguments
733/// let cmd = format_command("AUTHENTICATE", &[]);
734/// assert_eq!(cmd, "AUTHENTICATE\r\n");
735///
736/// // Command with single argument
737/// let cmd = format_command("GETINFO", &["version"]);
738/// assert_eq!(cmd, "GETINFO version\r\n");
739///
740/// // Command with multiple arguments
741/// let cmd = format_command("SETCONF", &["key1=value1", "key2=value2"]);
742/// assert_eq!(cmd, "SETCONF key1=value1 key2=value2\r\n");
743/// ```
744pub fn format_command(command: &str, args: &[&str]) -> String {
745    if args.is_empty() {
746        format!("{}\r\n", command)
747    } else {
748        format!("{} {}\r\n", command, args.join(" "))
749    }
750}
751
752/// Quotes a string for use in control protocol commands.
753///
754/// Wraps the string in double quotes and escapes special characters:
755/// - `"` → `\"`
756/// - `\` → `\\`
757/// - newline → `\n`
758/// - carriage return → `\r`
759/// - tab → `\t`
760///
761/// This is the inverse of the unescaping performed by [`ControlLine::pop`]
762/// with `escaped = true`.
763///
764/// # Arguments
765///
766/// * `s` - The string to quote
767///
768/// # Returns
769///
770/// A quoted and escaped string.
771///
772/// # Example
773///
774/// ```rust
775/// use stem_rs::protocol::quote_string;
776///
777/// // Simple string
778/// assert_eq!(quote_string("hello"), "\"hello\"");
779///
780/// // String with special characters
781/// assert_eq!(quote_string("hello\nworld"), "\"hello\\nworld\"");
782/// assert_eq!(quote_string("say \"hi\""), "\"say \\\"hi\\\"\"");
783/// assert_eq!(quote_string("path\\to\\file"), "\"path\\\\to\\\\file\"");
784/// ```
785///
786/// # Round-Trip Property
787///
788/// For any string `s`, quoting and then unquoting should return the
789/// original string:
790///
791/// ```rust
792/// use stem_rs::protocol::{quote_string, ControlLine};
793///
794/// let original = "hello\nworld";
795/// let quoted = quote_string(original);
796/// let mut line = ControlLine::new(&quoted);
797/// let unquoted = line.pop(true, true).unwrap();
798/// assert_eq!(original, unquoted);
799/// ```
800pub fn quote_string(s: &str) -> String {
801    let mut result = String::with_capacity(s.len() + 2);
802    result.push('"');
803    for c in s.chars() {
804        match c {
805            '"' => result.push_str("\\\""),
806            '\\' => result.push_str("\\\\"),
807            '\n' => result.push_str("\\n"),
808            '\r' => result.push_str("\\r"),
809            '\t' => result.push_str("\\t"),
810            _ => result.push(c),
811        }
812    }
813    result.push('"');
814    result
815}
816
817#[cfg(test)]
818mod tests {
819    use super::*;
820
821    #[test]
822    fn test_parse_line_simple() {
823        let parsed = ParsedLine::parse("250 OK").unwrap();
824        assert_eq!(parsed.status_code, 250);
825        assert_eq!(parsed.divider, ' ');
826        assert_eq!(parsed.content, "OK");
827        assert!(parsed.is_final());
828    }
829
830    #[test]
831    fn test_parse_line_continuation() {
832        let parsed = ParsedLine::parse("250-version=0.4.7.1").unwrap();
833        assert_eq!(parsed.status_code, 250);
834        assert_eq!(parsed.divider, '-');
835        assert_eq!(parsed.content, "version=0.4.7.1");
836        assert!(parsed.is_continuation());
837    }
838
839    #[test]
840    fn test_parse_line_data() {
841        let parsed = ParsedLine::parse("250+getinfo").unwrap();
842        assert_eq!(parsed.status_code, 250);
843        assert_eq!(parsed.divider, '+');
844        assert_eq!(parsed.content, "getinfo");
845        assert!(parsed.is_data());
846    }
847
848    #[test]
849    fn test_parse_line_error() {
850        let parsed = ParsedLine::parse("515 Authentication failed").unwrap();
851        assert_eq!(parsed.status_code, 515);
852        assert_eq!(parsed.divider, ' ');
853        assert_eq!(parsed.content, "Authentication failed");
854    }
855
856    #[test]
857    fn test_parse_line_too_short() {
858        assert!(ParsedLine::parse("25").is_err());
859    }
860
861    #[test]
862    fn test_parse_line_invalid_code() {
863        assert!(ParsedLine::parse("abc OK").is_err());
864    }
865
866    #[test]
867    fn test_control_line_pop_unquoted() {
868        let mut line = ControlLine::new("hello world test");
869        assert_eq!(line.pop(false, false).unwrap(), "hello");
870        assert_eq!(line.pop(false, false).unwrap(), "world");
871        assert_eq!(line.pop(false, false).unwrap(), "test");
872        assert!(line.is_empty());
873    }
874
875    #[test]
876    fn test_control_line_pop_quoted() {
877        let mut line = ControlLine::new("\"hello world\" test");
878        assert_eq!(line.pop(true, false).unwrap(), "hello world");
879        assert_eq!(line.pop(false, false).unwrap(), "test");
880    }
881
882    #[test]
883    fn test_control_line_pop_escaped() {
884        let mut line = ControlLine::new("\"hello\\nworld\" test");
885        assert_eq!(line.pop(true, true).unwrap(), "hello\nworld");
886    }
887
888    #[test]
889    fn test_control_line_pop_mapping() {
890        let mut line = ControlLine::new("key=value other=data");
891        let (k, v) = line.pop_mapping(false, false).unwrap();
892        assert_eq!(k, "key");
893        assert_eq!(v, "value");
894        let (k2, v2) = line.pop_mapping(false, false).unwrap();
895        assert_eq!(k2, "other");
896        assert_eq!(v2, "data");
897    }
898
899    #[test]
900    fn test_control_line_pop_mapping_quoted() {
901        let mut line = ControlLine::new("key=\"hello world\"");
902        let (k, v) = line.pop_mapping(true, false).unwrap();
903        assert_eq!(k, "key");
904        assert_eq!(v, "hello world");
905    }
906
907    #[test]
908    fn test_control_line_is_next_mapping() {
909        let line = ControlLine::new("key=value");
910        assert!(line.is_next_mapping(None, false));
911        assert!(line.is_next_mapping(Some("key"), false));
912        assert!(!line.is_next_mapping(Some("other"), false));
913    }
914
915    #[test]
916    fn test_control_line_peek_key() {
917        let line = ControlLine::new("mykey=myvalue");
918        assert_eq!(line.peek_key(), Some("mykey"));
919    }
920
921    #[test]
922    fn test_format_command_no_args() {
923        assert_eq!(format_command("AUTHENTICATE", &[]), "AUTHENTICATE\r\n");
924    }
925
926    #[test]
927    fn test_format_command_with_args() {
928        assert_eq!(
929            format_command("GETINFO", &["version", "config-file"]),
930            "GETINFO version config-file\r\n"
931        );
932    }
933
934    #[test]
935    fn test_quote_string_simple() {
936        assert_eq!(quote_string("hello"), "\"hello\"");
937    }
938
939    #[test]
940    fn test_quote_string_with_escapes() {
941        assert_eq!(quote_string("hello\nworld"), "\"hello\\nworld\"");
942        assert_eq!(quote_string("say \"hi\""), "\"say \\\"hi\\\"\"");
943        assert_eq!(quote_string("path\\to\\file"), "\"path\\\\to\\\\file\"");
944    }
945
946    #[test]
947    fn test_unescape_string() {
948        assert_eq!(unescape_string("hello\\nworld"), "hello\nworld");
949        assert_eq!(unescape_string("say \\\"hi\\\""), "say \"hi\"");
950        assert_eq!(unescape_string("path\\\\to"), "path\\to");
951        assert_eq!(unescape_string("tab\\there"), "tab\there");
952    }
953
954    #[test]
955    fn test_parse_line_with_crlf() {
956        let parsed = ParsedLine::parse("250 OK\r\n").unwrap();
957        assert_eq!(parsed.status_code, 250);
958        assert_eq!(parsed.content, "OK");
959    }
960
961    #[test]
962    fn test_control_line_empty() {
963        let line = ControlLine::new("");
964        assert!(line.is_empty());
965    }
966
967    #[test]
968    fn test_control_line_whitespace_handling() {
969        let mut line = ControlLine::new("  hello   world  ");
970        assert_eq!(line.pop(false, false).unwrap(), "hello");
971        assert_eq!(line.pop(false, false).unwrap(), "world");
972    }
973
974    #[test]
975    fn test_parse_line_status_codes() {
976        let codes = [200, 250, 251, 500, 510, 515, 550, 650];
977        for code in codes {
978            let line = format!("{} Test message", code);
979            let parsed = ParsedLine::parse(&line).unwrap();
980            assert_eq!(parsed.status_code, code);
981        }
982    }
983
984    #[test]
985    fn test_control_line_complex_mapping() {
986        let mut line = ControlLine::new("key1=value1 key2=\"quoted value\" key3=value3");
987
988        let (k1, v1) = line.pop_mapping(false, false).unwrap();
989        assert_eq!(k1, "key1");
990        assert_eq!(v1, "value1");
991
992        let (k2, v2) = line.pop_mapping(true, false).unwrap();
993        assert_eq!(k2, "key2");
994        assert_eq!(v2, "quoted value");
995
996        let (k3, v3) = line.pop_mapping(false, false).unwrap();
997        assert_eq!(k3, "key3");
998        assert_eq!(v3, "value3");
999    }
1000
1001    #[test]
1002    fn test_control_line_escaped_quotes() {
1003        let mut line = ControlLine::new("\"hello \\\"world\\\"\"");
1004        let value = line.pop(true, true).unwrap();
1005        assert_eq!(value, "hello \"world\"");
1006    }
1007
1008    #[test]
1009    fn test_control_line_escaped_backslash() {
1010        let mut line = ControlLine::new("\"path\\\\to\\\\file\"");
1011        let value = line.pop(true, true).unwrap();
1012        assert_eq!(value, "path\\to\\file");
1013    }
1014
1015    #[test]
1016    fn test_control_line_all_escape_sequences() {
1017        let mut line = ControlLine::new("\"\\n\\r\\t\\\\\\\"\"");
1018        let value = line.pop(true, true).unwrap();
1019        assert_eq!(value, "\n\r\t\\\"");
1020    }
1021
1022    #[test]
1023    fn test_quote_string_all_special_chars() {
1024        let input = "hello\nworld\r\ttab\\backslash\"quote";
1025        let quoted = quote_string(input);
1026        assert_eq!(quoted, "\"hello\\nworld\\r\\ttab\\\\backslash\\\"quote\"");
1027    }
1028
1029    #[test]
1030    fn test_format_command_single_arg() {
1031        assert_eq!(
1032            format_command("GETINFO", &["version"]),
1033            "GETINFO version\r\n"
1034        );
1035    }
1036
1037    #[test]
1038    fn test_format_command_multiple_args() {
1039        assert_eq!(
1040            format_command("SETCONF", &["key1=value1", "key2=value2"]),
1041            "SETCONF key1=value1 key2=value2\r\n"
1042        );
1043    }
1044
1045    #[test]
1046    fn test_parsed_line_divider_types() {
1047        let final_line = ParsedLine::parse("250 OK").unwrap();
1048        assert!(final_line.is_final());
1049        assert!(!final_line.is_continuation());
1050        assert!(!final_line.is_data());
1051
1052        let cont_line = ParsedLine::parse("250-More data").unwrap();
1053        assert!(!cont_line.is_final());
1054        assert!(cont_line.is_continuation());
1055        assert!(!cont_line.is_data());
1056
1057        let data_line = ParsedLine::parse("250+Data block").unwrap();
1058        assert!(!data_line.is_final());
1059        assert!(!data_line.is_continuation());
1060        assert!(data_line.is_data());
1061    }
1062
1063    #[test]
1064    fn test_control_line_is_next_quoted() {
1065        let line = ControlLine::new("\"quoted\" unquoted");
1066        assert!(line.is_next_quoted());
1067
1068        let line2 = ControlLine::new("unquoted \"quoted\"");
1069        assert!(!line2.is_next_quoted());
1070    }
1071
1072    #[test]
1073    fn test_control_line_pop_error_on_empty() {
1074        let mut line = ControlLine::new("");
1075        assert!(line.pop(false, false).is_err());
1076    }
1077
1078    #[test]
1079    fn test_control_line_pop_error_on_missing_quote() {
1080        let mut line = ControlLine::new("unquoted");
1081        assert!(line.pop(true, false).is_err());
1082    }
1083}
1084
1085#[cfg(test)]
1086mod proptests {
1087    use super::*;
1088    use proptest::char::range as char_range;
1089    use proptest::prelude::*;
1090
1091    fn valid_status_code() -> impl Strategy<Value = u16> {
1092        prop_oneof![
1093            Just(200u16),
1094            Just(250u16),
1095            Just(251u16),
1096            Just(500u16),
1097            Just(510u16),
1098            Just(515u16),
1099            Just(550u16),
1100            Just(650u16),
1101        ]
1102    }
1103
1104    fn safe_content_char() -> impl Strategy<Value = char> {
1105        prop_oneof![
1106            char_range('a', 'z'),
1107            char_range('A', 'Z'),
1108            char_range('0', '9'),
1109            Just(' '),
1110            Just('='),
1111            Just('-'),
1112            Just('_'),
1113            Just('.'),
1114            Just('/'),
1115        ]
1116    }
1117
1118    fn safe_content_string() -> impl Strategy<Value = String> {
1119        proptest::collection::vec(safe_content_char(), 0..50)
1120            .prop_map(|chars| chars.into_iter().collect())
1121    }
1122
1123    proptest! {
1124        #![proptest_config(ProptestConfig::with_cases(100))]
1125
1126        #[test]
1127        fn prop_parsed_line_roundtrip(
1128            status_code in valid_status_code(),
1129            content in safe_content_string()
1130        ) {
1131            let line = format!("{} {}", status_code, content);
1132            let parsed = ParsedLine::parse(&line).expect("should parse");
1133            prop_assert_eq!(parsed.status_code, status_code);
1134            prop_assert_eq!(parsed.divider, ' ');
1135            prop_assert_eq!(parsed.content, content);
1136        }
1137
1138        #[test]
1139        fn prop_parsed_line_continuation_roundtrip(
1140            status_code in valid_status_code(),
1141            content in safe_content_string()
1142        ) {
1143            let line = format!("{}-{}", status_code, content);
1144            let parsed = ParsedLine::parse(&line).expect("should parse");
1145            prop_assert_eq!(parsed.status_code, status_code);
1146            prop_assert_eq!(parsed.divider, '-');
1147            prop_assert!(parsed.is_continuation());
1148        }
1149
1150        #[test]
1151        fn prop_quote_unquote_roundtrip(content in safe_content_string()) {
1152            let quoted = quote_string(&content);
1153            prop_assert!(quoted.starts_with('"'));
1154            prop_assert!(quoted.ends_with('"'));
1155            let inner = &quoted[1..quoted.len()-1];
1156            let unquoted = unescape_string(inner);
1157            prop_assert_eq!(content, unquoted);
1158        }
1159
1160        #[test]
1161        fn prop_format_command_ends_with_crlf(
1162            cmd in "[A-Z]{3,10}",
1163            args in proptest::collection::vec("[a-z0-9]{1,10}", 0..3)
1164        ) {
1165            let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
1166            let formatted = format_command(&cmd, &args_refs);
1167            prop_assert!(formatted.ends_with("\r\n"));
1168            prop_assert!(formatted.starts_with(&cmd));
1169        }
1170
1171        #[test]
1172        fn prop_control_line_pop_preserves_content(
1173            word1 in "[a-z]{1,10}",
1174            word2 in "[a-z]{1,10}",
1175            word3 in "[a-z]{1,10}"
1176        ) {
1177            let content = format!("{} {} {}", word1, word2, word3);
1178            let mut line = ControlLine::new(&content);
1179            let popped1 = line.pop(false, false).expect("should pop");
1180            let popped2 = line.pop(false, false).expect("should pop");
1181            let popped3 = line.pop(false, false).expect("should pop");
1182            prop_assert_eq!(popped1, word1);
1183            prop_assert_eq!(popped2, word2);
1184            prop_assert_eq!(popped3, word3);
1185            prop_assert!(line.is_empty());
1186        }
1187
1188        #[test]
1189        fn prop_control_line_mapping_roundtrip(
1190            key in "[a-z]{1,10}",
1191            value in "[a-z0-9]{1,10}"
1192        ) {
1193            let content = format!("{}={}", key, value);
1194            let mut line = ControlLine::new(&content);
1195            let (k, v) = line.pop_mapping(false, false).expect("should pop mapping");
1196            prop_assert_eq!(k, key);
1197            prop_assert_eq!(v, value);
1198        }
1199    }
1200}