config_lib/parsers/
ini_parser.rs1use crate::error::{Error, Result};
12use crate::value::Value;
13use std::collections::BTreeMap;
14
15pub fn parse(source: &str) -> Result<Value> {
17 parse_ini(source)
18}
19
20pub 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 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 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(); let start = self.position;
109
110 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(); 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(()); }
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(); self.skip_whitespace_and_comments()?;
169
170 let value = self.parse_value()?;
171
172 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, _ => 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 }
213 '\\' if in_quotes => {
214 self.advance(); 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 break;
238 }
239 '\n' | '\r' | '\0' if !in_quotes => break,
240 ';' | '#' if !in_quotes => break, _ => {
242 value_chars.push(ch);
243 self.advance();
244 }
245 }
246 }
247
248 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 let processed_value = if in_quotes {
261 value_str } else {
263 self.process_escape_sequences(&value_str)
264 };
265
266 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 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 if let Ok(int_val) = value.parse::<i64>() {
325 return Ok(Value::Integer(int_val));
326 }
327
328 if let Ok(float_val) = value.parse::<f64>() {
330 return Ok(Value::Float(float_val));
331 }
332
333 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 let content = "[section";
495 assert!(parse_ini(content).is_err());
496
497 let content = "key_without_value";
499 assert!(parse_ini(content).is_err());
500 }
501}