gerbil_ini/
ini.rs

1use alloc::borrow::ToOwned;
2use alloc::collections::BTreeMap;
3use alloc::string::{String, ToString};
4use core::fmt::{Display, Formatter};
5
6const COMMENT_CHARS: &[char] = &[';', '#'];
7
8/// Describes a method for parsing ini files.
9///
10/// The ini format isn't a universally agreed upon standard, and it can have different rules depending on the program
11/// it is written for.
12///
13/// Some ini files will successfully parse on some programs, but not so on others.
14#[derive(Copy, Clone, Debug, PartialEq)]
15pub enum IniMode {
16    /// Simple X=Y, where everything is passed through.
17    ///
18    /// There are some restrictions to this:
19    /// * Keys, values, and sections cannot be multi-line
20    /// * Keys cannot contain `=` characters
21    /// * Keys cannot start with `;`, `#`, or `[`
22    /// * Comments must exist in their own lines with no whitespace before the comment delimiter
23    Simple,
24
25    /// Same as `Simple`, but trim whitespace for keys and values.
26    ///
27    /// For example, `key = value` has the same meaning as `key=value`.
28    ///
29    /// This adds two additional restrictions:
30    /// * Keys cannot end with whitespace
31    /// * Values cannot begin with whitespace
32    SimpleTrimmed
33}
34
35/// Ini parser.
36#[derive(Clone, Debug, PartialEq, Default)]
37pub struct Ini {
38    sections: BTreeMap<String, IniSection>
39}
40
41/// Section for an ini.
42#[derive(Clone, Debug, PartialEq, Default)]
43pub struct IniSection {
44    values: BTreeMap<String, String>
45}
46
47impl Ini {
48    /// Parse the ini.
49    pub fn parse(string: &str, config: IniMode) -> Result<Self, IniParsingError> {
50        match config {
51            IniMode::Simple => Self::parse_simple(string, config),
52            IniMode::SimpleTrimmed => Self::parse_simple(string, config),
53        }
54    }
55
56    /// Get the section.
57    ///
58    /// Returns `None` if the section does not exist in the ini.
59    pub fn get_section(&self, section: &str) -> Option<&IniSection> {
60        self.sections.get(section)
61    }
62
63    fn parse_simple(string: &str, config: IniMode) -> Result<Self, IniParsingError> {
64        let mut ini = Ini::default();
65
66        let mut lines = string.lines().enumerate();
67        let mut section = None;
68
69        while let Some((line_number, line)) = lines.next() {
70            if line.chars().next().iter().any(|i| COMMENT_CHARS.contains(i)) || line.is_empty() || line.chars().all(|c| c.is_whitespace()) {
71                continue
72            }
73
74            if line.starts_with('[') {
75                let end = line.find(']').ok_or(IniParsingError::BrokenSectionTitle { line_number })?;
76                let title = line[1..end].to_owned();
77                if ini.sections.contains_key(&title) {
78                    return Err(IniParsingError::DuplicateSection { line_number, section: title })
79                }
80                section = Some(title.clone());
81                ini.sections.insert(title, Default::default());
82                continue
83            }
84
85            let Some(section) = section.as_ref() else {
86                return Err(IniParsingError::ExpectedSectionTitle { line_number })
87            };
88
89            let l = line.find('=').ok_or(IniParsingError::MissingEquals { line_number })?;
90            let (key_str, value_eq) = line.split_at(l);
91            let value_str = &value_eq[1..];
92
93            let key: String;
94            let value: String;
95
96            match config {
97                IniMode::Simple => {
98                    key = key_str.to_owned();
99                    value = value_str.to_owned();
100                }
101                IniMode::SimpleTrimmed => {
102                    key = key_str.trim_end().to_owned();
103                    value = value_str.trim_start().to_owned();
104                }
105            }
106
107            let s = ini.sections.get_mut(section).unwrap();
108            if s.values.contains_key(&key) {
109                return Err(IniParsingError::DuplicateSectionKey { line_number, section: section.to_string(), key })
110            }
111            s.values.insert(key, value);
112        }
113
114        Ok(ini)
115    }
116}
117
118impl IniSection {
119    /// Get the value for a key.
120    ///
121    /// Returns `None` if the key is not present.
122    pub fn get(&self, key: &str) -> Option<&str> {
123        self.values.get(key).map(String::as_str)
124    }
125}
126
127/// An error generated by the ini parser.
128#[derive(Clone, Debug, PartialEq)]
129pub enum IniParsingError {
130    MissingEquals { line_number: usize },
131    ExpectedSectionTitle { line_number: usize },
132    BrokenSectionTitle { line_number: usize },
133    DuplicateSection { line_number: usize, section: String },
134    DuplicateSectionKey { line_number: usize, section: String, key: String },
135}
136
137impl Display for IniParsingError {
138    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
139        match self {
140            Self::MissingEquals { line_number } => f.write_fmt(format_args!("{line_number}: Missing an `=` to separate the key and value")),
141            Self::ExpectedSectionTitle { line_number } => f.write_fmt(format_args!("{line_number}: Expected a section title")),
142            Self::BrokenSectionTitle { line_number } => f.write_fmt(format_args!("{line_number}: Expected a `]` to close a `[`")),
143            Self::DuplicateSection { line_number, section } => f.write_fmt(format_args!("{line_number}: Duplicate section `{section}`")),
144            Self::DuplicateSectionKey { line_number, section, key } => f.write_fmt(format_args!("{line_number}: Duplicate key `{key}` in section `{section}`"))
145        }
146    }
147}
148
149#[cfg(test)]
150mod test;