use std::fs;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use crate::error::{Error, Result};
use crate::etree::{self, ParseOps};
use crate::ledger;
use super::{AuditLogSubcmd, CommonArgs, PinSubcmd, SnapshotSubcmd, walk_for_chains};
fn compute_chain_head(path: &str) -> Result<String> {
let mut paops = ParseOps::new(Box::new(crate::crypto::CryptoPolicyDefault {}))?;
paops.runtime.fname = path.to_string();
let reader: Box<dyn BufRead> = if path == "-" {
Box::new(BufReader::new(std::io::stdin()))
} else {
Box::new(BufReader::new(std::fs::File::open(path)?))
};
let tree = etree::parse(reader, &mut paops)?;
let mut blob = Vec::new();
etree::tree_write(&mut blob, &tree, &mut paops)?;
let policy = crate::crypto::CryptoPolicyDefault {};
crate::crypto::hexdigest("sha3-256", &blob, &policy)
}
pub fn snapshot(a: SnapshotSubcmd) -> Result<()> {
let head = compute_chain_head(&a.file)?;
println!("{}", head);
Ok(())
}
pub fn pin(a: PinSubcmd) -> Result<()> {
let head = compute_chain_head(&a.file)?;
if head == a.expected {
println!("OK");
Ok(())
} else {
Err(Error::SignatureVerify {
key_id: format!("chain head mismatch: expected {}, got {}", a.expected, head),
})
}
}
pub fn audit_log_stream(_common: CommonArgs, a: AuditLogSubcmd) -> Result<()> {
let priv_pem = fs::read_to_string(&a.signer)?;
let mut tree: etree::TextTree = if Path::new(&a.file).exists() {
let mut paops = ParseOps::new(Box::new(crate::crypto::CryptoPolicyDefault {}))?;
paops.runtime.fname = a.file.clone();
let f = std::fs::File::open(&a.file)?;
etree::parse(BufReader::new(f), &mut paops)?
} else {
Vec::new()
};
let mut last_anchor = latest_anchor_hash(&tree);
let stdin = std::io::stdin();
let mut line_count = 0usize;
for line_in in stdin.lock().lines() {
let line = line_in?;
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
tree.push(etree::TextNode::Plain(trimmed.to_string()));
let chain_node =
build_chain_anchor_node_with_parent(&tree, &priv_pem, "append", "", last_anchor)?;
if let etree::TextNode::Chain { extfields } = &chain_node
&& let Ok(signed) = ledger::SignedAnchor::from_extfields(extfields)
&& let Ok(h) = signed.id()
{
last_anchor = Some(h);
}
tree.push(chain_node);
line_count += 1;
}
if line_count == 0 {
eprintln!("audit-log: no lines read from stdin; file unchanged.");
return Ok(());
}
let tmp_path = format!("{}.tmp", a.file);
let mut paops = ParseOps::new(Box::new(crate::crypto::CryptoPolicyDefault {}))?;
paops.runtime.fname = a.file.clone();
let mut writer = BufWriter::new(std::fs::File::create(&tmp_path)?);
etree::tree_write(&mut writer, &tree, &mut paops)?;
writer.flush()?;
drop(writer);
fs::rename(&tmp_path, &a.file)?;
eprintln!("audit-log: appended {} anchor(s) to {}", line_count, a.file);
Ok(())
}
fn latest_anchor_hash(tree: &etree::TextTree) -> Option<ledger::AnchorHash> {
let mut all = Vec::new();
let _ = walk_for_chains(tree, &mut all);
all.pop()
}
fn build_chain_anchor_node_with_parent(
tree: &etree::TextTree,
priv_pem: &str,
operation: &str,
words_csv: &str,
parent: Option<ledger::AnchorHash>,
) -> Result<etree::TextNode> {
use crate::ledger::{Anchor, PayloadHash, SignerId};
use crate::pki::SigAlgKind;
use std::collections::BTreeMap;
let botan_priv = botan::Privkey::load_pem(priv_pem).map_err(Error::botan)?;
let botan_pub = botan_priv.pubkey().map_err(Error::botan)?;
let pub_pem = botan_pub.pem_encode().map_err(Error::botan)?;
let fp = crate::capability::KeyFp::from_pem(&pub_pem)?;
let mut paops = ParseOps::new(Box::new(crate::crypto::CryptoPolicyDefault {}))?;
let blob = etree::tree_to_blob(tree, &mut paops)?;
let policy = crate::crypto::CryptoPolicyDefault {};
let payload_hex = crate::crypto::hexdigest("sha3-256", &blob, &policy)?;
let mut payload_arr = [0u8; 32];
payload_arr.copy_from_slice(&hex::decode(payload_hex)?);
let payload_hash = PayloadHash(payload_arr);
let mutations = if words_csv.is_empty() {
operation.to_string()
} else {
format!("{}+{}", operation, words_csv)
};
let parents: Vec<_> = parent.into_iter().collect();
let signer = SignerId::new(SigAlgKind::Ed25519, fp);
let anchor = Anchor::builder(signer, payload_hash)
.with_parents(parents)
.with_mutations(mutations)
.build();
let signed = anchor.sign(priv_pem, &pub_pem, SigAlgKind::Ed25519)?;
let extfields: BTreeMap<String, String> = signed.to_extfields();
Ok(etree::TextNode::Chain { extfields })
}