use std::fs;
use std::path::Path;
use super::catalogue::Val;
use super::fact_types::{ConfigValue, ValueObj};
use super::site_types::Parsed;
use super::values::classify;
const UNREADABLE: &str = "unreadable-configuration";
#[derive(Debug, PartialEq)]
enum Json {
Str(String),
Object(Vec<(String, Json)>),
Other,
}
enum Found {
Missing,
Text(String),
Unreadable,
}
struct Reader<'a> {
chars: std::iter::Peekable<std::str::Chars<'a>>,
}
impl Reader<'_> {
fn skip(&mut self) -> Option<()> {
loop {
match self.chars.peek() {
Some(c) if c.is_whitespace() => {
self.chars.next();
}
Some('/') => {
self.chars.next();
match self.chars.next()? {
'/' => while self.chars.next().is_some_and(|c| c != '\n') {},
'*' => self.block_comment()?,
_ => return None,
}
}
_ => return Some(()),
}
}
}
fn block_comment(&mut self) -> Option<()> {
let mut star = false;
loop {
let c = self.chars.next()?;
if star && c == '/' {
return Some(());
}
star = c == '*';
}
}
fn value(&mut self) -> Option<Json> {
self.skip()?;
match *self.chars.peek()? {
'{' => self.object(),
'[' => self.array(),
'"' => self.string().map(Json::Str),
_ => self.scalar(),
}
}
fn object(&mut self) -> Option<Json> {
self.chars.next();
let mut entries = Vec::new();
loop {
self.skip()?;
if self.chars.peek() == Some(&'}') {
self.chars.next();
return Some(Json::Object(entries));
}
let key = self.string()?;
self.skip()?;
if self.chars.next()? != ':' {
return None;
}
entries.push((key, self.value()?));
self.skip()?;
match self.chars.next()? {
',' => {}
'}' => return Some(Json::Object(entries)),
_ => return None,
}
}
}
fn array(&mut self) -> Option<Json> {
self.chars.next();
loop {
self.skip()?;
if self.chars.peek() == Some(&']') {
self.chars.next();
return Some(Json::Other);
}
self.value()?;
self.skip()?;
match self.chars.next()? {
',' => {}
']' => return Some(Json::Other),
_ => return None,
}
}
}
fn string(&mut self) -> Option<String> {
if self.chars.next()? != '"' {
return None;
}
let mut out = String::new();
loop {
match self.chars.next()? {
'"' => return Some(out),
'\\' => out.push(self.escape()?),
c if c < ' ' => return None,
c => out.push(c),
}
}
}
fn escape(&mut self) -> Option<char> {
Some(match self.chars.next()? {
'n' => '\n',
't' => '\t',
'r' => '\r',
'b' => '\u{8}',
'f' => '\u{c}',
c @ ('"' | '\\' | '/') => c,
'u' => {
let unit = self.hex4()?;
if !(0xD800..0xDC00).contains(&unit) {
return char::from_u32(unit);
}
if self.chars.next()? != '\\' || self.chars.next()? != 'u' {
return None;
}
let low = self.hex4()?;
if !(0xDC00..0xE000).contains(&low) {
return None;
}
char::from_u32(0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00))?
}
_ => return None,
})
}
fn hex4(&mut self) -> Option<u32> {
let mut unit = 0;
for _ in 0..4 {
unit = unit * 16 + self.chars.next()?.to_digit(16)?;
}
Some(unit)
}
fn scalar(&mut self) -> Option<Json> {
let mut token = String::new();
while let Some(c) = self
.chars
.peek()
.copied()
.filter(|c| c.is_ascii_alphanumeric() || "+-.".contains(*c))
{
token.push(c);
self.chars.next();
}
(matches!(token.as_str(), "true" | "false" | "null") || is_number(&token))
.then_some(Json::Other)
}
}
fn is_number(token: &str) -> bool {
fn digits(text: &str) -> (usize, &str) {
let n = text.bytes().take_while(u8::is_ascii_digit).count();
(n, &text[n..])
}
let rest = token.strip_prefix('-').unwrap_or(token);
let (n, rest) = digits(rest);
if n == 0 || (n > 1 && token.trim_start_matches('-').starts_with('0')) {
return false;
}
let rest = match rest.strip_prefix('.') {
Some(fraction) => match digits(fraction) {
(0, _) => return false,
(_, rest) => rest,
},
None => rest,
};
let rest = match rest.strip_prefix(['e', 'E']) {
Some(exponent) => {
let exponent = exponent.strip_prefix(['+', '-']).unwrap_or(exponent);
match digits(exponent) {
(0, _) => return false,
(_, rest) => rest,
}
}
None => rest,
};
rest.is_empty()
}
fn parse(text: &str) -> Option<Json> {
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let mut reader = Reader {
chars: text.chars().peekable(),
};
let value = reader.value()?;
reader.skip()?;
reader.chars.peek().is_none().then_some(value)
}
fn lookup(json: &Json, key: &str) -> Found {
let mut node = json;
for part in key.split(':') {
let Json::Object(entries) = node else {
return Found::Missing;
};
let mut matching = entries.iter().filter(|(k, _)| k.eq_ignore_ascii_case(part));
match (matching.next(), matching.next()) {
(None, _) => return Found::Missing,
(Some(_), Some(_)) => return Found::Unreadable,
(Some((_, value)), None) => node = value,
}
}
match node {
Json::Str(s) => Found::Text(s.clone()),
_ => Found::Unreadable,
}
}
fn part(parsed: Parsed, val: Val) -> Option<String> {
match val {
Val::HostName => parsed.host,
Val::VirtualHost => parsed.virtual_host,
Val::Region => parsed.region,
_ => parsed.value,
}
}
fn unknown(reason: &str, key: &str) -> ValueObj {
ValueObj::Unknown {
reason: reason.to_string(),
key: Some(key.to_string()),
}
}
pub(crate) fn configured_value(root: &Path, files: &[String], key: &str, val: Val) -> ValueObj {
let mut values = Vec::new();
for file in files {
let Some(json) = fs::read_to_string(root.join(file))
.ok()
.as_deref()
.and_then(parse)
else {
return unknown(UNREADABLE, key);
};
let text = match lookup(&json, key) {
Found::Missing => continue,
Found::Unreadable => return unknown(UNREADABLE, key),
Found::Text(text) => text,
};
match classify(&text, val).map(|p| part(p, val)) {
Ok(Some(value)) => values.push(ConfigValue {
file: file.clone(),
value,
}),
Ok(None) => return unknown(super::values::UNREAD, key),
Err(reason) => return unknown(reason, key),
}
}
ValueObj::Configuration {
key: key.to_string(),
values,
}
}
#[cfg(test)]
#[path = "configuration_tests.rs"]
mod tests;