tor_config/
cmdline.rs
1use once_cell::sync::Lazy;
4use regex::Regex;
5
6#[derive(Debug, Clone)]
13pub struct CmdLine {
14 #[allow(dead_code)]
18 name: String,
19 contents: Vec<String>,
21}
22
23impl Default for CmdLine {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl CmdLine {
30 pub fn new() -> Self {
32 CmdLine {
33 name: "command line".to_string(),
34 contents: Vec::new(),
35 }
36 }
37 pub fn push_toml_line(&mut self, line: String) {
39 self.contents.push(line);
40 }
41
42 fn convert_toml_error(
45 &self,
46 toml_str: &str,
47 error_message: &str,
48 span: &Option<std::ops::Range<usize>>,
49 ) -> String {
50 let linepos = |idx| toml_str.bytes().take(idx).filter(|b| *b == b'\n').count();
52
53 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 match (source_line, span.as_ref()) {
65 (Some(source), _) => {
66 format!("Couldn't parse command line: {error_message} in {source:?}")
67 }
68 (None, Some(range)) if toml_str.get(range.clone()).is_some() => format!(
69 "Couldn't parse command line: {error_message} within {:?}",
70 &toml_str[range.clone()]
71 ),
72 _ => format!("Couldn't parse command line: {error_message}"),
73 }
74 }
75
76 fn build_toml(&self) -> String {
78 let mut toml_s = String::new();
79 for line in &self.contents {
80 toml_s.push_str(tweak_toml_bareword(line).as_ref().unwrap_or(line));
81 toml_s.push('\n');
82 }
83 toml_s
84 }
85}
86
87impl figment::Provider for CmdLine {
88 fn metadata(&self) -> figment::Metadata {
89 figment::Metadata::named("command line")
90 }
91
92 fn data(&self) -> figment::Result<figment::value::Map<figment::Profile, figment::value::Dict>> {
93 let toml_str = self.build_toml();
94 let toml: toml::Value = toml::from_str(&toml_str).map_err(|toml_err| {
95 self.convert_toml_error(&toml_str, toml_err.message(), &toml_err.span())
96 })?;
97
98 figment::providers::Serialized::defaults(toml).data()
99 }
100}
101
102fn tweak_toml_bareword(s: &str) -> Option<String> {
110 static RE: Lazy<Regex> = Lazy::new(|| {
112 Regex::new(
113 r#"(?x:
114 ^
115 [ \t]*
116 # first capture group: dotted barewords
117 ((?:[a-zA-Z0-9_\-]+\.)*
118 [a-zA-Z0-9_\-]+)
119 [ \t]*=[ \t]*
120 # second group: one bareword without hyphens
121 ([a-zA-Z0-9_]+)
122 [ \t]*
123 $)"#,
124 )
125 .expect("Built-in regex compilation failed")
126 });
127
128 RE.captures(s).map(|c| format!("{}=\"{}\"", &c[1], &c[2]))
129}
130
131#[cfg(test)]
132mod test {
133 #![allow(clippy::bool_assert_comparison)]
135 #![allow(clippy::clone_on_copy)]
136 #![allow(clippy::dbg_macro)]
137 #![allow(clippy::mixed_attributes_style)]
138 #![allow(clippy::print_stderr)]
139 #![allow(clippy::print_stdout)]
140 #![allow(clippy::single_char_pattern)]
141 #![allow(clippy::unwrap_used)]
142 #![allow(clippy::unchecked_duration_subtraction)]
143 #![allow(clippy::useless_vec)]
144 #![allow(clippy::needless_pass_by_value)]
145 use super::*;
147 use figment::Provider as _;
148
149 #[test]
150 fn bareword_expansion() {
151 assert_eq!(tweak_toml_bareword("dsfklj"), None);
152 assert_eq!(tweak_toml_bareword("=99"), None);
153 assert_eq!(tweak_toml_bareword("=[1,2,3]"), None);
154 assert_eq!(tweak_toml_bareword("a=b-c"), None);
155
156 assert_eq!(tweak_toml_bareword("a=bc"), Some("a=\"bc\"".into()));
157 assert_eq!(tweak_toml_bareword("a=b_c"), Some("a=\"b_c\"".into()));
158 assert_eq!(
159 tweak_toml_bareword("hello.there.now=a_greeting"),
160 Some("hello.there.now=\"a_greeting\"".into())
161 );
162 }
163
164 #[test]
165 fn conv_toml_error() {
166 let mut cl = CmdLine::new();
167 cl.push_toml_line("Hello=world".to_string());
168 cl.push_toml_line("Hola=mundo".to_string());
169 cl.push_toml_line("Bonjour=monde".to_string());
170 let toml_s = cl.build_toml();
171
172 assert_eq!(
173 &cl.convert_toml_error(&toml_s, "Nice greeting", &Some(0..13)),
174 "Couldn't parse command line: Nice greeting in \"Hello=world\""
175 );
176
177 assert_eq!(
178 &cl.convert_toml_error(&toml_s, "Nice greeting", &Some(99..333)),
179 "Couldn't parse command line: Nice greeting"
180 );
181
182 assert_eq!(
183 &cl.convert_toml_error(&toml_s, "Nice greeting with a thing", &Some(0..13)),
184 "Couldn't parse command line: Nice greeting with a thing in \"Hello=world\""
185 );
186 }
187
188 #[test]
189 fn parse_good() {
190 let mut cl = CmdLine::default();
191 cl.push_toml_line("a=3".to_string());
192 cl.push_toml_line("bcd=hello".to_string());
193 cl.push_toml_line("ef=\"gh i\"".to_string());
194 cl.push_toml_line("w=[1,2,3]".to_string());
195
196 let v = cl
197 .data()
198 .unwrap()
199 .remove(&figment::Profile::Default)
200 .unwrap();
201
202 assert_eq!(v["a"], "3".into());
203 assert_eq!(v["bcd"], "hello".into());
204 assert_eq!(v["ef"], "gh i".into());
205 assert_eq!(v["w"], vec![1, 2, 3].into());
206 }
207
208 #[test]
209 fn parse_bad() {
210 let mut cl = CmdLine::default();
211 cl.push_toml_line("x=1 1 1 1 1".to_owned());
212 let v = cl.data();
213 assert!(v.is_err());
214 }
215}