stem_rs/interpreter/
arguments.rs

1//! Command-line argument parsing for the interpreter prompt.
2//!
3//! This module provides argument parsing for the Tor interpreter command-line
4//! interface, handling connection options, execution modes, and display settings.
5//!
6//! # Overview
7//!
8//! The interpreter accepts various command-line arguments to configure:
9//! - Control interface connection (TCP port or Unix socket)
10//! - Tor binary path for auto-starting
11//! - Single command or script execution
12//! - Output formatting options
13//!
14//! # Supported Arguments
15//!
16//! | Short | Long | Description |
17//! |-------|------|-------------|
18//! | `-i` | `--interface` | Control interface `[ADDRESS:]PORT` |
19//! | `-s` | `--socket` | Unix domain socket path |
20//! | | `--tor` | Path to Tor binary |
21//! | | `--run` | Command or script file to execute |
22//! | | `--no-color` | Disable colored output |
23//! | `-h` | `--help` | Show help message |
24//!
25//! # Example
26//!
27//! ```rust
28//! use stem_rs::interpreter::arguments::Arguments;
29//!
30//! // Parse command-line arguments
31//! let args = Arguments::parse(&[
32//!     "-i".to_string(),
33//!     "9051".to_string(),
34//!     "--no-color".to_string(),
35//! ]).unwrap();
36//!
37//! assert_eq!(args.control_port, Some(9051));
38//! assert!(args.disable_color);
39//! ```
40//!
41//! # Python Stem Equivalent
42//!
43//! This module corresponds to Python Stem's `stem.interpreter.arguments` module.
44
45use crate::util::{is_valid_ipv4_address, is_valid_port};
46
47/// Parsed command-line arguments for the interpreter.
48///
49/// This struct holds all configuration options that can be specified
50/// via command-line arguments when launching the interpreter.
51///
52/// # Default Values
53///
54/// | Field | Default |
55/// |-------|---------|
56/// | `control_address` | `"127.0.0.1"` |
57/// | `control_port` | `None` (uses Tor's default) |
58/// | `control_socket` | `"/var/run/tor/control"` |
59/// | `tor_path` | `"tor"` |
60///
61/// # Example
62///
63/// ```rust
64/// use stem_rs::interpreter::arguments::Arguments;
65///
66/// // Use defaults
67/// let defaults = Arguments::default();
68/// assert_eq!(defaults.control_address, "127.0.0.1");
69///
70/// // Parse from command line
71/// let args = Arguments::parse(&["--interface".to_string(), "192.168.1.1:9051".to_string()]).unwrap();
72/// assert_eq!(args.control_address, "192.168.1.1");
73/// assert_eq!(args.control_port, Some(9051));
74/// ```
75#[derive(Debug, Clone)]
76pub struct Arguments {
77    /// IP address for the control interface.
78    ///
79    /// Defaults to `"127.0.0.1"` (localhost).
80    pub control_address: String,
81    /// Port number for the control interface.
82    ///
83    /// If `None`, the default Tor control port is used.
84    pub control_port: Option<u16>,
85    /// Whether the user explicitly specified a port.
86    ///
87    /// Used to determine connection priority when both port and socket
88    /// are available.
89    pub user_provided_port: bool,
90    /// Path to the Unix domain socket for control connection.
91    ///
92    /// Defaults to `"/var/run/tor/control"`.
93    pub control_socket: String,
94    /// Whether the user explicitly specified a socket path.
95    ///
96    /// Used to determine connection priority when both port and socket
97    /// are available.
98    pub user_provided_socket: bool,
99    /// Path to the Tor binary.
100    ///
101    /// Used when Tor needs to be started automatically.
102    /// Defaults to `"tor"` (found via PATH).
103    pub tor_path: String,
104    /// Single command to execute and exit.
105    ///
106    /// If set, the interpreter runs this command and exits instead of
107    /// entering interactive mode.
108    pub run_cmd: Option<String>,
109    /// Path to a script file to execute.
110    ///
111    /// If set, the interpreter runs all commands in the file and exits.
112    /// Takes precedence over `run_cmd` if the path exists.
113    pub run_path: Option<String>,
114    /// Whether to disable colored output.
115    ///
116    /// When `true`, all output is plain text without ANSI color codes.
117    pub disable_color: bool,
118    /// Whether to print help and exit.
119    ///
120    /// When `true`, the interpreter prints usage information and exits
121    /// without connecting to Tor.
122    pub print_help: bool,
123}
124
125impl Default for Arguments {
126    fn default() -> Self {
127        Self {
128            control_address: "127.0.0.1".to_string(),
129            control_port: None,
130            user_provided_port: false,
131            control_socket: "/var/run/tor/control".to_string(),
132            user_provided_socket: false,
133            tor_path: "tor".to_string(),
134            run_cmd: None,
135            run_path: None,
136            disable_color: false,
137            print_help: false,
138        }
139    }
140}
141
142impl Arguments {
143    /// Parses command-line arguments into an `Arguments` struct.
144    ///
145    /// # Arguments
146    ///
147    /// * `argv` - Slice of command-line argument strings (excluding program name)
148    ///
149    /// # Returns
150    ///
151    /// Parsed arguments on success, or an error message on failure.
152    ///
153    /// # Errors
154    ///
155    /// Returns an error string if:
156    /// - An argument requires a value but none is provided
157    /// - An IP address is invalid
158    /// - A port number is invalid (not 1-65535)
159    /// - An unrecognized argument is provided
160    ///
161    /// # Example
162    ///
163    /// ```rust
164    /// use stem_rs::interpreter::arguments::Arguments;
165    ///
166    /// // Parse port only
167    /// let args = Arguments::parse(&["-i".to_string(), "9051".to_string()]).unwrap();
168    /// assert_eq!(args.control_port, Some(9051));
169    ///
170    /// // Parse address and port
171    /// let args = Arguments::parse(&["-i".to_string(), "192.168.1.1:9051".to_string()]).unwrap();
172    /// assert_eq!(args.control_address, "192.168.1.1");
173    /// assert_eq!(args.control_port, Some(9051));
174    ///
175    /// // Invalid port returns error
176    /// let result = Arguments::parse(&["-i".to_string(), "99999".to_string()]);
177    /// assert!(result.is_err());
178    /// ```
179    pub fn parse(argv: &[String]) -> Result<Self, String> {
180        let mut args = Arguments::default();
181        let mut i = 0;
182
183        while i < argv.len() {
184            let arg = &argv[i];
185            match arg.as_str() {
186                "-i" | "--interface" => {
187                    i += 1;
188                    if i >= argv.len() {
189                        return Err("--interface requires an argument".to_string());
190                    }
191                    let interface = &argv[i];
192                    if let Some((addr, port_str)) = interface.rsplit_once(':') {
193                        if !is_valid_ipv4_address(addr) {
194                            return Err(format!("'{}' isn't a valid IPv4 address", addr));
195                        }
196                        if !is_valid_port(port_str) {
197                            return Err(format!("'{}' isn't a valid port number", port_str));
198                        }
199                        args.control_address = addr.to_string();
200                        args.control_port = Some(port_str.parse().unwrap());
201                    } else {
202                        if !is_valid_port(interface) {
203                            return Err(format!("'{}' isn't a valid port number", interface));
204                        }
205                        args.control_port = Some(interface.parse().unwrap());
206                    }
207                    args.user_provided_port = true;
208                }
209                "-s" | "--socket" => {
210                    i += 1;
211                    if i >= argv.len() {
212                        return Err("--socket requires an argument".to_string());
213                    }
214                    args.control_socket = argv[i].clone();
215                    args.user_provided_socket = true;
216                }
217                "--tor" => {
218                    i += 1;
219                    if i >= argv.len() {
220                        return Err("--tor requires an argument".to_string());
221                    }
222                    args.tor_path = argv[i].clone();
223                }
224                "--run" => {
225                    i += 1;
226                    if i >= argv.len() {
227                        return Err("--run requires an argument".to_string());
228                    }
229                    let run_arg = &argv[i];
230                    if std::path::Path::new(run_arg).exists() {
231                        args.run_path = Some(run_arg.clone());
232                    } else {
233                        args.run_cmd = Some(run_arg.clone());
234                    }
235                }
236                "--no-color" => {
237                    args.disable_color = true;
238                }
239                "-h" | "--help" => {
240                    args.print_help = true;
241                }
242                other => {
243                    return Err(format!(
244                        "'{}' isn't a recognized argument (for usage provide --help)",
245                        other
246                    ));
247                }
248            }
249            i += 1;
250        }
251
252        Ok(args)
253    }
254
255    /// Returns the help message for command-line usage.
256    ///
257    /// The help message includes all available options with their
258    /// descriptions and default values.
259    ///
260    /// # Example
261    ///
262    /// ```rust
263    /// use stem_rs::interpreter::arguments::Arguments;
264    ///
265    /// let help = Arguments::get_help();
266    /// assert!(help.contains("--interface"));
267    /// assert!(help.contains("--socket"));
268    /// assert!(help.contains("--help"));
269    /// ```
270    pub fn get_help() -> String {
271        let defaults = Arguments::default();
272        format!(
273            r#"Interactive interpreter for Tor. This provides you with direct access
274to Tor's control interface via either python or direct requests.
275
276  -i, --interface [ADDRESS:]PORT  change control interface from {}:{}
277  -s, --socket SOCKET_PATH        attach using unix domain socket if present,
278                                    SOCKET_PATH defaults to: {}
279      --tor PATH                  tor binary if tor isn't already running
280      --run                       executes the given command or file of commands
281  --no-color                      disables colorized output
282  -h, --help                      presents this help
283"#,
284            defaults.control_address,
285            defaults
286                .control_port
287                .map(|p| p.to_string())
288                .unwrap_or_else(|| "default".to_string()),
289            defaults.control_socket
290        )
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn test_default_arguments() {
300        let args = Arguments::default();
301        assert_eq!(args.control_address, "127.0.0.1");
302        assert_eq!(args.control_port, None);
303        assert!(!args.user_provided_port);
304        assert_eq!(args.control_socket, "/var/run/tor/control");
305        assert!(!args.user_provided_socket);
306        assert_eq!(args.tor_path, "tor");
307        assert!(args.run_cmd.is_none());
308        assert!(args.run_path.is_none());
309        assert!(!args.disable_color);
310        assert!(!args.print_help);
311    }
312
313    #[test]
314    fn test_parse_help() {
315        let args = Arguments::parse(&["--help".to_string()]).unwrap();
316        assert!(args.print_help);
317    }
318
319    #[test]
320    fn test_parse_help_short() {
321        let args = Arguments::parse(&["-h".to_string()]).unwrap();
322        assert!(args.print_help);
323    }
324
325    #[test]
326    fn test_parse_interface_port_only() {
327        let args = Arguments::parse(&["-i".to_string(), "9051".to_string()]).unwrap();
328        assert_eq!(args.control_port, Some(9051));
329        assert!(args.user_provided_port);
330    }
331
332    #[test]
333    fn test_parse_interface_address_and_port() {
334        let args =
335            Arguments::parse(&["--interface".to_string(), "192.168.1.1:9051".to_string()]).unwrap();
336        assert_eq!(args.control_address, "192.168.1.1");
337        assert_eq!(args.control_port, Some(9051));
338        assert!(args.user_provided_port);
339    }
340
341    #[test]
342    fn test_parse_socket() {
343        let args = Arguments::parse(&["-s".to_string(), "/tmp/tor.sock".to_string()]).unwrap();
344        assert_eq!(args.control_socket, "/tmp/tor.sock");
345        assert!(args.user_provided_socket);
346    }
347
348    #[test]
349    fn test_parse_tor_path() {
350        let args = Arguments::parse(&["--tor".to_string(), "/usr/bin/tor".to_string()]).unwrap();
351        assert_eq!(args.tor_path, "/usr/bin/tor");
352    }
353
354    #[test]
355    fn test_parse_run_cmd() {
356        let args = Arguments::parse(&["--run".to_string(), "GETINFO version".to_string()]).unwrap();
357        assert_eq!(args.run_cmd, Some("GETINFO version".to_string()));
358        assert!(args.run_path.is_none());
359    }
360
361    #[test]
362    fn test_parse_no_color() {
363        let args = Arguments::parse(&["--no-color".to_string()]).unwrap();
364        assert!(args.disable_color);
365    }
366
367    #[test]
368    fn test_parse_invalid_port() {
369        let result = Arguments::parse(&["-i".to_string(), "99999".to_string()]);
370        assert!(result.is_err());
371    }
372
373    #[test]
374    fn test_parse_invalid_address() {
375        let result = Arguments::parse(&["-i".to_string(), "invalid:9051".to_string()]);
376        assert!(result.is_err());
377    }
378
379    #[test]
380    fn test_parse_unrecognized_argument() {
381        let result = Arguments::parse(&["--unknown".to_string()]);
382        assert!(result.is_err());
383    }
384
385    #[test]
386    fn test_parse_multiple_arguments() {
387        let args = Arguments::parse(&[
388            "-i".to_string(),
389            "9051".to_string(),
390            "-s".to_string(),
391            "/tmp/tor.sock".to_string(),
392            "--no-color".to_string(),
393        ])
394        .unwrap();
395        assert_eq!(args.control_port, Some(9051));
396        assert!(args.user_provided_port);
397        assert_eq!(args.control_socket, "/tmp/tor.sock");
398        assert!(args.user_provided_socket);
399        assert!(args.disable_color);
400    }
401
402    #[test]
403    fn test_get_help() {
404        let help = Arguments::get_help();
405        assert!(help.contains("--interface"));
406        assert!(help.contains("--socket"));
407        assert!(help.contains("--tor"));
408        assert!(help.contains("--run"));
409        assert!(help.contains("--no-color"));
410        assert!(help.contains("--help"));
411    }
412}