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;