use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use crate::error::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
struct Key {
section: String,
subsection: Option<String>,
name: String,
}
impl Key {
fn new(section: &str, subsection: Option<&str>, name: &str) -> Self {
Key {
section: section.to_ascii_lowercase(),
subsection: subsection.map(|s| s.to_string()),
name: name.to_ascii_lowercase(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Config {
entries: Vec<(Key, String)>,
}
impl Config {
pub fn new() -> Self {
Config::default()
}
pub fn parse(text: &str) -> Result<Self> {
let mut cfg = Config::new();
let mut section: Option<(String, Option<String>)> = None;
for (lineno, raw) in text.lines().enumerate() {
let line = strip_comment(raw).trim();
if line.is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix('[') {
let close = rest.find(']').ok_or_else(|| {
Error::Config(format!("line {}: unclosed section", lineno + 1))
})?;
section = Some(parse_section_header(&rest[..close])?);
continue;
}
let (sec, subsec) = section
.as_ref()
.map(|(s, ss)| (s.as_str(), ss.as_deref()))
.ok_or_else(|| {
Error::Config(format!("line {}: entry before any section", lineno + 1))
})?;
let (name, value) = match line.split_once('=') {
Some((n, v)) => (n.trim(), unquote_value(v.trim())),
None => (line, "true".to_string()), };
cfg.entries.push((Key::new(sec, subsec, name), value));
}
Ok(cfg)
}
pub fn get(&self, section: &str, subsection: Option<&str>, name: &str) -> Option<&str> {
let key = Key::new(section, subsection, name);
self.entries
.iter()
.rev()
.find(|(k, _)| *k == key)
.map(|(_, v)| v.as_str())
}
pub fn get_all(&self, section: &str, subsection: Option<&str>, name: &str) -> Vec<&str> {
let key = Key::new(section, subsection, name);
self.entries
.iter()
.filter(|(k, _)| *k == key)
.map(|(_, v)| v.as_str())
.collect()
}
pub fn get_bool(&self, section: &str, subsection: Option<&str>, name: &str) -> Option<bool> {
self.get(section, subsection, name).map(parse_bool)
}
pub fn set(&mut self, section: &str, subsection: Option<&str>, name: &str, value: &str) {
let key = Key::new(section, subsection, name);
self.entries.retain(|(k, _)| *k != key);
self.entries.push((key, value.to_string()));
}
pub fn serialize(&self) -> String {
let mut out = String::new();
let mut current: Option<(String, Option<String>)> = None;
for (key, value) in &self.entries {
let header = (key.section.clone(), key.subsection.clone());
if current.as_ref() != Some(&header) {
if !out.is_empty() {
out.push('\n');
}
match &key.subsection {
Some(ss) => out.push_str(&format!("[{} \"{}\"]\n", key.section, ss)),
None => out.push_str(&format!("[{}]\n", key.section)),
}
current = Some(header);
}
out.push_str(&format!("\t{} = {}\n", key.name, value));
}
out
}
}
fn parse_bool(v: &str) -> bool {
matches!(v.to_ascii_lowercase().as_str(), "true" | "yes" | "on" | "1")
}
fn strip_comment(line: &str) -> &str {
let bytes = line.as_bytes();
let mut in_quotes = false;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'"' => in_quotes = !in_quotes,
b'#' | b';' if !in_quotes => return &line[..i],
_ => {}
}
}
line
}
fn unquote_value(v: &str) -> String {
if v.len() >= 2 && v.starts_with('"') && v.ends_with('"') {
v[1..v.len() - 1].to_string()
} else {
v.to_string()
}
}
fn parse_section_header(inner: &str) -> Result<(String, Option<String>)> {
let inner = inner.trim();
if let Some(q) = inner.find('"') {
let section = inner[..q].trim().to_ascii_lowercase();
let rest = &inner[q + 1..];
let end = rest
.rfind('"')
.ok_or_else(|| Error::Config("unterminated subsection".to_string()))?;
Ok((section, Some(rest[..end].to_string())))
} else {
Ok((inner.to_ascii_lowercase(), None))
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"
[core]
repositoryformatversion = 0
bare = false
[remote "origin"]
url = https://example.com/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
"#;
#[test]
fn reads_values() {
let cfg = Config::parse(SAMPLE).unwrap();
assert_eq!(cfg.get("core", None, "repositoryformatversion"), Some("0"));
assert_eq!(cfg.get_bool("core", None, "bare"), Some(false));
assert_eq!(
cfg.get("remote", Some("origin"), "url"),
Some("https://example.com/repo.git")
);
}
#[test]
fn section_case_insensitive_subsection_sensitive() {
let cfg = Config::parse("[Core]\n Bare = true\n").unwrap();
assert_eq!(cfg.get_bool("core", None, "bare"), Some(true));
}
#[test]
fn set_and_serialize_roundtrips() {
let mut cfg = Config::new();
cfg.set("user", None, "name", "Alice");
cfg.set("user", None, "email", "alice@example.com");
let text = cfg.serialize();
let back = Config::parse(&text).unwrap();
assert_eq!(back.get("user", None, "name"), Some("Alice"));
assert_eq!(back.get("user", None, "email"), Some("alice@example.com"));
}
}