pub mod error;
pub mod model;
pub mod options;
pub mod reader;
#[cfg(feature = "parse")]
mod parser;
#[cfg(feature = "printer")]
pub mod printer;
#[cfg(feature = "serde-deserialize")]
pub mod de;
#[cfg(feature = "serde-serialize")]
pub mod ser;
#[cfg(feature = "document")]
pub mod document;
#[cfg(feature = "document")]
pub use document::{edit_str, load_document, update_str, Document};
pub use error::{Error, Result};
pub use model::{BoolOptions, CclObject, Entry, ListOptions};
pub use reader::CclReader;
pub use options::{CrlfBehavior, DelimiterStrategy, ParserOptions, SpacingBehavior, TabBehavior};
#[cfg(feature = "printer")]
pub use printer::{print, round_trip, CclPrinter, PrinterConfig};
#[cfg(feature = "parse")]
pub fn parse(input: &str) -> Result<Vec<Entry>> {
parse_with_options_internal(input, &ParserOptions::default())
}
#[cfg(all(feature = "parse", feature = "unstable"))]
pub fn parse_with_options(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
parse_with_options_internal(input, options)
}
#[cfg(feature = "parse")]
fn parse_with_options_internal(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
parser::parse_to_entries(input, options)
}
#[cfg(feature = "hierarchy")]
pub fn build_hierarchy(entries: &[Entry]) -> Result<CclObject> {
let mut map: indexmap::IndexMap<String, Vec<String>> = indexmap::IndexMap::new();
for entry in entries {
map.entry(entry.key.clone())
.or_default()
.push(entry.value.clone());
}
build_model(map)
}
#[cfg(feature = "hierarchy")]
fn is_valid_ccl_key(key: &str) -> bool {
if key.is_empty() {
return true; }
if key.starts_with('-') {
return false;
}
if key.contains(" = ") || key.contains(" =\t") {
return false;
}
true
}
#[cfg(feature = "hierarchy")]
const MAX_RECURSION_DEPTH: usize = 64;
#[cfg(feature = "hierarchy")]
fn build_model(map: indexmap::IndexMap<String, Vec<String>>) -> Result<CclObject> {
build_model_with_depth(map, 0)
}
#[cfg(feature = "hierarchy")]
fn build_model_with_depth(
map: indexmap::IndexMap<String, Vec<String>>,
depth: usize,
) -> Result<CclObject> {
if depth > MAX_RECURSION_DEPTH {
return Err(Error::ParseError(format!(
"maximum recursion depth ({MAX_RECURSION_DEPTH}) exceeded"
)));
}
let mut result = indexmap::IndexMap::new();
for (key, values) in map {
#[cfg(feature = "reference_compliant")]
let values = {
let mut v = values;
if v.len() > 1 && !key.is_empty() {
v.sort();
}
v
};
let mut nested_values = Vec::new();
for value in values {
if value.contains('\n') && value.contains('=') {
match load_with_depth(&value, depth + 1) {
Ok(parsed) => {
if !parsed.is_empty() {
let has_valid_keys = parsed.keys().all(|k| is_valid_ccl_key(k));
if has_valid_keys {
nested_values.push(parsed);
} else {
nested_values.push(CclObject::from_string(value));
}
} else {
nested_values.push(CclObject::from_string(value));
}
}
Err(Error::ParseError(ref msg)) if msg.contains("recursion depth") => {
return Err(Error::ParseError(msg.clone()));
}
Err(_) => {
nested_values.push(CclObject::from_string(value));
}
}
} else if value.starts_with(' ') || value.starts_with('\t') {
let trimmed = value.trim();
if !trimmed.is_empty() && !trimmed.contains('=') {
let child = CclObject::from_string(String::new());
let mut parent_map = indexmap::IndexMap::new();
parent_map.insert(trimmed.to_string(), vec![child]);
nested_values.push(CclObject::from_map(parent_map));
} else {
nested_values.push(CclObject::from_string(value));
}
} else {
nested_values.push(CclObject::from_string(value));
}
}
if !key.is_empty() && nested_values.len() > 1 {
let all_simple_strings = nested_values.iter().all(|obj| {
obj.len() == 1 && obj.iter().next().is_some_and(|(_, v)| v.is_empty())
});
if all_simple_strings {
result.insert(key, nested_values);
} else {
let composed = nested_values
.iter()
.fold(CclObject::new(), |acc, obj| acc.compose(obj));
result.insert(key, vec![composed]);
}
} else {
result.insert(key, nested_values);
}
}
Ok(CclObject::from_map(result))
}
#[cfg(feature = "parse")]
pub fn parse_indented(input: &str) -> Result<Vec<Entry>> {
parse_indented_with_options_internal(input, &ParserOptions::default())
}
#[cfg(all(feature = "parse", feature = "unstable"))]
pub fn parse_indented_with_options(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
parse_indented_with_options_internal(input, options)
}
#[cfg(feature = "parse")]
fn parse_indented_with_options_internal(
input: &str,
options: &ParserOptions,
) -> Result<Vec<Entry>> {
let min_indent = input
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| line.len() - line.trim_start().len())
.min()
.unwrap_or(0);
let dedented = input
.lines()
.map(|line| {
let for_dedent = line.replace('\t', " ");
if for_dedent.trim().is_empty() {
options.process_tabs(line).into_owned()
} else if for_dedent.len() > min_indent {
if options.preserve_tabs() {
if line.len() > min_indent {
line[min_indent..].to_string()
} else {
line.trim_start().to_string()
}
} else {
for_dedent[min_indent..].to_string()
}
} else if options.preserve_tabs() {
line.trim_start().to_string()
} else {
for_dedent.trim_start().to_string()
}
})
.collect::<Vec<_>>()
.join("\n");
let entries_at_min_indent = dedented
.lines()
.filter(|line| {
let indent = line.len() - line.trim_start().len();
indent == 0 && line.trim().contains('=')
})
.count();
if entries_at_min_indent > 1 {
parse_flat_entries(&dedented, options)
} else {
parse_single_entry_with_raw_value(&dedented, options)
}
}
#[cfg(feature = "parse")]
fn parse_flat_entries(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
let mut entries = Vec::new();
for line in input.lines() {
if line.trim().is_empty() {
continue;
}
let indent = line.len() - line.trim_start().len();
let trimmed = line.trim();
if trimmed.contains('=') {
if let Some(eq_pos) = trimmed.find('=') {
let key = trimmed[..eq_pos].trim().to_string();
let value_raw = &trimmed[eq_pos + 1..];
let value = if options.is_strict_spacing() {
value_raw
.trim_start_matches(' ')
.trim_end_matches(' ')
.to_string()
} else {
value_raw.trim().to_string()
};
entries.push(Entry::new(key, value));
}
} else if indent > 0 && !entries.is_empty() {
let last = entries.last().unwrap();
if !last.value.is_empty() {
let last: &mut Entry = entries.last_mut().unwrap();
last.value.push('\n');
last.value.push_str(line);
} else {
entries.push(Entry::new(trimmed.to_string(), String::new()));
}
} else if !entries.is_empty() {
let last = entries.last().unwrap();
if last.key.is_empty() {
let last: &mut Entry = entries.last_mut().unwrap();
last.value.push('\n');
last.value.push_str(trimmed);
} else {
entries.push(Entry::new(trimmed.to_string(), String::new()));
}
} else {
entries.push(Entry::new(trimmed.to_string(), String::new()));
}
}
Ok(entries)
}
#[cfg(feature = "parse")]
fn parse_single_entry_with_raw_value(input: &str, options: &ParserOptions) -> Result<Vec<Entry>> {
let mut lines = input.lines();
let first_line = lines.next().unwrap_or("");
if let Some(eq_pos) = first_line.find('=') {
let key = first_line[..eq_pos].trim().to_string();
let first_value = first_line[eq_pos + 1..].trim_start().to_string();
let remaining_lines: Vec<&str> = lines.collect();
let value = if !remaining_lines.is_empty() {
let joined = if first_value.trim().is_empty() {
"\n".to_string() + &remaining_lines.join("\n")
} else {
first_value + "\n" + &remaining_lines.join("\n")
};
options.process_tabs(&joined).into_owned()
} else {
first_value
};
Ok(vec![Entry::new(key, value)])
} else {
Ok(vec![Entry::new(
first_line.trim().to_string(),
String::new(),
)])
}
}
#[cfg(feature = "hierarchy")]
pub fn load(input: &str) -> Result<CclObject> {
load_with_options_internal(input, &ParserOptions::default())
}
#[cfg(all(feature = "hierarchy", feature = "unstable"))]
pub fn load_with_options(input: &str, options: &ParserOptions) -> Result<CclObject> {
load_with_options_internal(input, options)
}
#[cfg(feature = "hierarchy")]
fn load_with_options_internal(input: &str, options: &ParserOptions) -> Result<CclObject> {
let entries = parse_with_options_internal(input, options)?;
build_hierarchy(&entries)
}
#[cfg(feature = "hierarchy")]
fn load_with_depth(input: &str, depth: usize) -> Result<CclObject> {
let entries = parse_with_options_internal(input, &ParserOptions::default())?;
let mut map: indexmap::IndexMap<String, Vec<String>> = indexmap::IndexMap::new();
for entry in &entries {
map.entry(entry.key.clone())
.or_default()
.push(entry.value.clone());
}
build_model_with_depth(map, depth)
}
#[cfg(feature = "serde-deserialize")]
pub use de::{from_str, from_str_with_options};
#[cfg(feature = "serde-serialize")]
pub use ser::{to_string, to_string_with_config};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_recursion_depth_limit() {
let mut input = String::from("a = b");
for _ in 0..=MAX_RECURSION_DEPTH {
let indented: String = input.lines().map(|l| format!(" {l}\n")).collect();
input = format!("outer =\n{indented}");
}
let result = load(&input);
assert!(
result.is_err(),
"expected a recursion depth error, got: {result:?}"
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("recursion depth"),
"error should mention recursion depth, got: {err}"
);
}
#[test]
fn test_normal_nesting_still_works() {
let input = "outer =\n inner = value";
let result = load(input);
assert!(
result.is_ok(),
"moderate nesting should succeed: {result:?}"
);
}
}