pub(super) const BYTE_ORDER_MARK: char = '\u{feff}';
pub(super) enum Value<'a> {
String(String),
Strings(Vec<String>),
Bare(&'a str),
}
impl Value<'_> {
pub(super) fn string(self, line: usize) -> Result<String, String> {
match self {
Value::String(value) => Ok(value),
Value::Strings(_) | Value::Bare(_) => {
Err(format!("line {line}: expected a quoted string"))
}
}
}
pub(super) fn strings(self, line: usize) -> Result<Vec<String>, String> {
match self {
Value::Strings(values) => Ok(values),
Value::String(_) | Value::Bare(_) => {
Err(format!("line {line}: expected a string array"))
}
}
}
pub(super) fn integer<T: std::str::FromStr>(self, line: usize) -> Result<T, String> {
let invalid = || format!("line {line}: expected a nonnegative integer");
let Value::Bare(token) = self else {
return Err(invalid());
};
if ["0x", "0o", "0b"].iter().any(|prefix| token.starts_with(prefix)) {
return Err(format!(
"line {line}: hexadecimal, octal, and binary integers are not supported"
));
}
let unsigned = token.strip_prefix('+').unwrap_or(token);
separated_digits(unsigned, false)
.ok_or_else(invalid)?
.parse()
.map_err(|_| format!("line {line}: {token} is out of range for this field"))
}
}
pub(super) fn separated_digits(text: &str, leading_zero: bool) -> Option<String> {
let bytes = text.as_bytes();
let valid = bytes.first().is_some_and(u8::is_ascii_digit)
&& bytes.last().is_some_and(u8::is_ascii_digit)
&& bytes.iter().all(|byte| byte.is_ascii_digit() || *byte == b'_')
&& !text.contains("__")
&& (leading_zero || bytes.len() == 1 || bytes[0] != b'0');
valid.then(|| text.replace('_', ""))
}
pub(super) struct Document<'a> {
source: &'a str,
at: usize,
pub(super) line: usize,
}
impl<'a> Document<'a> {
pub(super) const fn new(source: &'a str) -> Self {
Self { source, at: 0, line: 1 }
}
pub(super) fn peek(&self) -> Option<u8> {
self.source.as_bytes().get(self.at).copied()
}
fn starts_with(&self, text: &str) -> bool {
self.source.as_bytes()[self.at..].starts_with(text.as_bytes())
}
fn skip_whitespace(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t')) {
self.at += 1;
}
}
fn skip_comment(&mut self) {
if self.peek() == Some(b'#') {
while !matches!(self.peek(), None | Some(b'\n')) && !self.starts_with("\r\n") {
self.at += 1;
}
}
}
fn newline(&mut self) -> bool {
let width = if self.peek() == Some(b'\n') {
1
} else if self.starts_with("\r\n") {
2
} else {
return false;
};
self.at += width;
self.line += 1;
true
}
fn skip_blank(&mut self) {
loop {
self.skip_whitespace();
self.skip_comment();
if !self.newline() {
return;
}
}
}
pub(super) fn next_line(&mut self) -> bool {
self.skip_blank();
self.peek().is_some()
}
pub(super) fn end_of_line(&mut self, what: &str) -> Result<(), String> {
self.skip_whitespace();
self.skip_comment();
if self.newline() || self.peek().is_none() {
Ok(())
} else {
Err(format!("line {}: unexpected text after the {what}", self.line))
}
}
pub(super) fn table_header(&mut self, tables: &str) -> Result<&'a str, String> {
let line = self.line;
if !self.starts_with("[[") {
return Err(format!(
"line {line}: single-bracket tables are not supported; use {tables}"
));
}
self.at += 2;
self.skip_whitespace();
let name = self.bare_key(line)?;
self.skip_whitespace();
if self.peek() == Some(b'.') {
return Err(format!("line {line}: dotted keys are not supported"));
}
if !self.starts_with("]]") {
return Err(format!("line {line}: expected ]] to close the table header"));
}
self.at += 2;
Ok(name)
}
fn bare_key(&mut self, line: usize) -> Result<&'a str, String> {
let start = self.at;
while matches!(self.peek(), Some(b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_' | b'-')) {
self.at += 1;
}
if self.at == start {
return Err(match self.peek() {
Some(b'"' | b'\'') => format!("line {line}: quoted keys are not supported"),
_ => format!("line {line}: expected key = value"),
});
}
Ok(&self.source[start..self.at])
}
pub(super) fn key(&mut self) -> Result<&'a str, String> {
let line = self.line;
let key = self.bare_key(line)?;
self.skip_whitespace();
match self.peek() {
Some(b'=') => {
self.at += 1;
self.skip_whitespace();
Ok(key)
}
Some(b'.') => Err(format!("line {line}: dotted keys are not supported")),
_ => Err(format!("line {line}: expected key = value")),
}
}
pub(super) fn value(&mut self) -> Result<Value<'a>, String> {
let line = self.line;
match self.peek() {
Some(b'"' | b'\'') => self.string().map(Value::String),
Some(b'[') => self.string_array().map(Value::Strings),
Some(b'{') => Err(format!("line {line}: inline tables are not supported")),
_ => {
let start = self.at;
while self
.peek()
.is_some_and(|byte| !matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | b'#'))
{
self.at += 1;
}
if self.at == start {
return Err(format!("line {line}: expected a value"));
}
Ok(Value::Bare(&self.source[start..self.at]))
}
}
}
fn string_array(&mut self) -> Result<Vec<String>, String> {
let line = self.line;
self.at += 1;
let mut values = Vec::new();
loop {
self.skip_blank();
match self.peek() {
Some(b']') => {
self.at += 1;
return Ok(values);
}
Some(b'"' | b'\'') => values.push(self.string()?),
Some(b'[') => {
return Err(format!("line {}: nested arrays are not supported", self.line));
}
Some(b'{') => {
return Err(format!("line {}: inline tables are not supported", self.line));
}
None => return Err(format!("line {line}: unterminated array")),
Some(_) => return Err(format!("line {}: expected a string array", self.line)),
}
self.skip_blank();
match self.peek() {
Some(b',') => self.at += 1,
Some(b']') => {
self.at += 1;
return Ok(values);
}
None => return Err(format!("line {line}: unterminated array")),
Some(_) => {
return Err(format!("line {}: expected , or ] in an array", self.line));
}
}
}
}
fn string(&mut self) -> Result<String, String> {
let line = self.line;
let quote = self.peek().expect("a string starts at a quote");
let literal = quote == b'\'';
let multiline = self.starts_with(if literal { "'''" } else { "\"\"\"" });
if multiline {
self.at += 3;
self.newline();
} else {
self.at += 1;
}
let mut value = String::new();
loop {
if self.peek() == Some(quote) {
if !multiline {
self.at += 1;
return Ok(value);
}
let run = self.source.as_bytes()[self.at..]
.iter()
.take_while(|byte| **byte == quote)
.count();
self.at += run;
if run < 3 {
value.extend(std::iter::repeat_n(char::from(quote), run));
continue;
}
if run > 5 {
return Err(format!(
"line {}: too many quotes close a multi-line string",
self.line
));
}
value.extend(std::iter::repeat_n(char::from(quote), run - 3));
return Ok(value);
}
let Some(character) = self.source[self.at..].chars().next() else {
return Err(format!("line {line}: unterminated string"));
};
match character {
'\\' if !literal => self.escape(&mut value, multiline)?,
'\n' | '\r' if multiline => {
if !self.newline() {
return Err(format!(
"line {}: a carriage return must be followed by a line feed",
self.line
));
}
value.push('\n');
}
'\n' | '\r' => {
return Err(format!("line {line}: unterminated string"));
}
'\u{0}'..='\u{8}' | '\u{a}'..='\u{1f}' | '\u{7f}' => {
return Err(format!(
"line {}: control characters in strings must be escaped",
self.line
));
}
_ => {
value.push(character);
self.at += character.len_utf8();
}
}
}
}
fn escape(&mut self, value: &mut String, multiline: bool) -> Result<(), String> {
let line = self.line;
self.at += 1;
let Some(escaped) = self.peek() else {
return Err(format!("line {line}: unterminated string"));
};
let simple = match escaped {
b'b' => Some('\u{8}'),
b't' => Some('\t'),
b'n' => Some('\n'),
b'f' => Some('\u{c}'),
b'r' => Some('\r'),
b'"' => Some('"'),
b'\\' => Some('\\'),
_ => None,
};
if let Some(character) = simple {
value.push(character);
self.at += 1;
return Ok(());
}
match escaped {
b'u' | b'U' => {
let digits = if escaped == b'u' { 4 } else { 8 };
let name = char::from(escaped);
let hex = self
.source
.get(self.at + 1..self.at + 1 + digits)
.filter(|hex| hex.bytes().all(|byte| byte.is_ascii_hexdigit()))
.ok_or_else(|| {
format!("line {line}: \\{name} needs {digits} hexadecimal digits")
})?;
let character =
u32::from_str_radix(hex, 16).ok().and_then(char::from_u32).ok_or_else(
|| format!("line {line}: \\{name}{hex} is not a Unicode scalar value"),
)?;
value.push(character);
self.at += 1 + digits;
}
b' ' | b'\t' | b'\r' | b'\n' if multiline => {
self.skip_whitespace();
if !self.newline() {
return Err(format!(
"line {line}: a backslash followed by whitespace must end the line"
));
}
loop {
self.skip_whitespace();
if !self.newline() {
break;
}
}
}
_ => {
let shown = self.source[self.at..].chars().next().unwrap_or_default();
return Err(format!("line {line}: invalid escape \\{shown}"));
}
}
Ok(())
}
}