alpm_parsers/custom_ini/
parser.rs

1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4use winnow::{
5    ModalResult,
6    Parser,
7    ascii::{newline, space0, till_line_ending},
8    combinator::{
9        alt,
10        cut_err,
11        eof,
12        opt,
13        preceded,
14        repeat,
15        repeat_till,
16        separated_pair,
17        terminated,
18    },
19    error::{StrContext, StrContextValue},
20    token::none_of,
21};
22
23use super::de::Error;
24
25const INVALID_KEY_NAME_SYMBOLS: [char; 3] = ['=', ' ', '\n'];
26
27/// Representation of parsed items.
28#[derive(Debug, Clone, Deserialize, PartialEq)]
29#[serde(untagged)]
30pub enum Item {
31    Value(String),
32    List(Vec<String>),
33}
34
35impl Item {
36    pub fn value_or_error(&self) -> Result<&str, Error> {
37        match self {
38            Item::Value(value) => Ok(value),
39            Item::List(_) => Err(Error::InvalidState),
40        }
41    }
42}
43
44/// Representation of a parsed line.
45#[derive(Debug)]
46enum ParsedLine<'s> {
47    /// A key value pair.
48    KeyValue { key: &'s str, value: &'s str },
49    /// A comment.
50    Comment(&'s str),
51}
52
53/// Take all chars, until we hit a char that isn't allowed in a key.
54fn key(input: &mut &str) -> ModalResult<()> {
55    repeat(1.., none_of(INVALID_KEY_NAME_SYMBOLS)).parse_next(input)
56}
57
58/// Parse a single key value pair.
59/// The delimiter includes two surrounding spaces, i.e. ` = `.
60///
61/// ## Examples
62///
63/// ```ini
64/// key = value
65/// ```
66fn key_value<'s>(input: &mut &'s str) -> ModalResult<(&'s str, &'s str)> {
67    separated_pair(
68        cut_err(key.take())
69            .context(StrContext::Label("key"))
70            .context(StrContext::Expected(StrContextValue::Description(
71                "a key followed by a ` = ` delimiter.",
72            ))),
73        cut_err((" ", "=", " "))
74            .context(StrContext::Label("delimiter"))
75            .context(StrContext::Expected(StrContextValue::Description(
76                "a '=' that delimits the key value pair, surrounded by a single space.",
77            ))),
78        till_line_ending,
79    )
80    .parse_next(input)
81}
82
83/// One or multiple newlines.
84/// This also handles the case where there might be multiple lines with spaces.
85fn newlines(input: &mut &str) -> ModalResult<()> {
86    repeat(0.., (newline, space0)).parse_next(input)
87}
88
89/// Parse a comment (a line starting with `#`).
90fn comment<'s>(input: &mut &'s str) -> ModalResult<&'s str> {
91    preceded('#', till_line_ending).parse_next(input)
92}
93
94/// Parse a single line consisting of a key value pair or a comment, followed by 0 or more newlines.
95fn line<'s>(input: &mut &'s str) -> ModalResult<ParsedLine<'s>> {
96    alt((
97        terminated(comment, opt(newlines)).map(ParsedLine::Comment),
98        terminated(key_value, opt(newlines))
99            .map(|(key, value)| ParsedLine::KeyValue { key, value }),
100    ))
101    .parse_next(input)
102}
103
104/// Parse multiple lines.
105fn lines<'s>(input: &mut &'s str) -> ModalResult<Vec<ParsedLine<'s>>> {
106    let (value, _terminator) = repeat_till(0.., line, eof).parse_next(input)?;
107
108    Ok(value)
109}
110
111/// Parse the content of a whole ini file.
112pub fn ini_file(input: &mut &str) -> ModalResult<BTreeMap<String, Item>> {
113    let mut items: BTreeMap<String, Vec<String>> = BTreeMap::new();
114
115    // Ignore any preceding newlines at the start of the file.
116    let parsed_lines = preceded(newlines, lines).parse_next(input)?;
117    for parsed_line in parsed_lines {
118        match parsed_line {
119            ParsedLine::KeyValue { key, value } => {
120                let values = items.entry(key.to_string()).or_default();
121                values.push(value.to_string());
122            }
123            ParsedLine::Comment(_v) => {}
124        }
125    }
126
127    // Collapse the list of all items into their final representation.
128    //
129    // Keys that only occur a single time are interpreted as a single item.
130    // Keys that occur multiple times are interpreted as a list.
131    Ok(items
132        .into_iter()
133        .map(|(key, mut values)| {
134            if values.len() == 1 {
135                (key, Item::Value(values.remove(0)))
136            } else {
137                (key, Item::List(values))
138            }
139        })
140        .collect())
141}
142
143#[cfg(test)]
144mod test {
145    use testresult::TestResult;
146
147    use super::*;
148
149    static TEST_NEWLINES_INPUT: &str = "
150
151foo = bar
152
153test = nice
154
155";
156
157    /// Make sure that newlines at any place are just ignored.
158    #[test]
159    fn test_newlines() -> TestResult<()> {
160        let results = ini_file(&mut TEST_NEWLINES_INPUT.to_string().as_str())?;
161
162        let mut expected = BTreeMap::new();
163        expected.insert("foo".to_string(), Item::Value("bar".to_string()));
164        expected.insert("test".to_string(), Item::Value("nice".to_string()));
165
166        assert_eq!(expected, results);
167
168        Ok(())
169    }
170
171    static TEST_LISTS_INPUT: &str = "foo = bar
172
173test = very
174test = nice
175test = indeed";
176
177    /// Ensure that parsing lists works.
178    #[test]
179    fn test_lists() -> TestResult<()> {
180        let results = ini_file(&mut TEST_LISTS_INPUT.to_string().as_str())?;
181
182        let mut expected = BTreeMap::new();
183        expected.insert("foo".to_string(), Item::Value("bar".to_string()));
184        expected.insert(
185            "test".to_string(),
186            Item::List(vec![
187                "very".to_string(),
188                "nice".to_string(),
189                "indeed".to_string(),
190            ]),
191        );
192
193        assert_eq!(expected, results);
194
195        Ok(())
196    }
197
198    static TEST_COMMENT_INPUT: &str = "
199# Hey
200# This is a comment
201foo = bar
202# This is another comment
203bar = baz
204# And another one";
205
206    /// Ensure that comments are ignored.
207    #[test]
208    fn test_comments() -> TestResult<()> {
209        let results = ini_file(&mut TEST_COMMENT_INPUT.to_string().as_str())?;
210
211        let mut expected = BTreeMap::new();
212        expected.insert("foo".to_string(), Item::Value("bar".to_string()));
213        expected.insert("bar".to_string(), Item::Value("baz".to_string()));
214
215        assert_eq!(expected, results);
216
217        Ok(())
218    }
219}