use std::ops::Range;
use ini_roundtrip::{Item, Parser};
use super::{Container, Format, FormatError, Leaf, LeafVisitor, Object, Splicer, comment};
#[derive(Debug, Clone, Copy, Default)]
pub struct Ini;
impl Format for Ini {
fn name(&self) -> &str {
"ini"
}
fn extensions(&self) -> &[&str] {
&[
"ini",
"cfg",
"conf",
"cnf",
"inf",
"desktop",
"service",
"editorconfig",
]
}
fn file_names(&self) -> &[&str] {
&[
".npmrc",
".yarnrc",
".gitconfig",
".gitmodules",
".editorconfig",
".pypirc",
".pgpass",
".my.cnf",
".s3cfg",
".boto",
"credentials",
"config",
"setup.cfg",
"tox.ini",
"pip.conf",
]
}
fn rewrite(&self, input: &[u8], visitor: &mut dyn LeafVisitor) -> Result<Vec<u8>, FormatError> {
rewrite_lines(
&LineStyle {
name: "ini",
sections: true,
export_prefix: false,
},
input,
visitor,
)
}
}
pub(super) struct LineStyle {
pub name: &'static str,
pub sections: bool,
pub export_prefix: bool,
}
enum Line<'a> {
Property {
key: &'a str,
value: &'a str,
},
Text(&'a str),
}
struct Section<'a> {
name: Option<&'a str>,
lines: Vec<Line<'a>>,
}
pub(super) fn rewrite_lines(
style: &LineStyle,
input: &[u8],
visitor: &mut dyn LeafVisitor,
) -> Result<Vec<u8>, FormatError> {
let text = std::str::from_utf8(input).map_err(|e| FormatError::new(style.name, e))?;
let mut sections = vec![Section {
name: None,
lines: Vec::new(),
}];
let want_comments = visitor.wants_comments();
let mut comments = Vec::new();
for item in Parser::new(text) {
let line = match item {
Item::Section { name, .. } if style.sections => {
sections.push(Section {
name: Some(name),
lines: Vec::new(),
});
continue;
}
Item::Section { raw, .. } | Item::Error(raw) => Line::Text(raw),
Item::Property {
key,
val: Some(val),
..
} => Line::Property {
key: if style.export_prefix {
key.strip_prefix("export")
.filter(|rest| rest.starts_with([' ', '\t']))
.map_or(key, str::trim_start)
} else {
key
},
value: unquote(val),
},
Item::Property { raw, val: None, .. } => Line::Text(raw),
Item::Comment { raw } => {
if want_comments {
comments.push(locate(text, raw));
}
continue;
}
Item::Blank { .. } | Item::SectionEnd => continue,
};
sections
.last_mut()
.expect("root section exists")
.lines
.push(line);
}
let mut splicer = Splicer::new(input);
comment::visit(text, &comments, visitor, &mut splicer);
let (root, named) = sections.split_first().expect("root section exists");
enter_section(visitor, root);
visit_lines(text, visitor, &mut splicer, &root.lines);
for section in named {
enter_section(visitor, section);
visit_lines(text, visitor, &mut splicer, §ion.lines);
visitor.exit();
}
visitor.exit();
Ok(splicer.finish())
}
fn enter_section(visitor: &mut dyn LeafVisitor, section: &Section<'_>) {
let summary: Object<'_> = section
.lines
.iter()
.filter_map(|line| match line {
Line::Property { key, value } => Some((*key, Some(*value))),
Line::Text(_) => None,
})
.collect();
visitor.enter(section.name, Container::Object(&summary));
}
fn visit_lines(
text: &str,
visitor: &mut dyn LeafVisitor,
splicer: &mut Splicer<'_>,
lines: &[Line<'_>],
) {
for line in lines {
let (key, value) = match line {
Line::Property { key, value } => (Some(*key), *value),
Line::Text(raw) => (None, *raw),
};
if value.trim().is_empty() {
continue;
}
let range = locate(text, value);
let leaf = Leaf::new(value).with_key(key).with_offset(range.start);
if let Some(replacement) = visitor.leaf(&leaf) {
splicer.apply(range.clone(), range, value, &replacement, str::to_owned);
}
}
}
fn locate(text: &str, part: &str) -> Range<usize> {
let start = part.as_ptr() as usize - text.as_ptr() as usize;
start..start + part.len()
}
fn unquote(value: &str) -> &str {
let bytes = value.as_bytes();
match bytes {
[q @ (b'"' | b'\''), .., last] if bytes.len() >= 2 && last == q => {
&value[1..value.len() - 1]
}
_ => value,
}
}