stem_rs/interpreter/
mod.rs

1//! Interactive interpreter for Tor control protocol.
2//!
3//! This module provides an interactive command interpreter for interacting
4//! with Tor's control interface, supporting both interpreter commands (like
5//! `/help`, `/events`, `/info`) and direct Tor control commands.
6//!
7//! # Overview
8//!
9//! The interpreter provides a REPL-like interface for communicating with Tor,
10//! adding usability features such as:
11//!
12//! - IRC-style interpreter commands (prefixed with `/`)
13//! - Direct Tor control protocol command passthrough
14//! - Event buffering and filtering
15//! - Relay information lookup by fingerprint, nickname, or IP address
16//! - Tab completion support via the [`autocomplete`] module
17//! - Built-in help system via the [`help`] module
18//!
19//! # Interpreter Commands
20//!
21//! Commands prefixed with `/` are handled by the interpreter itself:
22//!
23//! | Command | Description |
24//! |---------|-------------|
25//! | `/help [topic]` | Display help information |
26//! | `/events [types...]` | Show buffered events, optionally filtered by type |
27//! | `/events CLEAR` | Clear the event buffer |
28//! | `/info [relay]` | Show information about a relay |
29//! | `/python enable\|disable` | Toggle Python command mode |
30//! | `/quit` | Exit the interpreter |
31//!
32//! All other commands are passed directly to Tor's control interface.
33//!
34//! # Architecture
35//!
36//! The interpreter wraps a [`Controller`] and maintains:
37//! - A bounded event buffer (most recent 100 events)
38//! - Multiline context state for complex commands
39//! - Python command mode toggle
40//!
41//! # Example
42//!
43//! ```rust,no_run
44//! use stem_rs::Controller;
45//! use stem_rs::interpreter::ControlInterpreter;
46//!
47//! # async fn example() -> Result<(), stem_rs::Error> {
48//! let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
49//! controller.authenticate(None).await?;
50//!
51//! let mut interpreter = ControlInterpreter::new(&mut controller);
52//!
53//! // Run interpreter commands
54//! let help = interpreter.run_command("/help").await?;
55//! println!("{}", help);
56//!
57//! // Run Tor control commands
58//! let version = interpreter.run_command("GETINFO version").await?;
59//! println!("Tor version: {}", version);
60//! # Ok(())
61//! # }
62//! ```
63//!
64//! # See Also
65//!
66//! - [`Controller`] - The underlying control interface
67//! - [`arguments`] - Command-line argument parsing
68//! - [`autocomplete`] - Tab completion functionality
69//! - [`help`] - Help system implementation
70//!
71//! # Python Stem Equivalent
72//!
73//! This module corresponds to Python Stem's `stem.interpreter` module.
74
75pub mod arguments;
76pub mod autocomplete;
77pub mod help;
78
79use std::collections::VecDeque;
80
81use crate::controller::Controller;
82use crate::events::ParsedEvent;
83use crate::util::{is_valid_fingerprint, is_valid_ipv4_address, is_valid_nickname, is_valid_port};
84use crate::Error;
85
86/// Maximum number of events to buffer.
87///
88/// The interpreter maintains a rolling buffer of the most recent events
89/// for display via the `/events` command. Older events are discarded
90/// when this limit is reached.
91const MAX_EVENTS: usize = 100;
92
93/// Interactive command interpreter for Tor control protocol.
94///
95/// `ControlInterpreter` provides a high-level interface for interacting with
96/// Tor, combining direct control protocol access with convenience commands
97/// for common operations.
98///
99/// # Conceptual Role
100///
101/// The interpreter sits between user input and the [`Controller`], providing:
102/// - Command routing (interpreter vs. Tor commands)
103/// - Event buffering and retrieval
104/// - Relay lookup by various identifiers
105/// - Help and documentation access
106///
107/// # Invariants
108///
109/// - The event buffer never exceeds [`MAX_EVENTS`] entries
110/// - Events are stored in reverse chronological order (newest first)
111/// - The underlying controller connection must remain valid
112///
113/// # Thread Safety
114///
115/// `ControlInterpreter` is `Send` but not `Sync` due to the mutable
116/// reference to the underlying [`Controller`].
117///
118/// # Example
119///
120/// ```rust,no_run
121/// use stem_rs::Controller;
122/// use stem_rs::interpreter::ControlInterpreter;
123///
124/// # async fn example() -> Result<(), stem_rs::Error> {
125/// let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
126/// controller.authenticate(None).await?;
127///
128/// let mut interpreter = ControlInterpreter::new(&mut controller);
129///
130/// // Query relay information
131/// let info = interpreter.run_command("/info MyRelay").await?;
132///
133/// // Send a signal to Tor
134/// let result = interpreter.run_command("SIGNAL NEWNYM").await?;
135/// # Ok(())
136/// # }
137/// ```
138pub struct ControlInterpreter<'a> {
139    /// Reference to the underlying Tor controller.
140    controller: &'a mut Controller,
141    /// Buffer of received events, newest first.
142    received_events: VecDeque<ParsedEvent>,
143    /// Whether to interpret non-interpreter commands as Python.
144    run_python_commands: bool,
145    /// Whether the interpreter is in a multiline input context.
146    ///
147    /// This is set to `true` when the user is entering a multiline command
148    /// (such as `LOADCONF` or `POSTDESCRIPTOR`). The prompt should change
149    /// to indicate continuation (e.g., `... ` instead of `>>> `).
150    pub is_multiline_context: bool,
151}
152
153impl<'a> ControlInterpreter<'a> {
154    /// Creates a new interpreter wrapping the given controller.
155    ///
156    /// The interpreter is initialized with:
157    /// - An empty event buffer
158    /// - Python command mode enabled
159    /// - Multiline context disabled
160    ///
161    /// # Arguments
162    ///
163    /// * `controller` - A mutable reference to an authenticated [`Controller`]
164    ///
165    /// # Example
166    ///
167    /// ```rust,no_run
168    /// use stem_rs::Controller;
169    /// use stem_rs::interpreter::ControlInterpreter;
170    ///
171    /// # async fn example() -> Result<(), stem_rs::Error> {
172    /// let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
173    /// controller.authenticate(None).await?;
174    ///
175    /// let interpreter = ControlInterpreter::new(&mut controller);
176    /// # Ok(())
177    /// # }
178    /// ```
179    pub fn new(controller: &'a mut Controller) -> Self {
180        Self {
181            controller,
182            received_events: VecDeque::with_capacity(MAX_EVENTS),
183            run_python_commands: true,
184            is_multiline_context: false,
185        }
186    }
187
188    /// Adds an event to the buffer.
189    ///
190    /// Events are stored in reverse chronological order (newest first).
191    /// If the buffer is full, the oldest event is discarded.
192    ///
193    /// # Arguments
194    ///
195    /// * `event` - The parsed event to add
196    ///
197    /// # Example
198    ///
199    /// ```rust,no_run
200    /// use stem_rs::Controller;
201    /// use stem_rs::interpreter::ControlInterpreter;
202    /// use stem_rs::events::ParsedEvent;
203    ///
204    /// # async fn example() -> Result<(), stem_rs::Error> {
205    /// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
206    /// # controller.authenticate(None).await?;
207    /// let mut interpreter = ControlInterpreter::new(&mut controller);
208    ///
209    /// // Events would typically come from the controller's event stream
210    /// // interpreter.add_event(event);
211    /// # Ok(())
212    /// # }
213    /// ```
214    pub fn add_event(&mut self, event: ParsedEvent) {
215        self.received_events.push_front(event);
216        if self.received_events.len() > MAX_EVENTS {
217            self.received_events.pop_back();
218        }
219    }
220
221    /// Retrieves buffered events, optionally filtered by type.
222    ///
223    /// Returns events in reverse chronological order (newest first).
224    ///
225    /// # Arguments
226    ///
227    /// * `event_types` - Event types to filter by (case-insensitive).
228    ///   If empty, all events are returned.
229    ///
230    /// # Returns
231    ///
232    /// A vector of references to matching events.
233    ///
234    /// # Example
235    ///
236    /// ```rust,no_run
237    /// use stem_rs::Controller;
238    /// use stem_rs::interpreter::ControlInterpreter;
239    ///
240    /// # async fn example() -> Result<(), stem_rs::Error> {
241    /// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
242    /// # controller.authenticate(None).await?;
243    /// let interpreter = ControlInterpreter::new(&mut controller);
244    ///
245    /// // Get all events
246    /// let all_events = interpreter.get_events(&[]);
247    ///
248    /// // Get only bandwidth events
249    /// let bw_events = interpreter.get_events(&["BW"]);
250    ///
251    /// // Get circuit and stream events
252    /// let circ_stream = interpreter.get_events(&["CIRC", "STREAM"]);
253    /// # Ok(())
254    /// # }
255    /// ```
256    pub fn get_events(&self, event_types: &[&str]) -> Vec<&ParsedEvent> {
257        if event_types.is_empty() {
258            self.received_events.iter().collect()
259        } else {
260            self.received_events
261                .iter()
262                .filter(|e| {
263                    event_types
264                        .iter()
265                        .any(|t| e.event_type().eq_ignore_ascii_case(t))
266                })
267                .collect()
268        }
269    }
270
271    /// Executes a command and returns the result.
272    ///
273    /// Commands are routed based on their prefix:
274    /// - Commands starting with `/` are interpreter commands
275    /// - All other commands are sent to Tor's control interface
276    ///
277    /// # Arguments
278    ///
279    /// * `command` - The command string to execute
280    ///
281    /// # Returns
282    ///
283    /// The command output as a string, or an error.
284    ///
285    /// # Errors
286    ///
287    /// - [`Error::SocketClosed`] - If `/quit` or `QUIT` is executed
288    /// - [`Error::Socket`] - If communication with Tor fails
289    /// - [`Error::InvalidArguments`] - If relay lookup fails
290    ///
291    /// # Example
292    ///
293    /// ```rust,no_run
294    /// use stem_rs::Controller;
295    /// use stem_rs::interpreter::ControlInterpreter;
296    ///
297    /// # async fn example() -> Result<(), stem_rs::Error> {
298    /// # let mut controller = Controller::from_port("127.0.0.1:9051".parse()?).await?;
299    /// # controller.authenticate(None).await?;
300    /// let mut interpreter = ControlInterpreter::new(&mut controller);
301    ///
302    /// // Interpreter command
303    /// let help = interpreter.run_command("/help").await?;
304    ///
305    /// // Tor control command
306    /// let version = interpreter.run_command("GETINFO version").await?;
307    ///
308    /// // Empty commands return empty string
309    /// let empty = interpreter.run_command("").await?;
310    /// assert!(empty.is_empty());
311    /// # Ok(())
312    /// # }
313    /// ```
314    pub async fn run_command(&mut self, command: &str) -> Result<String, Error> {
315        let command = command.trim();
316        if command.is_empty() {
317            return Ok(String::new());
318        }
319
320        let (cmd, arg) = match command.split_once(' ') {
321            Some((c, a)) => (c, a.trim()),
322            None => (command, ""),
323        };
324
325        if cmd.starts_with('/') {
326            self.run_interpreter_command(cmd, arg).await
327        } else {
328            self.run_tor_command(cmd, arg).await
329        }
330    }
331
332    /// Executes an interpreter command (prefixed with `/`).
333    ///
334    /// # Arguments
335    ///
336    /// * `cmd` - The command name (e.g., `/help`)
337    /// * `arg` - The command arguments
338    ///
339    /// # Returns
340    ///
341    /// The command output or an error.
342    async fn run_interpreter_command(&mut self, cmd: &str, arg: &str) -> Result<String, Error> {
343        match cmd.to_lowercase().as_str() {
344            "/quit" => Err(Error::SocketClosed),
345            "/events" => Ok(self.do_events(arg)),
346            "/info" => self.do_info(arg).await,
347            "/python" => Ok(self.do_python(arg)),
348            "/help" => Ok(help::response(self.controller, arg).await),
349            _ => Ok(format!("'{}' isn't a recognized command", cmd)),
350        }
351    }
352
353    /// Executes a Tor control protocol command.
354    ///
355    /// The command is converted to uppercase and sent to Tor.
356    /// Multiline commands (`LOADCONF`, `POSTDESCRIPTOR`) are not yet supported.
357    ///
358    /// # Arguments
359    ///
360    /// * `cmd` - The command name (e.g., `GETINFO`)
361    /// * `arg` - The command arguments
362    ///
363    /// # Returns
364    ///
365    /// The Tor response or an error.
366    async fn run_tor_command(&mut self, cmd: &str, arg: &str) -> Result<String, Error> {
367        let cmd_upper = cmd.to_uppercase();
368
369        if cmd_upper == "LOADCONF"
370            || cmd_upper == "+LOADCONF"
371            || cmd_upper == "POSTDESCRIPTOR"
372            || cmd_upper == "+POSTDESCRIPTOR"
373        {
374            return Ok("Multi-line control options like this are not yet implemented.".to_string());
375        }
376
377        if cmd_upper == "QUIT" {
378            return Err(Error::SocketClosed);
379        }
380
381        let full_command = if arg.is_empty() {
382            cmd_upper
383        } else {
384            format!("{} {}", cmd_upper, arg)
385        };
386
387        let response = self.controller.msg(&full_command).await?;
388        Ok(response)
389    }
390
391    /// Handles the `/events` command.
392    ///
393    /// Displays buffered events, optionally filtered by type.
394    /// If `CLEAR` is specified, clears the event buffer instead.
395    fn do_events(&mut self, arg: &str) -> String {
396        let event_types: Vec<&str> = arg.split_whitespace().collect();
397
398        if event_types.iter().any(|t| t.eq_ignore_ascii_case("CLEAR")) {
399            self.received_events.clear();
400            return "cleared event backlog".to_string();
401        }
402
403        let events = self.get_events(&event_types);
404        if events.is_empty() {
405            return String::new();
406        }
407
408        events
409            .iter()
410            .map(|e| format!("{}", e))
411            .collect::<Vec<_>>()
412            .join("\n")
413    }
414
415    /// Handles the `/info` command.
416    ///
417    /// Displays information about a relay identified by fingerprint,
418    /// nickname, or IP address.
419    async fn do_info(&mut self, arg: &str) -> Result<String, Error> {
420        let fingerprint = self.resolve_fingerprint(arg).await?;
421
422        let ns_desc = self
423            .controller
424            .get_info(&format!("ns/id/{}", fingerprint))
425            .await;
426
427        match ns_desc {
428            Ok(ns_content) => {
429                let mut output = Vec::new();
430                output.push(format!("Fingerprint: {}", fingerprint));
431
432                for line in ns_content.lines() {
433                    if line.starts_with("r ") {
434                        let parts: Vec<&str> = line.split_whitespace().collect();
435                        if parts.len() >= 2 {
436                            output.push(format!("Nickname: {}", parts[1]));
437                        }
438                        if parts.len() >= 7 {
439                            output.push(format!(
440                                "Address: {}:{}",
441                                parts[6],
442                                parts.get(7).unwrap_or(&"0")
443                            ));
444                        }
445                    } else if let Some(stripped) = line.strip_prefix("s ") {
446                        output.push(format!("Flags: {}", stripped));
447                    } else if let Some(stripped) = line.strip_prefix("v ") {
448                        output.push(format!("Version: {}", stripped));
449                    }
450                }
451
452                Ok(output.join("\n"))
453            }
454            Err(_) => Ok(format!(
455                "Unable to find consensus information for {}",
456                fingerprint
457            )),
458        }
459    }
460
461    /// Resolves a relay identifier to a fingerprint.
462    ///
463    /// Accepts:
464    /// - 40-character hex fingerprint (returned as-is)
465    /// - Relay nickname (looked up via `ns/name/`)
466    /// - IPv4 address with optional port (looked up via `ns/all`)
467    ///
468    /// # Errors
469    ///
470    /// Returns [`Error::InvalidArguments`] if:
471    /// - The identifier format is not recognized
472    /// - No relay matches the identifier
473    /// - Multiple relays match an IP address without port
474    async fn resolve_fingerprint(&mut self, arg: &str) -> Result<String, Error> {
475        if arg.is_empty() {
476            return self.controller.get_info("fingerprint").await.map_err(|_| {
477                Error::InvalidArguments("We aren't a relay, no information to provide".to_string())
478            });
479        }
480
481        if is_valid_fingerprint(arg) {
482            return Ok(arg.to_string());
483        }
484
485        if is_valid_nickname(arg) {
486            let ns_info = self
487                .controller
488                .get_info(&format!("ns/name/{}", arg))
489                .await?;
490            for line in ns_info.lines() {
491                if line.starts_with("r ") {
492                    let parts: Vec<&str> = line.split_whitespace().collect();
493                    if parts.len() >= 3 {
494                        return Ok(parts[2].to_string());
495                    }
496                }
497            }
498            return Err(Error::InvalidArguments(format!(
499                "Unable to find a relay with the nickname of '{}'",
500                arg
501            )));
502        }
503
504        if arg.contains(':') || is_valid_ipv4_address(arg) {
505            let (address, port) = if arg.contains(':') {
506                let (addr, port_str) = arg.rsplit_once(':').unwrap();
507                if !is_valid_ipv4_address(addr) {
508                    return Err(Error::InvalidArguments(format!(
509                        "'{}' isn't a valid IPv4 address",
510                        addr
511                    )));
512                }
513                if !port_str.is_empty() && !is_valid_port(port_str) {
514                    return Err(Error::InvalidArguments(format!(
515                        "'{}' isn't a valid port",
516                        port_str
517                    )));
518                }
519                let port: Option<u16> = port_str.parse().ok();
520                (addr, port)
521            } else {
522                (arg, None)
523            };
524
525            let ns_all = self.controller.get_info("ns/all").await?;
526            let mut matches: Vec<(u16, String)> = Vec::new();
527
528            for line in ns_all.lines() {
529                if line.starts_with("r ") {
530                    let parts: Vec<&str> = line.split_whitespace().collect();
531                    if parts.len() >= 8 {
532                        let relay_addr = parts[6];
533                        let relay_port: u16 = parts[7].parse().unwrap_or(0);
534                        let relay_fp = parts[2];
535
536                        if relay_addr == address && (port.is_none() || port == Some(relay_port)) {
537                            matches.push((relay_port, relay_fp.to_string()));
538                        }
539                    }
540                }
541            }
542
543            match matches.len() {
544                0 => Err(Error::InvalidArguments(format!(
545                    "No relays found at {}",
546                    arg
547                ))),
548                1 => Ok(matches[0].1.clone()),
549                _ => {
550                    let mut response = format!(
551                        "There's multiple relays at {}, include a port to specify which.\n\n",
552                        arg
553                    );
554                    for (i, (or_port, fp)) in matches.iter().enumerate() {
555                        response.push_str(&format!(
556                            "  {}. {}:{}, fingerprint: {}\n",
557                            i + 1,
558                            address,
559                            or_port,
560                            fp
561                        ));
562                    }
563                    Err(Error::InvalidArguments(response))
564                }
565            }
566        } else {
567            Err(Error::InvalidArguments(format!(
568                "'{}' isn't a fingerprint, nickname, or IP address",
569                arg
570            )))
571        }
572    }
573
574    /// Handles the `/python` command.
575    ///
576    /// Toggles whether non-interpreter commands are treated as Python
577    /// expressions or passed directly to Tor.
578    fn do_python(&mut self, arg: &str) -> String {
579        if arg.is_empty() {
580            let status = if self.run_python_commands {
581                "enabled"
582            } else {
583                "disabled"
584            };
585            return format!("Python support is currently {}.", status);
586        }
587
588        match arg.to_lowercase().as_str() {
589            "enable" => {
590                self.run_python_commands = true;
591                "Python support enabled, we'll now run non-interpreter commands as python."
592                    .to_string()
593            }
594            "disable" => {
595                self.run_python_commands = false;
596                "Python support disabled, we'll now pass along all commands to tor.".to_string()
597            }
598            _ => format!(
599                "'{}' is not recognized. Please run either '/python enable' or '/python disable'.",
600                arg
601            ),
602        }
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn test_event_buffer_capacity() {
612        assert_eq!(MAX_EVENTS, 100);
613    }
614
615    #[test]
616    fn test_do_python_status() {
617        let enabled_msg = "Python support is currently enabled.";
618        assert!(enabled_msg.contains("enabled"));
619
620        let disabled_msg = "Python support is currently disabled.";
621        assert!(disabled_msg.contains("disabled"));
622    }
623
624    #[test]
625    fn test_do_python_enable_disable_messages() {
626        let enable_msg =
627            "Python support enabled, we'll now run non-interpreter commands as python.";
628        assert!(enable_msg.contains("enabled"));
629
630        let disable_msg = "Python support disabled, we'll now pass along all commands to tor.";
631        assert!(disable_msg.contains("disabled"));
632    }
633
634    #[test]
635    fn test_do_python_invalid_arg_message() {
636        let invalid_arg = "invalid";
637        let expected = format!(
638            "'{}' is not recognized. Please run either '/python enable' or '/python disable'.",
639            invalid_arg
640        );
641        assert!(expected.contains("not recognized"));
642        assert!(expected.contains("/python enable"));
643        assert!(expected.contains("/python disable"));
644    }
645
646    #[test]
647    fn test_do_events_clear_message() {
648        let clear_msg = "cleared event backlog";
649        assert!(clear_msg.contains("cleared"));
650    }
651
652    #[test]
653    fn test_multiline_command_message() {
654        let msg = "Multi-line control options like this are not yet implemented.";
655        assert!(msg.contains("Multi-line"));
656        assert!(msg.contains("not yet implemented"));
657    }
658
659    #[test]
660    fn test_unrecognized_command_format() {
661        let cmd = "/unknown";
662        let msg = format!("'{}' isn't a recognized command", cmd);
663        assert!(msg.contains("/unknown"));
664        assert!(msg.contains("isn't a recognized command"));
665    }
666
667    #[test]
668    fn test_resolve_fingerprint_validation() {
669        let valid_fp = "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234";
670        assert!(is_valid_fingerprint(valid_fp));
671
672        let invalid_fp = "ABCD";
673        assert!(!is_valid_fingerprint(invalid_fp));
674    }
675
676    #[test]
677    fn test_resolve_nickname_validation() {
678        let valid_nick = "MyRelay";
679        assert!(is_valid_nickname(valid_nick));
680
681        let invalid_nick = "my-relay";
682        assert!(!is_valid_nickname(invalid_nick));
683    }
684
685    #[test]
686    fn test_resolve_ipv4_validation() {
687        let valid_ip = "192.168.1.1";
688        assert!(is_valid_ipv4_address(valid_ip));
689
690        let invalid_ip = "256.0.0.1";
691        assert!(!is_valid_ipv4_address(invalid_ip));
692    }
693
694    #[test]
695    fn test_resolve_port_validation() {
696        let valid_port = "9051";
697        assert!(is_valid_port(valid_port));
698
699        let invalid_port = "0";
700        assert!(!is_valid_port(invalid_port));
701
702        let invalid_port2 = "65536";
703        assert!(!is_valid_port(invalid_port2));
704    }
705
706    #[test]
707    fn test_command_parsing_with_args() {
708        let command = "GETINFO version";
709        let (cmd, arg) = command.split_once(' ').unwrap();
710        assert_eq!(cmd, "GETINFO");
711        assert_eq!(arg, "version");
712    }
713
714    #[test]
715    fn test_command_parsing_without_args() {
716        let command = "QUIT";
717        let result = command.split_once(' ');
718        assert!(result.is_none());
719    }
720
721    #[test]
722    fn test_interpreter_command_detection() {
723        assert!("/help".starts_with('/'));
724        assert!("/events".starts_with('/'));
725        assert!("/info".starts_with('/'));
726        assert!("/python".starts_with('/'));
727        assert!("/quit".starts_with('/'));
728
729        assert!(!"GETINFO".starts_with('/'));
730        assert!(!"SETCONF".starts_with('/'));
731    }
732
733    #[test]
734    fn test_event_type_filtering_logic() {
735        let event_types = ["BW", "CIRC"];
736        let test_type = "BW";
737
738        let matches = event_types
739            .iter()
740            .any(|t| t.eq_ignore_ascii_case(test_type));
741        assert!(matches);
742
743        let test_type2 = "bw";
744        let matches2 = event_types
745            .iter()
746            .any(|t| t.eq_ignore_ascii_case(test_type2));
747        assert!(matches2);
748
749        let test_type3 = "STREAM";
750        let matches3 = event_types
751            .iter()
752            .any(|t| t.eq_ignore_ascii_case(test_type3));
753        assert!(!matches3);
754    }
755
756    #[test]
757    fn test_clear_event_detection() {
758        let event_types = ["BW", "CLEAR", "CIRC"];
759        let has_clear = event_types.iter().any(|t| t.eq_ignore_ascii_case("CLEAR"));
760        assert!(has_clear);
761
762        let event_types2 = ["BW", "CIRC"];
763        let has_clear2 = event_types2.iter().any(|t| t.eq_ignore_ascii_case("CLEAR"));
764        assert!(!has_clear2);
765    }
766
767    #[test]
768    fn test_multiline_commands_detection() {
769        let multiline_cmds = ["LOADCONF", "+LOADCONF", "POSTDESCRIPTOR", "+POSTDESCRIPTOR"];
770
771        for cmd in multiline_cmds {
772            let cmd_upper = cmd.to_uppercase();
773            let is_multiline = cmd_upper == "LOADCONF"
774                || cmd_upper == "+LOADCONF"
775                || cmd_upper == "POSTDESCRIPTOR"
776                || cmd_upper == "+POSTDESCRIPTOR";
777            assert!(is_multiline, "Expected {} to be detected as multiline", cmd);
778        }
779
780        let regular_cmds = ["GETINFO", "SETCONF", "SIGNAL"];
781        for cmd in regular_cmds {
782            let cmd_upper = cmd.to_uppercase();
783            let is_multiline = cmd_upper == "LOADCONF"
784                || cmd_upper == "+LOADCONF"
785                || cmd_upper == "POSTDESCRIPTOR"
786                || cmd_upper == "+POSTDESCRIPTOR";
787            assert!(
788                !is_multiline,
789                "Expected {} to NOT be detected as multiline",
790                cmd
791            );
792        }
793    }
794
795    #[test]
796    fn test_quit_command_detection() {
797        let cmd = "QUIT";
798        assert_eq!(cmd.to_uppercase(), "QUIT");
799
800        let cmd2 = "quit";
801        assert_eq!(cmd2.to_uppercase(), "QUIT");
802    }
803
804    #[test]
805    fn test_empty_command_handling() {
806        let command = "";
807        let trimmed = command.trim();
808        assert!(trimmed.is_empty());
809
810        let command2 = "   ";
811        let trimmed2 = command2.trim();
812        assert!(trimmed2.is_empty());
813    }
814
815    #[test]
816    fn test_command_uppercase_conversion() {
817        let cmd = "getinfo";
818        assert_eq!(cmd.to_uppercase(), "GETINFO");
819
820        let cmd2 = "SetConf";
821        assert_eq!(cmd2.to_uppercase(), "SETCONF");
822    }
823
824    #[test]
825    fn test_full_command_construction() {
826        let cmd = "GETINFO";
827        let arg = "version";
828        let full = format!("{} {}", cmd, arg);
829        assert_eq!(full, "GETINFO version");
830
831        let cmd2 = "QUIT";
832        let arg2 = "";
833        let full2 = if arg2.is_empty() {
834            cmd2.to_string()
835        } else {
836            format!("{} {}", cmd2, arg2)
837        };
838        assert_eq!(full2, "QUIT");
839    }
840
841    #[test]
842    fn test_ns_line_parsing() {
843        let r_line = "r MyRelay ABCD1234 2023-01-01 12:00:00 192.168.1.1 9001 0";
844        let parts: Vec<&str> = r_line.split_whitespace().collect();
845
846        assert!(r_line.starts_with("r "));
847        assert!(parts.len() >= 2);
848        assert_eq!(parts[1], "MyRelay");
849    }
850
851    #[test]
852    fn test_flags_line_parsing() {
853        let s_line = "s Fast Guard Stable Valid";
854        assert!(s_line.starts_with("s "));
855
856        let stripped = s_line.strip_prefix("s ").unwrap();
857        assert_eq!(stripped, "Fast Guard Stable Valid");
858    }
859
860    #[test]
861    fn test_version_line_parsing() {
862        let v_line = "v Tor 0.4.7.10";
863        assert!(v_line.starts_with("v "));
864
865        let stripped = v_line.strip_prefix("v ").unwrap();
866        assert_eq!(stripped, "Tor 0.4.7.10");
867    }
868
869    #[test]
870    fn test_ip_port_parsing() {
871        let addr_port = "192.168.1.1:9051";
872        let (addr, port_str) = addr_port.rsplit_once(':').unwrap();
873        assert_eq!(addr, "192.168.1.1");
874        assert_eq!(port_str, "9051");
875
876        let addr_only = "192.168.1.1";
877        let result = addr_only.rsplit_once(':');
878        assert!(result.is_none());
879    }
880
881    #[test]
882    fn test_multiple_relay_response_format() {
883        let address = "192.168.1.1";
884        let matches = [(9001u16, "FP1".to_string()), (9002u16, "FP2".to_string())];
885
886        let mut response = format!(
887            "There's multiple relays at {}, include a port to specify which.\n\n",
888            address
889        );
890        for (i, (or_port, fp)) in matches.iter().enumerate() {
891            response.push_str(&format!(
892                "  {}. {}:{}, fingerprint: {}\n",
893                i + 1,
894                address,
895                or_port,
896                fp
897            ));
898        }
899
900        assert!(response.contains("multiple relays"));
901        assert!(response.contains("192.168.1.1:9001"));
902        assert!(response.contains("192.168.1.1:9002"));
903        assert!(response.contains("FP1"));
904        assert!(response.contains("FP2"));
905    }
906}