use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use crate::error::{Error, Result};
use crate::etree::{self, ParseOps};
use crate::output;
use super::pipeline::pair_inputs_to_outputs;
use super::{CommonArgs, OutputArgs, apply_common, resolve_policy};
pub fn run(common: CommonArgs, output_args: OutputArgs) -> Result<()> {
let policy = resolve_policy(&common)?;
let mut paops = ParseOps::new(policy)?;
apply_common(&common, &mut paops);
let files = pair_inputs_to_outputs(
&output_args.files,
&output_args.output,
&output_args.prefix,
output_args.output_dir.as_deref(),
);
let stdout = std::io::stdout();
let mut out = stdout.lock();
let mut json_listings: Vec<output::FileListing> = Vec::new();
for (path_in, _) in &files {
let reader: Box<dyn BufRead> = if path_in == "-" {
Box::new(BufReader::new(std::io::stdin()))
} else {
Box::new(BufReader::new(File::open(path_in).map_err(|e| {
Error::Io(std::io::Error::other(format!(
"Failed to open {path_in}: {e}"
)))
})?))
};
paops.runtime.fname = path_in.clone();
let tree = etree::parse(reader, &mut paops)?;
match common.format {
output::OutputFormat::Text => {
if files.len() > 1 {
writeln!(out, "== {} ==", path_in)?;
}
list_tree(&tree, 0, &mut out)?;
}
output::OutputFormat::Json => {
let mut nodes = Vec::new();
list_tree_to_nodes(&tree, 0, &mut nodes);
json_listings.push(output::FileListing {
path: path_in.clone(),
nodes,
});
}
}
}
if matches!(common.format, output::OutputFormat::Json) {
let payload = output::ListOutput {
files: json_listings,
};
writeln!(out, "{}", output::to_json(&payload)?)?;
}
Ok(())
}
pub(super) fn list_tree<W: Write>(tree: &etree::TextTree, depth: usize, out: &mut W) -> Result<()> {
let indent = " ".repeat(depth);
for node in tree {
match node {
etree::TextNode::BeginEnd { keyw, txt } => {
writeln!(out, "{}BEGIN/END {}", indent, keyw)?;
list_tree(txt, depth + 1, out)?;
}
etree::TextNode::Encrypted {
keyw, extfields, ..
} => {
let cipher = extfields
.get("cipher")
.map(|s| s.as_str())
.unwrap_or("aes-256-siv");
let pbkdf = extfields
.get("pbkdf")
.map(|s| s.split('$').nth(1).unwrap_or("?"))
.unwrap_or("legacy");
writeln!(
out,
"{}ENCRYPTED {} cipher={} pbkdf={}",
indent, keyw, cipher, pbkdf
)?;
}
etree::TextNode::Stored { keyw, cas } => {
writeln!(
out,
"{}STORED {} cas={}…",
indent,
keyw,
&cas[..cas.len().min(16)]
)?;
}
etree::TextNode::Plain(_) | etree::TextNode::Data(_) => {}
etree::TextNode::Chain { extfields } => {
let signer = extfields.get("signer").map(|s| s.as_str()).unwrap_or("?");
let payload = extfields.get("payload").map(|s| s.as_str()).unwrap_or("?");
let short_payload = &payload[..payload.len().min(16)];
writeln!(
out,
"{}CHAIN signer={} payload={}…",
indent, signer, short_payload
)?;
}
etree::TextNode::Include { hash } => {
writeln!(out, "{}INCLUDE {}…", indent, &hash[..hash.len().min(16)])?;
}
etree::TextNode::Conflict { keyw, .. } => {
writeln!(out, "{}CONFLICT {}", indent, keyw)?;
}
etree::TextNode::Immutable { name, .. } => {
writeln!(out, "{}IMMUTABLE {}", indent, name)?;
}
etree::TextNode::Muted { name, .. } => {
writeln!(out, "{}MUTED {}", indent, name)?;
}
etree::TextNode::Key { name, .. } => {
writeln!(out, "{}KEY {}", indent, name)?;
}
etree::TextNode::Unkey { name } => {
writeln!(out, "{}UNKEY {}", indent, name)?;
}
etree::TextNode::Cert { name, .. } => {
writeln!(out, "{}CERT {}", indent, name)?;
}
etree::TextNode::Uncert { name } => {
writeln!(out, "{}UNCERT {}", indent, name)?;
}
}
}
Ok(())
}
fn list_tree_to_nodes(tree: &etree::TextTree, depth: usize, out: &mut Vec<output::ListNode>) {
for node in tree {
match node {
etree::TextNode::BeginEnd { keyw, txt } => {
let mut children = Vec::new();
list_tree_to_nodes(txt, depth + 1, &mut children);
out.push(output::ListNode {
kind: "begin-end",
word: keyw.clone(),
depth,
cipher: None,
pbkdf: None,
cas: None,
signer: None,
payload: None,
children,
});
}
etree::TextNode::Encrypted {
keyw, extfields, ..
} => {
let cipher = extfields
.get("cipher")
.cloned()
.or_else(|| Some("aes-256-siv".to_string()));
let pbkdf = extfields
.get("pbkdf")
.and_then(|s| s.split('$').nth(1).map(String::from))
.or_else(|| Some("legacy".to_string()));
out.push(output::ListNode {
kind: "encrypted",
word: keyw.clone(),
depth,
cipher,
pbkdf,
cas: None,
signer: None,
payload: None,
children: Vec::new(),
});
}
etree::TextNode::Stored { keyw, cas } => {
out.push(output::ListNode {
kind: "stored",
word: keyw.clone(),
depth,
cipher: None,
pbkdf: None,
cas: Some(cas.clone()),
signer: None,
payload: None,
children: Vec::new(),
});
}
etree::TextNode::Plain(_) | etree::TextNode::Data(_) => {}
etree::TextNode::Chain { extfields } => {
out.push(output::ListNode {
kind: "chain",
word: String::new(),
depth,
cipher: None,
pbkdf: None,
cas: None,
signer: extfields.get("signer").cloned(),
payload: extfields.get("payload").cloned(),
children: Vec::new(),
});
}
etree::TextNode::Include { hash } => {
out.push(output::ListNode {
kind: "include",
word: hash.clone(),
depth,
cipher: None,
pbkdf: None,
cas: None,
signer: None,
payload: None,
children: Vec::new(),
});
}
etree::TextNode::Conflict { keyw, .. } => {
out.push(output::ListNode {
kind: "conflict",
word: keyw.clone(),
depth,
cipher: None,
pbkdf: None,
cas: None,
signer: None,
payload: None,
children: Vec::new(),
});
}
etree::TextNode::Immutable { name, .. } => {
out.push(output::ListNode {
kind: "immutable",
word: name.clone(),
depth,
cipher: None,
pbkdf: None,
cas: None,
signer: None,
payload: None,
children: Vec::new(),
});
}
etree::TextNode::Muted { name, .. } => {
out.push(output::ListNode {
kind: "muted",
word: name.clone(),
depth,
cipher: None,
pbkdf: None,
cas: None,
signer: None,
payload: None,
children: Vec::new(),
});
}
etree::TextNode::Key { .. }
| etree::TextNode::Unkey { .. }
| etree::TextNode::Cert { .. }
| etree::TextNode::Uncert { .. } => {
}
}
}
}