use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use serde::Serialize;
use crate::error::{Error, Result};
use crate::manifest::{Manifest, MatchMode, StrictCase};
use crate::provenance::Provenance;
#[derive(Serialize)]
struct Dump {
manifest: String,
sources: Vec<String>,
policy: Policy,
scopes: Vec<ScopeEntry>,
probes: Vec<ProbeEntry>,
commands: Vec<CommandEntry>,
}
#[derive(Serialize)]
struct Policy {
source: String,
gitignore: bool,
cache_dir: String,
strict: Vec<&'static str>,
on_hit: Option<String>,
probe_shell: Vec<String>,
#[serde(skip)]
declared: BTreeSet<&'static str>,
}
#[derive(Serialize)]
struct ScopeEntry {
name: String,
globs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
exclude: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
gitignore: Option<bool>,
source: String,
}
#[derive(Serialize)]
struct ProbeEntry {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
run: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
json: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ast: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
capture: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
lang: Option<String>,
allow_empty: bool,
source: String,
}
#[derive(Serialize)]
struct CommandEntry {
name: String,
#[serde(rename = "match")]
match_mode: &'static str,
#[serde(skip_serializing_if = "Vec::is_empty")]
inputs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
exclude: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
outputs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
on_hit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
ttl: Option<String>,
source: String,
}
pub fn dump(cwd: &Path) -> Result<String> {
let dump = collect(cwd)?;
Ok(render_text(&dump))
}
pub fn dump_json(cwd: &Path) -> Result<String> {
let dump = collect(cwd)?;
let text = serde_json::to_string_pretty(&dump)
.map_err(|err| Error::Internal(format!("serializing dump-config json: {err}")))?;
Ok(format!("{text}\n"))
}
fn collect(cwd: &Path) -> Result<Dump> {
let located = Manifest::locate(cwd)?;
let manifest = &located.manifest;
let base = located.root.as_path();
let provenance = &located.provenance;
let sources = provenance
.sources
.iter()
.map(|path| Provenance::display(path, base))
.collect();
let policy = collect_policy(manifest, &located.path, base)?;
let scopes = manifest
.scopes
.iter()
.map(|(name, scope)| ScopeEntry {
name: name.clone(),
globs: scope.globs.clone(),
exclude: scope.exclude.clone(),
gitignore: scope.gitignore,
source: source_of(&provenance.scopes, name, base),
})
.collect();
let probes = manifest
.probes
.iter()
.map(|(name, probe)| ProbeEntry {
name: name.clone(),
run: probe.run.clone(),
file: probe.file.as_ref().map(|path| path.display().to_string()),
json: probe.json.clone(),
ast: probe.ast.clone(),
capture: probe.capture.clone(),
lang: probe.lang.clone(),
allow_empty: probe.allow_empty,
source: source_of(&provenance.probes, name, base),
})
.collect();
let commands = manifest
.commands
.iter()
.map(|command| CommandEntry {
name: command.name.clone(),
match_mode: match command.match_mode {
MatchMode::Prefix => "prefix",
MatchMode::Exact => "exact",
},
inputs: command.inputs.clone(),
exclude: command.exclude.clone(),
outputs: command
.outputs
.iter()
.map(|path| path.display().to_string())
.collect(),
tags: command.tags.clone(),
on_hit: command.on_hit.clone(),
ttl: command.ttl.as_ref().map(ToString::to_string),
source: source_of(&provenance.commands, &command.name, base),
})
.collect();
Ok(Dump {
manifest: Provenance::display(&located.path, base),
sources,
policy,
scopes,
probes,
commands,
})
}
fn collect_policy(manifest: &Manifest, root_path: &Path, base: &Path) -> Result<Policy> {
let declared = crate::compose::declared_policy_keys(root_path, base)?;
Ok(Policy {
source: Provenance::display(root_path, base),
gitignore: manifest.gitignore,
cache_dir: manifest.cache_dir.clone(),
strict: [
(StrictCase::NoMatch, "no_match"),
(StrictCase::NoInputs, "no_inputs"),
]
.into_iter()
.filter(|(case, _)| manifest.strict.enforces(*case))
.map(|(_, label)| label)
.collect(),
on_hit: manifest.on_hit.clone(),
probe_shell: manifest.probe_shell.clone(),
declared,
})
}
fn source_of(
sources: &std::collections::BTreeMap<String, PathBuf>,
name: &str,
base: &Path,
) -> String {
let path = sources
.get(name)
.expect("provenance recorded for every entry the merged manifest carries");
Provenance::display(path, base)
}
fn render_policy(policy: &Policy) -> String {
let on_hit = policy
.on_hit
.as_deref()
.map_or_else(|| "(none)".to_owned(), |value| format!("{value:?}"));
let probe_shell = policy
.probe_shell
.iter()
.map(|part| format!("{part:?}"))
.collect::<Vec<_>>()
.join(", ");
let mut out = format!("\npolicy: # {}\n", policy.source);
for (key, value) in [
("gitignore", policy.gitignore.to_string()),
("cache_dir", policy.cache_dir.clone()),
("strict", format!("[{}]", policy.strict.join(", "))),
("on_hit", on_hit),
("probe_shell", format!("[{probe_shell}]")),
] {
out.push_str(&format!(
" {key}: {value}{}\n",
default_marker(key, &policy.declared)
));
}
out
}
fn render_text(dump: &Dump) -> String {
let mut out = String::from("sources:\n");
for (index, source) in dump.sources.iter().enumerate() {
out.push_str(&format!(" {} {source}\n", index + 1));
}
out.push_str(&render_policy(&dump.policy));
if !dump.scopes.is_empty() {
out.push_str("\nscopes:\n");
for scope in &dump.scopes {
out.push_str(&format!(" {}: # {}\n", scope.name, scope.source));
out.push_str(&format!(" globs: [{}]\n", scope.globs.join(", ")));
if !scope.exclude.is_empty() {
out.push_str(&format!(" exclude: [{}]\n", scope.exclude.join(", ")));
}
if let Some(gitignore) = scope.gitignore {
out.push_str(&format!(" gitignore: {gitignore}\n"));
}
}
}
render_probes(&dump.probes, &mut out);
if !dump.commands.is_empty() {
out.push_str("\ncommands:\n");
for command in &dump.commands {
out.push_str(&format!(" {}: # {}\n", command.name, command.source));
out.push_str(&format!(" match: {}\n", command.match_mode));
if !command.inputs.is_empty() {
out.push_str(&format!(" inputs: [{}]\n", command.inputs.join(", ")));
}
if !command.exclude.is_empty() {
out.push_str(&format!(" exclude: [{}]\n", command.exclude.join(", ")));
}
if !command.outputs.is_empty() {
out.push_str(&format!(" outputs: [{}]\n", command.outputs.join(", ")));
}
if !command.tags.is_empty() {
out.push_str(&format!(" tags: [{}]\n", command.tags.join(", ")));
}
if let Some(on_hit) = &command.on_hit {
out.push_str(&format!(" on_hit: {on_hit}\n"));
}
if let Some(ttl) = &command.ttl {
out.push_str(&format!(" ttl: {ttl}\n"));
}
}
}
out
}
fn render_probes(probes: &[ProbeEntry], out: &mut String) {
if probes.is_empty() {
return;
}
out.push_str("\nprobes:\n");
for probe in probes {
out.push_str(&format!(" {}: # {}\n", probe.name, probe.source));
if let Some(run) = &probe.run {
out.push_str(&format!(" run: {run}\n"));
}
if let Some(file) = &probe.file {
out.push_str(&format!(" file: {file}\n"));
}
if let Some(json) = &probe.json {
out.push_str(&format!(" json: {json}\n"));
}
if let Some(ast) = &probe.ast {
out.push_str(&format!(" ast: {ast}\n"));
}
if let Some(capture) = &probe.capture {
out.push_str(&format!(" capture: [{}]\n", capture.join(", ")));
}
if let Some(lang) = &probe.lang {
out.push_str(&format!(" lang: {lang}\n"));
}
out.push_str(&format!(" allow_empty: {}\n", probe.allow_empty));
}
}
fn default_marker(key: &str, declared: &BTreeSet<&'static str>) -> &'static str {
if declared.contains(key) {
""
} else {
" (default)"
}
}
#[cfg(test)]
#[path = "dump_tests.rs"]
mod tests;