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#[derive(Copy, Clone, Debug, PartialEq)]
15pub enum IniMode {
16 Simple,
24
25 SimpleTrimmed
33}
34
35#[derive(Clone, Debug, PartialEq, Default)]
37pub struct Ini {
38 sections: BTreeMap<String, IniSection>
39}
40
41#[derive(Clone, Debug, PartialEq, Default)]
43pub struct IniSection {
44 values: BTreeMap<String, String>
45}
46
47impl Ini {
48 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 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 pub fn get(&self, key: &str) -> Option<&str> {
123 self.values.get(key).map(String::as_str)
124 }
125}
126
127#[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;