pub const CONFIG_TEXT_LIMIT: usize = 1 << 20;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigEntry {
pub key: String,
pub value: Option<Vec<u8>>,
}
struct Cursor<'a> {
bytes: &'a [u8],
at: usize,
}
impl Cursor<'_> {
fn peek(&self) -> Option<u8> {
return self.bytes.get(self.at).copied();
}
fn advance(&mut self) {
self.at += 1;
}
fn skip_line(&mut self) {
while let Some(byte) = self.peek() {
self.advance();
if byte == b'\n' {
return;
}
}
}
}
fn is_name_byte(byte: u8) -> bool {
return byte.is_ascii_alphanumeric() || byte == b'-';
}
fn read_header(cursor: &mut Cursor<'_>) -> Option<String> {
let mut section: String = String::new();
while let Some(byte) = cursor.peek() {
if !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.') {
break;
}
section.push(byte.to_ascii_lowercase() as char);
cursor.advance();
}
if section.is_empty() {
return None;
}
if cursor.peek() == Some(b' ') || cursor.peek() == Some(b'\t') {
while matches!(cursor.peek(), Some(b' ' | b'\t')) {
cursor.advance();
}
if cursor.peek() != Some(b'"') {
return None;
}
cursor.advance();
let mut subsection: Vec<u8> = Vec::new();
loop {
let byte: u8 = cursor.peek()?;
cursor.advance();
match byte {
b'"' => break,
b'\n' => return None,
b'\\' => {
let escaped: u8 = cursor.peek()?;
if escaped == b'\n' {
return None;
}
cursor.advance();
subsection.push(escaped);
}
other => subsection.push(other),
}
}
if cursor.peek() != Some(b']') {
return None;
}
cursor.advance();
if subsection.contains(&0) {
return None;
}
let text: String = String::from_utf8(subsection).ok()?;
return Some(format!("{section}.{text}"));
}
if cursor.peek() != Some(b']') {
return None;
}
cursor.advance();
return Some(section);
}
fn read_value(cursor: &mut Cursor<'_>) -> Option<Vec<u8>> {
let mut value: Vec<u8> = Vec::new();
let mut spaces: usize = 0;
let mut quoted: bool = false;
while let Some(byte) = cursor.peek() {
cursor.advance();
if byte == 0 {
return None;
}
if byte == b'\n' {
if quoted {
return None;
}
return Some(value);
}
if byte.is_ascii_whitespace() && !quoted {
if !value.is_empty() {
spaces += 1;
}
continue;
}
if (byte == b'#' || byte == b';') && !quoted {
cursor.skip_line();
return Some(value);
}
value.extend(std::iter::repeat_n(b' ', spaces));
spaces = 0;
if byte == b'\\' {
let escaped: u8 = cursor.peek()?;
cursor.advance();
match escaped {
b'\n' => {}
b'\r' if cursor.peek() == Some(b'\n') => cursor.advance(),
b'n' => value.push(b'\n'),
b't' => value.push(b'\t'),
b'b' => value.push(8),
b'\\' | b'"' => value.push(escaped),
_ => return None,
}
} else if byte == b'"' {
quoted = !quoted;
} else {
value.push(byte);
}
}
if quoted {
return None;
}
return Some(value);
}
pub fn parse_config_text(text: &[u8]) -> Option<Vec<ConfigEntry>> {
if text.len() > CONFIG_TEXT_LIMIT {
return None;
}
let body: &[u8] = text.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(text);
let mut cursor: Cursor<'_> = Cursor { bytes: body, at: 0 };
let mut prefix: Option<String> = None;
let mut entries: Vec<ConfigEntry> = Vec::new();
while let Some(byte) = cursor.peek() {
if byte.is_ascii_whitespace() {
cursor.advance();
} else if byte == b'#' || byte == b';' {
cursor.skip_line();
} else if byte == b'[' {
cursor.advance();
prefix = Some(read_header(&mut cursor)?);
} else if byte.is_ascii_alphabetic() {
let section: &str = prefix.as_deref()?;
let mut name: String = String::new();
while let Some(next) = cursor.peek() {
if !is_name_byte(next) {
break;
}
name.push(next.to_ascii_lowercase() as char);
cursor.advance();
}
while matches!(cursor.peek(), Some(b' ' | b'\t' | b'\r')) {
cursor.advance();
}
let key: String = format!("{section}.{name}");
match cursor.peek() {
Some(b'=') => {
cursor.advance();
entries.push(ConfigEntry { key, value: Some(read_value(&mut cursor)?) });
}
Some(b'\n') | None => entries.push(ConfigEntry { key, value: None }),
Some(_) => return None,
}
} else {
return None;
}
}
return Some(entries);
}
pub fn last_entry<'a>(entries: &'a [ConfigEntry], key: &str) -> Option<&'a ConfigEntry> {
return entries.iter().rev().find(|entry| { return entry.key == key; });
}
pub fn parse_bool(value: Option<&[u8]>) -> Option<bool> {
let Some(text) = value else {
return Some(true);
};
let lowered: String = String::from_utf8(text.to_vec()).ok()?.to_ascii_lowercase();
return match lowered.as_str() {
"true" | "yes" | "on" | "1" => Some(true),
"false" | "no" | "off" | "0" => Some(false),
_ => None,
};
}
#[cfg(test)]
#[path = "repo_read_config_text_tests.rs"]
mod tests;