xref: /cloud-hypervisor/option_parser/src/lib.rs (revision 20296e909a88ab1dd9d94b6bd2160ca84c430f5f)
1 // Copyright © 2020 Intel Corporation
2 //
3 // SPDX-License-Identifier: Apache-2.0
4 //
5 
6 use std::collections::HashMap;
7 use std::num::ParseIntError;
8 use std::str::FromStr;
9 
10 use thiserror::Error;
11 
12 #[derive(Default)]
13 pub struct OptionParser {
14     options: HashMap<String, OptionParserValue>,
15 }
16 
17 struct OptionParserValue {
18     value: Option<String>,
19     requires_value: bool,
20 }
21 
22 #[derive(Error, Debug)]
23 pub enum OptionParserError {
24     #[error("unknown option: {0}")]
25     UnknownOption(String),
26     #[error("unknown option: {0}")]
27     InvalidSyntax(String),
28     #[error("unable to convert {1} for {0}")]
29     Conversion(String /* field */, String /* value */),
30     #[error("invalid value: {0}")]
31     InvalidValue(String),
32 }
33 type OptionParserResult<T> = std::result::Result<T, OptionParserError>;
34 
35 fn split_commas(s: &str) -> OptionParserResult<Vec<String>> {
36     let mut list: Vec<String> = Vec::new();
37     let mut opened_brackets = 0;
38     let mut in_quotes = false;
39     let mut current = String::new();
40 
41     for c in s.trim().chars() {
42         match c {
43             '[' => {
44                 opened_brackets += 1;
45                 current.push('[');
46             }
47             ']' => {
48                 opened_brackets -= 1;
49                 if opened_brackets < 0 {
50                     return Err(OptionParserError::InvalidSyntax(s.to_owned()));
51                 }
52                 current.push(']');
53             }
54             '"' => in_quotes = !in_quotes,
55             ',' => {
56                 if opened_brackets > 0 || in_quotes {
57                     current.push(',')
58                 } else {
59                     list.push(current);
60                     current = String::new();
61                 }
62             }
63             c => current.push(c),
64         }
65     }
66     list.push(current);
67 
68     if opened_brackets != 0 || in_quotes {
69         return Err(OptionParserError::InvalidSyntax(s.to_owned()));
70     }
71 
72     Ok(list)
73 }
74 
75 impl OptionParser {
76     pub fn new() -> Self {
77         Self {
78             options: HashMap::new(),
79         }
80     }
81 
82     pub fn parse(&mut self, input: &str) -> OptionParserResult<()> {
83         if input.trim().is_empty() {
84             return Ok(());
85         }
86 
87         for option in split_commas(input)?.iter() {
88             let parts: Vec<&str> = option.splitn(2, '=').collect();
89 
90             match self.options.get_mut(parts[0]) {
91                 None => return Err(OptionParserError::UnknownOption(parts[0].to_owned())),
92                 Some(value) => {
93                     if value.requires_value {
94                         if parts.len() != 2 {
95                             return Err(OptionParserError::InvalidSyntax((*option).to_owned()));
96                         }
97                         value.value = Some(parts[1].trim().to_owned());
98                     } else {
99                         value.value = Some(String::new());
100                     }
101                 }
102             }
103         }
104 
105         Ok(())
106     }
107 
108     pub fn add(&mut self, option: &str) -> &mut Self {
109         self.options.insert(
110             option.to_owned(),
111             OptionParserValue {
112                 value: None,
113                 requires_value: true,
114             },
115         );
116 
117         self
118     }
119 
120     pub fn add_valueless(&mut self, option: &str) -> &mut Self {
121         self.options.insert(
122             option.to_owned(),
123             OptionParserValue {
124                 value: None,
125                 requires_value: false,
126             },
127         );
128 
129         self
130     }
131 
132     pub fn get(&self, option: &str) -> Option<String> {
133         self.options
134             .get(option)
135             .and_then(|v| v.value.clone())
136             .and_then(|s| if s.is_empty() { None } else { Some(s) })
137     }
138 
139     pub fn is_set(&self, option: &str) -> bool {
140         self.options
141             .get(option)
142             .and_then(|v| v.value.as_ref())
143             .is_some()
144     }
145 
146     pub fn convert<T: FromStr>(&self, option: &str) -> OptionParserResult<Option<T>> {
147         match self.get(option) {
148             None => Ok(None),
149             Some(v) => Ok(Some(v.parse().map_err(|_| {
150                 OptionParserError::Conversion(option.to_owned(), v.to_owned())
151             })?)),
152         }
153     }
154 }
155 
156 pub struct Toggle(pub bool);
157 
158 #[derive(Error, Debug)]
159 pub enum ToggleParseError {
160     #[error("invalid value: {0}")]
161     InvalidValue(String),
162 }
163 
164 impl FromStr for Toggle {
165     type Err = ToggleParseError;
166 
167     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
168         match s.to_lowercase().as_str() {
169             "" => Ok(Toggle(false)),
170             "on" => Ok(Toggle(true)),
171             "off" => Ok(Toggle(false)),
172             "true" => Ok(Toggle(true)),
173             "false" => Ok(Toggle(false)),
174             _ => Err(ToggleParseError::InvalidValue(s.to_owned())),
175         }
176     }
177 }
178 
179 pub struct ByteSized(pub u64);
180 
181 #[derive(Error, Debug)]
182 pub enum ByteSizedParseError {
183     #[error("invalid value: {0}")]
184     InvalidValue(String),
185 }
186 
187 impl FromStr for ByteSized {
188     type Err = ByteSizedParseError;
189 
190     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
191         Ok(ByteSized({
192             let s = s.trim();
193             let shift = if s.ends_with('K') {
194                 10
195             } else if s.ends_with('M') {
196                 20
197             } else if s.ends_with('G') {
198                 30
199             } else {
200                 0
201             };
202 
203             let s = s.trim_end_matches(['K', 'M', 'G']);
204             s.parse::<u64>()
205                 .map_err(|_| ByteSizedParseError::InvalidValue(s.to_owned()))?
206                 << shift
207         }))
208     }
209 }
210 
211 pub struct IntegerList(pub Vec<u64>);
212 
213 #[derive(Error, Debug)]
214 pub enum IntegerListParseError {
215     #[error("invalid value: {0}")]
216     InvalidValue(String),
217 }
218 
219 impl FromStr for IntegerList {
220     type Err = IntegerListParseError;
221 
222     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
223         let mut integer_list = Vec::new();
224         let ranges_list: Vec<&str> = s
225             .trim()
226             .trim_matches(|c| c == '[' || c == ']')
227             .split(',')
228             .collect();
229 
230         for range in ranges_list.iter() {
231             let items: Vec<&str> = range.split('-').collect();
232 
233             if items.len() > 2 {
234                 return Err(IntegerListParseError::InvalidValue((*range).to_string()));
235             }
236 
237             let start_range = items[0]
238                 .parse::<u64>()
239                 .map_err(|_| IntegerListParseError::InvalidValue(items[0].to_owned()))?;
240 
241             integer_list.push(start_range);
242 
243             if items.len() == 2 {
244                 let end_range = items[1]
245                     .parse::<u64>()
246                     .map_err(|_| IntegerListParseError::InvalidValue(items[1].to_owned()))?;
247                 if start_range >= end_range {
248                     return Err(IntegerListParseError::InvalidValue((*range).to_string()));
249                 }
250 
251                 for i in start_range..end_range {
252                     integer_list.push(i + 1);
253                 }
254             }
255         }
256 
257         Ok(IntegerList(integer_list))
258     }
259 }
260 
261 pub trait TupleValue {
262     fn parse_value(input: &str) -> Result<Self, TupleError>
263     where
264         Self: Sized;
265 }
266 
267 impl TupleValue for u64 {
268     fn parse_value(input: &str) -> Result<Self, TupleError> {
269         input.parse::<u64>().map_err(TupleError::InvalidInteger)
270     }
271 }
272 
273 impl TupleValue for Vec<u8> {
274     fn parse_value(input: &str) -> Result<Self, TupleError> {
275         Ok(IntegerList::from_str(input)
276             .map_err(TupleError::InvalidIntegerList)?
277             .0
278             .iter()
279             .map(|v| *v as u8)
280             .collect())
281     }
282 }
283 
284 impl TupleValue for Vec<u64> {
285     fn parse_value(input: &str) -> Result<Self, TupleError> {
286         Ok(IntegerList::from_str(input)
287             .map_err(TupleError::InvalidIntegerList)?
288             .0)
289     }
290 }
291 
292 impl TupleValue for Vec<usize> {
293     fn parse_value(input: &str) -> Result<Self, TupleError> {
294         Ok(IntegerList::from_str(input)
295             .map_err(TupleError::InvalidIntegerList)?
296             .0
297             .iter()
298             .map(|v| *v as usize)
299             .collect())
300     }
301 }
302 
303 pub struct Tuple<S, T>(pub Vec<(S, T)>);
304 
305 #[derive(Error, Debug)]
306 pub enum TupleError {
307     #[error("invalid value: {0}")]
308     InvalidValue(String),
309     #[error("split outside brackets: {0}")]
310     SplitOutsideBrackets(#[source] OptionParserError),
311     #[error("invalid integer list: {0}")]
312     InvalidIntegerList(#[source] IntegerListParseError),
313     #[error("invalid integer: {0}")]
314     InvalidInteger(#[source] ParseIntError),
315 }
316 
317 impl<S: FromStr, T: TupleValue> FromStr for Tuple<S, T> {
318     type Err = TupleError;
319 
320     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
321         let mut list: Vec<(S, T)> = Vec::new();
322 
323         let body = s
324             .trim()
325             .strip_prefix('[')
326             .and_then(|s| s.strip_suffix(']'))
327             .ok_or_else(|| TupleError::InvalidValue(s.to_string()))?;
328 
329         let tuples_list = split_commas(body).map_err(TupleError::SplitOutsideBrackets)?;
330         for tuple in tuples_list.iter() {
331             let items: Vec<&str> = tuple.split('@').collect();
332 
333             if items.len() != 2 {
334                 return Err(TupleError::InvalidValue((*tuple).to_string()));
335             }
336 
337             let item1 = items[0]
338                 .parse::<S>()
339                 .map_err(|_| TupleError::InvalidValue(items[0].to_owned()))?;
340             let item2 = TupleValue::parse_value(items[1])?;
341 
342             list.push((item1, item2));
343         }
344 
345         Ok(Tuple(list))
346     }
347 }
348 
349 #[derive(Default)]
350 pub struct StringList(pub Vec<String>);
351 
352 #[derive(Error, Debug)]
353 pub enum StringListParseError {
354     #[error("invalid value: {0}")]
355     InvalidValue(String),
356 }
357 
358 impl FromStr for StringList {
359     type Err = StringListParseError;
360 
361     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
362         let string_list: Vec<String> = s
363             .trim()
364             .trim_matches(|c| c == '[' || c == ']')
365             .split(',')
366             .map(|e| e.to_owned())
367             .collect();
368 
369         Ok(StringList(string_list))
370     }
371 }
372 
373 #[cfg(test)]
374 mod tests {
375     use super::*;
376 
377     #[test]
378     fn test_option_parser() {
379         let mut parser = OptionParser::new();
380         parser
381             .add("size")
382             .add("mergeable")
383             .add("hotplug_method")
384             .add("hotplug_size")
385             .add("topology")
386             .add("cmdline");
387 
388         parser.parse("size=128M,hanging_param").unwrap_err();
389         parser
390             .parse("size=128M,too_many_equals=foo=bar")
391             .unwrap_err();
392         parser.parse("size=128M,file=/dev/shm").unwrap_err();
393 
394         parser.parse("size=128M").unwrap();
395         assert_eq!(parser.get("size"), Some("128M".to_owned()));
396         assert!(!parser.is_set("mergeable"));
397         assert!(parser.is_set("size"));
398 
399         parser.parse("size=128M,mergeable=on").unwrap();
400         assert_eq!(parser.get("size"), Some("128M".to_owned()));
401         assert_eq!(parser.get("mergeable"), Some("on".to_owned()));
402 
403         parser
404             .parse("size=128M,mergeable=on,topology=[1,2]")
405             .unwrap();
406         assert_eq!(parser.get("size"), Some("128M".to_owned()));
407         assert_eq!(parser.get("mergeable"), Some("on".to_owned()));
408         assert_eq!(parser.get("topology"), Some("[1,2]".to_owned()));
409 
410         parser
411             .parse("size=128M,mergeable=on,topology=[[1,2],[3,4]]")
412             .unwrap();
413         assert_eq!(parser.get("size"), Some("128M".to_owned()));
414         assert_eq!(parser.get("mergeable"), Some("on".to_owned()));
415         assert_eq!(parser.get("topology"), Some("[[1,2],[3,4]]".to_owned()));
416 
417         parser.parse("topology=[").unwrap_err();
418         parser.parse("topology=[[[]]]]").unwrap_err();
419 
420         parser.parse("cmdline=\"console=ttyS0,9600n8\"").unwrap();
421         assert_eq!(
422             parser.get("cmdline"),
423             Some("console=ttyS0,9600n8".to_owned())
424         );
425         parser.parse("cmdline=\"").unwrap_err();
426         parser.parse("cmdline=\"\"\"").unwrap_err();
427     }
428 }
429