#![forbid(unsafe_code)]
use std::path::PathBuf;
use std::process::ExitCode;
use lonca_audit::{Options, audit, compare};
use lonca_core::Store;
use lonca_core::oplog::OpLog;
fn usage() -> ExitCode {
eprintln!(
"usage:\n lonca-audit verify <git-dir> [--shallow] [--no-refs] [--all-refs] [--json] [--freeze-flag <file>]\n lonca-audit compare <local-git-dir> <published-git-dir>"
);
ExitCode::from(2)
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("verify") => verify(&args[1..]),
Some("compare") => compare_cmd(&args[1..]),
_ => usage(),
}
}
fn verify(args: &[String]) -> ExitCode {
let mut dir = None;
let mut opts = Options::default();
let mut json = false;
let mut freeze_flag: Option<PathBuf> = None;
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--shallow" => opts.deep = false,
"--no-refs" => opts.check_refs = false,
"--all-refs" => opts.all_refs = true,
"--json" => json = true,
"--freeze-flag" => match it.next() {
Some(p) => freeze_flag = Some(PathBuf::from(p)),
None => return usage(),
},
p if dir.is_none() && !p.starts_with('-') => dir = Some(PathBuf::from(p)),
_ => return usage(),
}
}
let Some(dir) = dir else { return usage() };
let store = match Store::open(&dir) {
Ok(s) => s,
Err(e) => {
eprintln!("lonca-audit: {}", open_error(&dir, &e));
return ExitCode::from(2);
}
};
let report = match audit(&store, opts) {
Ok(r) => r,
Err(e) => {
eprintln!("lonca-audit: {e}");
return ExitCode::from(2);
}
};
if json {
println!(
"{}",
serde_json::to_string_pretty(&report).expect("serializable")
);
} else {
println!("operations: {}", report.ops);
println!("head: {}", report.head_hash);
for k in &report.key_switches {
println!(
"daemon key {:>6} {:<14} {} by {}",
k.seq, k.kind, k.key, k.actor
);
}
for f in &report.skipped {
println!(
"skipped {:>6} {:<9} {}",
f.seq.map(|s| s.to_string()).unwrap_or_default(),
f.kind,
f.detail
);
}
for f in &report.findings {
println!(
"FINDING {:>6} {:<9} {}",
f.seq.map(|s| s.to_string()).unwrap_or_default(),
f.kind,
f.detail
);
}
println!("{}", if report.ok() { "ok" } else { "FAILED" });
}
if !report.ok() {
if let Some(flag) = freeze_flag {
let reason = report
.findings
.first()
.map(|f| format!("audit: {} {}", f.kind, f.detail))
.unwrap_or_default();
if let Err(e) = std::fs::write(&flag, reason) {
eprintln!("lonca-audit: could not write freeze flag: {e}");
}
}
return ExitCode::from(1);
}
ExitCode::SUCCESS
}
fn open_error(dir: &std::path::Path, e: &impl std::fmt::Display) -> String {
match std::fs::read_dir(dir) {
Err(io) if io.kind() == std::io::ErrorKind::PermissionDenied => format!(
"cannot read {}: permission denied. A hardened daemon's store is readable by \
its user only: verify a backup bundle instead (`lonca backup`, then \
git init --bare x.git && git -C x.git fetch <bundle> '+refs/*:refs/*' && \
lonca-audit verify x.git)",
dir.display()
),
_ => e.to_string(),
}
}
fn compare_cmd(args: &[String]) -> ExitCode {
let [local, published] = args else {
return usage();
};
let read = |p: &str| -> Result<Vec<lonca_core::oplog::SignedOp>, String> {
let p = std::path::Path::new(p);
let s = Store::open(p).map_err(|e| open_error(p, &e))?;
OpLog::new(&s).read_all().map_err(|e| e.to_string())
};
let (l, p) = match (read(local), read(published)) {
(Ok(l), Ok(p)) => (l, p),
(Err(e), _) | (_, Err(e)) => {
eprintln!("lonca-audit: {e}");
return ExitCode::from(2);
}
};
let findings = compare(&l, &p);
for f in &findings {
println!(
"FINDING {:>6} {:<9} {}",
f.seq.map(|s| s.to_string()).unwrap_or_default(),
f.kind,
f.detail
);
}
if findings.is_empty() {
println!(
"ok: published log ({} entries) is a prefix of the local log ({} entries)",
p.len(),
l.len()
);
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}