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