use std::borrow::Cow;
use std::collections::HashMap;
use deser_core::{Error, ErrorKind};
use crate::de::DeserializerConfig;
use crate::{Continuation, InlineComments, Quotes, Syntax};
pub(crate) type Range = (usize, usize);
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum NodeKind {
Table,
Key,
}
pub(crate) struct Node<'a> {
pub(crate) name: Cow<'a, str>,
pub(crate) range: Range,
pub(crate) kind: NodeKind,
pub(crate) values: Vec<(Option<Cow<'a, str>>, Range)>,
pub(crate) children: Vec<usize>,
}
pub(crate) struct Document<'a> {
pub(crate) nodes: Vec<Node<'a>>,
lookup: HashMap<(usize, Cow<'a, str>), usize>,
}
impl<'a> Document<'a> {
fn new(len: usize) -> Document<'a> {
Document {
nodes: vec![Node {
name: Cow::Borrowed(""),
range: (0, len),
kind: NodeKind::Table,
values: Vec::new(),
children: Vec::new(),
}],
lookup: HashMap::new(),
}
}
fn child(
&mut self,
parent: usize,
name: Cow<'a, str>,
range: Range,
kind: NodeKind,
) -> Result<usize, Error> {
let id = (parent, name);
if let Some(&node) = self.lookup.get(&id) {
if self.nodes[node].kind != kind {
return Err(Error::with_offset(
ErrorKind::Syntax,
format!("`{}` is a key and a section", id.1),
range.0,
));
}
return Ok(node);
}
let node = self.nodes.len();
self.nodes.push(Node {
name: id.1.clone(),
range,
kind,
values: Vec::new(),
children: Vec::new(),
});
self.nodes[parent].children.push(node);
self.lookup.insert(id, node);
Ok(node)
}
fn table(&mut self, parent: usize, name: Cow<'a, str>, range: Range) -> Result<usize, Error> {
self.child(parent, name, range, NodeKind::Table)
}
fn value(
&mut self,
table: usize,
key: Cow<'a, str>,
key_range: Range,
value: Option<Cow<'a, str>>,
value_range: Range,
) -> Result<(), Error> {
let node = self.child(table, key, key_range, NodeKind::Key)?;
self.nodes[node].values.push((value, value_range));
Ok(())
}
}
pub(crate) fn parse<'a>(
input: &'a str,
config: &DeserializerConfig,
) -> Result<Document<'a>, Error> {
let start = if input.starts_with('\u{feff}') { 3 } else { 0 };
match config.syntax {
Syntax::Ini => IniParser {
input,
config,
lines: Lines { input, pos: start },
doc: Document::new(input.len()),
}
.parse(),
Syntax::Git => GitParser {
bytes: input.as_bytes(),
pos: start,
eof: false,
doc: Document::new(input.len()),
}
.parse(),
}
}
#[inline]
fn is_ws(c: char) -> bool {
c == ' ' || c == '\t'
}
struct Lines<'a> {
input: &'a str,
pos: usize,
}
impl Iterator for Lines<'_> {
type Item = Range;
fn next(&mut self) -> Option<Range> {
let bytes = self.input.as_bytes();
if self.pos >= bytes.len() {
return None;
}
let start = self.pos;
let end = bytes[start..]
.iter()
.position(|&b| b == b'\n' || b == b'\r')
.map_or(bytes.len(), |pos| start + pos);
self.pos = match bytes.get(end) {
Some(b'\r') if bytes.get(end + 1) == Some(&b'\n') => end + 2,
Some(_) => end + 1,
None => end,
};
Some((start, end))
}
}
struct Pending<'a> {
table: usize,
key: Cow<'a, str>,
key_range: Range,
indent: usize,
value: Option<Cow<'a, str>>,
value_range: Range,
blanks: usize,
continued: bool,
}
enum Bounds {
Plain(usize, usize),
Quoted {
start: usize,
end: usize,
escapes: bool,
},
}
struct IniParser<'a, 'c> {
input: &'a str,
config: &'c DeserializerConfig,
lines: Lines<'a>,
doc: Document<'a>,
}
impl<'a> IniParser<'a, '_> {
fn parse(mut self) -> Result<Document<'a>, Error> {
let mut table = 0;
let mut pending: Option<Pending<'a>> = None;
while let Some((start, end)) = self.lines.next() {
let line = &self.input[start..end];
let trimmed = line.trim_start_matches(is_ws);
let content_start = end - trimmed.len();
let indent = content_start - start;
let content = trimmed.trim_end_matches(is_ws);
if content.is_empty() {
if let Some(ref mut pending) = pending {
pending.blanks += 1;
}
continue;
}
if content.starts_with([';', '#']) {
continue;
}
if self.config.continuation == Continuation::Indented
&& let Some(ref mut pending) = pending
&& indent > pending.indent
{
self.continue_value(pending, content, content_start)?;
continue;
}
if let Some(pending) = pending.take() {
self.finish(pending)?;
}
if content.starts_with('[') {
table = self.section(content, content_start)?;
} else {
pending = Some(self.entry(table, content, content_start, indent)?);
}
}
if let Some(pending) = pending.take() {
self.finish(pending)?;
}
Ok(self.doc)
}
fn finish(&mut self, pending: Pending<'a>) -> Result<(), Error> {
self.doc.value(
pending.table,
pending.key,
pending.key_range,
pending.value,
pending.value_range,
)
}
fn name(&self, name: &'a str) -> Cow<'a, str> {
if self.config.lowercase_names && name.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(name.to_ascii_lowercase())
} else {
Cow::Borrowed(name)
}
}
fn section(&mut self, content: &'a str, start: usize) -> Result<usize, Error> {
let close = content.match_indices(']').map(|(pos, _)| pos).find(|&pos| {
let rest = content[pos + 1..].trim_start_matches(is_ws);
rest.is_empty() || rest.starts_with([';', '#'])
});
let Some(close) = close else {
let msg = if content.contains(']') {
"unexpected text after the section header"
} else {
"missing `]` of the section header"
};
return Err(Error::with_offset(ErrorKind::Syntax, msg, start));
};
let name = self.name(&content[1..close]);
self.doc.table(0, name, (start, start + close + 1))
}
fn entry(
&mut self,
table: usize,
content: &'a str,
start: usize,
indent: usize,
) -> Result<Pending<'a>, Error> {
let colon = self.config.colon_delimiter;
let Some(delimiter) = content.find(|c| c == '=' || (colon && c == ':')) else {
if !self.config.allow_no_value {
return Err(Error::with_offset(
ErrorKind::Syntax,
if colon {
"expected `=` or `:` after the key"
} else {
"expected `=` after the key"
},
start,
));
}
let end = comment_start(content, self.config.inline_comments);
let key = content[..end].trim_end_matches(is_ws);
let key_range = (start, start + key.len());
return Ok(Pending {
table,
key: self.name(key),
key_range,
indent,
value: None,
value_range: (key_range.1, key_range.1),
blanks: 0,
continued: false,
});
};
let key = content[..delimiter].trim_end_matches(is_ws);
if key.is_empty() {
return Err(Error::with_offset(
ErrorKind::Syntax,
"missing key before the delimiter",
start,
));
}
let key_range = (start, start + key.len());
let raw_start = start + delimiter + 1;
let raw = &content[delimiter + 1..];
let (value, value_range) =
if self.config.continuation == Continuation::Backslash && raw.ends_with('\\') {
let mut text = raw[..raw.len() - 1].to_string();
let mut end = start + content.len();
for (line_start, line_end) in self.lines.by_ref() {
let line = self.input[line_start..line_end].trim_end_matches(is_ws);
end = line_start + line.len();
match line.strip_suffix('\\') {
Some(line) => text.push_str(line),
None => {
text.push_str(line);
break;
}
}
}
let bounds = value_bounds(&text, self.config);
let value = value_text(&text, &bounds).into_owned();
(Cow::Owned(value), (raw_start, end))
} else {
let bounds = value_bounds(raw, self.config);
let range = match bounds {
Bounds::Plain(s, e) => (raw_start + s, raw_start + e),
Bounds::Quoted {
start: s, end: e, ..
} => (raw_start + s - 1, raw_start + e + 1),
};
(value_text(raw, &bounds), range)
};
Ok(Pending {
table,
key: self.name(key),
key_range,
indent,
value: Some(value),
value_range,
blanks: 0,
continued: false,
})
}
fn continue_value(
&mut self,
pending: &mut Pending<'a>,
content: &'a str,
start: usize,
) -> Result<(), Error> {
let Some(value) = pending.value.take() else {
return Err(Error::with_offset(
ErrorKind::Syntax,
"a key without value cannot be continued on the next line",
start,
));
};
let text =
content[..comment_start(content, self.config.inline_comments)].trim_end_matches(is_ws);
let mut value = value.into_owned();
if value.is_empty() && !pending.continued {
pending.value_range.0 = start;
} else {
value.extend(std::iter::repeat_n('\n', pending.blanks + 1));
}
value.push_str(text);
pending.value = Some(Cow::Owned(value));
pending.value_range.1 = start + text.len();
pending.blanks = 0;
pending.continued = true;
Ok(())
}
}
pub(crate) fn comment_start(text: &str, mode: InlineComments) -> usize {
let bytes = text.as_bytes();
match mode {
InlineComments::None => bytes.len(),
InlineComments::Anywhere => bytes
.iter()
.position(|&b| b == b';' || b == b'#')
.unwrap_or(bytes.len()),
InlineComments::AfterWhitespace => {
let mut text_before = false;
for (pos, &b) in bytes.iter().enumerate() {
let after_ws = pos > 0 && matches!(bytes[pos - 1], b' ' | b'\t');
if after_ws && (b == b';' || (b == b'#' && text_before)) {
return pos;
}
if b != b' ' && b != b'\t' {
text_before = true;
}
}
bytes.len()
}
}
}
fn value_bounds(raw: &str, config: &DeserializerConfig) -> Bounds {
let lead = raw.len() - raw.trim_start_matches(is_ws).len();
if config.quotes == Quotes::Value
&& let Some(quote @ (b'"' | b'\'')) = raw.as_bytes().get(lead).copied()
{
let bytes = &raw.as_bytes()[lead + 1..];
let mut pos = 0;
let mut escapes = false;
while pos < bytes.len() {
match bytes[pos] {
b'\\' if quote == b'"' && matches!(bytes.get(pos + 1), Some(b'"' | b'\\')) => {
escapes = true;
pos += 2;
continue;
}
b if b == quote => break,
_ => pos += 1,
}
}
if pos < bytes.len() {
let rest = raw[lead + 1 + pos + 1..].trim_start_matches(is_ws);
if rest.is_empty()
|| (config.inline_comments != InlineComments::None && rest.starts_with([';', '#']))
{
return Bounds::Quoted {
start: lead + 1,
end: lead + 1 + pos,
escapes,
};
}
}
}
let end = comment_start(raw, config.inline_comments);
let start = lead.min(end);
Bounds::Plain(start, start + raw[start..end].trim_end_matches(is_ws).len())
}
fn value_text<'t>(raw: &'t str, bounds: &Bounds) -> Cow<'t, str> {
match *bounds {
Bounds::Plain(start, end)
| Bounds::Quoted {
start,
end,
escapes: false,
} => Cow::Borrowed(&raw[start..end]),
Bounds::Quoted { start, end, .. } => {
let mut out = String::with_capacity(end - start);
let mut chars = raw[start..end].chars();
while let Some(c) = chars.next() {
if c == '\\' {
out.extend(chars.next());
} else {
out.push(c);
}
}
Cow::Owned(out)
}
}
}
struct GitParser<'a> {
bytes: &'a [u8],
pos: usize,
eof: bool,
doc: Document<'a>,
}
#[inline]
fn is_git_space(c: u8) -> bool {
matches!(c, b' ' | b'\t' | b'\n' | b'\r')
}
#[cold]
fn bad_line(offset: usize) -> Error {
Error::with_offset(ErrorKind::Syntax, "invalid line in config file", offset)
}
impl<'a> GitParser<'a> {
fn next_char(&mut self) -> u8 {
let Some(&c) = self.bytes.get(self.pos) else {
self.eof = true;
return b'\n';
};
self.pos += 1;
if c == b'\r' && self.bytes.get(self.pos) == Some(&b'\n') {
self.pos += 1;
return b'\n';
}
c
}
fn parse(mut self) -> Result<Document<'a>, Error> {
let mut table = 0;
let mut comment = false;
loop {
let start = self.pos;
let c = self.next_char();
if c == b'\n' {
if self.eof {
return Ok(self.doc);
}
comment = false;
continue;
}
if comment || is_git_space(c) {
continue;
}
if c == b'#' || c == b';' {
comment = true;
continue;
}
if c == b'[' {
table = self.section(start)?;
continue;
}
if !c.is_ascii_alphabetic() {
return Err(bad_line(start));
}
self.entry(table, start)?;
}
}
fn section(&mut self, start: usize) -> Result<usize, Error> {
let mut name = String::new();
loop {
let c = self.next_char();
if self.eof {
return Err(bad_line(start));
}
if c == b']' {
break;
}
if is_git_space(c) {
return self.extended_section(name, c, start);
}
if !c.is_ascii_alphanumeric() && c != b'-' && c != b'.' {
return Err(bad_line(start));
}
name.push(c.to_ascii_lowercase() as char);
}
if name.is_empty() {
return Err(bad_line(start));
}
let range = (start, self.pos);
match name.split_once('.') {
Some((section, subsection)) => {
let section = self.doc.table(0, Cow::Owned(section.to_string()), range)?;
self.doc
.table(section, Cow::Owned(subsection.to_string()), range)
}
None => self.doc.table(0, Cow::Owned(name), range),
}
}
fn extended_section(&mut self, name: String, mut c: u8, start: usize) -> Result<usize, Error> {
loop {
if c == b'\n' {
return Err(bad_line(start));
}
c = self.next_char();
if !is_git_space(c) {
break;
}
}
if c != b'"' || name.is_empty() {
return Err(bad_line(start));
}
let mut subsection = Vec::new();
loop {
let mut c = self.next_char();
if c == b'\n' {
return Err(bad_line(start));
}
if c == b'"' {
break;
}
if c == b'\\' {
c = self.next_char();
if c == b'\n' {
return Err(bad_line(start));
}
}
subsection.push(c);
}
if self.next_char() != b']' {
return Err(bad_line(start));
}
let range = (start, self.pos);
let subsection = String::from_utf8(subsection).map_err(|_| bad_line(start))?;
let section = self.doc.table(0, Cow::Owned(name), range)?;
self.doc.table(section, Cow::Owned(subsection), range)
}
fn entry(&mut self, table: usize, start: usize) -> Result<(), Error> {
let mut key = String::new();
key.push(self.bytes[start].to_ascii_lowercase() as char);
let mut key_end = self.pos;
let mut c;
loop {
c = self.next_char();
if self.eof || !(c.is_ascii_alphanumeric() || c == b'-') {
break;
}
key.push(c.to_ascii_lowercase() as char);
key_end = self.pos;
}
while c == b' ' || c == b'\t' {
c = self.next_char();
}
let (value, value_range) = if c == b'\n' {
(None, (key_end, key_end))
} else if c == b'=' {
let value_start = self.pos;
let value = self.value(start)?;
(Some(Cow::Owned(value)), (value_start, self.pos))
} else {
return Err(bad_line(start));
};
self.doc
.value(table, Cow::Owned(key), (start, key_end), value, value_range)
}
fn value(&mut self, start: usize) -> Result<String, Error> {
let mut out = Vec::new();
let (mut quote, mut comment) = (false, false);
let mut trim: Option<usize> = None;
loop {
let c = self.next_char();
if c == b'\n' {
if quote {
return Err(Error::with_offset(
ErrorKind::Syntax,
"missing closing quote of the value",
start,
));
}
if let Some(trim) = trim {
out.truncate(trim);
}
return String::from_utf8(out).map_err(|_| bad_line(start));
}
if comment {
continue;
}
if is_git_space(c) && !quote {
if !out.is_empty() {
trim.get_or_insert(out.len());
out.push(c);
}
continue;
}
if !quote && (c == b';' || c == b'#') {
comment = true;
continue;
}
trim = None;
match c {
b'\\' => {
let escape_start = self.pos - 1;
out.push(match self.next_char() {
b'\n' => continue,
b't' => b'\t',
b'b' => b'\x08',
b'n' => b'\n',
c @ (b'\\' | b'"') => c,
_ => {
return Err(Error::with_offset(
ErrorKind::Syntax,
"invalid escape sequence in the value",
escape_start,
));
}
});
}
b'"' => quote = !quote,
c => out.push(c),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_comment_start() {
let ws = InlineComments::AfterWhitespace;
assert_eq!(comment_start(" a ; b", ws), 3);
assert_eq!(comment_start(" ; b", ws), 1);
assert_eq!(comment_start(";b", ws), 2);
assert_eq!(comment_start(" a;b", ws), 4);
assert_eq!(comment_start(" #fff", ws), 5);
assert_eq!(comment_start(" a #b", ws), 3);
assert_eq!(comment_start("a#b", ws), 3);
assert_eq!(comment_start(" a;b#c", InlineComments::Anywhere), 2);
assert_eq!(comment_start(" a ; b", InlineComments::None), 6);
}
#[test]
fn test_lines() {
let lines: Vec<_> = Lines {
input: "a\nb\r\nc\rd\n\ne",
pos: 0,
}
.collect();
assert_eq!(lines, [(0, 1), (2, 3), (5, 6), (7, 8), (9, 9), (10, 11)]);
}
}