gerbil_ini/
ini.rs

1use alloc::borrow::ToOwned;
2use alloc::collections::BTreeMap;
3use alloc::string::{String, ToString};
4use core::fmt::{Debug, 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    /// Get the value in the section of the ini.
64    ///
65    /// Returns `None` if the section or key do not exist.
66    pub fn get_value(&self, section: &str, key: &str) -> Option<&str> {
67        self.get_section(section).and_then(|s| s.get(key))
68    }
69
70    fn parse_simple(string: &str, config: IniMode) -> Result<Self, IniParsingError> {
71        let mut ini = Ini::default();
72
73        let mut lines = string.lines().enumerate().map(|(line_index, line)| (line_index + 1, line));
74        let mut section = None;
75
76        while let Some((line_number, line)) = lines.next() {
77            if line.chars().next().iter().any(|i| COMMENT_CHARS.contains(i)) || line.is_empty() || line.chars().all(|c| c.is_whitespace()) {
78                continue
79            }
80
81            if line.starts_with('[') {
82                let end = line.find(']').ok_or(IniParsingError::BrokenSectionTitle { line_number })?;
83                let title = line[1..end].to_owned();
84                if ini.sections.contains_key(&title) {
85                    return Err(IniParsingError::DuplicateSection { line_number, section: title })
86                }
87                section = Some(title.clone());
88                ini.sections.insert(title, Default::default());
89                continue
90            }
91
92            let Some(section) = section.as_ref() else {
93                return Err(IniParsingError::ExpectedSectionTitle { line_number })
94            };
95
96            let l = line.find('=').ok_or(IniParsingError::MissingEquals { line_number })?;
97            let (key_str, value_eq) = line.split_at(l);
98            let value_str = &value_eq[1..];
99
100            let key: String;
101            let value: String;
102
103            match config {
104                IniMode::Simple => {
105                    key = key_str.to_owned();
106                    value = value_str.to_owned();
107                }
108                IniMode::SimpleTrimmed => {
109                    key = key_str.trim_end().to_owned();
110                    value = value_str.trim_start().to_owned();
111                }
112            }
113
114            let s = ini.sections.get_mut(section).unwrap();
115            if s.values.contains_key(&key) {
116                return Err(IniParsingError::DuplicateSectionKey { line_number, section: section.to_string(), key })
117            }
118            s.values.insert(key, value);
119        }
120
121        Ok(ini)
122    }
123}
124
125impl IniSection {
126    /// Get the value for a key.
127    ///
128    /// Returns `None` if the key is not present.
129    pub fn get(&self, key: &str) -> Option<&str> {
130        self.values.get(key).map(String::as_str)
131    }
132}
133
134/// An error generated by the ini parser.
135#[derive(Clone, PartialEq)]
136pub enum IniParsingError {
137    MissingEquals { line_number: usize },
138    ExpectedSectionTitle { line_number: usize },
139    BrokenSectionTitle { line_number: usize },
140    DuplicateSection { line_number: usize, section: String },
141    DuplicateSectionKey { line_number: usize, section: String, key: String },
142}
143
144impl Display for IniParsingError {
145    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
146        match self {
147            Self::MissingEquals { line_number } => f.write_fmt(format_args!("Parsing error on line {line_number}: Missing an `=` to separate the key and value")),
148            Self::ExpectedSectionTitle { line_number } => f.write_fmt(format_args!("Parsing error on line {line_number}: Expected a section title")),
149            Self::BrokenSectionTitle { line_number } => f.write_fmt(format_args!("Parsing error on line {line_number}: Expected a `]` to close a `[`")),
150            Self::DuplicateSection { line_number, section } => f.write_fmt(format_args!("Parsing error on line {line_number}: Duplicate section `{section}`")),
151            Self::DuplicateSectionKey { line_number, section, key } => f.write_fmt(format_args!("Parsing error on line {line_number}: Duplicate key `{key}` in section `{section}`"))
152        }
153    }
154}
155
156impl Debug for IniParsingError {
157    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
158        Display::fmt(self, f)
159    }
160}
161
162#[cfg(test)]
163mod test;