light_ini/
lib.rs

1//! # Light Ini file parser.
2//!
3//! `light-ini` implements an event-driven parser for the [INI file format](https://en.wikipedia.org/wiki/INI_file).
4//! The handler must implement `IniHandler`.
5//!
6//! ```
7//! use light_ini::{IniHandler, IniParser, IniHandlerError};
8//!
9//! struct Handler {}
10//!
11//! impl IniHandler for Handler {
12//!     type Error = IniHandlerError;
13//!
14//!     fn section(&mut self, name: &str) -> Result<(), Self::Error> {
15//!         println!("section {}", name);
16//!         Ok(())
17//!     }
18//!
19//!     fn option(&mut self, key: &str, value: &str) -> Result<(), Self::Error> {
20//!         println!("option {} is {}", key, value);
21//!         Ok(())
22//!     }
23//!
24//!     fn comment(&mut self, comment: &str) -> Result<(), Self::Error> {
25//!         println!("comment: {}", comment);
26//!         Ok(())
27//!     }
28//! }
29//!
30//! let mut handler = Handler{};
31//! let mut parser = IniParser::new(&mut handler);
32//! parser.parse_file("example.ini");
33//! ```
34
35use std::{
36    convert::From,
37    error, fmt,
38    fs::File,
39    io::{self, BufRead, BufReader, Read},
40    path::Path,
41};
42
43#[derive(Debug)]
44/// Convenient error type for handlers that don't need detailed errors.
45pub struct IniHandlerError {}
46
47impl fmt::Display for IniHandlerError {
48    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(fmt, "handler failure")
50    }
51}
52
53impl error::Error for IniHandlerError {}
54
55#[derive(Debug)]
56/// Errors for INI format parsing
57pub enum IniError<HandlerError: fmt::Debug + error::Error> {
58    InvalidLine(usize),
59    Handler(HandlerError),
60    Io(io::Error),
61}
62
63impl<HandlerError: fmt::Display + error::Error> fmt::Display for IniError<HandlerError> {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            IniError::InvalidLine(line) => write!(f, "invalid line: {}", line),
67            IniError::Handler(err) => write!(f, "handler error: {}", err),
68            IniError::Io(err) => write!(f, "input/output error: {}", err),
69        }
70    }
71}
72
73impl<HandlerError: fmt::Debug + error::Error> error::Error for IniError<HandlerError> {
74    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
75        match self {
76            IniError::InvalidLine(_) => None,
77            IniError::Handler(err) => err.source(),
78            IniError::Io(err) => err.source(),
79        }
80    }
81}
82
83impl<HandlerError: fmt::Debug + error::Error> From<HandlerError> for IniError<HandlerError> {
84    fn from(err: HandlerError) -> Self {
85        Self::Handler(err)
86    }
87}
88
89/// Interface for the INI format handler
90pub trait IniHandler {
91    type Error: fmt::Debug;
92
93    /// Called when a section is found
94    fn section(&mut self, name: &str) -> Result<(), Self::Error>;
95
96    /// Called when an option is found
97    fn option(&mut self, key: &str, value: &str) -> Result<(), Self::Error>;
98
99    /// Called for each comment
100    fn comment(&mut self, _: &str) -> Result<(), Self::Error> {
101        Ok(())
102    }
103}
104
105/// INI format parser.
106pub struct IniParser<'a, Error: fmt::Debug + error::Error> {
107    handler: &'a mut dyn IniHandler<Error = Error>,
108    start_comment: String,
109}
110
111impl<'a, Error: fmt::Debug + error::Error> IniParser<'a, Error> {
112    /// Create a parser using the given handler.
113    pub fn new(handler: &'a mut dyn IniHandler<Error = Error>) -> IniParser<'a, Error> {
114        Self::with_start_comment(handler, ';')
115    }
116
117    /// Create a parser using the given character as start of comment.
118    pub fn with_start_comment(
119        handler: &'a mut dyn IniHandler<Error = Error>,
120        start_comment: char,
121    ) -> IniParser<'a, Error> {
122        let start_comment = format!("{}", start_comment);
123        Self {
124            handler,
125            start_comment,
126        }
127    }
128
129    /// Parse one line without trailing newline character.
130    fn parse_ini_line(&mut self, line: &str, lineno: usize) -> Result<(), IniError<Error>> {
131        let line = line.trim_start();
132        if line.is_empty() {
133            Ok(())
134        } else {
135            let (prefix, rest) = if line.is_char_boundary(1) {
136                line.split_at(1)
137            } else {
138                ("", line)
139            };
140            if prefix == "[" {
141                match rest.find(']') {
142                    Some(pos) => {
143                        let (name, _) = rest.split_at(pos);
144                        self.handler.section(name.trim())?;
145                    }
146                    None => return Err(IniError::InvalidLine(lineno)),
147                }
148            } else if prefix == self.start_comment {
149                self.handler.comment(rest.trim_start())?;
150            } else {
151                match line.find('=') {
152                    Some(pos) => {
153                        let (name, rest) = line.split_at(pos);
154                        let (_, value) = rest.split_at(1);
155                        self.handler.option(name.trim(), value.trim())?;
156                    }
157                    None => return Err(IniError::InvalidLine(lineno)),
158                }
159            }
160            Ok(())
161        }
162    }
163
164    /// Parse input from a buffered reader.
165    pub fn parse_buffered<B: BufRead>(&mut self, input: B) -> Result<(), IniError<Error>> {
166        let mut lineno = 0;
167        for res in input.lines() {
168            lineno += 1;
169            match res {
170                Ok(line) => self.parse_ini_line(line.trim_end(), lineno)?,
171                Err(err) => return Err(IniError::Io(err)),
172            }
173        }
174        Ok(())
175    }
176
177    /// Parse input from a reader.
178    pub fn parse<R: Read>(&mut self, input: R) -> Result<(), IniError<Error>> {
179        let mut reader = BufReader::new(input);
180        self.parse_buffered(&mut reader)
181    }
182
183    /// Parse a file.
184    pub fn parse_file<P>(&mut self, path: P) -> Result<(), IniError<Error>>
185    where
186        P: AsRef<Path>,
187    {
188        let file = File::open(path).map_err(IniError::Io)?;
189        self.parse(file)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195
196    use super::{IniError, IniHandler, IniParser};
197
198    use std::{
199        error, fmt,
200        io::{self, Seek, Write},
201        str,
202    };
203
204    #[derive(Debug)]
205    enum TestError {
206        InvalidSection,
207        InvalidOption,
208        Io(io::Error),
209        Utf8(str::Utf8Error),
210    }
211
212    impl fmt::Display for TestError {
213        fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
214            match self {
215                TestError::InvalidSection => write!(fmt, "invalid section"),
216                TestError::InvalidOption => write!(fmt, "invalid option"),
217                TestError::Io(err) => write!(fmt, "i/o error: {}", err),
218                TestError::Utf8(err) => write!(fmt, "utf-8 error: {}", err),
219            }
220        }
221    }
222
223    impl error::Error for TestError {}
224
225    #[derive(Debug)]
226    /// Generic handler for tests
227    ///
228    /// Convert an ini file in a string where:
229    /// - section "[name]" are written <name>
230    /// - option "name = value" are written (name=value)
231    /// - comments are written /*comment*/
232    struct Handler {
233        stream: io::Cursor<Vec<u8>>,
234    }
235
236    impl Handler {
237        fn new() -> Self {
238            Self {
239                stream: io::Cursor::new(Vec::<u8>::new()),
240            }
241        }
242
243        fn get(&self) -> Result<&str, TestError> {
244            str::from_utf8(self.stream.get_ref()).map_err(TestError::Utf8)
245        }
246    }
247
248    impl IniHandler for Handler {
249        type Error = TestError;
250
251        fn section(&mut self, name: &str) -> Result<(), Self::Error> {
252            if name == "invalid" {
253                Err(TestError::InvalidSection)
254            } else {
255                write!(self.stream, "<{}>", name).map_err(Self::Error::Io)
256            }
257        }
258
259        fn option(&mut self, name: &str, value: &str) -> Result<(), Self::Error> {
260            if name == "invalid" {
261                Err(TestError::InvalidOption)
262            } else {
263                write!(self.stream, "({}={})", name, value).map_err(Self::Error::Io)
264            }
265        }
266
267        fn comment(&mut self, comment: &str) -> Result<(), Self::Error> {
268            write!(self.stream, "/*{}*/", comment).map_err(Self::Error::Io)
269        }
270    }
271
272    type ParserError = IniError<TestError>;
273    type ParserResult<T> = Result<T, ParserError>;
274
275    fn new_input_stream(content: &str) -> io::Result<io::Cursor<Vec<u8>>> {
276        let mut buf = io::Cursor::new(Vec::<u8>::new());
277        writeln!(buf, "{}", content)?;
278        buf.seek(io::SeekFrom::Start(0))?;
279        Ok(buf)
280    }
281
282    fn read_ini(content: &str, start_comment: Option<char>) -> ParserResult<String> {
283        let mut handler = Handler::new();
284        let buf = new_input_stream(content).map_err(IniError::Io)?;
285        let mut parser = match start_comment {
286            Some(ch) => IniParser::with_start_comment(&mut handler, ch),
287            None => IniParser::new(&mut handler),
288        };
289        parser.parse(buf)?;
290        handler
291            .get()
292            .map(|s| s.to_string())
293            .map_err(ParserError::Handler)
294    }
295
296    const VALID_INI: &str = "name = test suite
297
298; logging section
299[logging]
300level = error
301";
302
303    #[test]
304    fn parse_valid_ini() -> ParserResult<()> {
305        let result = read_ini(VALID_INI, None)?;
306        assert_eq!(
307            "(name=test suite)/*logging section*/<logging>(level=error)",
308            result
309        );
310        Ok(())
311    }
312
313    const VALID_INI_ALT_COMMENT: &str = "# logging section
314[logging]
315level = error
316";
317
318    #[test]
319    fn parse_valid_ini_alt_comment() -> ParserResult<()> {
320        let result = read_ini(VALID_INI_ALT_COMMENT, Some('#'))?;
321        assert_eq!("/*logging section*/<logging>(level=error)", result);
322        Ok(())
323    }
324
325    const VALID_INI_UNICODE: &str = "[ŝipo]
326ĵurnalo = ĉirkaŭ";
327
328    #[test]
329    fn parse_unicode_ini() -> ParserResult<()> {
330        let result = read_ini(VALID_INI_UNICODE, None)?;
331        assert_eq!("<ŝipo>(ĵurnalo=ĉirkaŭ)", result);
332        Ok(())
333    }
334
335    const INVALID_SECTION: &str = "name = ok
336
337[logging";
338
339    #[test]
340    fn parse_invalid_section() {
341        let res = dbg!(read_ini(INVALID_SECTION, None));
342        assert!(matches!(res, Err(IniError::InvalidLine(3))));
343    }
344
345    const INVALID_OPTION: &str = "[logging]
346level error";
347
348    #[test]
349    fn parse_invalid_option() {
350        let res = dbg!(read_ini(INVALID_OPTION, None));
351        assert!(matches!(res, Err(IniError::InvalidLine(2))));
352    }
353
354    const UNEXPECTED_SECTION: &str = "name = test suite
355
356[invalid]
357level = error
358";
359
360    #[test]
361    /// Parse ini-file with a section considered as invalid in the handler
362    fn parse_unexpected_section() {
363        let res = dbg!(read_ini(UNEXPECTED_SECTION, None));
364        assert!(matches!(
365            res,
366            Err(IniError::Handler(TestError::InvalidSection))
367        ));
368    }
369
370    const UNEXPECTED_OPTION: &str = "[logging]
371invalid = error
372";
373
374    #[test]
375    /// Parse ini-file with an option considered as invalid in the handler
376    fn parse_unexpected_option() {
377        let res = dbg!(read_ini(UNEXPECTED_OPTION, None));
378        assert!(matches!(
379            res,
380            Err(IniError::Handler(TestError::InvalidOption))
381        ));
382    }
383}