ini_core/lib.rs
1/*!
2Ini streaming parser
3====================
4
5Simple, straight forward, super fast, `no_std` compatible streaming INI parser.
6
7Examples
8--------
9
10```
11use ini_core as ini;
12
13let document = "\
14[SECTION]
15;this is a comment
16Key=Value";
17
18let elements = [
19 ini::Item::SectionEnd,
20 ini::Item::Section("SECTION"),
21 ini::Item::Comment("this is a comment"),
22 ini::Item::Property("Key", Some("Value")),
23 ini::Item::SectionEnd,
24];
25
26for (index, item) in ini::Parser::new(document).enumerate() {
27 assert_eq!(item, elements[index]);
28}
29```
30
31The `SectionEnd` pseudo element is returned before a new section and at the end of the document.
32This helps processing sections after their properties finished parsing.
33
34The parser is very much line-based, it will continue no matter what and return nonsense as an item:
35
36```
37use ini_core as ini;
38
39let document = "\
40[SECTION
41nonsense";
42
43let elements = [
44 ini::Item::SectionEnd,
45 ini::Item::Error("[SECTION"),
46 ini::Item::Property("nonsense", None),
47 ini::Item::SectionEnd,
48];
49
50for (index, item) in ini::Parser::new(document).enumerate() {
51 assert_eq!(item, elements[index]);
52}
53```
54
55Lines starting with `[` but contain either no closing `]` or a closing `]` not followed by a newline are returned as [`Item::Error`].
56Lines missing a `=` are returned as [`Item::Property`] with `None` value. See below for more details.
57
58Format
59------
60
61INI is not a well specified format, this parser tries to make as little assumptions as possible but it does make decisions.
62
63* Newline is either `"\r\n"`, `"\n"` or `"\r"`. It can be mixed in a single document but this is not recommended.
64* Section header is `"[" section "]" newline`. `section` can be anything except contain newlines.
65* Property is `key "=" value newline`. `key` and `value` can be anything except contain newlines.
66* Comment is `";" comment newline` and Blank is just `newline`. The comment character can be customized.
67
68Note that padding whitespace is not trimmed by default:
69
70* Section `[ SECTION ]`'s name is `<space>SECTION<space>`.
71* Property `KEY = VALUE` has key `KEY<space>` and value `<space>VALUE`.
72* Comment `; comment`'s comment is `<space>comment`.
73
74No further processing of the input is done, eg. if escape sequences are necessary they must be processed by the caller.
75*/
76
77#![cfg_attr(not(test), no_std)]
78
79#[allow(unused_imports)]
80use core::{fmt, str};
81
82// All the routines here work only with and slice only at ascii characters
83// This means conversion between `&str` and `&[u8]` is a noop even when slicing
84#[inline]
85fn from_utf8(v: &[u8]) -> &str {
86 #[cfg(not(debug_assertions))]
87 return unsafe { str::from_utf8_unchecked(v) };
88 #[cfg(debug_assertions)]
89 return str::from_utf8(v).unwrap();
90}
91
92mod parse;
93
94/// Ini element.
95///
96/// # Notes
97///
98/// Strings are not checked or escaped when displaying the item.
99///
100/// Ensure that they do not contain newlines or invalid characters.
101#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
102pub enum Item<'a> {
103 /// Syntax error.
104 ///
105 /// Section header element was malformed.
106 /// Malformed section headers are defined by a line starting with `[` but not ending with `]`.
107 ///
108 /// ```
109 /// assert_eq!(
110 /// ini_core::Parser::new("[Error").nth(1),
111 /// Some(ini_core::Item::Error("[Error")));
112 /// ```
113 Error(&'a str),
114
115 /// Section header element.
116 ///
117 /// ```
118 /// assert_eq!(
119 /// ini_core::Parser::new("[Section]").nth(1),
120 /// Some(ini_core::Item::Section("Section")));
121 /// ```
122 Section(&'a str),
123
124 /// End of section.
125 ///
126 /// Pseudo element emitted before a [`Section`](Item::Section) and at the end of the document.
127 /// This helps processing sections after their properties finished parsing.
128 ///
129 /// ```
130 /// assert_eq!(
131 /// ini_core::Parser::new("").next(),
132 /// Some(ini_core::Item::SectionEnd));
133 /// ```
134 SectionEnd,
135
136 /// Property element.
137 ///
138 /// Key value must not contain `=`.
139 ///
140 /// The value is `None` if there is no `=`.
141 ///
142 /// ```
143 /// assert_eq!(
144 /// ini_core::Parser::new("Key=Value").next(),
145 /// Some(ini_core::Item::Property("Key", Some("Value"))));
146 /// assert_eq!(
147 /// ini_core::Parser::new("Key").next(),
148 /// Some(ini_core::Item::Property("Key", None)));
149 /// ```
150 Property(&'a str, Option<&'a str>),
151
152 /// Comment.
153 ///
154 /// ```
155 /// assert_eq!(
156 /// ini_core::Parser::new(";comment").next(),
157 /// Some(ini_core::Item::Comment("comment")));
158 /// ```
159 Comment(&'a str),
160
161 /// Blank line.
162 ///
163 /// Allows faithful reproduction of the whole ini document including blank lines.
164 ///
165 /// ```
166 /// assert_eq!(
167 /// ini_core::Parser::new("\n").next(),
168 /// Some(ini_core::Item::Blank));
169 /// ```
170 Blank,
171}
172
173impl<'a> fmt::Display for Item<'a> {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 match self {
176 &Item::Error(error) => write!(f, "{}\n", error),
177 &Item::Section(section) => write!(f, "[{}]\n", section),
178 &Item::SectionEnd => Ok(()),
179 &Item::Property(key, Some(value)) => write!(f, "{}={}\n", key, value),
180 &Item::Property(key, None) => write!(f, "{}\n", key),
181 &Item::Comment(comment) => write!(f, ";{}\n", comment),
182 &Item::Blank => f.write_str("\n"),
183 }
184 }
185}
186
187/// Trims ascii whitespace from the start and end of the string slice.
188///
189/// See also [`Parser::auto_trim`] to automatically trim strings.
190#[inline(never)]
191pub fn trim(s: &str) -> &str {
192 s.trim_matches(|chr: char| chr.is_ascii_whitespace())
193}
194
195/// Ini streaming parser.
196///
197/// The whole document must be available before parsing starts.
198/// The parser then returns each element as it is being parsed.
199///
200/// See [`crate`] documentation for more information.
201#[derive(Clone, Debug)]
202pub struct Parser<'a> {
203 line: u32,
204 comment_char: u8,
205 auto_trim: bool,
206 section_ended: bool,
207 state: &'a [u8],
208}
209
210impl<'a> Parser<'a> {
211 /// Constructs a new `Parser` instance.
212 #[inline]
213 pub const fn new(s: &'a str) -> Parser<'a> {
214 let state = s.as_bytes();
215 Parser { line: 0, comment_char: b';', auto_trim: false, section_ended: false, state }
216 }
217
218 /// Sets the comment character, eg. `b'#'`.
219 ///
220 /// The default is `b';'`.
221 #[must_use]
222 #[inline]
223 pub const fn comment_char(self, chr: u8) -> Parser<'a> {
224 // Mask off high bit to ensure we don't corrupt utf8 strings
225 let comment_char = chr & 0x7f;
226 Parser { comment_char, ..self }
227 }
228
229 /// Sets auto trimming of all returned strings.
230 ///
231 /// The default is `false`.
232 #[must_use]
233 #[inline]
234 pub const fn auto_trim(self, auto_trim: bool) -> Parser<'a> {
235 Parser { auto_trim, ..self }
236 }
237
238 /// Returns the line number the parser is currently at.
239 #[inline]
240 pub const fn line(&self) -> u32 {
241 self.line
242 }
243
244 /// Returns the remainder of the input string.
245 #[inline]
246 pub fn remainder(&self) -> &'a str {
247 from_utf8(self.state)
248 }
249}
250
251impl<'a> Iterator for Parser<'a> {
252 type Item = Item<'a>;
253
254 // #[cfg_attr(test, mutagen::mutate)]
255 #[inline(never)]
256 fn next(&mut self) -> Option<Item<'a>> {
257 let mut s = self.state;
258
259 match s.first().cloned() {
260 // Terminal case
261 None => {
262 if self.section_ended {
263 None
264 }
265 else {
266 self.section_ended = true;
267 Some(Item::SectionEnd)
268 }
269 },
270 // Blank
271 Some(b'\r' | b'\n') => {
272 self.skip_ln(s);
273 Some(Item::Blank)
274 },
275 // Comment
276 Some(chr) if chr == self.comment_char => {
277 s = &s[1..];
278 let i = parse::find_nl(s);
279 let comment = from_utf8(&s[..i]);
280 let comment = if self.auto_trim { trim(comment) } else { comment };
281 self.skip_ln(&s[i..]);
282 Some(Item::Comment(comment))
283 },
284 // Section
285 Some(b'[') => {
286 if self.section_ended {
287 self.section_ended = false;
288 let i = parse::find_nl(s);
289 if s[i - 1] != b']' {
290 let error = from_utf8(&s[..i]);
291 self.skip_ln(&s[i..]);
292 return Some(Item::Error(error));
293 }
294 let section = from_utf8(&s[1..i - 1]);
295 let section = if self.auto_trim { trim(section) } else { section };
296 self.skip_ln(&s[i..]);
297 Some(Item::Section(section))
298 }
299 else {
300 self.section_ended = true;
301 Some(Item::SectionEnd)
302 }
303 },
304 // Property
305 _ => {
306 let key = {
307 let i = parse::find_nl_chr(s, b'=');
308 let key = from_utf8(&s[..i]);
309 let key = if self.auto_trim { trim(key) } else { key };
310 if s.get(i) != Some(&b'=') {
311 self.skip_ln(&s[i..]);
312 if key.is_empty() {
313 return Some(Item::Blank);
314 }
315 return Some(Item::Property(key, None));
316 }
317 s = &s[i + 1..];
318 key
319 };
320 let value = {
321 let i = parse::find_nl(s);
322 let value = from_utf8(&s[..i]);
323 let value = if self.auto_trim { trim(value) } else { value };
324 self.skip_ln(&s[i..]);
325 value
326 };
327 Some(Item::Property(key, Some(value)))
328 },
329 }
330 }
331}
332
333impl<'a> core::iter::FusedIterator for Parser<'a> {}
334
335impl<'a> Parser<'a> {
336 #[inline]
337 fn skip_ln(&mut self, mut s: &'a [u8]) {
338 if s.len() > 0 {
339 if s[0] == b'\r' {
340 s = &s[1..];
341 }
342 if s.len() > 0 {
343 if s[0] == b'\n' {
344 s = &s[1..];
345 }
346 }
347 self.line += 1;
348 }
349 self.state = s;
350 }
351}
352
353#[cfg(test)]
354mod tests;