use super::core::{Core, NestTarget, Settings};
use super::glob;
use super::loader::{FileKind, Loader};
use super::macros::{MacroCall, MacroKind, priority_bits};
use super::registered::MacroTable;
use super::{Error, ErrorKind, MAX_INCLUDE_DEPTH};
use crate::value::{DuplicateStrategy, UclValue};
use std::cell::Cell;
use std::io;
use std::path::{Path, PathBuf};
use std::rc::Rc;
#[derive(Debug)]
pub(crate) struct Budget {
limit: Option<u64>,
used: Cell<u64>,
}
impl Budget {
pub(crate) fn new(limit: Option<u64>) -> Rc<Self> {
Rc::new(Self {
limit,
used: Cell::new(0),
})
}
pub(crate) fn take(&self, len: usize) -> bool {
let used = self.used.get().saturating_add(len as u64);
self.used.set(used);
self.limit.is_none_or(|limit| used <= limit)
}
pub(crate) fn limit(&self) -> Option<u64> {
self.limit
}
}
pub(crate) enum Read {
Bytes(Vec<u8>),
TooLarge {
limit: u64,
},
}
pub(crate) struct Includes<'l> {
pub(crate) loader: &'l dyn Loader,
pub(crate) base: PathBuf,
search: Option<Vec<String>>,
default_search: Option<Vec<String>>,
pub(crate) files: Vec<Option<PathBuf>>,
pub(crate) budget: Rc<Budget>,
pub(crate) macros: Option<&'l MacroTable>,
units: usize,
pub(crate) open_units: Vec<usize>,
pub(crate) uncertain: Option<&'l Cell<u8>>,
pub(crate) inherit_limit: usize,
}
impl<'l> Includes<'l> {
pub(crate) fn new(
loader: &'l dyn Loader,
base: PathBuf,
search: Option<Vec<String>>,
budget: Rc<Budget>,
macros: Option<&'l MacroTable>,
) -> Self {
Self {
loader,
base,
search: search.clone(),
default_search: search,
files: Vec::new(),
budget,
macros,
units: 0,
open_units: Vec::new(),
uncertain: None,
inherit_limit: super::DEFAULT_INHERIT_DEPTH_LIMIT,
}
}
pub(crate) fn reached(&self, rule: super::Uncertain) {
if let Some(cell) = self.uncertain {
cell.set(cell.get() | rule.bit());
}
}
pub(crate) fn for_arguments(&self) -> Includes<'l> {
let mut includes = Includes::new(
self.loader,
self.base.clone(),
self.default_search.clone(),
Rc::clone(&self.budget),
None,
);
includes.files.push(None);
includes.uncertain = self.uncertain;
includes.inherit_limit = self.inherit_limit;
includes
}
pub(crate) fn new_unit(&mut self) -> usize {
self.units += 1;
self.units
}
pub(crate) fn read(&self, path: &Path) -> io::Result<Read> {
let Some(limit) = self.budget.limit else {
return self.loader.read(path).map(Read::Bytes);
};
let left = limit.saturating_sub(self.budget.used.get());
let bytes = self.loader.read_limited(path, left)?;
let len = bytes.len() as u64;
if len > left {
return Ok(Read::TooLarge { limit });
}
self.budget.used.set(self.budget.used.get() + len);
Ok(Read::Bytes(bytes))
}
fn resolve(&self, path: &str) -> PathBuf {
let path = Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
self.base.join(path)
}
}
}
enum Outcome {
Done,
Unusable(ErrorKind),
}
struct Request {
soft: bool,
try_: bool,
glob: bool,
wildcard_after_nul: bool,
prefix: bool,
key: Option<String>,
array: bool,
settings: Settings,
at: usize,
}
fn prefix_key(path: &Path) -> String {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
match name
.strip_suffix(".conf")
.or_else(|| name.strip_suffix(".ucl"))
{
Some(stem) => stem.to_owned(),
None => name,
}
}
fn duplicate_strategy(name: Option<&str>) -> DuplicateStrategy {
match name {
Some("merge") => DuplicateStrategy::Merge,
Some("rewrite") => DuplicateStrategy::Rewrite,
Some("error") => DuplicateStrategy::Error,
_ => DuplicateStrategy::Append,
}
}
impl Core<'_, '_, '_, '_> {
fn macro_path(&self, call: &MacroCall) -> Result<String, Error> {
let path = call.value.split(|&b| b == 0).next().unwrap_or_default();
String::from_utf8(path.to_vec())
.map_err(|_| self.error(ErrorKind::InvalidUtf8, call.value_at))
}
pub(super) fn include_macro(&mut self, call: &MacroCall) -> Result<(), Error> {
let params = call.args.resolve(call.kind.parameters());
let unsupported = |feature: &str| ErrorKind::Unsupported {
feature: feature.to_owned(),
};
if call.kind == MacroKind::Includes {
return Err(self.error(
unsupported("the macro .includes, which verifies signatures,"),
call.at,
));
}
if params.bool("sign") == Some(true) {
return Err(self.error(unsupported("signature checking (sign=true)"), call.at));
}
let path = self.macro_path(call)?;
if let Some(dirs) = params.array("path") {
let dirs = dirs
.iter()
.filter_map(UclValue::as_str)
.map(|dir| super::macros::before_nul(dir).to_owned());
self.includes.search = Some(dirs.collect());
}
let soft = call.kind == MacroKind::TryInclude;
let try_ = params.bool("try").unwrap_or(soft);
if params.bool("url") == Some(true) && path.contains("://") {
return if try_ {
Ok(())
} else {
Err(self.error(ErrorKind::UrlNotSupported { path }, call.value_at))
};
}
let glob = params.bool("glob").unwrap_or(false);
let after_nul = call.value.splitn(2, |&b| b == 0).nth(1).unwrap_or_default();
let request = Request {
soft,
try_,
glob,
wildcard_after_nul: glob && after_nul.iter().any(|&b| matches!(b, b'*' | b'?')),
prefix: params.bool("prefix").unwrap_or(false),
key: params.string("key").map(str::to_owned),
array: params
.string("target")
.is_some_and(|t| t.eq_ignore_ascii_case("array")),
settings: Settings {
flags: self.settings.flags,
priority: params.int("priority").map_or(0, priority_bits),
strategy: duplicate_strategy(params.string("duplicate")),
},
at: call.value_at,
};
let outcome = match self.includes.search.clone() {
None => self.include_path(&path, request.wildcard_after_nul, &request)?,
Some(dirs) => self.include_searched(&dirs, &path, &request)?,
};
match outcome {
Outcome::Done => Ok(()),
Outcome::Unusable(_) => Err(self.error(ErrorKind::Stopped { path }, call.at)),
}
}
fn include_searched(
&mut self,
dirs: &[String],
path: &str,
request: &Request,
) -> Result<Outcome, Error> {
let mut last = Outcome::Unusable(ErrorKind::FileNotFound {
path: path.to_owned(),
});
for dir in dirs {
last = self.include_path(&format!("{dir}/{path}"), false, request)?;
if matches!(last, Outcome::Done) && !request.glob {
break;
}
}
match last {
Outcome::Done => Ok(Outcome::Done),
Outcome::Unusable(kind) => Err(self.error(kind, request.at)),
}
}
fn include_path(
&mut self,
path: &str,
wildcard_after_nul: bool,
request: &Request,
) -> Result<Outcome, Error> {
if !(request.glob && (wildcard_after_nul || glob::has_wildcard(path))) {
if path.is_empty() {
return self.unusable_missing(path, request);
}
let named = path.trim_end_matches('/');
if named.len() < path.len()
&& !named.is_empty()
&& self.includes.loader.kind(&self.includes.resolve(named)) == Some(FileKind::File)
{
self.includes.reached(super::Uncertain::TrailingSlash);
}
let candidate = self.includes.resolve(path);
return self.include_candidate(&candidate, path, request, request.key.clone());
}
let expansion = if path.is_empty() {
glob::Expansion::default()
} else {
glob::expand(self.includes.loader, &self.includes.base, path)
};
if expansion.left_out_link {
self.includes.reached(super::Uncertain::TrailingSlash);
}
let matches = expansion.paths;
let Some(first) = matches.first() else {
return Ok(if request.try_ {
Outcome::Done
} else {
Outcome::Unusable(ErrorKind::FileNotFound {
path: path.to_owned(),
})
});
};
let key = request.key.clone().or_else(|| {
request.prefix.then(|| {
let first = self.includes.loader.canonicalize(first);
prefix_key(first.as_deref().unwrap_or(&matches[0]))
})
});
let mut included = false;
let mut skipped_self = None;
for candidate in &matches {
let shown = candidate.to_string_lossy().into_owned();
match self.include_candidate(candidate, &shown, request, key.clone())? {
Outcome::Done => included = true,
Outcome::Unusable(_) if request.try_ => {}
Outcome::Unusable(kind @ ErrorKind::IncludeSelf { .. }) if request.soft => {
skipped_self = Some(kind);
}
Outcome::Unusable(kind) => return Err(self.error(kind, request.at)),
}
}
match skipped_self {
Some(kind) if !included => Err(self.error(kind, request.at)),
_ => Ok(Outcome::Done),
}
}
fn unusable_missing(&self, shown: &str, request: &Request) -> Result<Outcome, Error> {
let kind = ErrorKind::FileNotFound {
path: shown.to_owned(),
};
if request.soft {
Ok(Outcome::Unusable(kind))
} else if request.try_ {
Ok(Outcome::Done)
} else {
Err(self.error(kind, request.at))
}
}
fn include_candidate(
&mut self,
candidate: &Path,
shown: &str,
request: &Request,
key: Option<String>,
) -> Result<Outcome, Error> {
let loader = self.includes.loader;
let Ok(canonical) = loader.canonicalize(candidate) else {
return self.unusable_missing(shown, request);
};
let not_a_file = || ErrorKind::NotAFile {
path: shown.to_owned(),
};
let unusable = |this: &Self, kind: ErrorKind| {
if !request.try_ {
Err(this.error(kind, request.at))
} else if request.soft {
Ok(Outcome::Unusable(kind))
} else {
Ok(Outcome::Done)
}
};
match loader.kind(&canonical) {
Some(FileKind::File) => {}
None => return self.unusable_missing(shown, request),
Some(_) => return unusable(self, not_a_file()),
}
if self.includes.files.last().and_then(Option::as_ref) == Some(&canonical) {
let kind = ErrorKind::IncludeSelf {
path: shown.to_owned(),
};
return if request.soft {
Ok(Outcome::Unusable(kind))
} else {
Err(self.error(kind, request.at))
};
}
if self.includes.files.len() >= MAX_INCLUDE_DEPTH {
return Err(self.error(
ErrorKind::IncludeTooDeep {
limit: MAX_INCLUDE_DEPTH,
},
request.at,
));
}
let bytes = match self.includes.read(&canonical) {
Ok(Read::Bytes(bytes)) => bytes,
Ok(Read::TooLarge { limit }) => {
let path = Some(shown.to_owned());
return Err(self.error(ErrorKind::InputTooLarge { limit, path }, request.at));
}
Err(_) => return unusable(self, not_a_file()),
};
let key = key.or_else(|| request.prefix.then(|| prefix_key(&canonical)));
self.include_unit(&bytes, &canonical, request, key)?;
Ok(Outcome::Done)
}
fn include_unit(
&mut self,
bytes: &[u8],
canonical: &Path,
request: &Request,
key: Option<String>,
) -> Result<(), Error> {
let open = self.open_containers();
if let Some(key) = key.clone() {
let target = NestTarget {
key,
array: request.array,
priority: request.settings.priority,
};
self.open_nest_target(&target, request.at)?;
}
let filename = canonical.to_string_lossy().into_owned();
let curdir = canonical
.parent()
.map(|d| d.to_string_lossy().into_owned())
.unwrap_or_default();
let saved = self.expander.enter_file(filename, curdir);
self.includes.files.push(Some(canonical.to_path_buf()));
let result = self.parse_included(bytes, request.settings, Some(canonical));
if !result.as_ref().is_err_and(Error::is_stopped) {
self.includes.files.pop();
self.expander.leave_file(saved);
}
result?;
if key.is_some() {
self.close_containers_above(open);
}
Ok(())
}
#[cfg(feature = "load")]
pub(super) fn load_macro(&mut self, call: &MacroCall) -> Result<(), Error> {
use crate::value::{Entry, ParserFlags, Slot};
let params = call.args.resolve(MacroKind::Load.parameters());
let try_ = params.bool("try").unwrap_or(false);
let Some(key) = params.string("key").filter(|k| !k.is_empty()) else {
return Err(self.error(ErrorKind::LoadKeyMissing, call.at));
};
let key = key.to_owned();
let path = self.macro_path(call)?;
if call.value.is_empty() {
return Err(self.error(ErrorKind::FileNotFound { path }, call.value_at));
}
let loader = self.includes.loader;
let not_found = || ErrorKind::FileNotFound { path: path.clone() };
let not_a_file = || ErrorKind::NotAFile { path: path.clone() };
let written = self.includes.resolve(&path);
let read = match loader.kind(&written) {
_ if path.is_empty() => Err(not_found()),
None => Err(not_found()),
Some(FileKind::File) => self.includes.read(&written).map_err(|_| not_a_file()),
Some(_) => Err(not_a_file()),
};
let bytes = match read {
Ok(Read::Bytes(bytes)) => bytes,
Ok(Read::TooLarge { limit }) => {
let path = Some(path.clone());
return Err(self.error(ErrorKind::InputTooLarge { limit, path }, call.value_at));
}
Err(_) if try_ => return Ok(()),
Err(kind) => return Err(self.error(kind, call.value_at)),
};
let key_lowercase = self.settings.flags.contains(ParserFlags::KEY_LOWERCASE);
let exists = self.find_current_key(&key, key_lowercase).is_some();
if exists {
return Err(self.error(ErrorKind::LoadKeyExists { key }, call.at));
}
let target = params.string("target").unwrap_or("string");
let value = if target.eq_ignore_ascii_case("string") {
if bytes.is_empty() {
return Ok(());
}
let mut text = bytes;
if params.bool("trim") == Some(true) {
text = trim(&text).to_vec();
}
if params.bool("escape") == Some(true) {
text = escape(&text);
}
let text = String::from_utf8(text)
.map_err(|_| self.error(ErrorKind::InvalidUtf8, call.value_at))?;
UclValue::String(text)
} else if target.eq_ignore_ascii_case("int") {
UclValue::Integer(leading_integer(&bytes))
} else {
return Ok(());
};
let priority = params.int("priority").map_or(0, priority_bits);
if key_lowercase && key.bytes().any(|b| b.is_ascii_uppercase()) {
self.uppercase_keys = true;
}
let multiline = value.is_string() && params.bool("multiline") == Some(true);
self.current()
.as_object_mut()
.expect("macros are read inside objects")
.insert_entry(key.clone(), Entry::from_slot(Slot::new(value, priority)));
let locating = self
.facts
.as_ref()
.is_some_and(super::OutputFacts::records_locations);
if (multiline || locating)
&& let Some(node) =
self.facts_node_below(&[crate::parse::PathSegment::Key { key, index: 0 }])
{
let facts = self.facts.as_mut().expect("checked above");
if multiline {
facts.update(node, |f| f.multiline = true);
}
facts.locate(node, call.value_at, Some(call.at));
}
Ok(())
}
}
#[cfg(feature = "load")]
fn is_load_space(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | b'\r' | 0x0B | 0x0C)
}
#[cfg(feature = "load")]
fn trim(bytes: &[u8]) -> &[u8] {
let start = bytes.iter().take_while(|&&b| is_load_space(b)).count();
let rest = &bytes[start..];
let end = rest.len() - rest.iter().rev().take_while(|&&b| is_load_space(b)).count();
&rest[..end]
}
#[cfg(feature = "load")]
fn escape(bytes: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(bytes.len());
for &b in bytes {
match b {
b'"' => out.extend_from_slice(b"\\\""),
b'\\' => out.extend_from_slice(b"\\\\"),
b'\n' => out.extend_from_slice(b"\\n"),
b'\r' => out.extend_from_slice(b"\\r"),
b'\t' => out.extend_from_slice(b"\\t"),
0x08 => out.extend_from_slice(b"\\b"),
0x0C => out.extend_from_slice(b"\\f"),
0 => out.extend_from_slice(b"\\u0000"),
0x0B => out.extend_from_slice(b"\\u000B"),
b => out.push(b),
}
}
out
}
#[cfg(feature = "load")]
fn leading_integer(bytes: &[u8]) -> i64 {
let start = bytes.iter().take_while(|&&b| is_load_space(b)).count();
let (negative, digits) = match &bytes[start..] {
[b'-', rest @ ..] => (true, rest),
[b'+', rest @ ..] => (false, rest),
rest => (false, rest),
};
let limit = i128::from(i64::MAX) + 1;
let magnitude = digits
.iter()
.take_while(|b| b.is_ascii_digit())
.fold(0i128, |n, &d| (n * 10 + i128::from(d - b'0')).min(limit));
let n = if negative { -magnitude } else { magnitude };
n.clamp(i128::from(i64::MIN), i128::from(i64::MAX)) as i64
}
#[cfg(all(test, feature = "load"))]
mod load_tests {
use super::*;
#[test]
fn load_helpers() {
assert_eq!(trim(b"\x0b\x0c\t \r\nx y\r\n\x0b\x0c \t"), b"x y");
assert_eq!(trim(b" \t\n "), b"");
assert_eq!(
escape(b"a\tb\x08c\x0cd\re\x0bf\"g\\h\x00i j\n"),
b"a\\tb\\bc\\fd\\re\\u000Bf\\\"g\\\\h\\u0000i j\\n".to_vec()
);
for (text, n) in [
(&b"42\n"[..], 42),
(b"42abc", 42),
(b"\n\t +17 rest", 17),
(b"abc", 0),
(b"", 0),
(b"12\x0034", 12),
(b"-99999999999999999999", i64::MIN),
(b"99999999999999999999", i64::MAX),
(b"- 1", 0),
] {
assert_eq!(leading_integer(text), n, "{text:?}");
}
}
}
#[cfg(test)]
mod tests {
use crate::parse::{
CommentPlacement, Error, ErrorKind, MAX_INCLUDE_DEPTH, MemoryLoader, Parser, PathSegment,
};
use crate::value::{DuplicateStrategy, ParserFlags, UclObject, UclValue};
use std::path::Path;
fn parser(files: &[(&str, &str)], flags: ParserFlags) -> Parser {
let mut loader = MemoryLoader::new();
loader.add_dir("/c/dir");
for (path, text) in files {
loader.add_file(path, *text);
}
let mut p = Parser::with_flags(flags);
p.set_loader(loader).set_base_dir("/c");
p
}
fn run(files: &[(&str, &str)], input: &str) -> Result<UclValue, Error> {
parser(files, ParserFlags::DEFAULT).parse(input.as_bytes())
}
fn obj(v: &UclValue) -> &UclObject {
v.as_object().expect("object")
}
fn keys(v: &UclValue) -> Vec<String> {
obj(v).keys().cloned().collect()
}
const A: (&str, &str) = ("/c/files/a.inc", "x = 1\ny = \"inc\"\n");
#[test]
fn uncertain_rules_reached_are_recorded() {
use crate::parse::Uncertain;
let files = [
("/c/elem.inc", "a = [ {\n.include \"sep.inc\""),
("/c/sep.inc", "x \"y{\" = \n"),
("/c/close.inc", "a = 1 }\n"),
("/c/left_open.inc", "x \"y{\" z"),
("/c/v.inc", "v = 1"),
("/c/reopen_int.inc", "\"s\".include {v.inc} # c"),
];
let reached = |input: &str| {
let mut p = parser(&files, ParserFlags::DEFAULT);
let _ = p.parse(input.as_bytes());
p.uncertain_reached()
};
assert_eq!(
reached(".include \"elem.inc\"x {.include \"close.inc\""),
[Uncertain::EndedUnitContainer]
);
assert_eq!(reached("a {\n.include \"left_open.inc\"\nk = 1"), []);
assert_eq!(
reached("a = [ { .include \"close.inc\"\n]"),
[Uncertain::ClosedArrayElement]
);
assert_eq!(
reached(".include \"reopen_int.inc\"\nk = 1"),
[Uncertain::ReopenedNotObject]
);
assert_eq!(reached("a = 1"), []);
}
#[cfg(feature = "fs")]
#[test]
fn a_trailing_slash_after_a_file_is_uncertain() {
use crate::parse::{FsLoader, Uncertain};
let dir =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/conformance/cases/spec/09-macros");
let reached = |input: &str| {
let mut p = Parser::new();
p.set_loader(FsLoader::new()).set_base_dir(&dir);
let _ = p.parse(input.as_bytes());
p.uncertain_reached()
};
for input in [
".include(glob=true, try=true) \"files/v4/*/\"",
".include(glob=true) \"files/v4/l*/\"",
".include \"files/c.conf/\"",
".try_include \"files/v4/link.inc//\"",
] {
assert_eq!(reached(input), [Uncertain::TrailingSlash], "{input}");
}
for input in [
".include(glob=true) \"files/v4/g/a.in?/\"",
".include(glob=true, try=true) \"files/v4/g/*\"",
".try_include \"files/v4/dir/\"",
".include \"files/c.conf\"",
] {
assert_eq!(reached(input), [], "{input}");
}
}
#[test]
fn a_unit_that_is_only_its_leading_bracket_adds_nothing() {
let files = [
("/c/b.inc", "{"),
("/c/nb.inc", "\n{"),
("/c/k.inc", "["),
("/c/sk.inc", " \t["),
("/c/cb.inc", "# c\n{"),
("/c/bb.inc", "/* c */{"),
("/c/ck.inc", "/* c */["),
("/c/lk.inc", "# c\n["),
("/c/sp.inc", "{ "),
("/c/ncb.inc", "\n# c\n{"),
("/c/csb.inc", "# c\n {"),
];
for file in [
"b.inc", "nb.inc", "k.inc", "sk.inc", "cb.inc", "bb.inc", "ck.inc", "lk.inc",
] {
let v = run(&files, &format!("a = 1\n.include \"{file}\"\nb = 2")).unwrap();
assert_eq!(keys(&v), ["a", "b"], "{file}");
let v = run(
&files,
&format!("x {{ .include \"{file}\"\nb = 2 }}\nc = 3"),
)
.unwrap();
assert_eq!(keys(&v), ["x", "c"], "{file}");
assert_eq!(keys(&obj(&v)["x"]), ["b"], "{file}");
}
assert!(run(&files, "a = 1\n.include \"b.inc\"\nb = 2\n}").is_err());
let v = run(&files, "x { .include \"sp.inc\"\nb = 2 }\nc = 3").unwrap();
assert_eq!(keys(&v), ["x"]);
assert_eq!(keys(&obj(&v)["x"]), ["b", "c"]);
for file in ["ncb.inc", "csb.inc"] {
assert!(
run(&files, &format!("a = 1\n.include \"{file}\"\nb = 2")).is_err(),
"{file}"
);
}
let v = run(&files, ".include(key=\"k\") \"b.inc\"\nb = 2").unwrap();
assert_eq!(keys(&v), ["k", "b"]);
assert!(obj(&obj(&v)["k"]).is_empty());
let mut p = parser(&files, ParserFlags::DEFAULT);
p.register_macro("emit", |call| {
let text = call.value().to_vec();
call.parse(text)
});
let v = p
.parse(b"a = 1\n.emit \"{\"\nb = 2\no { .emit \"{\" }\nc = 3\n.emit \" [\"")
.unwrap();
assert_eq!(keys(&v), ["a", "b", "o", "c"]);
assert!(obj(&obj(&v)["o"]).is_empty());
for text in ["[1]", "[]", "[1", "[ "] {
let e = p
.parse(format!("a = 1\n.emit \"{text}\"\nb = 2").as_bytes())
.unwrap_err();
assert_eq!(e.kind(), &ErrorKind::IncludeArrayRoot, "{text}");
}
}
#[test]
fn a_nul_byte_ends_a_path() {
let files = [A, ("/c/text.txt", "hello")];
for input in [".include {files/a.inc\0zzz}", ".include {files/a.inc\0}"] {
assert_eq!(keys(&run(&files, input).unwrap()), ["x", "y"], "{input:?}");
}
assert!(matches!(
run(&files, "a = 1\n.include {\0}\nb = 2").unwrap_err().kind(),
ErrorKind::FileNotFound { path } if path.is_empty()
));
let e = run(&files, "a = 1\n.try_include {\0}\nb = 2").unwrap_err();
assert!(e.is_stopped(), "{e}");
assert_eq!(keys(e.partial().unwrap()), ["a"]);
#[cfg(feature = "load")]
{
let v = run(&files, ".load(key=\"k\") {text.txt\0zz}").unwrap();
assert_eq!(obj(&v)["k"].as_str(), Some("hello"));
for input in [".load(key=\"k\") {\0zz}", ".load(key=\"k\", try=true) \"\""] {
assert!(
matches!(
run(&files, input).unwrap_err().kind(),
ErrorKind::FileNotFound { path } if path.is_empty()
),
"{input:?}"
);
}
let v = run(&files, ".load(key=\"k\", try=true) {\0zz}\nb = 2").unwrap();
assert_eq!(keys(&v), ["b"]);
}
}
#[test]
fn a_wildcard_after_a_nul_byte_makes_a_pattern() {
let files = [A];
let v = run(&files, "a = 1\n.include(glob=true) {files/a.inc\0*}\nb = 2").unwrap();
assert_eq!(keys(&v), ["a", "x", "y", "b"]);
for input in [
"a = 1\n.include(glob=true) {files/nomatch\0*}\nb = 2",
"a = 1\n.include(glob=true) {\0*}\nb = 2",
] {
let e = run(&files, input).unwrap_err();
assert!(e.is_stopped(), "{input:?}: {e}");
assert_eq!(keys(e.partial().unwrap()), ["a"], "{input:?}");
}
let v = run(
&files,
"a = 1\n.include(glob=true, try=true) {files/nomatch\0?}\nb = 2",
);
assert_eq!(keys(&v.unwrap()), ["a", "b"]);
for input in [
"a = 1\n.include {files/nomatch\0*}\nb = 2",
"a = 1\n.include(glob=true) {files/nomatch\0}\nb = 2",
] {
let e = run(&files, input).unwrap_err();
assert!(
matches!(e.kind(), ErrorKind::FileNotFound { .. }),
"{input:?}: {e}"
);
}
let e = run(
&files,
"a = 1\n.include(glob=true, path=[\".\"]) {files/a.in\0?}\nb = 2",
)
.unwrap_err();
assert!(matches!(e.kind(), ErrorKind::FileNotFound { .. }), "{e}");
}
#[test]
fn string_parameters_end_at_a_nul_byte() {
let files = [A, ("/c/text.txt", "hello"), ("/c/num.txt", "42")];
let v = run(&files, ".include(key=\"s\\u0000t\") \"files/a.inc\"").unwrap();
assert_eq!(keys(&v), ["s"]);
let v = run(
&files,
".include(key=\"s\\u0000t\", prefix=true) \"files/a.inc\"",
)
.unwrap();
assert_eq!(keys(&v), ["s"]);
let v = run(&files, ".include(key=\"\\u0000t\") \"files/a.inc\"").unwrap();
assert_eq!(keys(&v), [""]);
let v = run(&files, ".include(path=[\"files\\u0000zz\"]) \"a.inc\"").unwrap();
assert_eq!(keys(&v), ["x", "y"]);
let v = run(
&files,
"x = 1\n.include(duplicate=\"rewrite\\u0000zz\") \"files/a.inc\"",
)
.unwrap();
assert_eq!(obj(&v).entry("x").unwrap().len(), 1);
let v = run(
&files,
"x = 5\n.include(key=\"x\", target=\"array\\u0000q\") \"files/a.inc\"",
)
.unwrap();
assert_eq!(obj(&v)["x"].as_array().map(Vec::len), Some(2));
#[cfg(feature = "load")]
{
let v = run(&files, ".load(key=\"s\\u0000t\") \"text.txt\"").unwrap();
assert_eq!(keys(&v), ["s"]);
let e = run(&files, ".load(key=\"\\u0000t\") \"text.txt\"").unwrap_err();
assert_eq!(e.kind(), &ErrorKind::LoadKeyMissing);
let v = run(
&files,
".load(key=\"k\", target=\"int\\u0000z\") \"num.txt\"",
)
.unwrap();
assert_eq!(obj(&v)["k"], UclValue::Integer(42));
}
}
#[test]
fn included_entries_go_where_the_macro_stands() {
let v = run(
&[A],
"k = 0\ns { .include \"files/a.inc\" }\n.include {files/a.inc}",
)
.unwrap();
assert_eq!(keys(&v), ["k", "s", "x", "y"]);
assert_eq!(keys(&obj(&v)["s"]), ["x", "y"]);
let v = run(
&[A],
".priority 5\nx = 0\n.include(priority=2) \"files/a.inc\"",
)
.unwrap();
assert_eq!(obj(&v).entry("x").unwrap().slots()[0].priority(), 5);
let v = run(
&[A],
"x = 0\n.include(priority=17, duplicate=\"rewrite\") \"files/a.inc\"",
)
.unwrap();
let x = obj(&v).entry("x").unwrap();
assert_eq!((x.len(), x.slots()[0].priority()), (1, 1));
}
#[test]
fn file_variables_and_relative_paths() {
let files = [
(
"/c/sub/one.inc",
"f = \"$FILENAME\"\nd = \"${CURDIR}\"\n.include \"files/a.inc\"\n",
),
A,
];
let v = run(
&files,
".include \"sub/one.inc\"\ng = \"$FILENAME $CURDIR\"",
)
.unwrap();
let o = obj(&v);
assert_eq!(o["f"].as_str(), Some("/c/sub/one.inc"));
assert_eq!(o["d"].as_str(), Some("/c/sub"));
assert_eq!(o["x"], UclValue::Integer(1));
assert_eq!(o["g"].as_str(), Some("undef /c"));
let v = parser(&files, ParserFlags::NO_FILEVARS)
.parse(b"a = \"$FILENAME\"\n.include \"sub/one.inc\"\ng = \"$CURDIR\"")
.unwrap();
assert_eq!(obj(&v)["a"].as_str(), Some("$FILENAME"));
assert_eq!(obj(&v)["g"].as_str(), Some("/c/sub"));
}
#[test]
fn parse_file_reads_through_the_loader() {
let files = [(
"/c/main.conf",
"n = \"$FILENAME\"\n.include \"main.conf\"\n",
)];
for flags in [ParserFlags::DEFAULT, ParserFlags::NO_FILEVARS] {
let e = parser(&files, flags).parse_file("main.conf").unwrap_err();
assert!(matches!(e.kind(), ErrorKind::IncludeSelf { .. }), "{e}");
}
let files = [("/c/main.conf", "n = \"$FILENAME\"\n")];
let v = parser(&files, ParserFlags::NO_FILEVARS)
.parse_file("/c/./main.conf")
.unwrap();
assert_eq!(obj(&v)["n"].as_str(), Some("/c/main.conf"));
let e = parser(&files, ParserFlags::DEFAULT)
.parse_file("missing.conf")
.unwrap_err();
assert!(matches!(e.kind(), ErrorKind::Io { .. }));
}
#[test]
fn missing_and_unusable_files() {
let files = [
A,
(
"/c/self.inc",
"s = 1\n.try_include \"${CURDIR}/self.inc\"\nt = 2\n",
),
];
let stopped = |input: &str| {
let e = run(&files, input).unwrap_err();
assert!(e.is_stopped(), "{input:?}: {e}");
keys(e.partial().unwrap())
};
let failed = |input: &str| {
let e = run(&files, input).unwrap_err();
assert!(!e.is_stopped(), "{input:?}");
e.kind().clone()
};
let keys_of = |input: &str| keys(&run(&files, input).unwrap());
for input in [
"a = 1\n.try_include \"nope\"\nk = 1",
"a = 1\n.try_include(try=false) \"nope\"\nk = 1",
"a = 1\n.try_include \"\"\nk = 1",
"a = 1\n.try_include \"dir\"\nk = 1",
"a = 1\n.try_include()#",
] {
assert_eq!(stopped(input), ["a"], "{input:?}");
}
assert!(matches!(
failed(".include \"nope\""),
ErrorKind::FileNotFound { .. }
));
assert!(matches!(
failed(".include \"dir\""),
ErrorKind::NotAFile { .. }
));
assert!(matches!(
failed(".try_include(try=false) \"dir\""),
ErrorKind::NotAFile { .. }
));
assert_eq!(keys_of(".include(try=true) \"nope\"\nk = 1"), ["k"]);
assert_eq!(keys_of(".include(try=true) \"dir\"\nk = 1"), ["k"]);
assert_eq!(keys_of(".include(try=true) \"\"\nk = 1"), ["k"]);
let e = run(&files, ".include \"self.inc\"\nk = 1").unwrap_err();
assert!(e.is_stopped());
assert_eq!(e.file(), Some(Path::new("/c/self.inc")));
assert_eq!(e.position().line, 2);
assert_eq!(keys(e.partial().unwrap()), ["s"]);
let files = [("/c/self.inc", ".include(try=true) \"${CURDIR}/self.inc\"\n")];
let e = run(&files, ".include \"self.inc\"").unwrap_err();
assert!(matches!(e.kind(), ErrorKind::IncludeSelf { .. }));
}
#[test]
fn nesting_limit() {
let mut files = Vec::new();
for i in 1..=20 {
files.push((
format!("/c/{i}.inc"),
format!("l{i} = 1\n.include \"${{CURDIR}}/{}.inc\"\n", i + 1),
));
}
files.push(("/c/21.inc".into(), "end = 1\n".into()));
let files: Vec<(&str, &str)> = files
.iter()
.map(|(p, t)| (p.as_str(), t.as_str()))
.collect();
let v = run(&files, ".include \"7.inc\"").unwrap();
assert_eq!(obj(&v).len(), 15);
let e = run(&files, ".include \"6.inc\"").unwrap_err();
assert_eq!(
e.kind(),
&ErrorKind::IncludeTooDeep {
limit: MAX_INCLUDE_DEPTH
}
);
assert_eq!(e.file(), Some(Path::new("/c/20.inc")));
}
#[test]
fn errors_in_included_files_name_the_file() {
let files = [("/c/bad.inc", "a = 1\nb = \"open")];
let e = run(&files, "x = 1\n.include \"bad.inc\"").unwrap_err();
assert_eq!(e.kind(), &ErrorKind::UnterminatedString);
assert_eq!(e.file(), Some(Path::new("/c/bad.inc")));
assert_eq!((e.position().line, e.position().column), (2, 5));
assert!(e.to_string().ends_with("of /c/bad.inc)"), "{e}");
let e = run(&[], "x = 1\n.include \"nope.inc\"").unwrap_err();
assert_eq!(e.file(), None);
assert_eq!(e.position().line, 2);
}
#[test]
fn braces_around_included_files() {
let files = [
("/c/braced.inc", "{ a = 1 }\n"),
("/c/open.inc", "{ a = 1\n"),
("/c/close.inc", "a = 1 }\n"),
("/c/left_open.inc", "x \"y{\" z\n"),
("/c/array.inc", "[1]\n"),
("/c/unclosed.inc", "b {\n"),
];
let ok = |input: &str| run(&files, input).unwrap();
let err = |input: &str| run(&files, input).unwrap_err().kind().clone();
assert_eq!(keys(&ok(".include \"braced.inc\"\nq = 1")), ["a", "q"]);
assert!(matches!(
err("x { .include \"braced.inc\" }"),
ErrorKind::UnmatchedClose { .. }
));
assert_eq!(
keys(&ok(".include \"open.inc\"\nq = 1\n}\nr = 2")),
["a", "q", "r"]
);
assert_eq!(
err(".include \"open.inc\"\nq = 1"),
ErrorKind::UnterminatedObject
);
let v = ok("x { .include \"open.inc\"\n}\nq = 1");
assert_eq!(keys(&obj(&v)["x"]), ["a", "q"]);
let v = ok("x { .include \"close.inc\"\nq = 1");
assert_eq!(keys(&v), ["x", "q"]);
assert!(matches!(
err(".include \"close.inc\""),
ErrorKind::UnmatchedClose { .. }
));
assert_eq!(
err(".include \"unclosed.inc\"\n}"),
ErrorKind::UnterminatedObject
);
assert_eq!(err(".include \"array.inc\""), ErrorKind::IncludeArrayRoot);
let v = ok(".include \"left_open.inc\"\nk = 1");
assert_eq!(keys(&obj(&v)["x"]), ["y{", "k"]);
let v = ok("x \"y{\" z\n.include \"braced.inc\"\nq = 1");
assert_eq!(keys(&v), ["x", "q"]);
let v = ok("x \"y{\" z\n.include \"open.inc\"\nm = [1]\nn = 1");
assert_eq!(keys(&v), ["x", "n"]);
assert_eq!(keys(&obj(&v)["x"]), ["y{", "a", "m"]);
assert!(matches!(
err("x \"y{\" z\n.include \"open.inc\"\nm { }\n}"),
ErrorKind::UnmatchedClose { .. }
));
assert_eq!(
err("x \"y{\" z\n.include \"open.inc\"\nq = 1"),
ErrorKind::UnterminatedObject
);
let v = ok("a { b {\n.include \"left_open.inc\"\nk = 1");
assert_eq!(keys(&obj(&obj(&obj(&v)["a"])["b"])["x"]), ["y{", "k"]);
assert_eq!(
err("a {\n.include \"left_open.inc\"\nm { n = 1 }"),
ErrorKind::UnterminatedObject
);
let v = ok("x { .include(key=\"k\") \"braced.inc\"\ny = 1 }\nz = 2");
assert_eq!(keys(&obj(&v)["x"]), ["k", "y"]);
let v = ok(".include(key=\"k\") \"left_open.inc\"\nq = 1");
assert_eq!(keys(&v), ["k", "q"]);
assert!(matches!(
err(".include(key=\"k\") \"close.inc\"\nq = 1"),
ErrorKind::UnmatchedClose { .. }
));
let v = ok(".include \"open.inc\"\n.include(key=\"k\") \"close.inc\"\nq = 1\n}");
assert_eq!(keys(&v), ["a", "k", "q"]);
assert_eq!(keys(&obj(&v)["k"]), ["a"]);
let v = ok(
".include \"open.inc\"\n.include(key=\"k\", target=\"array\") \"close.inc\"\nq = 1\n}",
);
assert_eq!(keys(&v), ["a", "k", "q"]);
assert_eq!(keys(&obj(&v)["k"].as_array().unwrap()[0]), ["a"]);
let v = ok(".include \"open.inc\"\n.include(prefix=true) \"close.inc\"\nq = 1\n}");
assert_eq!(keys(&v), ["a", "close.inc", "q"]);
let v = ok(".include \"open.inc\"\n.include(key=\"k\") \"close.inc\"\n\
.include(key=\"k\") \"close.inc\"\nq = 1\n}");
assert_eq!(obj(&obj(&v)["k"]).entry("a").unwrap().len(), 2);
let v = ok(
"x \"y{\" z\n.include \"open.inc\"\n.include(key=\"k\") \"close.inc\"\nq = 1\n}\nr = 2",
);
assert_eq!(keys(&v), ["x", "r"]);
assert_eq!(keys(&obj(&v)["x"]), ["y{", "a", "k", "q"]);
let v = ok(".include \"open.inc\"\n.include(key=\"k\") \"braced.inc\"\nq = 1\n}");
assert_eq!(keys(&v), ["a", "k", "q"]);
for input in [
".include \"open.inc\"\n.include(key=\"k\") \"close2.inc\"\nq = 1",
".include \"open.inc\"\n.include(key=\"k\") \"close.inc\"\nq = 1",
".include \"open.inc\"\n.include \"open.inc\"\n.include(key=\"k\") \"close.inc\"\nq = 1\n}\n}",
"x { .include(key=\"k\") \"close.inc\"\nq = 1",
] {
assert!(run(&files_with_close2(&files), input).is_err(), "{input:?}");
}
}
#[test]
fn macros_directly_after_a_name() {
let files = [
A,
("/c/o.inc", "o {}\n"),
("/c/m.inc", "m {}\n"),
("/c/closed.inc", "\"s\".include \"o.inc\" # [\n"),
("/c/closed_ws.inc", "\"s\".include {o.inc}\n"),
(
"/c/closed_later.inc",
"\"s\".include {o.inc} # c\n.priority {1} # d\n",
),
(
"/c/closed_then_macro.inc",
"\"s\".include {o.inc} # c\n.priority {1}\n",
),
("/c/closed_brace.inc", "x {\n\"s\".include {o.inc} # c\n}\n"),
("/c/deep.inc", "\"s\".include(key=\"k\") \"m.inc\" # [\n"),
("/c/unit.inc", "a {\n\"s\".include(key=\"k\") {m.inc} # c\n"),
("/c/lower.inc", ".priority 1\n\"s\".include {o.inc} # c\n"),
("/c/key.inc", "\"s\".include {o.inc}\nz = 2\n"),
];
for flags in [ParserFlags::DEFAULT, ParserFlags::SAVE_COMMENTS] {
let ok = |input: &str| parser(&files, flags).parse(input.as_bytes()).unwrap();
let err = |input: &str| {
let result = parser(&files, flags).parse(input.as_bytes());
assert!(result.is_err(), "{input:?}");
};
for input in [
"\"s\".priority {3}k = [1]",
"\"s\".priority {3}\n\"k\" = [1]",
"\"s\".include \"files/a.inc\"k = [1]",
"\"s\".priority {3}\n.priority 4\nk = [1]",
"\"s\".priority {3}\n.include \"files/a.inc\"\nk = [1]",
"x { \"s\".include {o.inc} }\nk = [1]",
"\"s\".priority {3}\na = 1",
"\"s\".priority {3}\nk = \n{ z = 1 }",
".include \"key.inc\"",
] {
err(input);
}
let v = ok("\"s\".priority {3}k = l = n { z = 1 }\nm = 1");
assert_eq!(keys(&v), ["s", "m"]);
let l = &obj(&obj(&obj(&v)["s"])["k"])["l"];
assert_eq!(obj(l).entry("n").unwrap().slots()[0].priority(), 3);
let v = ok("\"s\".priority {3}\nk =\nl { z = 1 }");
assert_eq!(keys(&obj(&obj(&obj(&v)["s"])["k"])["l"]), ["z"]);
let v = ok("x { \"s\".priority {3}k = l {} }\nm = 1");
assert_eq!(keys(&v), ["x", "m"]);
let v = ok("\"s\".priority {3}\nk \"b{\" z\nm = [1]");
assert_eq!(keys(&obj(&obj(&v)["s"])["k"]), ["b{", "m"]);
let v = ok(".include \"closed.inc\"\nk = 1");
assert_eq!(keys(&v), ["s"]);
assert_eq!(keys(&obj(&v)["s"]), ["o", "k"]);
let v = ok(".include \"closed_later.inc\"\nk = 1");
assert_eq!(keys(&obj(&v)["s"]), ["o", "k"]);
for input in [
".include \"closed_ws.inc\"\nk = 1",
".include \"closed_then_macro.inc\"\nk = 1",
] {
assert_eq!(keys(&ok(input)), ["s", "k"], "{input:?}");
}
assert_eq!(
keys(&ok(".include \"closed_brace.inc\"\nk = 1")),
["x", "k"]
);
let v = ok(".include \"deep.inc\"\nk2 = 1");
assert_eq!(keys(&obj(&obj(&obj(&v)["s"])["k"])["m"]), ["k2"]);
let v = ok("a {\n.include \"closed.inc\"\nk = 1");
assert_eq!(keys(&obj(&obj(&v)["a"])["s"]), ["o", "k"]);
err("a {\n.include \"closed.inc\"\nk = 1\n}\nm = 1");
err(".include \"unit.inc\"\nk2 = 1");
let v = ok(".priority 5\ns = 1\n.include \"lower.inc\"\nk = 1");
assert_eq!(keys(&v), ["s"]);
}
}
#[test]
fn end_of_unit_check_and_first_key_share() {
let files = [
("/c/left_open.inc", "x \"y{\" z\n"),
("/c/braced.inc", "{ a = 1 }\n"),
("/c/lo_in_b.inc", "b {\n.include \"left_open.inc\"\n"),
("/c/lost_brace.inc", "x { .include {braced.inc}\n"),
("/c/arr.inc", "a = [ {\n.include \"left_open.inc\"\n"),
("/c/first.inc", "{\nx \"y{\" z\n"),
("/c/first_closed.inc", "{ x \"y{\" z\n}\n"),
("/c/first_closed2.inc", "{ x \"y{\" z\n}\n}\n"),
("/c/first_two_names.inc", "{ a b \"y{\" z\n}\n"),
(
"/c/first_after_macro.inc",
"{\n.priority 1\nx \"y{\" z\n}\n",
),
("/c/second.inc", "{ a = 1\nx \"y{\" z\n"),
("/c/first_run.inc", "{ \"s\".priority {3}\n}\n"),
("/c/brace_gone.inc", "{}x \"y{\" z\n"),
];
let ok = |input: &str| run(&files, input).unwrap();
let err = |input: &str| assert!(run(&files, input).is_err(), "{input:?}");
let v = ok("a {\n.include \"lo_in_b.inc\"\nm {}");
assert_eq!(keys(&obj(&obj(&obj(&v)["a"])["b"])["x"]), ["y{", "m"]);
let v = ok("a {\n.include \"lost_brace.inc\"\nk = 1");
assert_eq!(keys(&obj(&obj(&v)["a"])["x"]), ["a", "k"]);
let v = ok(".include \"arr.inc\"\nm { n = 1 }\n}");
assert_eq!(keys(&v), ["a"]);
for input in [
".include \"first.inc\"",
".include \"first.inc\"\nq = 1\n}",
"a {\n.include \"first.inc\"\nk = 1",
".include \"first_closed.inc\"\nq = 1",
".include \"first_closed2.inc\"\nq = 1\n}",
".include \"first_two_names.inc\"\nq = 1\n}",
".include \"first_after_macro.inc\"\nq = 1",
".include \"first_run.inc\"\nq = 1",
".include \"second.inc\"\nq = 1\n}",
] {
err(input);
}
assert_eq!(
keys(&ok(".include \"first_closed.inc\"\nq = 1\n}")),
["x", "q"]
);
assert_eq!(
keys(&ok(".include \"first_closed2.inc\"\nq = 1")),
["x", "q"]
);
assert_eq!(
keys(&ok(".include \"first_after_macro.inc\"\nq = 1\n}")),
["x", "q"]
);
assert_eq!(
keys(&ok(".include \"first_run.inc\"\nq = 1\n}")),
["s", "q"]
);
assert_eq!(
keys(&ok(".include(key=\"k\") \"first_closed.inc\"\nq = 1")),
["k", "q"]
);
let v = ok(".include \"second.inc\"\nq = 1");
assert_eq!(keys(&obj(&v)["x"]), ["y{", "q"]);
let v = ok(".include \"brace_gone.inc\"\nq = 1");
assert_eq!(keys(&obj(&v)["x"]), ["y{", "q"]);
}
#[test]
fn empty_files_and_merged_nulls() {
let files = [
("/c/empty.txt", ""),
("/c/ws.inc", "\n"),
("/c/kv.inc", "k =\n# c\n"),
];
let p = |flags| parser(&files, flags | ParserFlags::SAVE_COMMENTS);
let placements = |p: &Parser| -> Vec<(Vec<PathSegment>, CommentPlacement)> {
p.attached_comments()
.iter()
.map(|g| (g.path.clone(), g.placement))
.collect()
};
let key = |k: &str| PathSegment::Key {
key: k.into(),
index: 0,
};
let mut parser = p(ParserFlags::DEFAULT);
parser
.parse(b"a = 1\n# c\n.include \"empty.txt\"\nb = 1")
.unwrap();
assert_eq!(
placements(&parser),
[(vec![key("b")], CommentPlacement::Before)]
);
let mut parser = p(ParserFlags::DEFAULT);
parser
.parse(b"a = 1\n# c\n.include \"ws.inc\"\nb = 1")
.unwrap();
assert_eq!(
placements(&parser),
[(vec![key("a")], CommentPlacement::After)]
);
let mut parser = p(ParserFlags::DEFAULT);
parser
.parse(b"# c\n.include(key=\"k\") \"empty.txt\"\nb = 1")
.unwrap();
assert_eq!(
placements(&parser),
[(vec![key("b")], CommentPlacement::Before)]
);
for (input, merged) in [
("k { a = 1 }\n# c\nk =\n", true),
("k = [1]\nk =\n", true),
("k = 1\nk =\n", false),
("k { a = 1 }\nk = null", false),
] {
let mut parser = p(ParserFlags::DEFAULT);
parser.set_strategy(DuplicateStrategy::Merge);
let v = parser.parse(input.as_bytes()).unwrap();
let k = obj(&v).entry("k").unwrap();
assert_eq!(k.first().is_null(), !merged && k.len() == 1, "{input:?}");
assert_eq!(
k.first().is_object() || k.first().is_array(),
merged,
"{input:?}"
);
}
let mut parser = p(ParserFlags::DEFAULT);
parser.set_strategy(DuplicateStrategy::Merge);
parser.parse(b"k { a = 1 }\n# c\nk =\n").unwrap();
assert_eq!(
placements(&parser),
[(vec![key("k")], CommentPlacement::Before)]
);
let v = run(
&files,
"k { a = 1 }\n.include(duplicate=\"merge\") \"kv.inc\"\nm = 1",
)
.unwrap();
assert_eq!(keys(&obj(&v)["k"]), ["a"]);
}
#[test]
fn root_closed_by_included_file_and_names_before_end() {
let files = [
("/c/close.inc", "a = 1 }\n"),
("/c/close_more.inc", "a = 1 }\nb = 2\n"),
("/c/mid.inc", ".include \"close.inc\"\nz = 1\n"),
];
let ok = |input: &str| keys(&run(&files, input).unwrap());
for input in [
"{\n.include \"close.inc\"\n",
"{\n.include \"close.inc\";;\n\n",
"{\n.include \"close_more.inc\"",
] {
assert_eq!(ok(input), ["a"], "{input:?}");
}
for input in [
"{\n.include \"close.inc\"\nk = 1",
"{\n.include \"close.inc\"\n# c",
"{\n.include \"close.inc\"\n}",
"{\n.include \"mid.inc\"",
] {
let e = run(&files, input).unwrap_err();
assert_eq!(e.kind(), &ErrorKind::AfterRootClosedByInclude, "{input:?}");
}
assert_eq!(ok("a \x0c# {"), ["a"]);
assert_eq!(ok("a b \x0c/* { */\n\nc {}"), ["a"]);
let v = run(&files, "a \x0c# {\n\nb {}").unwrap();
assert_eq!(keys(&obj(&v)["a"]), ["b"]);
for input in ["a \x0c# {\n #", "a \x0c/* { */ #", "\"a\" \x0c# {\nb = 1"] {
assert!(run(&files, input).is_err(), "{input:?}");
}
}
#[test]
fn comments_follow_a_value_moved_into_a_key_array() {
let mut p = parser(&[A], ParserFlags::SAVE_COMMENTS);
p.parse(b"# c1\nk = 1\n# c2\nk = 2\n.include(key=\"k\", target=\"array\") \"files/a.inc\"")
.unwrap();
let paths: Vec<_> = p
.attached_comments()
.iter()
.map(|g| g.path.clone())
.collect();
let k = PathSegment::Key {
key: "k".into(),
index: 0,
};
assert_eq!(paths, [vec![k, PathSegment::Index(0)]]);
}
fn files_with_close2<'a>(files: &[(&'a str, &'a str)]) -> Vec<(&'a str, &'a str)> {
let mut all = files.to_vec();
all.push(("/c/close2.inc", "a = 1 } }\n"));
all
}
#[test]
fn nesting_under_a_key() {
let files = [A, ("/c/c.conf", "q = 1\n"), ("/c/d.ucl", "w = 1\n")];
let ok = |input: &str| run(&files, input).unwrap();
let v = ok(
".include(prefix=true) \"files/a.inc\"\n.include(prefix=true) \"c.conf\"\n\
.include(prefix=true) \"d.ucl\"\n.include(prefix=true, key=\"\") \"d.ucl\"",
);
assert_eq!(keys(&v), ["a.inc", "c", "d", ""]);
let v =
ok("k = 1\nk = 2\n.include(key=\"k\", target=\"ARRAY\", priority=2) \"files/a.inc\"");
let k = obj(&v).entry("k").unwrap();
assert_eq!((k.len(), k.slots()[0].priority()), (1, 0));
let items = k.first().as_array().unwrap();
assert_eq!(items[0], UclValue::Integer(1));
assert_eq!(keys(&items[1]), ["x", "y"]);
let v = ok(
".include(key=\"k\", target=\"array\", priority=3) \"files/a.inc\"\n\
.include(key=\"k\", target=\"array\") \"c.conf\"",
);
let k = obj(&v).entry("k").unwrap();
assert_eq!(k.slots()[0].priority(), 3);
assert_eq!(k.first().as_array().unwrap().len(), 2);
let v =
ok(".priority 5\nk { a = 1 }\nk { b = 1 }\n.include(key=\"k\", priority=2) \"c.conf\"");
let k = obj(&v).entry("k").unwrap();
assert_eq!(k.len(), 2);
assert_eq!(keys(k.first()), ["a", "q"]);
assert_eq!(k.slots()[0].priority(), 5);
for input in ["k = [1]\n", "k = 1\nk { b = 2 }\n"] {
let e = run(&files, &format!("{input}.include(key=\"k\") \"c.conf\"")).unwrap_err();
assert!(
matches!(e.kind(), ErrorKind::IncludeTargetNotObject { .. }),
"{input:?}"
);
}
let p = |input: &str| {
parser(&files, ParserFlags::NO_IMPLICIT_ARRAYS)
.parse(input.as_bytes())
.unwrap()
};
let v = p("k = 1\n.include(key=\"k\", target=\"array\") \"c.conf\"\nk = 3");
assert_eq!(obj(&v)["k"].as_array().unwrap().len(), 3);
let v = p(".include(key=\"k\", target=\"array\") \"c.conf\"\nk = 3");
assert_eq!(obj(&v)["k"].as_array().unwrap().len(), 2);
let v = parser(&files, ParserFlags::KEY_LOWERCASE)
.parse(b".include(key=\"NEW\") \"c.conf\"\nnew { z = 1 }")
.unwrap();
assert_eq!(keys(&v), ["NEW"]);
assert_eq!(obj(&v).entry("NEW").unwrap().len(), 2);
}
#[test]
fn globs_and_search_paths() {
let files = [
("/c/g/b.inc", "gb = 1\n"),
("/c/g/a.inc", "ga = 1\n"),
("/c/g/.h.inc", "h = 1\n"),
("/c/g/sub/x.inc", "gx = 1\n"),
("/c/p1/pa.inc", "pa = 1\n"),
("/c/p2/pa.inc", "pb = 1\n"),
("/c/p2/pc.inc", "pc = 1\n"),
];
let ok = |input: &str| keys(&run(&files, input).unwrap());
assert_eq!(ok(".include(glob=true) \"g/*.inc\""), ["ga", "gb"]);
assert_eq!(ok(".include(glob=true, try=true) \"g/*\""), ["ga", "gb"]);
assert!(run(&files, ".include(glob=true) \"g/*\"").is_err());
assert_eq!(ok(".include(glob=true, try=true) \"g/.*\""), ["h"]);
assert_eq!(
ok(".include(glob=true, prefix=true) \"g/[ab]*\""),
["a.inc"]
);
assert_eq!(ok(".include(glob=true) \"*/sub/x.inc\""), ["gx"]);
assert_eq!(ok(".include(glob=true) \"g\\/*.inc\""), ["ga", "gb"]);
assert!(matches!(
run(&files, ".include(glob=true) \"g/[ab].inc\"")
.unwrap_err()
.kind(),
ErrorKind::FileNotFound { .. }
));
let e = run(&files, "k = 1\n.include(glob=true) \"g/none*\"\nm = 1").unwrap_err();
assert!(e.is_stopped());
assert_eq!(ok(".try_include(glob=true) \"g/none*\"\nm = 1"), ["m"]);
let files = [
(
"/c/t/main.inc",
".try_include(glob=true, try=false) \"t/*.inc\"\nafter = 1\n",
),
("/c/t/other.inc", "other = 1\n"),
(
"/c/t2/only.inc",
".try_include(glob=true, try=false) \"t2/*.inc\"\n",
),
(
"/c/t3/only.inc",
".include(glob=true, try=true) \"t3/*.inc\"\n",
),
];
let v = run(&files, ".include \"t/main.inc\"\nk = 1").unwrap();
assert_eq!(keys(&v), ["other", "after", "k"]);
for input in [".include \"t2/only.inc\"", ".include \"t3/only.inc\""] {
let e = run(&files, input).unwrap_err();
assert!(
matches!(e.kind(), ErrorKind::IncludeSelf { .. }),
"{input:?}: {e}"
);
}
let files = [
("/c/g/b.inc", "gb = 1\n"),
("/c/g/a.inc", "ga = 1\n"),
("/c/p1/pa.inc", "pa = 1\n"),
("/c/p2/pa.inc", "pb = 1\n"),
("/c/p2/pc.inc", "pc = 1\n"),
];
let ok = |input: &str| keys(&run(&files, input).unwrap());
assert_eq!(ok(".include(path=[\"p1\", \"p2\"]) \"pa.inc\""), ["pa"]);
assert!(run(&files, ".include(path=[\"p1\", \"p2\"]) \"pc.inc\"").is_err());
assert_eq!(ok(".try_include(path=[\"p1\", \"p2\"]) \"pc.inc\""), ["pc"]);
assert!(run(&files, ".try_include(path=[\"p1\"]) \"zz.inc\"").is_err());
assert_eq!(
ok(".include(path=[\"p2\"]) \"pa.inc\"\n.include \"pc.inc\""),
["pb", "pc"]
);
assert_eq!(
ok(".include(path=[\"p1\", \"p2\"], glob=true) \"p*.inc\""),
["pa", "pb", "pc"]
);
assert!(
run(
&files,
".include(path=[\"p2\", \"p1\"], glob=true) \"pc*.inc\""
)
.is_err()
);
assert!(run(&files, ".include(path=[], try=true) \"g/a.inc\"").is_err());
assert_eq!(ok(".include(path=\"p1\") \"g/a.inc\""), ["ga"]);
}
#[test]
fn search_path_set_on_the_parser() {
let files = [
("/c/g/a.inc", "ga = 1\n"),
("/c/p1/pa.inc", "pa = 1\n"),
("/c/p2/pa.inc", "pb = 1\n"),
("/c/p2/pc.inc", "pc = 1\n"),
("/c/p2/abs/x.inc", "px = 1\n"),
("/abs/x.inc", "ax = 1\n"),
("/c/prio.conf", "priority = 3\n"),
("/c/p1/prio.conf", "priority = 5\n"),
];
let with_list = |dirs: &[&str], input: &str| {
let mut parser = parser(&files, ParserFlags::DEFAULT);
parser.set_search_path(dirs.iter().copied());
assert_eq!(parser.search_path().map(<[String]>::len), Some(dirs.len()));
parser.parse(input.as_bytes())
};
let outcome = |result: Result<UclValue, Error>| match result {
Ok(v) => Ok(keys(&v)),
Err(e) => Err((e.kind().clone(), e.is_stopped())),
};
for (dirs, input, expected) in [
(&["p1", "p2"][..], ".include \"pa.inc\"", Some(vec!["pa"])),
(&["p1", "p2"], ".include \"pc.inc\"", None),
(
&["p1", "p2"],
".include(try=true) \"pc.inc\"\nk = 1",
Some(vec!["k"]),
),
(&["p1", "p2"], ".try_include \"pc.inc\"", Some(vec!["pc"])),
(&["p1"], ".try_include \"zz.inc\"", None),
(
&["p1", "p2"],
".include(glob=true) \"p*.inc\"",
Some(vec!["pa", "pb", "pc"]),
),
(&["p2", "p1"], ".include(glob=true) \"pc*.inc\"", None),
(&["p2"], ".include \"/abs/x.inc\"", Some(vec!["px"])),
] {
let in_document = format!(
".include(path=[{}], try=true) \"none.inc\"\n{input}",
dirs.iter()
.map(|d| format!("{d:?}"))
.collect::<Vec<_>>()
.join(", ")
);
let set = outcome(with_list(dirs, input));
assert_eq!(
set,
outcome(run(&files, &in_document)),
"{dirs:?} {input:?}"
);
match expected {
Some(keys) => assert_eq!(set, Ok(keys.iter().map(|k| k.to_string()).collect())),
None => assert!(
set.is_err_and(|(_, stopped)| !stopped),
"{dirs:?} {input:?}"
),
}
}
let e = with_list(&[], ".include(try=true) \"g/a.inc\"").unwrap_err();
assert!(matches!(e.kind(), ErrorKind::FileNotFound { .. }), "{e}");
let v = with_list(
&["p1"],
".include(path=[\"p2\"]) \"pa.inc\"\n.include \"pc.inc\"",
);
assert_eq!(keys(&v.unwrap()), ["pb", "pc"]);
let mut parser = parser(&files, ParserFlags::DEFAULT);
parser.set_search_path(["p1"]).clear_search_path();
assert_eq!(
keys(&parser.parse(b".include \"g/a.inc\"").unwrap()),
["ga"]
);
let v = with_list(&["p1"], ".priority(.include \"prio.conf\");\na = 1").unwrap();
assert_eq!(obj(&v).entry("a").unwrap().slots()[0].priority(), 5);
let v = run(
&files,
".include(path=[\"p1\"], try=true) \"none.inc\"\n.priority(.include \"prio.conf\");\na = 1",
)
.unwrap();
assert_eq!(obj(&v).entry("a").unwrap().slots()[0].priority(), 3);
}
#[cfg(feature = "load")]
#[test]
fn load_ignores_the_search_path() {
let files = [("/c/g/a.inc", "text"), ("/c/p1/g/a.inc", "other")];
let mut parser = parser(&files, ParserFlags::DEFAULT);
parser.set_search_path(["p1"]);
let v = parser.parse(b".load(key=\"k\") \"g/a.inc\"").unwrap();
assert_eq!(obj(&v)["k"].as_str(), Some("text"));
}
fn too_large(result: Result<UclValue, Error>) -> Option<(u64, Option<String>, usize)> {
let e = result.err()?;
match e.kind() {
ErrorKind::InputTooLarge { limit, path } => {
Some((*limit, path.clone(), e.position().offset))
}
_ => None,
}
}
#[test]
fn input_limit() {
let files = [
("/c/a.inc", "a = 1\n"),
("/c/g/x.inc", "x = 1\n"),
("/c/g/y.inc", "y = 22\n"),
("/c/prio.conf", "priority = 3\n"),
];
let with_limit = |limit: u64, input: &str| {
let mut parser = parser(&files, ParserFlags::DEFAULT);
parser.set_max_input_bytes(Some(limit));
assert_eq!(parser.max_input_bytes(), Some(limit));
parser.parse(input.as_bytes())
};
let len = |s: &str| s.len() as u64;
assert!(with_limit(5, "k = 1").is_ok());
assert_eq!(too_large(with_limit(4, "k = 1")), Some((4, None, 0)));
let input = "k = 1\n.include \"a.inc\"";
let total = len(input) + len("a = 1\n");
assert!(with_limit(total, input).is_ok());
let over = Some((total - 1, Some("a.inc".to_string()), 15));
assert_eq!(too_large(with_limit(total - 1, input)), over);
for input in [
"k = 1\n.include(try=true) \"a.inc\"",
"k = 1\n.try_include \"a.inc\"",
] {
let total = len(input) + len("a = 1\n");
assert!(with_limit(total, input).is_ok(), "{input}");
let found = too_large(with_limit(total - 1, input));
assert_eq!(
found.map(|(_, p, _)| p),
Some(Some("a.inc".into())),
"{input}"
);
}
let twice = ".include \"a.inc\"\n.include \"a.inc\"";
assert!(with_limit(len(twice) + 12, twice).is_ok());
assert!(too_large(with_limit(len(twice) + 11, twice)).is_some());
let glob = ".include(glob=true) \"g/*.inc\"";
let total = len(glob) + len("x = 1\n") + len("y = 22\n");
assert!(with_limit(total, glob).is_ok());
let found = too_large(with_limit(total - 1, glob)).unwrap();
assert_eq!(found.1.as_deref(), Some("/c/g/y.inc"));
let args = ".priority(.include \"prio.conf\");\na = 1";
let total = len(args) + len("priority = 3\n");
assert!(with_limit(total, args).is_ok());
assert!(too_large(with_limit(total - 1, args)).is_some());
let mut parser = parser(&files, ParserFlags::DEFAULT);
assert_eq!(parser.max_input_bytes(), None);
assert!(parser.parse(twice.as_bytes()).is_ok());
let mut parser = parser_with_limit(&files, 5);
assert!(too_large(parser.parse_file("/c/a.inc")).is_some());
let mut parser = parser_with_limit(&files, 6);
assert!(parser.parse_file("/c/a.inc").is_ok());
}
fn parser_with_limit(files: &[(&str, &str)], limit: u64) -> Parser {
let mut parser = parser(files, ParserFlags::DEFAULT);
parser.set_max_input_bytes(Some(limit));
parser
}
#[test]
fn input_limit_reads_files_limited() {
use crate::parse::{FileKind, Loader};
use std::cell::RefCell;
use std::rc::Rc;
struct Recording(MemoryLoader, Rc<RefCell<Vec<Option<u64>>>>);
impl Loader for Recording {
fn current_dir(&self) -> std::io::Result<std::path::PathBuf> {
self.0.current_dir()
}
fn canonicalize(&self, path: &Path) -> std::io::Result<std::path::PathBuf> {
self.0.canonicalize(path)
}
fn kind(&self, path: &Path) -> Option<FileKind> {
self.0.kind(path)
}
fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
self.1.borrow_mut().push(None);
self.0.read(path)
}
fn read_dir(&self, path: &Path) -> std::io::Result<Vec<String>> {
self.0.read_dir(path)
}
fn read_limited(&self, path: &Path, limit: u64) -> std::io::Result<Vec<u8>> {
self.1.borrow_mut().push(Some(limit));
self.0.read_limited(path, limit)
}
}
let mut files = MemoryLoader::new();
files.add_file("/c/big.inc", "x".repeat(1 << 20));
files.add_file("/c/main.conf", ".include \"big.inc\"");
let calls = Rc::new(RefCell::new(Vec::new()));
let mut parser = Parser::new();
parser
.set_loader(Recording(files, Rc::clone(&calls)))
.set_base_dir("/c")
.set_max_input_bytes(Some(100));
assert!(too_large(parser.parse_file("main.conf")).is_some());
let document = ".include \"big.inc\"".len() as u64;
assert_eq!(*calls.borrow(), [Some(100), Some(100 - document)]);
parser.set_max_input_bytes(None);
calls.borrow_mut().clear();
assert!(parser.parse(b"a = 1").is_ok());
assert!(parser.parse_file("main.conf").is_err());
assert_eq!(*calls.borrow(), [None, None]);
}
#[cfg(feature = "load")]
#[test]
fn input_limit_counts_load() {
let files = [("/c/t.txt", "0123456789")];
for input in [
".load(key=\"k\") \"t.txt\"",
".load(key=\"k\", try=true) \"t.txt\"",
] {
let total = input.len() as u64 + 10;
assert!(
parser_with_limit(&files, total)
.parse(input.as_bytes())
.is_ok()
);
let found = too_large(parser_with_limit(&files, total - 1).parse(input.as_bytes()));
assert_eq!(
found.map(|(_, p, _)| p),
Some(Some("t.txt".into())),
"{input}"
);
}
}
#[test]
fn urls_and_signatures() {
for input in [
".include(url=true) \"http://example.invalid/x.inc\"",
".try_include(url=true, try=false) \"http://example.invalid/x.inc\"",
".include(url=true, glob=true, key=\"k\") \"http://example.invalid/*\"",
] {
let e = run(&[A], input).unwrap_err();
assert!(
matches!(e.kind(), ErrorKind::UrlNotSupported { .. }),
"{input:?}"
);
}
let files = [
A,
("/c/p1/pa.inc", "pa = 1\n"),
("/c/p2/pc.inc", "pc = 1\n"),
];
for (input, expected) in [
(
"a = 1\n.include(url=true, try=true) \"http://example.invalid/x.inc\"\nb = 2",
&["a", "b"][..],
),
(
"a = 1\n.try_include(url=true) \"http://example.invalid/x.inc\"\nb = 2",
&["a", "b"],
),
(
".include(url=true, try=true, key=\"k\") \"http://example.invalid/x.inc\"\nb = 2",
&["b"],
),
(
".include(path=[\"p1\"]) \"pa.inc\"\n\
.include(url=true, try=true) \"http://example.invalid/x.inc\"\nb = 2",
&["pa", "b"],
),
(
".include(url=true, try=true, path=[\"p2\"]) \"http://x.invalid/y\"\n\
.include \"pc.inc\"",
&["pc"],
),
] {
assert_eq!(keys(&run(&files, input).unwrap()), expected, "{input:?}");
}
assert_eq!(
keys(&run(&[A], ".include(url=true) \"files/a.inc\"").unwrap()),
["x", "y"]
);
assert_eq!(
keys(
&run(
&[A],
".include(try=true) \"http://example.invalid/x.inc\"\nk = 1"
)
.unwrap()
),
["k"]
);
assert!(
run(&[A], ".includes \"files/a.inc\"")
.unwrap_err()
.is_unsupported()
);
assert!(
run(&[A], ".include(sign=true) \"files/a.inc\"")
.unwrap_err()
.is_unsupported()
);
assert!(run(&[A], ".include(sign=false) \"files/a.inc\"").is_ok());
}
#[test]
fn argument_documents_may_include() {
let files = [("/c/pri.inc", "priority = 3\n")];
let v = run(&files, ".priority(.include \"pri.inc\");\na = 1").unwrap();
assert_eq!(obj(&v).entry("a").unwrap().slots()[0].priority(), 3);
let e = run(
&files,
".priority(.try_include \"nope\"; priority = 3);\na = 1",
)
.unwrap_err();
assert!(
matches!(e.kind(), ErrorKind::StoppedInArguments { .. }),
"{e}"
);
let e = run(
&[("/c/bad.inc", "a = \"")],
".priority(.include \"bad.inc\") 1",
)
.unwrap_err();
assert_eq!(e.file(), Some(Path::new("/c/bad.inc")));
assert_eq!(e.position().offset, 4);
}
#[test]
fn comments_across_units() {
let files = [("/c/c.inc", "\n# in\nx = 1 # g\n")];
let mut p = parser(&files, ParserFlags::SAVE_COMMENTS);
p.set_strategy(DuplicateStrategy::Append);
p.parse(b"# c\n.include \"c.inc\"\nb = 1 # t").unwrap();
let texts: Vec<_> = p
.comments()
.iter()
.map(|c| (c.text.as_str(), c.position.line, c.position.column))
.collect();
assert_eq!(
texts,
[("# c", 1, 1), ("# in", 2, 1), ("# g", 3, 7), ("# t", 3, 7)]
);
let groups: Vec<_> = p
.attached_comments()
.iter()
.map(|g| (g.path.clone(), g.placement, g.comments.clone()))
.collect();
let key = |k: &str| {
vec![PathSegment::Key {
key: k.into(),
index: 0,
}]
};
assert_eq!(
groups,
[
(key("x"), CommentPlacement::Before, vec![0, 1, 2]),
(key("b"), CommentPlacement::After, vec![3]),
]
);
}
#[cfg(feature = "load")]
#[test]
fn load_macro() {
let files = [
("/c/num.txt", "42\n"),
("/c/text.txt", " a\"b\n"),
("/c/empty.txt", ""),
("/c/ws.txt", " \t\n "),
("/c/bad.txt", "\u{0}\u{1}"),
];
let ok = |input: &str| run(&files, input).unwrap();
let err = |input: &str| run(&files, input).unwrap_err().kind().clone();
let v = ok(
".load(key=\"k\") \"num.txt\"\n.load(key=\"n\", target=\"Int\", priority=19) \"num.txt\"",
);
assert_eq!(obj(&v)["k"].as_str(), Some("42\n"));
assert_eq!(obj(&v)["n"], UclValue::Integer(42));
assert_eq!(obj(&v).entry("n").unwrap().slots()[0].priority(), 3);
let v = ok(".load(key=\"t\", tri=true, escape=true) \"text.txt\"");
assert_eq!(obj(&v)["t"].as_str(), Some("a\\\"b"));
assert_eq!(keys(&ok(".load(key=\"k\") \"empty.txt\"\nj = 1")), ["j"]);
assert_eq!(
obj(&ok(".load(key=\"k\", trim=true) \"ws.txt\""))["k"].as_str(),
Some("")
);
assert_eq!(
obj(&ok(".load(key=\"k\", target=\"int\") \"empty.txt\""))["k"],
UclValue::Integer(0)
);
assert_eq!(
keys(&ok(".load(key=\"k\", target=\"float\") \"num.txt\"\nj = 1")),
["j"]
);
assert_eq!(
keys(&ok("t = 1\n.load(key=\"t\", try=true) \"nope\"\nj = 1")),
["t", "j"]
);
assert_eq!(
keys(&ok(".load(key=\"k\", try=true) \"dir\"\nj = 1")),
["j"]
);
assert_eq!(err(".load \"num.txt\""), ErrorKind::LoadKeyMissing);
assert_eq!(
err(".load(key=\"\", try=true) \"nope\""),
ErrorKind::LoadKeyMissing
);
assert!(matches!(
err(".load(key=\"k\", try=true) \"\""),
ErrorKind::FileNotFound { .. }
));
assert!(matches!(
err(".load(key=\"k\") \"nope\""),
ErrorKind::FileNotFound { .. }
));
assert!(matches!(
err(".load(key=\"k\") \"dir\""),
ErrorKind::NotAFile { .. }
));
assert!(matches!(
err("t = 1\n.load(key=\"t\", target=\"float\") \"num.txt\""),
ErrorKind::LoadKeyExists { .. }
));
assert!(matches!(
err("t = 1\n.load(key=\"t\") \"empty.txt\""),
ErrorKind::LoadKeyExists { .. }
));
let v = ok(".load(key=\"k\") \"bad.txt\"");
assert_eq!(obj(&v)["k"].as_str(), Some("\u{0}\u{1}"));
let mut p = Parser::new();
let mut loader = MemoryLoader::new();
loader.add_file("/x.bin", vec![0xFF_u8]);
p.set_loader(loader);
assert_eq!(
p.parse(b".load(key=\"k\") \"/x.bin\"").unwrap_err().kind(),
&ErrorKind::InvalidUtf8
);
let p = |input: &str| parser(&files, ParserFlags::KEY_LOWERCASE).parse(input.as_bytes());
assert!(p("K = 1\n.load(key=\"k\") \"num.txt\"").is_err());
let v = p(".load(key=\"K\") \"num.txt\"\nk = 2").unwrap();
assert_eq!(keys(&v), ["K"]);
assert_eq!(obj(&v).entry("K").unwrap().len(), 2);
}
}