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() -> crate::Result<()> {
360 let content = r#"
361[section1]
362key1=value1
363
364[section2]
365key2=value2
366 "#;
367
368 let result = parse_ini(content)?;
369 if let Value::Table(map) = result {
370 let key1 = map
371 .get("section1.key1")
372 .ok_or_else(|| crate::Error::KeyNotFound {
373 key: "section1.key1".to_string(),
374 available: map.keys().cloned().collect(),
375 })?;
376 assert_eq!(key1.as_string()?, "value1");
377
378 let key2 = map
379 .get("section2.key2")
380 .ok_or_else(|| crate::Error::KeyNotFound {
381 key: "section2.key2".to_string(),
382 available: map.keys().cloned().collect(),
383 })?;
384 assert_eq!(key2.as_string()?, "value2");
385 } else {
386 return Err(crate::Error::Parse {
387 message: "Expected table".to_string(),
388 line: 0,
389 column: 0,
390 file: None,
391 });
392 }
393 Ok(())
394 }
395
396 #[test]
397 fn test_comments() {
398 let content = r#"
399; This is a comment
400key1=value1 ; Inline comment
401# Hash comment
402key2=value2
403 "#;
404
405 let result = parse_ini(content).unwrap();
406 if let Value::Table(map) = result {
407 assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
408 assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
409 } else {
410 panic!("Expected table");
411 }
412 }
413
414 #[test]
415 fn test_quoted_values() {
416 let content = r#"
417key1="quoted value"
418key2='single quoted'
419key3="value with spaces"
420 "#;
421
422 let result = parse_ini(content).unwrap();
423 if let Value::Table(map) = result {
424 assert_eq!(
425 map.get("key1").unwrap().as_string().unwrap(),
426 "quoted value"
427 );
428 assert_eq!(
429 map.get("key2").unwrap().as_string().unwrap(),
430 "single quoted"
431 );
432 assert_eq!(
433 map.get("key3").unwrap().as_string().unwrap(),
434 "value with spaces"
435 );
436 } else {
437 panic!("Expected table");
438 }
439 }
440
441 #[test]
442 fn test_escape_sequences() {
443 let content = r#"
444key1="line1\nline2"
445key2="tab\there"
446key3="quote\"here"
447 "#;
448
449 let result = parse_ini(content).unwrap();
450 if let Value::Table(map) = result {
451 assert_eq!(
452 map.get("key1").unwrap().as_string().unwrap(),
453 "line1\nline2"
454 );
455 assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "tab\there");
456 assert_eq!(map.get("key3").unwrap().as_string().unwrap(), "quote\"here");
457 } else {
458 panic!("Expected table");
459 }
460 }
461
462 #[test]
463 fn test_data_types() {
464 let content = r#"
465string_val=hello
466int_val=42
467float_val=1.234
468bool_true=true
469bool_false=false
470bool_yes=yes
471bool_no=no
472 "#;
473
474 let result = parse_ini(content).unwrap();
475 if let Value::Table(map) = result {
476 assert_eq!(map.get("string_val").unwrap().as_string().unwrap(), "hello");
477 assert_eq!(map.get("int_val").unwrap().as_integer().unwrap(), 42);
478 assert_eq!(map.get("float_val").unwrap().as_float().unwrap(), 1.234);
479 assert!(map.get("bool_true").unwrap().as_bool().unwrap());
480 assert!(!map.get("bool_false").unwrap().as_bool().unwrap());
481 assert!(map.get("bool_yes").unwrap().as_bool().unwrap());
482 assert!(!map.get("bool_no").unwrap().as_bool().unwrap());
483 } else {
484 panic!("Expected table");
485 }
486 }
487
488 #[test]
489 fn test_colon_separator() {
490 let content = r#"
491key1:value1
492key2:value2
493 "#;
494
495 let result = parse_ini(content).unwrap();
496 if let Value::Table(map) = result {
497 assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
498 assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
499 } else {
500 panic!("Expected table");
501 }
502 }
503
504 #[test]
505 fn test_error_handling() {
506 let content = "[section";
508 assert!(parse_ini(content).is_err());
509
510 let content = "key_without_value";
512 assert!(parse_ini(content).is_err());
513 }
514}