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