use std::collections::BTreeMap;
use anyhow::Result;
use crate::config::RuntimeConfig;
use crate::parsers::{ParseResult, structured::infer_value_type_match};
use crate::results::{IniMetadata, IniTypeInfo};
struct IniSyntax {
comment_semicolon: char,
comment_hash: char,
section_start: char,
section_end: char,
key_value_sep: char,
}
impl IniSyntax {
const fn new() -> Self {
Self {
comment_semicolon: ';',
comment_hash: '#',
section_start: '[',
section_end: ']',
key_value_sep: '=',
}
}
fn is_comment(&self, trimmed: &str) -> bool {
trimmed.starts_with(self.comment_semicolon) || trimmed.starts_with(self.comment_hash)
}
fn is_section(&self, trimmed: &str) -> bool {
trimmed.starts_with(self.section_start) && trimmed.ends_with(self.section_end)
}
}
fn is_continuation_line(line: &str) -> bool {
line.starts_with(' ') || line.starts_with('\t')
}
#[inline]
fn commit_multiline_kv(
sections: &mut BTreeMap<String, BTreeMap<String, String>>,
current: &str,
key: &str,
value: &str,
) {
sections
.entry(current.to_string())
.or_default()
.insert(key.to_string(), value.to_string());
}
fn parse_ini_sections_map(
s: &str,
syntax: &IniSyntax,
) -> (
usize,
usize,
usize,
BTreeMap<String, BTreeMap<String, String>>,
) {
let mut section_count = 0usize;
let mut key_count = 0usize;
let mut comment_count = 0usize;
let mut sections: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
let mut current = String::new();
let mut in_multiline = false;
let mut multiline_key = String::new();
let mut multiline_value = String::new();
for line in s.lines() {
if in_multiline {
let trimmed = line.trim();
if trimmed.is_empty() {
commit_multiline_kv(&mut sections, ¤t, &multiline_key, &multiline_value);
in_multiline = false;
continue;
}
if syntax.is_comment(trimmed) {
commit_multiline_kv(&mut sections, ¤t, &multiline_key, &multiline_value);
in_multiline = false;
comment_count += 1;
continue;
}
if syntax.is_section(trimmed) {
commit_multiline_kv(&mut sections, ¤t, &multiline_key, &multiline_value);
in_multiline = false;
section_count += 1;
current = trimmed[1..trimmed.len() - 1].trim().to_string();
continue;
}
if is_continuation_line(line) {
if !multiline_value.is_empty() {
multiline_value.push('\n');
}
multiline_value.push_str(line);
continue;
}
commit_multiline_kv(&mut sections, ¤t, &multiline_key, &multiline_value);
in_multiline = false;
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if syntax.is_comment(trimmed) {
comment_count += 1;
continue;
}
if syntax.is_section(trimmed) {
section_count += 1;
current = trimmed[1..trimmed.len() - 1].trim().to_string();
continue;
}
if let Some(eq) = trimmed.find(syntax.key_value_sep) {
key_count += 1;
let key = trimmed[..eq].trim().to_string();
let value = trimmed[eq + 1..].trim().to_string();
if value.is_empty() {
in_multiline = true;
multiline_key = key;
multiline_value = String::new();
continue;
}
sections
.entry(current.clone())
.or_default()
.insert(key, value);
}
}
if in_multiline {
commit_multiline_kv(&mut sections, ¤t, &multiline_key, &multiline_value);
}
(section_count, key_count, comment_count, sections)
}
fn ini_schema_from_sections(
sections: BTreeMap<String, BTreeMap<String, String>>,
) -> (Option<BTreeMap<String, IniTypeInfo>>, Option<usize>) {
let schema: BTreeMap<String, IniTypeInfo> = sections
.into_iter()
.map(|(sec, kvs)| {
let table = kvs
.into_iter()
.map(|(k, v)| (k, IniTypeInfo::Scalar(infer_value_type_match(&v))))
.collect();
(sec, IniTypeInfo::Table(table))
})
.collect();
let max_depth = if schema.is_empty() { None } else { Some(2) };
let schema_opt = if schema.is_empty() {
None
} else {
Some(schema)
};
(schema_opt, max_depth)
}
pub fn extract_ini_metadata(
content: &[u8],
stats: &ParseResult,
_config: &RuntimeConfig,
) -> Result<IniMetadata> {
let s = std::str::from_utf8(content)
.map_err(|e| anyhow::anyhow!("INI must be valid UTF-8: {e}"))?;
let syntax = IniSyntax::new();
let (section_count, key_count, comment_count, sections) = parse_ini_sections_map(s, &syntax);
let (schema, max_depth) = ini_schema_from_sections(sections);
Ok(IniMetadata {
file_size: Some(stats.byte_count),
section_count: Some(section_count),
key_count: Some(key_count),
comment_count: Some(comment_count),
max_depth,
schema,
})
}
crate::no_template_mining!(extract_ini_templates, "INI: config; no template mining.");