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}