use crate::parser::TOP_LEVEL_KEY;
use crate::scanner::{QuoteKind, Span};
use crate::NcclError;
use std::hash::{Hash, Hasher};
use std::ops::Index;
#[cfg(not(fuzzing))]
use indexmap::IndexMap;
#[cfg(not(fuzzing))]
pub type HashMap<K, V> = IndexMap<K, V, fnv::FnvBuildHasher>;
#[cfg(not(fuzzing))]
pub(crate) fn make_map<K, V>() -> HashMap<K, V> {
HashMap::with_hasher(fnv::FnvBuildHasher::default())
}
#[cfg(fuzzing)]
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
#[cfg(fuzzing)]
pub(crate) fn make_map<K, V>() -> HashMap<K, V> {
HashMap::default()
}
#[derive(Clone, Debug, Eq)]
#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
pub struct Config<'a> {
pub(crate) quotes: Option<QuoteKind>,
pub(crate) key: &'a str,
pub(crate) value: HashMap<&'a str, Config<'a>>,
pub(crate) span: Span,
}
impl PartialEq for Config<'_> {
fn eq(&self, rhs: &Config<'_>) -> bool {
self.quoted() == rhs.quoted() && self.key == rhs.key && self.value == rhs.value
}
}
impl Hash for Config<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.key.hash(state);
}
}
impl<'a> Config<'a> {
pub(crate) fn new(key: &'a str, quotes: Option<QuoteKind>) -> Self {
Config {
quotes,
key,
value: make_map(),
span: Span::default(),
}
}
pub(crate) fn new_with_span(key: &'a str, span: Span, quotes: Option<QuoteKind>) -> Self {
Config {
quotes,
key,
value: make_map(),
span,
}
}
pub(crate) fn add_child(&mut self, child: Config<'a>) {
self.value.insert(child.key, child);
}
pub fn quoted(&self) -> bool {
self.quotes.is_some()
}
pub fn quote_kind(&self) -> Option<QuoteKind> {
self.quotes
}
pub fn has_value(&self, value: &str) -> bool {
self.value.contains_key(value)
}
pub fn children(&self) -> impl Iterator<Item = &Config<'a>> {
self.value.values()
}
pub fn child(&self) -> Option<&Config<'a>> {
self.children().next()
}
pub fn key(&self) -> &'a str {
self.key
}
pub fn span(&self) -> Span {
self.span
}
pub fn values(&self) -> impl Iterator<Item = &str> {
self.value.keys().copied()
}
pub fn value(&self) -> Option<&'a str> {
self.value.iter().next().map(|opt| *opt.0)
}
fn pretty_print(&self) -> String {
self.pp(0)
}
fn pp(&self, indent: usize) -> String {
let mut s = String::new();
if self.key != TOP_LEVEL_KEY && indent != 0 {
for _ in 0..indent - 1 {
s.push_str(" ");
}
if let Some(quote) = self.quotes {
s.push(quote.char());
}
s.push_str(self.key);
if let Some(quote) = self.quotes {
s.push(quote.char());
}
s.push('\n');
}
for (_, v) in self.value.iter() {
s.push_str(&v.pp(indent + 1));
}
s
}
pub fn parse_quoted(&self) -> Result<String, NcclError> {
if !self.quoted() {
Ok(String::from(self.key))
} else {
let mut value = Vec::with_capacity(self.key.len());
let bytes = self.key.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' {
i += 1;
if i >= bytes.len() {
return Err(NcclError::UnterminatedString {
start: self.span.line,
});
}
match bytes[i] {
b'n' => {
value.push(b'\n');
i += 1;
}
b'r' => {
value.push(b'\r');
i += 1;
}
b'\\' => {
value.push(b'\\');
i += 1;
}
code @ (b'"' | b'\'') => {
value.push(code);
i += 1;
}
b'\r' | b'\n' => {
i += 1;
if i >= bytes.len() {
return Err(NcclError::UnterminatedString {
start: self.span.line,
});
}
while bytes[i] == b' ' || bytes[i] == b'\t' {
i += 1;
if i >= bytes.len() {
return Err(NcclError::UnterminatedString {
start: self.span.line,
});
}
}
}
_ => {
return Err(NcclError::ParseUnknownEscape {
escape: bytes[i] as char,
});
}
}
} else {
value.push(bytes[i]);
i += 1;
}
}
Ok(String::from_utf8(value)?)
}
}
}
impl<'a> Index<&str> for Config<'a> {
type Output = Config<'a>;
fn index(&self, index: &str) -> &Self::Output {
&self.value[index]
}
}
impl ToString for Config<'_> {
fn to_string(&self) -> String {
self.pretty_print()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn quoted() {
let s = "hello\\\n world";
assert_eq!(
Config::new(s, Some(QuoteKind::Single))
.parse_quoted()
.unwrap(),
"helloworld"
);
let s = "hello \\\n world";
assert_eq!(
Config::new(s, Some(QuoteKind::Single))
.parse_quoted()
.unwrap(),
"hello world"
);
let s = "hello\\\n\tworld";
assert_eq!(
Config::new(s, Some(QuoteKind::Single))
.parse_quoted()
.unwrap(),
"helloworld"
);
let s = "hello \\\n\tworld";
assert_eq!(
Config::new(s, Some(QuoteKind::Single))
.parse_quoted()
.unwrap(),
"hello world"
);
let s = r#"\"\"\"\""#;
assert_eq!(
Config::new(s, Some(QuoteKind::Single))
.parse_quoted()
.unwrap(),
"\"\"\"\""
);
let s = r#"\'\'\'\'"#;
assert_eq!(
Config::new(s, Some(QuoteKind::Single))
.parse_quoted()
.unwrap(),
"''''"
);
let s = r#"\\\"#;
assert!(dbg!(Config::new(s, Some(QuoteKind::Single)).parse_quoted()).is_err());
let s = "\\\r\t";
assert!(dbg!(Config::new(s, Some(QuoteKind::Single)).parse_quoted()).is_err());
}
#[test]
fn single_file() {
let s = std::fs::read_to_string("examples/config.nccl").unwrap();
let mut c = Config::new(&s[0..3], None);
c.add_child(Config {
quotes: None,
key: &s[3..6],
value: make_map(),
span: Span::default(),
});
assert_eq!(
c,
Config {
quotes: None,
key: "ser",
span: Span::default(),
value: {
let mut map = make_map();
map.insert("ver", Config::new("ver", None));
map
}
}
)
}
#[test]
fn multi_file() {
let s1 = std::fs::read_to_string("examples/config.nccl").unwrap();
let mut c = Config::new(&s1[0..3], None);
let s2 = std::fs::read_to_string("examples/config_dos.nccl").unwrap();
c.add_child(Config {
quotes: None,
key: &s2[3..6],
value: make_map(),
span: Span::default(),
});
assert_eq!(
c,
Config {
quotes: None,
key: "ser",
span: Span::default(),
value: {
let mut map = make_map();
map.insert("ver", Config::new("ver", None));
map
}
}
)
}
#[test]
fn to_string() {
let orig_source = std::fs::read_to_string("examples/all-of-em.nccl").unwrap();
println!("orig\n{}", orig_source);
let orig_config = crate::parse_config(&orig_source).unwrap();
println!("{:#?}\n\n\n", orig_config);
let new_source = orig_config.to_string();
println!("new\n{}", new_source);
let new_config = crate::parse_config(&new_source).unwrap();
println!("{:#?}\n\n\n", new_config);
assert_eq!(new_config, orig_config);
}
#[test]
fn key() {
let source = "key\n value\n";
let config = crate::parse_config(&source).unwrap();
assert_eq!(config["key"].key(), "key");
assert_eq!(config.key(), TOP_LEVEL_KEY);
let orig_source = std::fs::read_to_string("examples/all-of-em.nccl").unwrap();
let orig_config = crate::parse_config(&orig_source).unwrap();
assert_eq!(orig_config["h"]["k"].key(), "k");
}
}