use crate::error::{Error, Result};
use crate::value::Value;
use std::collections::BTreeMap;
pub fn parse(source: &str) -> Result<Value> {
parse_ini(source)
}
pub fn parse_ini(content: &str) -> Result<Value> {
let mut parser = IniParser::new(content);
parser.parse()
}
struct IniParser<'a> {
content: &'a str,
position: usize,
line: usize,
current_section: Option<String>,
result: BTreeMap<String, Value>,
}
impl<'a> IniParser<'a> {
fn new(content: &'a str) -> Self {
Self {
content,
position: 0,
line: 1,
current_section: None,
result: BTreeMap::new(),
}
}
fn parse(&mut self) -> Result<Value> {
while self.position < self.content.len() {
self.skip_whitespace_and_comments()?;
if self.position >= self.content.len() {
break;
}
let ch = self.current_char();
match ch {
'[' => self.parse_section()?,
'\n' | '\r' => {
self.advance();
self.line += 1;
}
_ => self.parse_key_value()?,
}
}
Ok(Value::Table(self.result.clone()))
}
fn current_char(&self) -> char {
self.content.chars().nth(self.position).unwrap_or('\0')
}
fn advance(&mut self) {
if self.position < self.content.len() {
self.position += 1;
}
}
fn skip_whitespace_and_comments(&mut self) -> Result<()> {
loop {
let ch = self.current_char();
match ch {
' ' | '\t' => self.advance(),
';' | '#' => {
while self.current_char() != '\n' && self.current_char() != '\0' {
self.advance();
}
}
'\n' | '\r' => {
self.advance();
self.line += 1;
}
'\0' => break,
_ => break,
}
}
Ok(())
}
fn parse_section(&mut self) -> Result<()> {
self.advance(); let start = self.position;
while self.current_char() != ']' && self.current_char() != '\0' {
if self.current_char() == '\n' {
return Err(Error::Parse {
message: "Unterminated section".to_string(),
line: self.line,
column: 1,
file: None,
});
}
self.advance();
}
if self.current_char() != ']' {
return Err(Error::Parse {
message: "Missing closing bracket for section".to_string(),
line: self.line,
column: 1,
file: None,
});
}
let section_name = self.content[start..self.position].trim().to_string();
self.advance();
if section_name.is_empty() {
return Err(Error::Parse {
message: "Empty section name".to_string(),
line: self.line,
column: 1,
file: None,
});
}
self.current_section = Some(section_name);
Ok(())
}
fn parse_key_value(&mut self) -> Result<()> {
let key = self.parse_key()?;
if key.is_empty() {
return Ok(()); }
self.skip_whitespace_and_comments()?;
let ch = self.current_char();
if ch != '=' && ch != ':' {
return Err(Error::Parse {
message: format!("Expected '=' or ':' after key '{key}'"),
line: self.line,
column: 1,
file: None,
});
}
self.advance(); self.skip_whitespace_and_comments()?;
let value = self.parse_value()?;
let full_key = match &self.current_section {
Some(section) => format!("{section}.{key}"),
None => key,
};
self.result.insert(full_key, value);
Ok(())
}
fn parse_key(&mut self) -> Result<String> {
let start = self.position;
while self.position < self.content.len() {
let ch = self.current_char();
match ch {
'=' | ':' | '\n' | '\r' | '\0' => break,
';' | '#' => break, _ => self.advance(),
}
}
let key = self.content[start..self.position].trim();
Ok(key.to_string())
}
fn parse_value(&mut self) -> Result<Value> {
let mut value_chars = Vec::new();
let mut in_quotes = false;
let mut quote_char = '\0';
while self.position < self.content.len() {
let ch = self.current_char();
match ch {
'"' | '\'' if !in_quotes => {
in_quotes = true;
quote_char = ch;
self.advance();
}
'\\' if in_quotes => {
self.advance(); if self.position < self.content.len() {
let escaped_char = self.current_char();
match escaped_char {
'n' => value_chars.push('\n'),
't' => value_chars.push('\t'),
'r' => value_chars.push('\r'),
'\\' => value_chars.push('\\'),
'"' => value_chars.push('"'),
'\'' => value_chars.push('\''),
_ => {
value_chars.push('\\');
value_chars.push(escaped_char);
}
}
self.advance();
}
}
ch if in_quotes && ch == quote_char => {
in_quotes = false;
self.advance();
break;
}
'\n' | '\r' | '\0' if !in_quotes => break,
';' | '#' if !in_quotes => break, _ => {
value_chars.push(ch);
self.advance();
}
}
}
let value_str = if !in_quotes {
value_chars
.iter()
.collect::<String>()
.trim_end()
.to_string()
} else {
value_chars.iter().collect::<String>()
};
let processed_value = if in_quotes {
value_str } else {
self.process_escape_sequences(&value_str)
};
self.parse_typed_value(&processed_value)
}
fn process_escape_sequences(&self, value: &str) -> String {
let mut result = String::new();
let mut chars = value.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
match chars.peek() {
Some('n') => {
chars.next();
result.push('\n');
}
Some('t') => {
chars.next();
result.push('\t');
}
Some('r') => {
chars.next();
result.push('\r');
}
Some('\\') => {
chars.next();
result.push('\\');
}
Some('"') => {
chars.next();
result.push('"');
}
Some('\'') => {
chars.next();
result.push('\'');
}
_ => result.push(ch),
}
} else {
result.push(ch);
}
}
result
}
fn parse_typed_value(&self, value: &str) -> Result<Value> {
if value.is_empty() {
return Ok(Value::String(String::new()));
}
match value.to_lowercase().as_str() {
"true" | "yes" | "on" | "1" => return Ok(Value::Bool(true)),
"false" | "no" | "off" | "0" => return Ok(Value::Bool(false)),
_ => {}
}
if let Ok(int_val) = value.parse::<i64>() {
return Ok(Value::Integer(int_val));
}
if let Ok(float_val) = value.parse::<f64>() {
return Ok(Value::Float(float_val));
}
Ok(Value::String(value.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_ini() {
let content = r#"
key1=value1
key2=value2
"#;
let result = parse_ini(content).unwrap();
if let Value::Table(map) = result {
assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
} else {
panic!("Expected table");
}
}
#[test]
fn test_sections() -> crate::Result<()> {
let content = r#"
[section1]
key1=value1
[section2]
key2=value2
"#;
let result = parse_ini(content)?;
if let Value::Table(map) = result {
let key1 = map
.get("section1.key1")
.ok_or_else(|| crate::Error::KeyNotFound {
key: "section1.key1".to_string(),
available: map.keys().cloned().collect(),
})?;
assert_eq!(key1.as_string()?, "value1");
let key2 = map
.get("section2.key2")
.ok_or_else(|| crate::Error::KeyNotFound {
key: "section2.key2".to_string(),
available: map.keys().cloned().collect(),
})?;
assert_eq!(key2.as_string()?, "value2");
} else {
return Err(crate::Error::Parse {
message: "Expected table".to_string(),
line: 0,
column: 0,
file: None,
});
}
Ok(())
}
#[test]
fn test_comments() {
let content = r#"
; This is a comment
key1=value1 ; Inline comment
# Hash comment
key2=value2
"#;
let result = parse_ini(content).unwrap();
if let Value::Table(map) = result {
assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
} else {
panic!("Expected table");
}
}
#[test]
fn test_quoted_values() {
let content = r#"
key1="quoted value"
key2='single quoted'
key3="value with spaces"
"#;
let result = parse_ini(content).unwrap();
if let Value::Table(map) = result {
assert_eq!(
map.get("key1").unwrap().as_string().unwrap(),
"quoted value"
);
assert_eq!(
map.get("key2").unwrap().as_string().unwrap(),
"single quoted"
);
assert_eq!(
map.get("key3").unwrap().as_string().unwrap(),
"value with spaces"
);
} else {
panic!("Expected table");
}
}
#[test]
fn test_escape_sequences() {
let content = r#"
key1="line1\nline2"
key2="tab\there"
key3="quote\"here"
"#;
let result = parse_ini(content).unwrap();
if let Value::Table(map) = result {
assert_eq!(
map.get("key1").unwrap().as_string().unwrap(),
"line1\nline2"
);
assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "tab\there");
assert_eq!(map.get("key3").unwrap().as_string().unwrap(), "quote\"here");
} else {
panic!("Expected table");
}
}
#[test]
fn test_data_types() {
let content = r#"
string_val=hello
int_val=42
float_val=1.234
bool_true=true
bool_false=false
bool_yes=yes
bool_no=no
"#;
let result = parse_ini(content).unwrap();
if let Value::Table(map) = result {
assert_eq!(map.get("string_val").unwrap().as_string().unwrap(), "hello");
assert_eq!(map.get("int_val").unwrap().as_integer().unwrap(), 42);
assert_eq!(map.get("float_val").unwrap().as_float().unwrap(), 1.234);
assert!(map.get("bool_true").unwrap().as_bool().unwrap());
assert!(!map.get("bool_false").unwrap().as_bool().unwrap());
assert!(map.get("bool_yes").unwrap().as_bool().unwrap());
assert!(!map.get("bool_no").unwrap().as_bool().unwrap());
} else {
panic!("Expected table");
}
}
#[test]
fn test_colon_separator() {
let content = r#"
key1:value1
key2:value2
"#;
let result = parse_ini(content).unwrap();
if let Value::Table(map) = result {
assert_eq!(map.get("key1").unwrap().as_string().unwrap(), "value1");
assert_eq!(map.get("key2").unwrap().as_string().unwrap(), "value2");
} else {
panic!("Expected table");
}
}
#[test]
fn test_error_handling() {
let content = "[section";
assert!(parse_ini(content).is_err());
let content = "key_without_value";
assert!(parse_ini(content).is_err());
}
}