1
//! The command-line interface.
2
//!
3
//! See [`Cli`].
4

            
5
use std::ffi::OsString;
6
use std::path::PathBuf;
7

            
8
use clap::{Args, Command, Parser, Subcommand, ValueEnum};
9
use fs_mistrust::anon_home::PathExt as _;
10
use std::sync::LazyLock;
11
use tor_config::{ConfigurationSource, ConfigurationSources};
12
use tor_config_path::CfgPathError;
13

            
14
use crate::config::default_config_paths;
15

            
16
/// A cached copy of the default config paths.
17
///
18
/// We cache the values to ensure they are consistent between the help text and the values used.
19
static DEFAULT_CONFIG_PATHS: LazyLock<Result<Vec<PathBuf>, CfgPathError>> =
20
    LazyLock::new(default_config_paths);
21

            
22
/// A Rust Tor relay implementation.
23
#[derive(Clone, Debug, Parser)]
24
#[command(author = "The Tor Project Developers")]
25
#[command(version)]
26
#[command(defer = cli_cmd_post_processing)]
27
pub(crate) struct Cli {
28
    /// Sub-commands.
29
    #[command(subcommand)]
30
    pub(crate) command: Commands,
31

            
32
    /// Global arguments available for all sub-commands.
33
    ///
34
    /// These arguments may be specified before or after the subcommand argument.
35
    #[clap(flatten)]
36
    pub(crate) global: GlobalArgs,
37
}
38

            
39
/// Perform post-processing on the [`Command`] generated by clap for [`Cli`].
40
///
41
/// We use this to append the default config paths to the help text.
42
18
fn cli_cmd_post_processing(cli: Command) -> Command {
43
    /// Append the paths to the help text.
44
18
    fn fmt_help(help: Option<&str>, paths: &[PathBuf]) -> String {
45
27
        let help = help.map(|x| format!("{x}\n\n")).unwrap_or("".to_string());
46
18
        let paths: Vec<_> = paths
47
18
            .iter()
48
45
            .map(|path| {
49
36
                let mut anon = path.anonymize_home().to_string();
50
                // Best-effort attempt to re-add the trailing '/'
51
                // if it was stripped by `anonymize_home()`.
52
                // If the original string ended with '/' and the anonymized
53
                // path doesn't, then re-add it.
54
36
                if path.to_string_lossy().ends_with('/') && !anon.ends_with('/') {
55
18
                    anon.push('/');
56
18
                }
57
36
                anon
58
36
            })
59
18
            .collect();
60
18
        let paths = paths.join("\n");
61

            
62
        const DESC: &str =
63
            "If no paths are provided, the following config paths will be used if they exist:";
64
18
        format!("{help}{DESC}\n\n{paths}")
65
18
    }
66

            
67
    // Show the default paths in the "--help" text.
68
18
    match &*DEFAULT_CONFIG_PATHS {
69
27
        Ok(paths) => cli.mut_arg("config", |arg| {
70
18
            if let Some(help) = arg.get_long_help() {
71
                let help = help.to_string();
72
                arg.long_help(fmt_help(Some(&help), paths))
73
18
            } else if let Some(help) = arg.get_help() {
74
18
                let help = help.to_string();
75
18
                arg.long_help(fmt_help(Some(&help), paths))
76
            } else {
77
                arg.long_help(fmt_help(None, paths))
78
            }
79
18
        }),
80
        Err(_e) => cli,
81
    }
82
18
}
83

            
84
/// Main subcommands.
85
#[derive(Clone, Debug, Subcommand)]
86
pub(crate) enum Commands {
87
    /// Run the relay.
88
    Run(RunArgs),
89
    /// Print build information.
90
    BuildInfo,
91
}
92

            
93
/// Global arguments for all commands.
94
// NOTE: `global = true` should be set for each field (see the `global_args_are_global` unit test)
95
#[derive(Clone, Debug, Args)]
96
pub(crate) struct GlobalArgs {
97
    /// Override the log level from the configuration.
98
    #[arg(long, short, global = true)]
99
    #[arg(value_name = "LEVEL")]
100
    pub(crate) log_level: Option<LogLevel>,
101

            
102
    /// Don't check permissions on the files we use.
103
    #[arg(long, global = true)]
104
    pub(crate) disable_fs_permission_checks: bool,
105

            
106
    /// Override config file parameters, using TOML-like syntax.
107
    #[arg(long = "option", short, global = true)]
108
    #[arg(value_name = "KEY=VALUE")]
109
    pub(crate) options: Vec<String>,
110

            
111
    /// Config files and directories to read.
112
    // NOTE: We append the default config paths to the help text in `cli_cmd_post_processing`.
113
    // NOTE: This value does not take into account the default config paths,
114
    // so this is private while the `GlobalArgs::config()` method is public instead.
115
    #[arg(long, short, global = true)]
116
    #[arg(value_name = "PATH")]
117
    config: Vec<OsString>,
118
}
119

            
120
impl GlobalArgs {
121
    /// Get the configuration sources.
122
    ///
123
    /// You may also want to set a [`Mistrust`](fs_mistrust::Mistrust)
124
    /// and any additional configuration option overrides
125
    /// using [`push_option`](ConfigurationSources::push_option).
126
    pub(crate) fn config(&self) -> Result<ConfigurationSources, CfgPathError> {
127
        // Use `try_from_cmdline` to be consistent with Arti.
128
        let mut cfg_sources = ConfigurationSources::try_from_cmdline(
129
            || {
130
                Ok(DEFAULT_CONFIG_PATHS
131
                    .as_ref()
132
                    .map_err(Clone::clone)?
133
                    .iter()
134
                    .map(ConfigurationSource::from_path))
135
            },
136
            &self.config,
137
            &self.options,
138
        )?;
139

            
140
        // TODO: These text strings may become stale if the configuration structure changes,
141
        // and they're not checked at compile time.
142
        // Can we change `ConfigurationSources` in some way to allow overrides from an existing
143
        // builder?
144
        if self.disable_fs_permission_checks {
145
            cfg_sources.push_option("storage.permissions.dangerously_trust_everyone=true");
146
        }
147

            
148
        if let Some(log_level) = self.log_level {
149
            cfg_sources.push_option(format!("logging.console={log_level}"));
150
        }
151

            
152
        Ok(cfg_sources)
153
    }
154
}
155

            
156
/// Arguments when running an Arti relay.
157
#[derive(Clone, Debug, Args)]
158
pub(crate) struct RunArgs {}
159

            
160
/// Log levels allowed by the cli.
161
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
162
pub(crate) enum LogLevel {
163
    /// See [`tracing::Level::ERROR`].
164
    #[value(help = None)]
165
    Error,
166
    /// See [`tracing::Level::WARN`].
167
    #[value(help = None)]
168
    Warn,
169
    /// See [`tracing::Level::INFO`].
170
    #[value(help = None)]
171
    Info,
172
    /// See [`tracing::Level::DEBUG`].
173
    #[value(help = None)]
174
    Debug,
175
    /// See [`tracing::Level::TRACE`].
176
    #[value(help = None)]
177
    Trace,
178
}
179

            
180
impl std::fmt::Display for LogLevel {
181
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182
        match self {
183
            Self::Error => write!(f, "error"),
184
            Self::Warn => write!(f, "warn"),
185
            Self::Info => write!(f, "info"),
186
            Self::Debug => write!(f, "debug"),
187
            Self::Trace => write!(f, "trace"),
188
        }
189
    }
