use super::comments::CommentGroups;
use super::core::{Document, Settings};
use super::include::{Budget, Includes, Read};
use super::loader::Loader;
use super::registered::MacroTable;
use super::vars::{self, Expander};
use super::{Comment, Error, ErrorKind, MAX_INCLUDE_DEPTH, OutputFacts};
use crate::error::Position;
use crate::value::{DuplicateStrategy, MAX_PRIORITY, ParserFlags, UclValue};
use indexmap::IndexMap;
use std::borrow::Cow;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Input<'a> {
source: Source<'a>,
priority: Option<u8>,
strategy: Option<DuplicateStrategy>,
}
#[derive(Debug, Clone)]
enum Source<'a> {
Bytes(&'a [u8]),
File(PathBuf),
Read {
canonical: PathBuf,
bytes: &'a [u8],
},
}
impl<'a> Input<'a> {
pub fn bytes<T: AsRef<[u8]> + ?Sized>(input: &'a T) -> Self {
Self {
source: Source::Bytes(input.as_ref()),
priority: None,
strategy: None,
}
}
pub fn file(path: impl Into<PathBuf>) -> Input<'static> {
Input {
source: Source::File(path.into()),
priority: None,
strategy: None,
}
}
pub(crate) fn read_file(canonical: PathBuf, bytes: &'a [u8]) -> Self {
Self {
source: Source::Read { canonical, bytes },
priority: None,
strategy: None,
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = Some(priority & MAX_PRIORITY);
self
}
pub fn with_strategy(mut self, strategy: DuplicateStrategy) -> Self {
self.strategy = Some(strategy);
self
}
}
struct Outputs<'p> {
comments: &'p mut Vec<Comment>,
attached: &'p mut CommentGroups,
facts: &'p mut OutputFacts,
}
pub struct Inputs<'p> {
flags: ParserFlags,
priority: u8,
strategy: DuplicateStrategy,
variables: &'p IndexMap<String, String>,
base_dir: Option<&'p Path>,
expander: Expander<'p>,
includes: Includes<'p>,
document: Option<Document>,
failed: Option<Error>,
out: Outputs<'p>,
}
impl std::fmt::Debug for Inputs<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Inputs")
.field("inputs", &self.includes.files.len())
.field("failed", &self.failed)
.finish()
}
}
pub(crate) struct Parts<'p> {
pub(crate) flags: ParserFlags,
pub(crate) priority: u8,
pub(crate) strategy: DuplicateStrategy,
pub(crate) variables: &'p IndexMap<String, String>,
pub(crate) handler: Option<Box<vars::Handler<'p>>>,
pub(crate) loader: &'p dyn Loader,
pub(crate) base_dir: Option<&'p Path>,
pub(crate) search_path: Option<Vec<String>>,
pub(crate) max_input_bytes: Option<u64>,
pub(crate) inherit_depth_limit: usize,
pub(crate) macros: &'p MacroTable,
pub(crate) uncertain: &'p std::cell::Cell<u8>,
pub(crate) comments: &'p mut Vec<Comment>,
pub(crate) attached: &'p mut CommentGroups,
pub(crate) facts: &'p mut OutputFacts,
}
pub(crate) fn input_base(
base_dir: Option<&Path>,
loader: &dyn Loader,
file: Option<&Path>,
) -> PathBuf {
match (base_dir, file) {
(Some(dir), _) => dir.to_path_buf(),
(None, Some(file)) => file.parent().map(Path::to_path_buf).unwrap_or_default(),
(None, None) => loader.current_dir().unwrap_or_else(|_| PathBuf::from(".")),
}
}
pub(crate) fn first_variables(
flags: ParserFlags,
registered: &IndexMap<String, String>,
file: Option<&Path>,
base: &Path,
) -> Vec<(String, String)> {
let filevars = match file {
Some(file) => Some((
file.to_string_lossy().into_owned(),
file.parent()
.map(|d| d.to_string_lossy().into_owned())
.unwrap_or_default(),
)),
None => (!flags.contains(ParserFlags::NO_FILEVARS))
.then(|| ("undef".to_string(), base.to_string_lossy().into_owned())),
};
let mut variables: IndexMap<String, String> = IndexMap::new();
if let Some((filename, curdir)) = filevars {
variables.insert("FILENAME".to_string(), filename);
variables.insert("CURDIR".to_string(), curdir);
}
for (name, value) in registered {
let is_filevar = name == "FILENAME" || name == "CURDIR";
if file.is_some() && is_filevar && variables.contains_key(name) {
continue;
}
variables.insert(name.clone(), value.clone());
}
variables.into_iter().collect()
}
impl<'p> Inputs<'p> {
pub(crate) fn new(parts: Parts<'p>) -> Self {
let Parts {
flags,
priority,
strategy,
variables,
handler,
loader,
base_dir,
search_path,
max_input_bytes,
inherit_depth_limit,
macros,
uncertain,
comments,
attached,
facts,
} = parts;
comments.clear();
*attached = CommentGroups::default();
facts.clear();
macros.ran.set(false);
let expander = Expander::new(
Vec::new(),
handler,
!flags.contains(ParserFlags::DISABLE_MACRO),
);
let mut includes = Includes::new(
loader,
PathBuf::new(),
search_path,
Budget::new(max_input_bytes),
(!macros.is_empty()).then_some(macros),
);
includes.uncertain = Some(uncertain);
includes.inherit_limit = inherit_depth_limit;
let document = Document::new(
flags.contains(ParserFlags::SAVE_COMMENTS),
Some(OutputFacts::new()),
0,
);
Self {
flags,
priority,
strategy,
variables,
base_dir,
expander,
includes,
document: Some(document),
failed: None,
out: Outputs {
comments,
attached,
facts,
},
}
}
pub fn add(&mut self, input: Input<'_>) -> Result<(), Error> {
self.read(input).map_err(|e| {
if e.is_stopped()
&& let Some(document) = &self.document
{
e.with_partial(document.snapshot())
} else {
e
}
})
}
pub(crate) fn read(&mut self, input: Input<'_>) -> Result<(), Error> {
if let Some(error) = &self.failed {
return Err(error.clone());
}
if self.includes.files.len() >= MAX_INCLUDE_DEPTH {
let limit = MAX_INCLUDE_DEPTH;
return self.fail(Error::new(
ErrorKind::TooManyInputs { limit },
Position::new(),
));
}
let settings = Settings {
flags: self.flags,
priority: input.priority.unwrap_or(self.priority),
strategy: input.strategy.unwrap_or(self.strategy),
};
let too_large = |limit: Option<u64>| {
Error::new(
ErrorKind::InputTooLarge {
limit: limit.unwrap_or_default(),
path: None,
},
Position::new(),
)
};
let (bytes, file): (Cow<'_, [u8]>, Option<PathBuf>) = match input.source {
Source::Bytes(bytes) => {
if !self.includes.budget.take(bytes.len()) {
return self.fail(too_large(self.includes.budget.limit()));
}
(Cow::Borrowed(bytes), None)
}
Source::Read { canonical, bytes } => {
if !self.includes.budget.take(bytes.len()) {
return self.fail(too_large(self.includes.budget.limit()));
}
(Cow::Borrowed(bytes), Some(canonical))
}
Source::File(path) => {
let loader = self.includes.loader;
let path = input_base(self.base_dir, loader, None).join(path);
let io = |e: std::io::Error| {
let message = format!("{}: {e}", path.display());
Error::new(ErrorKind::Io { message }, Position::new())
};
let canonical = match loader.canonicalize(&path) {
Ok(canonical) => canonical,
Err(e) => return self.fail(io(e)),
};
match self.includes.read(&canonical) {
Ok(Read::Bytes(bytes)) => (Cow::Owned(bytes), Some(canonical)),
Ok(Read::TooLarge { limit }) => return self.fail(too_large(Some(limit))),
Err(e) => return self.fail(io(e)),
}
}
};
let loader = self.includes.loader;
self.includes.base = input_base(self.base_dir, loader, file.as_deref());
if self.includes.files.is_empty() {
let variables = first_variables(
self.flags,
self.variables,
file.as_deref(),
&self.includes.base,
);
self.expander.set_variables(variables);
} else if let Some(file) = &file {
let curdir = file
.parent()
.map(|d| d.to_string_lossy().into_owned())
.unwrap_or_default();
self.expander
.set_file_vars(file.to_string_lossy().into_owned(), curdir);
}
let current = file.or_else(|| self.includes.files.last().cloned().flatten());
self.includes.files.push(current);
let document = self.document.as_mut().expect("a parse that has not failed");
match document.read(&bytes, settings, &mut self.expander, &mut self.includes) {
Ok(()) => Ok(()),
Err(e) if e.is_stopped() => Err(e),
Err(e) => self.fail(e),
}
}
fn fail(&mut self, error: Error) -> Result<(), Error> {
if let Some(document) = self.document.take() {
*self.out.comments = document.into_comments();
}
self.failed = Some(error.clone());
Err(error)
}
pub fn finish(mut self) -> Result<UclValue, Error> {
if let Some(error) = self.failed.take() {
return Err(error);
}
let document = self.document.take().expect("a parse that has not failed");
let finished = match document.finish() {
Ok(finished) => finished,
Err(failed) => {
let (error, comments) = *failed;
*self.out.comments = comments;
return Err(error);
}
};
if let Some((comments, attached)) = finished.comments {
*self.out.comments = comments;
*self.out.attached = attached;
}
if let Some(facts) = finished.facts {
*self.out.facts = facts;
}
Ok(finished.root)
}
}