Skip to main content

tor_config/
cmdline.rs

1//! Implement a configuration source based on command-line arguments.
2
3use regex::Regex;
4use std::sync::LazyLock;
5
6/// A CmdLine holds a set of command-line arguments that augment a
7/// configuration.
8///
9/// These arguments are formatted in toml, and concatenated into a
10/// single toml object.  With arguments of the form "key=bareword",
11/// the bareword is quoted for convenience.
12#[derive(Debug, Clone)]
13pub struct CmdLine {
14    /// String for decorating Values.
15    //
16    // TODO(nickm): not yet used.
17    #[allow(dead_code)]
18    name: String,
19    /// List of toml lines as given on the command line.
20    contents: Vec<String>,
21}
22
23impl Default for CmdLine {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29impl CmdLine {
30    /// Make a new empty command-line
31    pub fn new() -> Self {
32        CmdLine {
33            name: "command line".to_string(),
34            contents: Vec::new(),
35        }
36    }
37    /// Add a single line of toml to the configuration.
38    pub fn push_toml_line(&mut self, line: String) {
39        self.contents.push(line);
40    }
41
42    /// Try to adjust the contents of a toml deserialization error so
43    /// that instead it refers to a single command-line argument.
44    fn convert_toml_error(
45        &self,
46        toml_str: &str,
47        error_message: &str,
48        span: &Option<std::ops::Range<usize>>,
49    ) -> String {
50        // Function to translate a string index to a 0-offset line number.
51        let linepos = |idx| toml_str.bytes().take(idx).filter(|b| *b == b'\n').count();
52
53        // Find the source position as a line within toml_str, and convert that
54        // to an index into self.contents.
55        let source_line = span
56            .as_ref()
57            .and_then(|range| {
58                let startline = linepos(range.start);
59                let endline = linepos(range.end);
60                (startline == endline).then_some(startline)
61            })
62            .and_then(|pos| self.contents.get(pos));
63
64        let within = span.as_ref().and_then(|r| toml_str.get(r.clone()));
65
66        match (source_line, within) {
67            (Some(source), _) => {
68                format!("Couldn't parse command line: {error_message} in {source:?}")
69            }
70            (None, Some(within)) => {
71                format!("Couldn't parse command line: {error_message} within {within:?}",)
72            }
73            _ => format!("Couldn't parse command line: {error_message}"),
74        }
75    }
76
77    /// Compose elements of this cmdline into a single toml string.
78    fn build_toml(&self) -> String {
79        let mut toml_s = String::new();
80        for line in &self.contents {
81            toml_s.push_str(tweak_toml_bareword(line).as_ref().unwrap_or(line));
82            toml_s.push('\n');
83        }
84        toml_s
85    }
86}
87
88impl figment::Provider for CmdLine {
89    fn metadata(&self) -> figment::Metadata {
90        figment::Metadata::named("command line")
91    }
92
93    fn data(&self) -> figment::Result<figment::value::Map<figment::Profile, figment::value::Dict>> {
94        let toml_str = self.build_toml();
95        let toml: toml::Value = toml::from_str(&toml_str).map_err(|toml_err| {
96            self.convert_toml_error(&toml_str, toml_err.message(), &toml_err.span())
97        })?;
98
99        figment::providers::Serialized::defaults(toml).data()
100    }
101}
102
103/// If `s` is a string of the form "keyword=bareword", return a new string
104/// where `bareword` is quoted. Otherwise return None.
105///
106/// This isn't a smart transformation outside the context of 'config',
107/// since many serde formats don't do so good a job when they get a
108/// string when they wanted a number or whatever.  But 'config' is
109/// pretty happy to convert strings to other stuff.
110fn tweak_toml_bareword(s: &str) -> Option<String> {
111    /// Regex to match a keyword=bareword item.
112    static RE: LazyLock<Regex> = LazyLock::new(|| {
113        Regex::new(
114            r#"(?x:
115               ^
116                [ \t]*
117                # first capture group: dotted barewords
118                ((?:[a-zA-Z0-9_\-]+\.)*
119                 [a-zA-Z0-9_\-]+)
120                [ \t]*=[ \t]*
121                # second group: a string without hyphens (e.g. bareword, a disk path, etc.)
122                ([a-zA-Z0-9_:\./\\]+)
123                [ \t]*
124                $)"#,
125        )
126        .expect("Built-in regex compilation failed")
127    });
128
129    RE.captures(s).map(|c| format!("{}=\"{}\"", &c[1], &c[2]))
130}
131
132#[cfg(test)]
133mod test {
134    // @@ begin test lint list maintained by maint/add_warning @@
135    #![allow(clippy::bool_assert_comparison)]
136    #![allow(clippy::clone_on_copy)]
137    #![allow(clippy::dbg_macro)]
138    #![allow(clippy::mixed_attributes_style)]
139    #![allow(clippy::print_stderr)]
140    #![allow(clippy::print_stdout)]
141    #![allow(clippy::single_char_pattern)]
142    #![allow(clippy::unwrap_used)]
143    #![allow(clippy::unchecked_time_subtraction)]
144    #![allow(clippy::useless_vec)]
145    #![allow(clippy::needless_pass_by_value)]
146    #![allow(clippy::string_slice)] // See arti#2571
147    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
148    use super::*;
149    use figment::Provider as _;
150
151    #[test]
152    fn bareword_expansion() {
153        assert_eq!(tweak_toml_bareword("dsfklj"), None);
154        assert_eq!(tweak_toml_bareword("=99"), None);
155        assert_eq!(tweak_toml_bareword("=[1,2,3]"), None);
156        assert_eq!(tweak_toml_bareword("a=b-c"), None);
157
158        assert_eq!(tweak_toml_bareword("a=bc"), Some("a=\"bc\"".into()));
159        assert_eq!(tweak_toml_bareword("a=b_c"), Some("a=\"b_c\"".into()));
160        assert_eq!(
161            tweak_toml_bareword("hello.there.now=a_greeting"),
162            Some("hello.there.now=\"a_greeting\"".into())
163        );
164    }
165
166    #[test]
167    fn conv_toml_error() {
168        let mut cl = CmdLine::new();
169        cl.push_toml_line("Hello=world".to_string());
170        cl.push_toml_line("Hola=mundo".to_string());
171        cl.push_toml_line("Bonjour=monde".to_string());
172        let toml_s = cl.build_toml();
173
174        assert_eq!(
175            &cl.convert_toml_error(&toml_s, "Nice greeting", &Some(0..13)),
176            "Couldn't parse command line: Nice greeting in \"Hello=world\""
177        );
178
179        assert_eq!(
180            &cl.convert_toml_error(&toml_s, "Nice greeting", &Some(99..333)),
181            "Couldn't parse command line: Nice greeting"
182        );
183
184        assert_eq!(
185            &cl.convert_toml_error(&toml_s, "Nice greeting with a thing", &Some(0..13)),
186            "Couldn't parse command line: Nice greeting with a thing in \"Hello=world\""
187        );
188    }
189
190    #[test]
191    fn parse_good() {
192        let mut cl = CmdLine::default();
193        cl.push_toml_line("a=3".to_string());
194        cl.push_toml_line("bcd=hello".to_string());
195        cl.push_toml_line("ef=\"gh i\"".to_string());
196        cl.push_toml_line("w=[1,2,3]".to_string());
197        cl.push_toml_line("dir1=.".to_string());
198        cl.push_toml_line("dir2=../".to_string());
199        cl.push_toml_line("dir3=../my_directory".to_string());
200        cl.push_toml_line("dir4=C:\\\\temp\\\\arti".to_string());
201
202        let v = cl
203            .data()
204            .unwrap()
205            .remove(&figment::Profile::Default)
206            .unwrap();
207
208        assert_eq!(v["a"], "3".into());
209        assert_eq!(v["bcd"], "hello".into());
210        assert_eq!(v["ef"], "gh i".into());
211        assert_eq!(v["w"], vec![1, 2, 3].into());
212        assert_eq!(v["dir1"], ".".into());
213        assert_eq!(v["dir2"], "../".into());
214        assert_eq!(v["dir3"], "../my_directory".into());
215        assert_eq!(v["dir4"], "C:\\temp\\arti".into());
216    }
217
218    #[test]
219    fn parse_bad() {
220        let mut cl = CmdLine::default();
221        cl.push_toml_line("x=1 1 1 1 1".to_owned());
222        let v = cl.data();
223        assert!(v.is_err());
224    }
225}