use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Deserializer};
use crate::error::{Error, Result};
use crate::import_paths::expand_import;
use crate::manifest::{Command, Manifest, Scope, StrictPolicy};
use crate::probe::Probe;
use crate::provenance::Provenance;
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Document {
#[serde(default)]
imports: Vec<String>,
#[serde(default)]
scopes: BTreeMap<String, Scope>,
#[serde(default)]
probes: BTreeMap<String, Probe>,
#[serde(default)]
commands: Vec<Command>,
#[serde(default, deserialize_with = "double_option")]
gitignore: Option<Option<bool>>,
#[serde(default, deserialize_with = "double_option")]
cache_dir: Option<Option<String>>,
#[serde(default, deserialize_with = "double_option")]
strict: Option<Option<StrictPolicy>>,
#[serde(default, deserialize_with = "double_option")]
on_hit: Option<Option<String>>,
#[serde(default, deserialize_with = "double_option")]
probe_shell: Option<Option<Vec<String>>>,
}
fn double_option<'de, D, T>(deserializer: D) -> std::result::Result<Option<Option<T>>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Option::deserialize(deserializer).map(Some)
}
#[derive(Default)]
struct MergeState {
root: PathBuf,
stack: Vec<PathBuf>,
loaded: BTreeSet<PathBuf>,
order: Vec<PathBuf>,
scopes: BTreeMap<String, (Scope, PathBuf)>,
probes: BTreeMap<String, (Probe, PathBuf)>,
commands: Vec<(Command, PathBuf)>,
}
impl MergeState {
fn absorb(
&mut self,
source: &Path,
scopes: BTreeMap<String, Scope>,
probes: BTreeMap<String, Probe>,
commands: Vec<Command>,
) -> Result<()> {
for (name, scope) in scopes {
if let Some((_, first)) = self.scopes.get(&name) {
return Err(Error::DuplicateScope {
name,
first: Provenance::shorten(first, &self.root),
second: Provenance::shorten(source, &self.root),
});
}
self.scopes.insert(name, (scope, source.to_path_buf()));
}
for (name, probe) in probes {
if let Some((_, first)) = self.probes.get(&name) {
return Err(Error::DuplicateProbe {
name,
first: Provenance::shorten(first, &self.root),
second: Provenance::shorten(source, &self.root),
});
}
self.probes.insert(name, (probe, source.to_path_buf()));
}
for command in commands {
self.commands.push((command, source.to_path_buf()));
}
Ok(())
}
fn visit_imports(&mut self, importer: &Path, imports: &[String]) -> Result<()> {
let base_dir = importer
.parent()
.ok_or_else(|| Error::Internal("import path has no parent directory".to_owned()))?;
for entry in imports {
for target in expand_import(importer, base_dir, entry, &self.root)? {
self.visit_fragment(&target)?;
}
}
Ok(())
}
fn visit_fragment(&mut self, path: &Path) -> Result<()> {
let canonical = fs::canonicalize(path).map_err(|source| Error::ImportNotReadable {
path: Provenance::shorten(path, &self.root),
source,
})?;
if self.stack.contains(&canonical) {
let mut chain = self.stack.clone();
chain.push(canonical);
return Err(Error::ImportCycle {
chain: format_chain(&chain, &self.root),
});
}
if self.loaded.contains(&canonical) {
return Ok(());
}
let text = fs::read_to_string(&canonical).map_err(|source| Error::ImportNotReadable {
path: Provenance::shorten(&canonical, &self.root),
source,
})?;
let document = parse_text(&text, &canonical, &self.root)?;
check_no_policy_keys(&document, &canonical, &self.root)?;
self.stack.push(canonical.clone());
self.order.push(canonical.clone());
self.absorb(
&canonical,
document.scopes,
document.probes,
document.commands,
)?;
self.visit_imports(&canonical, &document.imports)?;
self.stack.pop();
self.loaded.insert(canonical);
Ok(())
}
}
pub(crate) fn load(path: &Path, root: &Path) -> Result<(Manifest, Provenance)> {
let text = fs::read_to_string(path).map_err(|source| unreadable(path, root, source))?;
let document = parse_text(&text, path, root)?;
let canonical = fs::canonicalize(path).map_err(|source| unreadable(path, root, source))?;
let Resolved {
gitignore,
cache_dir,
strict,
on_hit,
probe_shell,
} = policy::resolve(&document, path, root)?;
let mut state = MergeState {
root: root.to_path_buf(),
..MergeState::default()
};
state.stack.push(canonical.clone());
state.order.push(canonical.clone());
state.absorb(
&canonical,
document.scopes,
document.probes,
document.commands,
)?;
state.visit_imports(&canonical, &document.imports)?;
state.stack.pop();
state.loaded.insert(canonical);
check_cross_file_duplicate_commands(&state.commands, root)?;
let (scopes, scope_sources) = split(state.scopes);
let (probes, probe_sources) = split(state.probes);
let (commands, command_sources) = split_commands(state.commands);
let manifest = Manifest {
scopes,
probes,
commands,
gitignore,
cache_dir,
strict,
on_hit,
probe_shell,
};
manifest.validate()?;
Ok((
manifest,
Provenance {
sources: state.order,
scopes: scope_sources,
probes: probe_sources,
commands: command_sources,
},
))
}
pub(crate) fn unreadable(path: &Path, root: &Path, source: std::io::Error) -> Error {
Error::ManifestUnreadable {
path: Provenance::shorten(path, root),
source,
}
}
fn parse_text(text: &str, path: &Path, root: &Path) -> Result<Document> {
serde_yaml_ng::from_str(text).map_err(|source| Error::ManifestParse {
path: Provenance::shorten(path, root),
source: Box::new(source),
})
}
pub(crate) const POLICY_KEYS: [&str; 5] =
["cache_dir", "gitignore", "strict", "on_hit", "probe_shell"];
#[path = "compose_policy.rs"]
mod policy;
pub(crate) use policy::declared_policy_keys;
use policy::{Resolved, check_no_policy_keys};
fn format_chain(chain: &[PathBuf], root: &Path) -> String {
chain
.iter()
.map(|entry| Provenance::display(entry, root))
.collect::<Vec<_>>()
.join(" -> ")
}
fn check_cross_file_duplicate_commands(commands: &[(Command, PathBuf)], root: &Path) -> Result<()> {
let mut first_seen: BTreeMap<&str, &Path> = BTreeMap::new();
for (command, source) in commands {
match first_seen.get(command.name.as_str()) {
Some(&existing) if existing != source.as_path() => {
return Err(Error::DuplicateCommandAcrossFiles {
name: command.name.clone(),
first: Provenance::shorten(existing, root),
second: Provenance::shorten(source, root),
});
}
Some(_) => {}
None => {
first_seen.insert(command.name.as_str(), source.as_path());
}
}
}
Ok(())
}
fn split<V>(
merged: BTreeMap<String, (V, PathBuf)>,
) -> (BTreeMap<String, V>, BTreeMap<String, PathBuf>) {
let mut values = BTreeMap::new();
let mut sources = BTreeMap::new();
for (name, (value, source)) in merged {
sources.insert(name.clone(), source);
values.insert(name, value);
}
(values, sources)
}
fn split_commands(merged: Vec<(Command, PathBuf)>) -> (Vec<Command>, BTreeMap<String, PathBuf>) {
let mut commands = Vec::with_capacity(merged.len());
let mut sources: BTreeMap<String, PathBuf> = BTreeMap::new();
for (command, source) in merged {
sources.entry(command.name.clone()).or_insert(source);
commands.push(command);
}
(commands, sources)
}
#[cfg(test)]
#[path = "compose_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "compose_merge_tests.rs"]
mod merge_tests;
#[cfg(test)]
#[path = "compose_cycles_tests.rs"]
mod cycles_tests;