#![allow(clippy::result_large_err)]
use std::path::{Path, PathBuf};
use crate::ast::*;
use crate::error::ParseError;
#[derive(Debug, Clone)]
pub struct ExpandOptions {
pub max_include_level: usize,
pub follow_includes: bool,
pub follow_confdirs: bool,
}
impl Default for ExpandOptions {
fn default() -> Self {
Self {
max_include_level: 10,
follow_includes: true,
follow_confdirs: true,
}
}
}
impl ChronyConfig {
pub fn expand(&self, base_dir: &Path) -> Result<ChronyConfig, ParseError> {
let opts = ExpandOptions::default();
let mut expanded = Vec::new();
expand_nodes(&self.nodes, base_dir, 0, &opts, &mut expanded)?;
Ok(ChronyConfig { nodes: expanded })
}
pub fn expand_with_opts(&self, base_dir: &Path, opts: &ExpandOptions) -> Result<ChronyConfig, ParseError> {
let mut expanded = Vec::new();
expand_nodes(&self.nodes, base_dir, 0, opts, &mut expanded)?;
Ok(ChronyConfig { nodes: expanded })
}
}
fn expand_nodes(
nodes: &[ConfigNode],
base_dir: &Path,
depth: usize,
opts: &ExpandOptions,
out: &mut Vec<ConfigNode>,
) -> Result<(), ParseError> {
for node in nodes {
match node {
ConfigNode::Directive(d) => match &d.kind {
DirectiveKind::Include(c) if opts.follow_includes => {
if depth >= opts.max_include_level {
return Err(ParseError::IncludeLevelExceeded {
file: base_dir.to_path_buf(),
line: d.span.line_start,
});
}
let pattern = base_dir.join(&c.pattern);
let path = PathBuf::from(pattern.to_string_lossy().as_ref());
if path.exists() {
let content = std::fs::read_to_string(&path)
.map_err(ParseError::Io)?;
let sub = ChronyConfig::parse(&content)
.map_err(|e| adapt_error(e, Some(path.clone())))?;
let sub_dir = path.parent().unwrap_or(base_dir);
expand_nodes(&sub.nodes, sub_dir, depth + 1, opts, out)?;
}
}
DirectiveKind::ConfDir(c) if opts.follow_confdirs => {
let dir = base_dir.join(&c.directory);
if dir.is_dir() {
let mut entries: Vec<_> = std::fs::read_dir(&dir)
.map_err(ParseError::Io)?
.filter_map(|e| e.ok())
.filter(|e| {
e.path().extension().is_some_and(|ext| ext == "conf")
})
.collect();
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
let content = std::fs::read_to_string(&path)
.map_err(ParseError::Io)?;
let sub = ChronyConfig::parse(&content)
.map_err(|e| adapt_error(e, Some(path.clone())))?;
expand_nodes(
&sub.nodes,
path.parent().unwrap_or(base_dir),
depth + 1,
opts,
out,
)?;
}
}
}
_ => out.push(node.clone()),
},
_ => out.push(node.clone()),
}
}
Ok(())
}
fn adapt_error(e: ParseError, file: Option<PathBuf>) -> ParseError {
match e {
ParseError::Parse { file: _, inner } => ParseError::Parse { file, inner },
other => other,
}
}