mod builder;
mod comments;
mod core;
mod error;
pub(crate) mod facts;
mod glob;
mod include;
mod inputs;
mod loader;
mod macros;
mod number;
mod registered;
mod string;
pub(crate) mod tree;
mod vars;
pub use builder::ParserBuilder;
pub use error::{Error, ErrorKind};
pub use facts::{OutputFacts, ValueFacts};
pub use inputs::{Input, Inputs};
#[cfg(feature = "fs")]
pub use loader::FsLoader;
pub use loader::{FileKind, Loader, MemoryLoader};
pub use registered::{MacroCall, MacroError, MacroHandler};
use crate::error::Position;
use crate::value::{DuplicateStrategy, ParserFlags, UclValue};
use indexmap::IndexMap;
use std::cell::{Cell, OnceCell};
use std::fmt;
use std::path::{Path, PathBuf};
pub const MAX_NESTING: usize = 1024;
pub const MAX_INCLUDE_DEPTH: usize = 16;
pub const MAX_ARGUMENT_DEPTH: usize = 64;
pub const DEFAULT_INHERIT_DEPTH_LIMIT: usize = MAX_NESTING;
pub const MAX_INHERIT_DEPTH_LIMIT: usize = 2048;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comment {
pub text: String,
pub position: Position,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PathSegment {
Key { key: String, index: usize },
Index(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommentPlacement {
Before,
After,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachedComments {
pub path: Vec<PathSegment>,
pub placement: CommentPlacement,
pub comments: Vec<usize>,
}
pub type VariableHandler = dyn FnMut(&str) -> Option<String>;
pub struct Parser {
flags: ParserFlags,
priority: u8,
strategy: DuplicateStrategy,
variables: IndexMap<String, String>,
handler: Option<Box<VariableHandler>>,
loader: Box<dyn Loader>,
base_dir: Option<PathBuf>,
search_path: Option<Vec<String>>,
max_input_bytes: Option<u64>,
inherit_depth_limit: usize,
comments: Vec<Comment>,
attached: comments::CommentGroups,
attached_paths: OnceCell<Vec<AttachedComments>>,
facts: OutputFacts,
handler_answers: Vec<Option<String>>,
macros: registered::MacroTable,
uncertain: Cell<u8>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Uncertain {
ReopenedNotObject,
EndedUnitContainer,
ClosedArrayElement,
TrailingSlash,
}
impl Uncertain {
const ALL: [Uncertain; 4] = [
Uncertain::ReopenedNotObject,
Uncertain::EndedUnitContainer,
Uncertain::ClosedArrayElement,
Uncertain::TrailingSlash,
];
fn bit(self) -> u8 {
1 << self as u8
}
}
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Parser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Parser")
.field("flags", &self.flags)
.field("priority", &self.priority)
.field("strategy", &self.strategy)
.field("variables", &self.variables)
.field("handler", &self.handler.is_some())
.field("base_dir", &self.base_dir)
.field("search_path", &self.search_path)
.field("max_input_bytes", &self.max_input_bytes)
.field("inherit_depth_limit", &self.inherit_depth_limit)
.field("comments", &self.comments.len())
.field("attached", &self.attached.len())
.field("macros", &self.macros.names())
.finish()
}
}
impl Parser {
pub fn new() -> Self {
let loader: Box<dyn Loader> = Box::new(MemoryLoader::new());
Self {
flags: ParserFlags::DEFAULT,
priority: 0,
strategy: DuplicateStrategy::Append,
variables: IndexMap::new(),
handler: None,
loader,
base_dir: None,
search_path: None,
max_input_bytes: None,
inherit_depth_limit: DEFAULT_INHERIT_DEPTH_LIMIT,
comments: Vec::new(),
attached: comments::CommentGroups::default(),
attached_paths: OnceCell::new(),
facts: OutputFacts::new(),
handler_answers: Vec::new(),
macros: registered::MacroTable::default(),
uncertain: Cell::new(0),
}
}
pub fn builder() -> ParserBuilder {
ParserBuilder::new()
}
pub fn with_flags(flags: ParserFlags) -> Self {
let mut parser = Self::new();
parser.flags = flags;
parser
}
pub fn flags(&self) -> ParserFlags {
self.flags
}
pub fn set_flags(&mut self, flags: ParserFlags) -> &mut Self {
self.flags = flags;
self
}
pub fn set_priority(&mut self, priority: u8) -> &mut Self {
self.priority = priority & crate::value::MAX_PRIORITY;
self
}
pub fn set_strategy(&mut self, strategy: DuplicateStrategy) -> &mut Self {
self.strategy = strategy;
self
}
pub fn register_variable(
&mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> &mut Self {
self.variables.insert(name.into(), value.into());
self
}
pub fn set_variable_handler(
&mut self,
handler: impl FnMut(&str) -> Option<String> + 'static,
) -> &mut Self {
self.handler = Some(Box::new(handler));
self
}
pub fn register_macro(
&mut self,
name: impl Into<String>,
handler: impl Fn(&mut MacroCall<'_>) -> Result<(), MacroError> + 'static,
) -> &mut Self {
self.macros.insert(
name.into(),
registered::Registered {
handler: std::rc::Rc::new(handler),
context: false,
},
);
self
}
pub fn register_context_macro(
&mut self,
name: impl Into<String>,
handler: impl Fn(&mut MacroCall<'_>) -> Result<(), MacroError> + 'static,
) -> &mut Self {
self.macros.insert(
name.into(),
registered::Registered {
handler: std::rc::Rc::new(handler),
context: true,
},
);
self
}
pub fn set_loader(&mut self, loader: impl Loader + 'static) -> &mut Self {
self.loader = Box::new(loader);
self
}
pub fn set_base_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
self.base_dir = Some(dir.into());
self
}
pub fn base_dir(&self) -> Option<&Path> {
self.base_dir.as_deref()
}
pub fn set_search_path<I, S>(&mut self, dirs: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.search_path = Some(dirs.into_iter().map(Into::into).collect());
self
}
pub fn clear_search_path(&mut self) -> &mut Self {
self.search_path = None;
self
}
pub fn search_path(&self) -> Option<&[String]> {
self.search_path.as_deref()
}
pub fn set_max_input_bytes(&mut self, limit: Option<u64>) -> &mut Self {
self.max_input_bytes = limit;
self
}
pub fn max_input_bytes(&self) -> Option<u64> {
self.max_input_bytes
}
pub fn set_inherit_depth_limit(&mut self, limit: usize) -> &mut Self {
assert!(
limit <= MAX_INHERIT_DEPTH_LIMIT,
"the .inherit depth limit {limit} is greater than MAX_INHERIT_DEPTH_LIMIT \
({MAX_INHERIT_DEPTH_LIMIT})"
);
self.inherit_depth_limit = limit;
self
}
pub fn inherit_depth_limit(&self) -> usize {
self.inherit_depth_limit
}
pub fn comments(&self) -> &[Comment] {
&self.comments
}
pub fn attached_comments(&self) -> &[AttachedComments] {
self.attached_paths.get_or_init(|| self.attached.attached())
}
pub fn output_facts(&self) -> &OutputFacts {
&self.facts
}
pub fn emitter(&self, format: crate::emit::Format) -> crate::emit::Emitter<'_> {
crate::emit::Emitter::new(format).with_facts(&self.facts)
}
fn base(&self) -> PathBuf {
inputs::input_base(self.base_dir.as_deref(), &*self.loader, None)
}
pub fn inputs(&mut self) -> Inputs<'_> {
let Parser {
flags,
priority,
strategy,
variables,
handler,
loader,
base_dir,
search_path,
max_input_bytes,
inherit_depth_limit,
comments,
attached,
attached_paths,
facts,
handler_answers,
macros,
uncertain,
} = self;
handler_answers.clear();
uncertain.set(0);
*attached_paths = OnceCell::new();
let recording = handler.as_deref_mut().map(|handler| {
Box::new(move |name: &str| {
let answer = handler(name);
handler_answers.push(answer.clone());
answer
}) as Box<vars::Handler<'_>>
});
Inputs::new(inputs::Parts {
flags: *flags,
priority: *priority,
strategy: *strategy,
variables,
handler: recording,
loader: &**loader,
base_dir: base_dir.as_deref(),
search_path: search_path.clone(),
max_input_bytes: *max_input_bytes,
inherit_depth_limit: *inherit_depth_limit,
macros,
uncertain,
comments,
attached,
facts,
})
}
#[doc(hidden)]
pub fn uncertain_reached(&self) -> Vec<Uncertain> {
let bits = self.uncertain.get();
Uncertain::ALL
.into_iter()
.filter(|rule| bits & rule.bit() != 0)
.collect()
}
pub fn parse(&mut self, input: &[u8]) -> Result<UclValue, Error> {
self.parse_one(Input::bytes(input))
}
fn parse_one(&mut self, input: Input<'_>) -> Result<UclValue, Error> {
let mut inputs = self.inputs();
let stopped = match inputs.read(input) {
Ok(()) => None,
Err(e) if e.is_stopped() => Some(e),
Err(e) => return Err(e),
};
let value = inputs.finish()?;
match stopped {
Some(stop) => Err(stop.with_partial(value)),
None => Ok(value),
}
}
pub fn parse_file(&mut self, path: impl AsRef<Path>) -> Result<UclValue, Error> {
let (canonical, input) = self.read_file(path.as_ref()).map_err(|(path, e)| {
Error::new(
ErrorKind::Io {
message: format!("{}: {e}", path.display()),
},
Position::new(),
)
})?;
self.parse_read_file(canonical, &input)
}
pub(crate) fn read_file(
&self,
path: &Path,
) -> Result<(PathBuf, Vec<u8>), (PathBuf, std::io::Error)> {
let path = self.base().join(path);
let canonical = match self.loader.canonicalize(&path) {
Ok(canonical) => canonical,
Err(e) => return Err((path, e)),
};
let read = match self.max_input_bytes {
Some(limit) => self.loader.read_limited(&canonical, limit),
None => self.loader.read(&canonical),
};
match read {
Ok(input) => Ok((canonical, input)),
Err(e) => Err((path, e)),
}
}
pub(crate) fn parse_read_file(
&mut self,
canonical: PathBuf,
input: &[u8],
) -> Result<UclValue, Error> {
self.parse_one(Input::read_file(canonical, input))
}
fn settings(&self) -> core::Settings {
core::Settings {
flags: self.flags,
priority: self.priority,
strategy: self.strategy,
}
}
pub(crate) fn parse_located(&mut self, source: &Source<'_>) -> Option<(UclValue, OutputFacts)> {
if self.macros.ran.get() {
return None;
}
let (input, file) = match source {
Source::Bytes(input) => (*input, None),
#[cfg(feature = "fs")]
Source::File { canonical, input } => (*input, Some(canonical.clone())),
};
let base = inputs::input_base(self.base_dir.as_deref(), &*self.loader, file.as_deref());
let variables =
inputs::first_variables(self.flags, &self.variables, file.as_deref(), &base);
let mut answers = self.handler_answers.iter();
let replay = move |_: &str| answers.next().cloned().flatten();
let mut expander = vars::Expander::new(
variables,
self.handler
.is_some()
.then(|| Box::new(replay) as Box<vars::Handler<'_>>),
!self.flags.contains(ParserFlags::DISABLE_MACRO),
);
let budget = include::Budget::new(self.max_input_bytes);
budget.take(input.len());
let mut includes = include::Includes::new(
&*self.loader,
base,
self.search_path.clone(),
budget,
(!self.macros.is_empty()).then_some(&self.macros),
);
includes.files.push(file);
includes.inherit_limit = self.inherit_depth_limit;
let mut document = core::Document::new(false, Some(OutputFacts::locating()), 0);
match document.read(input, self.settings(), &mut expander, &mut includes) {
Ok(()) => {}
Err(e) if e.is_stopped() => {}
Err(_) => return None,
}
let finished = document.finish().ok()?;
Some((finished.root, finished.facts?))
}
}
pub(crate) enum Source<'a> {
Bytes(&'a [u8]),
#[cfg(feature = "fs")]
File { canonical: PathBuf, input: &'a [u8] },
}
impl Source<'_> {
pub(crate) fn input(&self) -> &[u8] {
match self {
Source::Bytes(input) => input,
#[cfg(feature = "fs")]
Source::File { input, .. } => input,
}
}
}
pub fn parse(input: &[u8]) -> Result<UclValue, Error> {
Parser::new().parse(input)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::UclObject;
fn obj(v: &UclValue) -> &UclObject {
v.as_object().expect("object")
}
fn kind(input: &[u8]) -> ErrorKind {
parse(input).expect_err("error").kind().clone()
}
#[test]
fn nesting_limit_counts_the_root() {
for (open, close) in [("[", "]"), ("{a=", "}")] {
let ok = format!("a = {}1{}", open.repeat(1023), close.repeat(1023));
assert!(parse(ok.as_bytes()).is_ok(), "{open}: 1023 levels");
let deep = format!("a = {}1{}", open.repeat(1024), close.repeat(1024));
assert_eq!(
kind(deep.as_bytes()),
ErrorKind::NestingTooDeep { limit: MAX_NESTING }
);
}
assert!(matches!(
kind("[".repeat(200_000).as_bytes()),
ErrorKind::NestingTooDeep { .. }
));
}
#[test]
fn root_bracket_only_after_whitespace_or_a_leading_comment_group() {
for ok in [
&b"\n\t [1]"[..],
b"# c\n# d\n/* e */[1]",
b"# c\r\n[1]",
b"/* c */[1]",
] {
assert!(
parse(ok).unwrap().is_array(),
"{}",
String::from_utf8_lossy(ok)
);
}
for bad in [
&b"# header\n\n{\n \"a\": 1\n}"[..],
b"# c\n [1]",
b"\n# c\n{a = 1}",
b"/* c */\n{a = 1}",
b"/* c */ /* d */[1]",
] {
assert!(
matches!(kind(bad), ErrorKind::InvalidKey { .. }),
"{}",
String::from_utf8_lossy(bad)
);
}
assert_eq!(obj(&parse(b"# header\n\na = 1").unwrap()).len(), 1);
}
#[test]
fn hash_as_last_byte_after_leading_whitespace() {
for bad in [
&b"\n#"[..],
b" #",
b"\t#",
b"\r#",
b"\x0b#",
b"# c\n #",
b"/* c */\n#",
] {
assert_eq!(kind(bad), ErrorKind::HashAtEnd, "{bad:?}");
}
for ok in [
&b"#"[..],
b"# c\n#",
b"/* c */#",
b"\n#\n",
b"\n# ",
b" # c\n#",
b"a = 1\n #",
] {
assert!(parse(ok).is_ok(), "{ok:?}");
}
}
#[test]
fn hash_as_last_byte_after_vt_or_ff_after_an_entry() {
for bad in [
&b"a {}\x0c#"[..],
b"a {}\x0b#",
b"a = 1;\x0b#",
b"a = 1\n\x0c\n#",
b"a = 1\n \x0c#",
b"a {b 1}\x0c/**/ #",
b"a {b 1}\x0c# c\n #",
b"a = \"x\"\n\x0c#",
b"o { a = 1 }\x0c\x0c\x0c#",
b"a = 1; # c\n\x0c#",
] {
assert_eq!(kind(bad), ErrorKind::HashAtEnd, "{bad:?}");
}
for ok in [
&b"a {}\t#"[..],
b"a {} #",
b"a {}\n #",
b"a {}\x0c# c",
b"a {}\x0c/* c */#",
b"a {b 1}\x0c# c\n#",
b"a = 1\n\x0b# c\n#",
b"a = 1;\x0c\x0c/**/#",
b"a {}\x0cb = 2\n #",
] {
assert!(parse(ok).is_ok(), "{ok:?}");
}
let v = parse(b"a = 1\x0c#").unwrap();
assert_eq!(obj(&v)["a"].as_str(), Some("1\x0c"));
let mut loader = MemoryLoader::new();
loader.add_file("f", "a = 1;\x0c#");
let mut parser = Parser::new();
parser.set_loader(loader);
let error = parser.parse(b".include \"f\"\nb = 2").unwrap_err();
assert_eq!(error.kind(), &ErrorKind::HashAtEnd);
}
#[test]
fn heredoc_opener_cut_by_the_end_of_its_unit() {
for bad in [
&b"k = <<EO"[..],
b"k <<AA",
b"\"a\"<<OD",
b"a=/**/<<EO",
b".priority(k = <<EOD) 1\na = 1",
] {
assert_eq!(kind(bad), ErrorKind::UnterminatedHeredoc, "{bad:?}");
}
let mut loader = MemoryLoader::new();
loader.add_file("f", "k = <<EO");
let mut parser = Parser::new();
parser.set_loader(loader);
let error = parser.parse(b".include \"f\"\nm = 1").unwrap_err();
assert_eq!(error.kind(), &ErrorKind::UnterminatedHeredoc);
for (ok, value) in [
(&b"k = <<E"[..], "<<E"),
(b"k = <<EO ", "<<EO"),
(b"k = <<EO;", "<<EO"),
(b"k = <<AB1", "<<AB1"),
(b"k = <<Ab", "<<Ab"),
] {
assert_eq!(
obj(&parse(ok).unwrap())["k"].as_str(),
Some(value),
"{ok:?}"
);
}
let v = parse(b"k = [<<EOD]").unwrap();
assert_eq!(obj(&v)["k"].as_array().unwrap()[0].as_str(), Some("<<EOD"));
}
#[test]
fn root_forms() {
assert_eq!(parse(b"").unwrap(), UclValue::Object(UclObject::new()));
assert_eq!(
parse(b" # only a comment\n").unwrap(),
UclValue::Object(UclObject::new())
);
let v = parse(b"[1, 2] ignored").unwrap();
assert_eq!(v.as_array().unwrap().len(), 2);
let v = parse(b"{ a = 1 } b = 2").unwrap();
assert_eq!(obj(&v).keys().collect::<Vec<_>>(), vec!["a"]);
assert_eq!(kind(b"a = 1 }"), ErrorKind::UnmatchedClose { found: '}' });
assert_eq!(kind(b"{ a = 1"), ErrorKind::UnterminatedObject);
assert_eq!(kind(b"a = [1"), ErrorKind::UnterminatedArray);
}
#[test]
fn sections_and_left_open_sections() {
let v = parse(b"a b c { d = 1 }").unwrap();
let d = &obj(&obj(&obj(&v)["a"])["b"])["c"];
assert_eq!(obj(d)["d"], UclValue::Integer(1));
let v = parse(b"x = 1\nc \"x{y\" z\nd = 1").unwrap();
let c = obj(&obj(&v)["c"]);
assert_eq!(c.keys().collect::<Vec<_>>(), vec!["x{y", "d"]);
assert_eq!(
kind(b"k { c \"x{y\" z\n}"),
ErrorKind::UnmatchedClose { found: '}' }
);
let keys = |v: &UclValue| obj(v).keys().cloned().collect::<Vec<_>>();
let v = parse(b"x \"y{\" z\na { b = 1 }\nd = 1").unwrap();
assert_eq!(keys(&v), ["x", "d"]);
assert_eq!(keys(&obj(&v)["x"]), ["y{", "a"]);
let v = parse(b"c \"x{y\" z\nd \"x{y\" w\ne = [1]\ng = 1").unwrap();
assert_eq!(keys(&v), ["c", "g"]);
let v = parse(b"k { c \"x{y\" z\na { b = 1 }\n}\nm = 1").unwrap();
assert_eq!(keys(&v), ["k", "m"]);
let v = parse(b"c \"x{y\" z\nd { e { f = 1 } g = 2 }\nh = 1").unwrap();
assert_eq!(keys(&obj(&v)["c"]), ["x{y", "d"]);
assert_eq!(
kind(b"x \"y{\" z\na { b = 1 }\n}"),
ErrorKind::UnmatchedClose { found: '}' }
);
for input in [
&b"a \x0cb { c = 1 }"[..],
b"a \x0b\x0c /* x */ b { c = 1 }",
b"a \x0cb = \x0cc { d = 1 }",
] {
let v = parse(input).unwrap();
assert!(obj(&obj(&v)["a"])["b"].is_object(), "{input:?}");
}
let v = parse(b"a \x0c# c\nb { c = 1 }").unwrap();
assert_eq!(obj(&v)["a"].as_str(), Some("b { c = 1 }"));
assert!(parse(b"a \x0c{ b = 1 }").is_err());
let v = parse(b"c \"x{\" =\n# c\n d {}").unwrap();
assert!(obj(&obj(&obj(&v)["c"])["x{"])["d"].is_object());
let v = parse(b"c \"x{\" =\x0b").unwrap();
assert!(obj(&obj(&obj(&v)["c"])["x{"]).is_empty());
assert_eq!(kind(b"c \"x{\" = "), ErrorKind::MissingValue);
assert_eq!(kind(b"c \"x{\" = # c\n"), ErrorKind::MissingValue);
assert_eq!(kind(b"c \"x{\" =\n #"), ErrorKind::HashAtEnd);
}
#[test]
fn first_array_element_after_a_comment_keeps_its_whitespace() {
let v = parse(b"[ /* c */ 1, /* d */ 2]").unwrap();
assert_eq!(
v.as_array().unwrap(),
&vec![UclValue::String(" 1".into()), UclValue::Integer(2)]
);
assert_eq!(parse(b"[ /* c */]").unwrap(), UclValue::Array(Vec::new()));
assert_eq!(kind(b"[ /* c */ ]"), ErrorKind::MissingValue);
}
#[test]
fn element_after_a_vt_or_ff_is_read_like_the_first() {
let s = |t: &str| UclValue::String(t.into());
let i = UclValue::Integer;
for (input, expected) in [
(&b"[1, \x0b# d\n 2]"[..], vec![i(1), s(" 2")]),
(b"[1, \x0b/* d */ 2]", vec![i(1), s(" 2")]),
(b"[1, \x0c/* d */2]", vec![i(1), i(2)]),
(b"[1, \x0b 2]", vec![i(1), i(2)]),
(b"[1,\x0b/* d */\x0b 2]", vec![i(1), s("\x0b 2")]),
(
b"[{x=1}\x0b/* d */ 2]",
vec![
UclValue::Object([("x", i(1))].into_iter().collect()),
s(" 2"),
],
),
(b"[1\n\x0b# d\n]", vec![i(1)]),
(b"[1\n\x0b]", vec![i(1)]),
(b"[1\n/* d */\n]", vec![i(1)]),
] {
assert_eq!(
parse(input).unwrap(),
UclValue::Array(expected),
"{}",
String::from_utf8_lossy(input)
);
}
for bad in [&b"[1\n\x0b/* d */\n]"[..], b"[1\n\x0c/* d */\n]"] {
assert_eq!(kind(bad), ErrorKind::MissingValue, "{bad:?}");
}
}
#[test]
fn value_on_a_following_line() {
let v = parse(b"a =\n\n v\nb # c\n= 1\nc \n{ x = 1 }").unwrap();
let o = obj(&v);
assert_eq!(o["a"], UclValue::String("v".into()));
assert_eq!(o["b"], UclValue::Integer(1));
assert!(o["c"].is_object());
assert_eq!(parse(b"a = \n").unwrap(), parse(b"a = null").unwrap());
assert_eq!(kind(b"a"), ErrorKind::MissingValue);
assert_eq!(
kind(b"a\nb = 1"),
ErrorKind::InvalidKey { found: Some('\n') }
);
assert_eq!(kind(b"a =\n/* c */\nb = 1"), ErrorKind::MissingValue);
}
#[test]
fn errors_carry_positions() {
let e = parse(b"a = 1\nb = \"open").unwrap_err();
assert_eq!(e.kind(), &ErrorKind::UnterminatedString);
assert_eq!((e.position().line, e.position().column), (2, 5));
let e = parse(b"a = 1\n\"\" = 2").unwrap_err();
assert_eq!(e.kind(), &ErrorKind::EmptyKey);
assert_eq!(e.position().offset, 6);
assert!(e.to_string().contains("line 2, column 1"));
}
#[test]
fn utf8_is_required_in_keys_and_strings_only() {
assert_eq!(kind(b"\xff = 1"), ErrorKind::InvalidUtf8);
assert_eq!(kind(b"a = \"\xc3\""), ErrorKind::InvalidUtf8);
assert_eq!(kind(b"a = x\xff"), ErrorKind::InvalidUtf8);
assert_eq!(kind(br#"a = "\uD800""#), ErrorKind::InvalidUtf8);
let v = parse(b"# \xff comment\na = 1\n/* \xfe */").unwrap();
assert_eq!(obj(&v)["a"], UclValue::Integer(1));
}
#[test]
fn macros_at_the_end_of_input() {
for ok in [
&b"a = 1\n.foo"[..],
b".",
b".foo;",
b".foo#",
b".priority\n",
b".include # c\n",
b".include\t# c",
b".include /* d */",
b".include\n# c\n# d\n",
b".include\n# c\n#",
] {
assert!(parse(ok).is_ok(), "{}", String::from_utf8_lossy(ok));
}
let v = parse(b"\"a\".51\"x{y\"z").unwrap();
assert!(obj(&obj(&v)["a"]).is_empty());
assert_eq!(kind(b"a { .foo"), ErrorKind::UnterminatedObject);
assert_eq!(
kind(b".foo\n"),
ErrorKind::UnknownMacro { name: "foo".into() }
);
assert_eq!(
kind(b"a .b {c = 1}"),
ErrorKind::UnknownMacro { name: "b".into() }
);
assert!(
matches!(kind(b".include\n#"), ErrorKind::FileNotFound { path } if path.is_empty())
);
assert_eq!(kind(b".priority\n#"), ErrorKind::InvalidPriority);
assert_eq!(kind(b".priority(priority=2)"), ErrorKind::MissingValue);
assert_eq!(
kind(b".priority(priority=2) # c\n"),
ErrorKind::MissingValue
);
assert_eq!(
kind(b".include(try=true \"x\"\nk = 1\n"),
ErrorKind::UnterminatedArguments
);
for input in [&br#".priority(p="a\") 1"#[..], br#".priority(p="a\\") 1"#] {
assert_eq!(kind(input), ErrorKind::UnterminatedArguments, "{input:?}");
}
assert!(parse(b".priority(p=\"(\") 1").is_ok());
assert!(matches!(
kind(b".include(t=\")\") \"no such file\""),
ErrorKind::FileNotFound { .. }
));
assert!(matches!(
kind(b".include # c\n\n"),
ErrorKind::FileNotFound { path } if path.is_empty()
));
assert_eq!(
Parser::with_flags(ParserFlags::DISABLE_MACRO)
.parse(b"a = 1\n.foo")
.unwrap_err()
.kind(),
&ErrorKind::MacrosDisabled
);
}
#[test]
fn key_lowercase_before_escapes() {
let v = Parser::with_flags(ParserFlags::KEY_LOWERCASE)
.parse(b"\"\\u0041\" = 1\n\"\\N\" = 2\n\"\\U0042\" = 3\n\"\\\\N\" = 4\n\"A\\tB\" = 5\n\"\\UZZZZ\" = 6\nC = 7")
.unwrap();
assert_eq!(
obj(&v).keys().collect::<Vec<_>>(),
["A", "\n", "B", "\\n", "a\tb", "\0", "c"]
);
let v = Parser::with_flags(ParserFlags::KEY_LOWERCASE)
.parse(b"\"\\u0041\\u0042\" = 1\nab = 2\n\"aB\" = 3\nC = 4\n\"\\u0043\" = 5")
.unwrap();
let o = obj(&v);
assert_eq!(o.keys().collect::<Vec<_>>(), ["AB", "c"]);
assert_eq!(
(o.entry("AB").unwrap().len(), o.entry("c").unwrap().len()),
(3, 2)
);
assert_eq!(
Parser::with_flags(ParserFlags::KEY_LOWERCASE)
.parse(b"\"\\uZZZZ\" = 1")
.unwrap_err()
.kind(),
&ErrorKind::InvalidUnicodeEscape
);
}
#[test]
fn key_lowercase_replacement_takes_the_new_spelling() {
let keys = |input: &[u8], strategy: DuplicateStrategy, flags: ParserFlags| {
let mut p = Parser::with_flags(ParserFlags::KEY_LOWERCASE | flags);
p.set_strategy(strategy);
let v = p.parse(input).unwrap();
obj(&v).keys().cloned().collect::<Vec<_>>()
};
let (append, none) = (DuplicateStrategy::Append, ParserFlags::DEFAULT);
let nia = ParserFlags::NO_IMPLICIT_ARRAYS;
for (input, strategy, flags, expected) in [
(
&b"x = 1\na = 6\n.priority 3\n\"\\u0041\" = 3\ny = 1"[..],
append,
none,
["x", "A", "y"],
),
(
b"x = 1\na = 1\n\"\\u0041\" = 2\ny = 1",
DuplicateStrategy::Rewrite,
none,
["x", "A", "y"],
),
(
b"x = 1\na = 1\na = 2\n.priority 3\n\"\\u0041\" = 3\ny = 1",
append,
nia,
["x", "A", "y"],
),
(
b"x = 1\na = 1\n.priority 3\n\"\\u0041\" = 2\ny = 1",
DuplicateStrategy::Merge,
none,
["x", "A", "y"],
),
(
b"x = 1\n\"\\u0041\" = 1\n.priority 3\na = 2\ny = 1",
append,
none,
["x", "a", "y"],
),
(
b"x = 1\na = 1\n\"\\u0041\" = 2\ny = 1",
append,
none,
["x", "a", "y"],
),
(
b"x = 1\na = 1\n\"\\u0041\" = 2\ny = 1",
append,
nia,
["x", "a", "y"],
),
(
b"x = 1\n.priority 3\na = 1\n.priority 1\n\"\\u0041\" = 2\ny = 1",
append,
none,
["x", "a", "y"],
),
(
b"x = 1\na { b = 1 }\n\"\\u0041\" { c = 1 }\ny = 1",
DuplicateStrategy::Merge,
none,
["x", "a", "y"],
),
(
b"x = 1\na { b = 1 }\n\"\\u0041\" = 2\ny = 1",
DuplicateStrategy::Merge,
none,
["x", "a", "y"],
),
] {
assert_eq!(
keys(input, strategy, flags),
expected,
"{}",
String::from_utf8_lossy(input)
);
}
let v = Parser::with_flags(ParserFlags::KEY_LOWERCASE)
.parse(b"d { a = 1 }\ne { .inherit \"d\"; \"\\u0041\" = 2 }")
.unwrap();
assert_eq!(obj(&obj(&v)["e"]).keys().collect::<Vec<_>>(), ["A"]);
let mut p = Parser::with_flags(ParserFlags::KEY_LOWERCASE | ParserFlags::SAVE_COMMENTS);
let v = p
.parse(b"a { x = 1 }\n.priority 3\n# c\n\"\\u0041\" { y = 2 # d\n}")
.unwrap();
assert_eq!(obj(&obj(&v)["A"]).keys().collect::<Vec<_>>(), ["y"]);
let paths: Vec<_> = p
.attached_comments()
.iter()
.map(|g| g.path.clone())
.collect();
let key = |k: &str| PathSegment::Key {
key: k.into(),
index: 0,
};
assert_eq!(paths, [vec![key("A")], vec![key("A"), key("y")]]);
}
#[test]
fn macros_are_recognised() {
let e = parse(b".includes \"x.conf\"").unwrap_err();
assert!(e.is_unsupported());
assert_eq!(
kind(b".unknown x"),
ErrorKind::UnknownMacro {
name: "unknown".into()
}
);
assert_eq!(
Parser::with_flags(ParserFlags::DISABLE_MACRO)
.parse(b".include \"x\"")
.unwrap_err()
.kind(),
&ErrorKind::MacrosDisabled
);
let v = parse(b"k = .include x\narr = [.include y]").unwrap();
assert_eq!(obj(&v)["k"], UclValue::String(".include x".into()));
}
#[test]
fn duplicate_strategies_and_priorities() {
let v = parse(b"a = 1\na = 2").unwrap();
assert_eq!(obj(&v).entry("a").unwrap().len(), 2);
let mut p = Parser::new();
p.set_strategy(DuplicateStrategy::Error);
let e = p.parse(b"a = 1\nb { a = 2 }\na = 3").unwrap_err();
assert_eq!(e.kind(), &ErrorKind::DuplicateKey { key: "a".into() });
assert_eq!(e.position().line, 3);
let mut p = Parser::new();
p.set_strategy(DuplicateStrategy::Merge).set_priority(3);
let v = p.parse(b"a { x = 1 }\na { y = 2 }").unwrap();
let entry = obj(&v).entry("a").unwrap();
assert_eq!(entry.len(), 1);
assert_eq!(entry.slots()[0].priority(), 3);
assert_eq!(obj(entry.first()).len(), 2);
let v = Parser::with_flags(ParserFlags::NO_IMPLICIT_ARRAYS | ParserFlags::KEY_LOWERCASE)
.parse(b"A { x = 1 }\na { y = 2 }")
.unwrap();
let items = obj(&v)["a"].as_array().unwrap();
assert_eq!(items.len(), 2);
assert!(items.iter().all(UclValue::is_object));
}
#[test]
fn variables_file_variables_and_handler() {
let mut p = Parser::new();
p.register_variable("FILENAME", "mine");
p.set_variable_handler(|name| (name == "H").then(|| "[h]".to_string()));
let v = p.parse(b"a = \"$FILENAME ${H} ${X}\"").unwrap();
assert_eq!(obj(&v)["a"], UclValue::String("mine [h] ${X}".into()));
let v = parse(b"a = $FILENAME").unwrap();
assert_eq!(obj(&v)["a"], UclValue::String("undef".into()));
let v = Parser::with_flags(ParserFlags::NO_FILEVARS)
.parse(b"a = $FILENAME")
.unwrap();
assert_eq!(obj(&v)["a"], UclValue::String("$FILENAME".into()));
let v = parse(b"a = $CURDIR").unwrap();
assert_eq!(obj(&v)["a"], UclValue::String("/".into()));
let v = Parser::builder()
.with_base_dir("/srv/app")
.build()
.parse(b"a = $CURDIR")
.unwrap();
assert_eq!(obj(&v)["a"], UclValue::String("/srv/app".into()));
}
#[cfg(feature = "fs")]
#[test]
fn file_variables_of_a_file_read_from_the_filesystem() {
let dir = std::env::temp_dir().join(format!("ucl-parse-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("vars.conf");
std::fs::write(&file, "f = \"$FILENAME\"\nd = \"$CURDIR\"\n").unwrap();
let mut p = Parser::new();
p.set_loader(FsLoader::new());
p.register_variable("FILENAME", "ignored for files");
let v = p.parse_file(&file).unwrap();
let expected = std::fs::canonicalize(&file).unwrap();
assert_eq!(
obj(&v)["f"].as_str(),
Some(expected.to_string_lossy().as_ref())
);
assert_eq!(
obj(&v)["d"].as_str(),
Some(expected.parent().unwrap().to_string_lossy().as_ref())
);
assert!(matches!(
Parser::new().parse_file(&file).unwrap_err().kind(),
ErrorKind::Io { .. }
));
std::fs::remove_dir_all(&dir).unwrap();
assert!(matches!(
Parser::new()
.set_loader(FsLoader::new())
.parse_file(dir.join("missing"))
.unwrap_err()
.kind(),
ErrorKind::Io { .. }
));
}
#[test]
fn relative_paths_in_a_file_resolve_against_its_directory() {
let mut loader = MemoryLoader::new();
loader
.add_file("/etc/app/main.conf", "a = 1\n.include \"sub/part.conf\"\n")
.add_file("/etc/app/sub/part.conf", "b = 2\n.include \"leaf.conf\"\n")
.add_file("/etc/app/leaf.conf", "c = 3\n")
.add_file("/etc/app/sub/leaf.conf", "c = 4\n")
.add_file("/srv/sub/part.conf", "b = 5\n")
.set_current_dir("/srv");
let mut p = Parser::new();
p.set_loader(loader.clone());
let v = p.parse_file("/etc/app/main.conf").unwrap();
let keys: Vec<_> = obj(&v)
.iter()
.map(|(k, e)| (k.to_string(), e.first().clone()))
.collect();
assert_eq!(
keys,
[
("a".to_string(), UclValue::Integer(1)),
("b".to_string(), UclValue::Integer(2)),
("c".to_string(), UclValue::Integer(3)),
]
);
let mut p = Parser::new();
p.set_loader(loader.clone()).set_base_dir("/srv");
let v = p.parse_file("/etc/app/main.conf").unwrap();
assert_eq!(obj(&v)["b"], UclValue::Integer(5));
let mut p = Parser::new();
p.set_loader(loader.clone());
let v = p.parse(b".include \"sub/part.conf\"").unwrap();
assert_eq!(obj(&v)["b"], UclValue::Integer(5));
loader.add_file(
"/etc/app/args.conf",
".include(key=\"$CURDIR\") \"leaf.conf\"\n",
);
let mut p = Parser::new();
p.set_loader(loader.clone());
let v = p.parse_file("/etc/app/args.conf").unwrap();
assert_eq!(obj(&obj(&v)["/etc/app"])["c"], UclValue::Integer(3));
let v = p
.parse(b".include(key=\"$CURDIR\") \"/etc/app/leaf.conf\"")
.unwrap();
assert_eq!(obj(&obj(&v)["/srv"])["c"], UclValue::Integer(3));
}
#[test]
fn the_default_loader_holds_no_files() {
let e = parse(b"a = 1\n.include \"/etc/hosts\"").unwrap_err();
assert!(!e.is_stopped() && !e.is_unsupported(), "{e}");
let e = parse(b"a = 1\n.try_include \"/etc/hosts\"\nb = 2").unwrap_err();
assert!(e.is_stopped(), "{e}");
assert_eq!(obj(e.partial().unwrap()).keys().collect::<Vec<_>>(), ["a"]);
}
#[test]
fn errors_convert_into_ucl_error() {
fn run(input: &[u8]) -> Result<UclValue, crate::UclError> {
Ok(parse(input)?)
}
let err = run(b"a = ").unwrap_err();
assert!(matches!(&err, crate::UclError::Syntax(e) if e.kind() == &ErrorKind::MissingValue));
let err = run(b"a = 1\n.try_include \"no such file\"\nb = 2").unwrap_err();
let crate::UclError::Stopped(e) = &err else {
panic!("{err:?}");
};
assert_eq!(obj(e.partial().unwrap()).keys().collect::<Vec<_>>(), ["a"]);
}
#[test]
fn saved_comments_have_positions() {
let mut p = Parser::with_flags(ParserFlags::SAVE_COMMENTS);
let v = p.parse(b"# one\na = 1 /* two\n */\nb = 2 # three").unwrap();
assert_eq!(obj(&v).len(), 2);
let found: Vec<_> = p
.comments()
.iter()
.map(|c| (c.text.as_str(), c.position.line, c.position.column))
.collect();
assert_eq!(
found,
vec![("# one", 1, 1), ("/* two\n */\n", 2, 7), ("# three", 4, 7)]
);
assert!(Parser::new().comments().is_empty());
p.parse(b"a = 1 /* c */").unwrap();
assert_eq!(p.comments()[0].text, "/* c */");
}
fn attached(
input: &[u8],
flags: ParserFlags,
strategy: DuplicateStrategy,
) -> Vec<(String, &'static str, Vec<String>)> {
let mut p = Parser::with_flags(flags | ParserFlags::SAVE_COMMENTS);
p.set_strategy(strategy);
p.parse(input).unwrap();
let mut out: Vec<_> = p
.attached_comments()
.iter()
.map(|g| {
let path = g
.path
.iter()
.map(|s| match s {
PathSegment::Key { key, index } => format!("{key}[{index}]"),
PathSegment::Index(i) => format!("[{i}]"),
})
.collect::<Vec<_>>()
.join("/");
let placement = match g.placement {
CommentPlacement::Before => "c",
CommentPlacement::After => "ca",
};
let texts = g
.comments
.iter()
.map(|&i| p.comments()[i].text.clone())
.collect();
(path, placement, texts)
})
.collect();
out.sort();
out
}
fn row(
path: &str,
placement: &'static str,
texts: &[&str],
) -> (String, &'static str, Vec<String>) {
(
path.to_string(),
placement,
texts.iter().map(|t| t.to_string()).collect(),
)
}
#[test]
fn comments_attach_to_values() {
let none = ParserFlags::DEFAULT;
let append = DuplicateStrategy::Append;
assert_eq!(
attached(
b"# c1\n# c2\na = 1 # c3\nb = /* c4 */ 2\nc = [ 1, # c5\n 2 ]\n",
none,
append
),
[
row("a[0]", "c", &["# c1", "# c2"]),
row("b[0]", "c", &["# c3", "/* c4 */ "]),
row("c[0]/[1]", "c", &["# c5"]),
]
);
assert_eq!(
attached(b"# c\na = 1 # d\n", none, append),
[row("a[0]", "c", &["# c", "# d"])]
);
assert_eq!(
attached(b"a = [ # c\n1 ] # d\n", none, append),
[row("a[0]/[0]", "c", &["# c", "# d"])]
);
assert_eq!(
attached(b"x { a = 1 # d\n} # e", none, append),
[row("x[0]/a[0]", "ca", &["# d", "# e"])]
);
assert_eq!(
attached(b"a b { c = 1 # z\n} # y", none, append),
[
row("a[0]", "ca", &["# y"]),
row("a[0]/b[0]/c[0]", "ca", &["# z"])
]
);
assert_eq!(
attached(b"x \"y{\" z\na { b = 1 }\n# c", none, append),
[row("x[0]", "ca", &["# c"])]
);
assert_eq!(
attached(b"# c\n{ }", none, append),
[row("", "ca", &["# c"])]
);
assert!(attached(b"{ } # c", none, append).is_empty());
assert!(attached(b"a = 1\n#", none, append).is_empty());
assert!(attached(b"a = [1];#", none, append).is_empty());
assert_eq!(
attached(b"a = 1\n# c\n#", none, append),
[row("a[0]", "ca", &["# c", "#"])]
);
assert_eq!(attached(b"#", none, append), [row("", "ca", &["#"])]);
}
#[test]
fn comments_follow_values_that_the_duplicate_rules_move() {
let nia = ParserFlags::NO_IMPLICIT_ARRAYS;
let none = ParserFlags::DEFAULT;
assert_eq!(
attached(b"# c\na = 1\na = 2\n# e", nia, DuplicateStrategy::Append),
[
row("a[0]/[0]", "c", &["# c"]),
row("a[0]/[1]", "ca", &["# e"])
]
);
assert_eq!(
attached(
b"a = 1\n# c\na = 2\n# d\na = 3",
nia,
DuplicateStrategy::Append
),
[
row("a[0]/[1]", "c", &["# c"]),
row("a[0]/[2]", "c", &["# d"])
]
);
assert_eq!(
attached(b"# c\na = 1\n# d\na = 2", none, DuplicateStrategy::Rewrite),
[row("a[0]", "c", &["# d"])]
);
assert_eq!(
attached(
b"# c\na { x = 1 }\n# d\na { y = 2 # e\n}",
none,
DuplicateStrategy::Merge
),
[
row("a[0]", "c", &["# c", "# d"]),
row("a[0]/y[0]", "ca", &["# e"])
]
);
assert_eq!(
attached(b"a { x = 1 }\na = 5\n# e", none, DuplicateStrategy::Merge),
[row("a[0]", "ca", &["# e"])]
);
assert_eq!(
attached(
b"# c\nk { m = 1 }\n# d\nk = 1",
none,
DuplicateStrategy::Merge
),
[row("k[0]", "c", &["# c", "# d"])]
);
assert_eq!(
attached(b"c =\n#z\nb\nd = 1", none, DuplicateStrategy::Append),
[row("d[0]", "c", &["#z"])]
);
assert_eq!(
attached(b"# p\na =\n# c\nv", none, DuplicateStrategy::Append),
[row("a[0]", "c", &["# p", "# c"])]
);
assert_eq!(
attached(b"a =\n# c\n", none, DuplicateStrategy::Append),
[row("a[0]", "ca", &["# c"])]
);
}
}