190
}
191

            
192
impl From<LogLevel> for tracing::metadata::Level {
193
    fn from(x: LogLevel) -> Self {
194
        match x {
195
            LogLevel::Error => Self::ERROR,
196
            LogLevel::Warn => Self::WARN,
197
            LogLevel::Info => Self::INFO,
198
            LogLevel::Debug => Self::DEBUG,
199
            LogLevel::Trace => Self::TRACE,
200
        }
201
    }
202
}
203

            
204
#[cfg(test)]
205
mod test {
206
    // @@ begin test lint list maintained by maint/add_warning @@
207
    #![allow(clippy::bool_assert_comparison)]
208
    #![allow(clippy::clone_on_copy)]
209
    #![allow(clippy::dbg_macro)]
210
    #![allow(clippy::mixed_attributes_style)]
211
    #![allow(clippy::print_stderr)]
212
    #![allow(clippy::print_stdout)]
213
    #![allow(clippy::single_char_pattern)]
214
    #![allow(clippy::unwrap_used)]
215
    #![allow(clippy::unchecked_time_subtraction)]
216
    #![allow(clippy::useless_vec)]
217
    #![allow(clippy::needless_pass_by_value)]
218
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
219

            
220
    use super::*;
221

            
222
    #[test]
223
    fn common_flags() {
224
        Cli::parse_from(["arti-relay", "build-info"]);
225
        Cli::parse_from(["arti-relay", "run"]);
226

            
227
        let cli = Cli::parse_from(["arti-relay", "--log-level", "warn", "run"]);
228
        assert_eq!(cli.global.log_level, Some(LogLevel::Warn));
229
        let cli = Cli::parse_from(["arti-relay", "run", "--log-level", "warn"]);
230
        assert_eq!(cli.global.log_level, Some(LogLevel::Warn));
231

            
232
        let cli = Cli::parse_from(["arti-relay", "--disable-fs-permission-checks", "run"]);
233
        assert!(cli.global.disable_fs_permission_checks);
234
        let cli = Cli::parse_from(["arti-relay", "run", "--disable-fs-permission-checks"]);
235
        assert!(cli.global.disable_fs_permission_checks);
236
    }
237

            
238
    #[test]
239
    fn clap_bug() {
240
        let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run"]);
241
        assert_eq!(cli.global.options, vec!["foo=1"]);
242

            
243
        let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "-o", "bar=2", "run"]);
244
        assert_eq!(cli.global.options, vec!["foo=1", "bar=2"]);
245

            
246
        // this is https://github.com/clap-rs/clap/issues/3938
247
        // TODO: this is a footgun, and we should consider alternatives to clap's 'global' args
248
        let cli = Cli::parse_from(["arti-relay", "-o", "foo=1", "run", "-o", "bar=2"]);
249
        assert_eq!(cli.global.options, vec!["bar=2"]);
250
    }
251

            
252
    #[test]
253
    fn global_args_are_global() {
254
        let cmd = Command::new("test");
255
        let cmd = GlobalArgs::augment_args(cmd);
256

            
257
        // check that each argument in `GlobalArgs` has "global" set
258
        for arg in cmd.get_arguments() {
259
            assert!(
260
                arg.is_global_set(),
261
                "'global' must be set for {:?}",
262
                arg.get_long()
263
            );
264
        }
265
    }
266
}