config_lib/parsers/
ini_parser.rs

1//! INI format parser implementation
2//!
3//! Supports standard INI format with:
4//! - Sections: \[section_name\]
5//! - Key-value pairs: key=value or key:value
6//! - Comments: ; comment or # comment
7//! - Escape sequences: \n, \t, \\, etc.
8//! - Quoted values with spaces
9//! - Case-sensitive keys and sections
10
11use crate::error::{Error, Result};
12use crate::value::Value;
13use std::collections::BTreeMap;
14
15/// Parse INI format configuration
16pub fn parse(source: &str) -> Result<Value> {
17    parse_ini(source)
18}
19
20/// Parse INI format string into a Value::Table
21pub fn parse_ini(content: &str) -> Result<Value> {
22    let mut parser = IniParser::new(content);
23    parser.parse()
24}
25
26struct IniParser<'a> {
27    content: &'a str,
28    position: usize,
29    line: usize,
30    current_section: Option<String>,
31    result: BTreeMap<String, Value>,
32}
33
34impl<'a> IniParser<'a> {
35    fn new(content: &'a str) -> Self {
36        Self {
37            content,
38            position: 0,
39            line: 1,
40            current_section: None,
41            result: BTreeMap::new(),
42        }
43    }
44
45    fn parse(&mut self) -> Result<Value> {
46        while self.position < self.content.len() {
47            self.skip_whitespace_and_comments()?;
48
49            if self.position >= self.content.len() {
50                break;
51            }
52
53            let ch = self.current_char();
54
55            match ch {
56                '[' => self.parse_section()?,
57                '\n' | '\r' => {
58                    self.advance();
59                    self.line += 1;
60                }
61                _ => self.parse_key_value()?,
62            }
63        }
64
65        Ok(Value::Table(self.result.clone()))
66    }
67
68    fn current_char(&self) -> char {
69        self.content.chars().nth(self.position).unwrap_or('\0')
70    }
71
72    fn advance(&mut self) {
73        if self.position < self.content.len() {
74            self.position += 1;
75        }
76    }
77
78    // Commented out to avoid unused warnings - could be useful for future enhancements
79    // fn peek_char(&self, offset: usize) -> char {
80    //     self.content.chars().nth(self.position + offset).unwrap_or('\0')
81    // }
82
83    fn skip_whitespace_and_comments(&mut self) -> Result<()> {
84        loop {
85            let ch = self.current_char();
86
87            match ch {
88                ' ' | '\t' => self.advance(),
89                ';' | '#' => {
90                    // Skip comment until end of line
91                    while self.current_char() != '\n' && self.current_char() != '\0' {
92                        self.advance();
93                    }
94                }
95                '\n' | '\r' => {
96                    self.advance();
97                    self.line += 1;
98                }
99                '\0' => break,
100                _ => break,
101            }
102        }
103        Ok(())
104    }
105
106    fn parse_section(&mut self) -> Result<()> {
107        self.advance(); // Skip '['
108        let start = self.position;
109
110        // Find closing bracket
111        while self.current_char() != ']' && self.current_char() != '\0' {
112            if self.current_char() == '\n' {
113                return Err(Error::Parse {
114                    message: "Unterminated section".to_string(),
115                    line: self.line,
116                    column: 1,
117                    file: None,
118                });
119            }
120            self.advance();
121        }
122
123        if self.current_char() != ']' {
124            return Err(Error::Parse {
125                message: "Missing closing bracket for section".to_string(),
126                line: self.line,
127                column: 1,
128                file: None,
129            });
130        }
131
132        let section_name = self.content[start..self.position].trim().to_string();
133        self.advance(); // Skip ']'
134
135        if section_name.is_empty() {
136            return Err(Error::Parse {
137                message: "Empty section name".to_string(),
138                line: self.line,
139                column: 1,
140                file: None,
141            });
142        }
143
144        self.current_section = Some(section_name);
145        Ok(())
146    }
147
148    fn parse_key_value(&mut self) -> Result<()> {
149        let key = self.parse_key()?;
150
151        if key.is_empty() {
152            return Ok(()); // Skip empty lines
153        }
154
155        self.skip_whitespace_and_comments()?;
156
157        let ch = self.current_char();
158        if ch != '=' && ch != ':' {
159            return Err(Error::Parse {
160                message: format!("Expected '=' or ':' after key '{key}'"),
161                line: self.line,
162                column: 1,
163                file: None,
164            });
165        }
166
167        self.advance(); // Skip separator
168        self.skip_whitespace_and_comments()?;
169
170        let value = self.parse_value()?;
171
172        // Store the key-value pair
173        let full_key = match &self.current_section {
174            Some(section) => format!("{section}.{key}"),
175            None => key,
176        };
177
178        self.result.insert(full_key, value);
179        Ok(())
180    }
181
182    fn parse_key(&mut self) -> Result<String> {
183        let start = self.position;
184
185        while self.position < self.content.len() {
186            let ch = self.current_char();
187            match ch {
188                '=' | ':' | '\n' | '\r' | '\0' => break,
189                ';' | '#' => break, // Comment starts
190                _ => self.advance(),
191            }
192        }
193
194        let key = self.content[start..self.position].trim();
195        Ok(key.to_string())
196    }
197
198    fn parse_value(&mut self) -> Result<Value> {
199        let mut value_chars = Vec::new();
200        let mut in_quotes = false;
201        let mut quote_char = '\0';
202
203        while self.position < self.content.len() {
204            let ch = self.current_char();
205
206            match ch {
207                '"' | '\'' if !in_quotes => {
208                    in_quotes = true;
209                    quote_char = ch;
210                    self.advance();
211                    // Don't include the opening quote
212                }
213                '\\' if in_quotes => {
214                    // Handle escape sequences within quotes
215                    self.advance(); // Skip backslash
216                    if self.position < self.content.len() {
217                        let escaped_char = self.current_char();
218                        match escaped_char {
219                            'n' => value_chars.push('\n'),
220                            't' => value_chars.push('\t'),
221                            'r' => value_chars.push('\r'),
222                            '\\' => value_chars.push('\\'),
223                            '"' => value_chars.push('"'),
224                            '\'' => value_chars.push('\''),
225                            _ => {
226                                value_chars.push('\\');
227                                value_chars.push(escaped_char);
228                            }
229                        }
230                        self.advance();
231                    }
232                }
233                ch if in_quotes && ch == quote_char => {
234                    in_quotes = false;
235                    self.advance();
236                    // Don't include the closing quote
237                    break;
238                }
239                '\n' | '\r' | '\0' if !in_quotes => break,
240                ';' | '#' if !in_quotes => break, // Comment starts
241                _ => {
242                    value_chars.push(ch);
243                    self.advance();
244                }
245            }
246        }
247
248        // If we're not in quotes, trim whitespace from the end
249        let value_str = if !in_quotes {
250            value_chars
251                .iter()
252                .collect::<String>()
253                .trim_end()
254                .to_string()
255        } else {
256            value_chars.iter().collect::<String>()
257        };
258
259        // For unquoted values, still process escape sequences
260        let processed_value = if in_quotes {
261            value_str // Already processed during parsing
262        } else {
263            self.process_escape_sequences(&value_str)
264        };
265
266        // Try to parse as different types
267        self.parse_typed_value(&processed_value)
268    }
269
270    fn process_escape_sequences(&self, value: &str) -> String {
271        let mut result = String::new();
272        let mut chars = value.chars().peekable();
273
274        while let Some(ch) = chars.next() {
275            if ch == '\\' {
276                match chars.peek() {
277                    Some('n') => {
278                        chars.next();
279                        result.push('\n');
280                    }
281                    Some('t') => {
282                        chars.next();
283                        result.push('\t');
284                    }
285                    Some('r') => {
286                        chars.next();
287                        result.push('\r');
288                    }
289                    Some('\\') => {
290                        chars.next();
291                        result.push('\\');
292                    }
293                    Some('"') => {
294                        chars.next();
295                        result.push('"');
296                    }
297                    Some('\'') => {
298                        chars.next();
299                        result.push('\'');
300                    }
301                    _ => result.push(ch),
302                }
303            } else {
304                result.push(ch);
305            }
306        }
307
308        result
309    }
310
311    fn parse_typed_value(&self, value: &str) -> Result<Value> {
312        if value.is_empty() {
313            return Ok(Value::String(String::new()));
314        }
315
316        // Try boolean
317        match value.to_lowercase().as_str() {
318            "true" | "yes" | "on" | "1" => return Ok(Value::Bool(true)),
319            "false" | "no" | "off" | "0" => return Ok(Value::Bool(false)),
320            _ => {}
321        }
322
323        // Try integer
324        if let Ok(int_val) = value.parse::<i64>() {
325            return Ok(Value::Integer(int_val));
326        }
327
328        // Try float
329        if let Ok(float_val) = value.parse::<f64>() {
330            return Ok(Value::Float(float_val));
331        }
332
333        // Default to string
334        Ok(Value::String(value.to_string()))
335    }
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_simple_ini() {
344        let content = r#"
345key1=value1
346key2=value2
347        "#;
348
349        let result = parse_ini(content).unwrap();
350        if let Value::Table(map) = result {
351            assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
352            assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
353        } else {
354            panic!("Expected table");
355        }
356    }
357
358    #[test]
359    fn test_sections() {
360        let content = r#"
361[section1]
362key1=value1
363
364[section2]
365key2=value2
366        "#;
367
368        let result = parse_ini(content).unwrap();
369        if let Value::Table(map) = result {
370            assert_eq!(
371                map.get("section1.key1").unwrap().as_string().unwrap(),
372                "value1"
373            );
374            assert_eq!(
375                map.get("section2.key2").unwrap().as_string().unwrap(),
376                "value2"
377            );
378        } else {
379            panic!("Expected table");
380        }
381    }
382
383    #[test]
384    fn test_comments() {
385        let content = r#"
386; This is a comment
387key1=value1  ; Inline comment
388# Hash comment
389key2=value2
390        "#;
391
392        let result = parse_ini(content).unwrap();
393        if let Value::Table(map) = result {
394            assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
395            assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
396        } else {
397            panic!("Expected table");
398        }
399    }
400
401    #[test]
402    fn test_quoted_values() {
403        let content = r#"
404key1="quoted value"
405key2='single quoted'
406key3="value with spaces"
407        "#;
408
409        let result = parse_ini(content).unwrap();
410        if let Value::Table(map) = result {
411            assert_eq!(
412                map.get("key1").unwrap().as_string().unwrap(),
413                "quoted value"
414            );
415            assert_eq!(
416                map.get("key2").unwrap().as_string().unwrap(),
417                "single quoted"
418            );
419            assert_eq!(
420                map.get("key3").unwrap().as_string().unwrap(),
421                "value with spaces"
422            );
423        } else {
424            panic!("Expected table");
425        }
426    }
427
428    #[test]
429    fn test_escape_sequences() {
430        let content = r#"
431key1="line1\nline2"
432key2="tab\there"
433key3="quote\"here"
434        "#;
435
436        let result = parse_ini(content).unwrap();
437        if let Value::Table(map) = result {
438            assert_eq!(
439                map.get("key1").unwrap().as_string().unwrap(),
440                "line1\nline2"
441            );
442            assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "tab\there");
443            assert_eq!(map.get("key3").unwrap().as_string().unwrap(), "quote\"here");
444        } else {
445            panic!("Expected table");
446        }
447    }
448
449    #[test]
450    fn test_data_types() {
451        let content = r#"
452string_val=hello
453int_val=42
454float_val=1.234
455bool_true=true
456bool_false=false
457bool_yes=yes
458bool_no=no
459        "#;
460
461        let result = parse_ini(content).unwrap();
462        if let Value::Table(map) = result {
463            assert_eq!(map.get("string_val").unwrap().as_string().unwrap(), "hello");
464            assert_eq!(map.get("int_val").unwrap().as_integer().unwrap(), 42);
465            assert_eq!(map.get("float_val").unwrap().as_float().unwrap(), 1.234);
466            assert!(map.get("bool_true").unwrap().as_bool().unwrap());
467            assert!(!map.get("bool_false").unwrap().as_bool().unwrap());
468            assert!(map.get("bool_yes").unwrap().as_bool().unwrap());
469            assert!(!map.get("bool_no").unwrap().as_bool().unwrap());
470        } else {
471            panic!("Expected table");
472        }
473    }
474
475    #[test]
476    fn test_colon_separator() {
477        let content = r#"
478key1:value1
479key2:value2
480        "#;
481
482        let result = parse_ini(content).unwrap();
483        if let Value::Table(map) = result {
484            assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
485            assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
486        } else {
487            panic!("Expected table");
488        }
489    }
490
491    #[test]
492    fn test_error_handling() {
493        // Test unterminated section
494        let content = "[section";
495        assert!(parse_ini(content).is_err());
496
497        // Test invalid key-value
498        let content = "key_without_value";
499        assert!(parse_ini(content).is_err());
500    }
501}