config_lib/parsers/
ini_parser.rs1use crate::error::{Error, Result};
12use crate::value::Value;
13use std::collections::BTreeMap;
14
15pub 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 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 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(); let start = self.position;
104
105 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(); 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(()); }
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(); self.skip_whitespace_and_comments()?;
164
165 let value = self.parse_value()?;
166
167 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, _ => 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 }
208 '\\' if in_quotes => {
209 self.advance(); 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 break;
233 }
234 '\n' | '\r' | '\0' if !in_quotes => break,
235 ';' | '#' if !in_quotes => break, _ => {
237 value_chars.push(ch);
238 self.advance();
239 }
240 }
241 }
242
243 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 let processed_value = if in_quotes {
256 value_str } else {
258 self.process_escape_sequences(&value_str)
259 };
260
261 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 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 if let Ok(int_val) = value.parse::<i64>() {
320 return Ok(Value::Integer(int_val));
321 }
322
323 if let Ok(float_val) = value.parse::<f64>() {
325 return Ok(Value::Float(float_val));
326 }
327
328 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 let content = "[section";
490 assert!(parse_ini(content).is_err());
491
492 let content = "key_without_value";
494 assert!(parse_ini(content).is_err());
495 }
496}