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#[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 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 pub fn get(&self, key: &str) -> Option<&str> {
130 self.values.get(key).map(String::as_str)
131 }
132}
133
134#[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;