use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::Result;
use clap::{Parser, Subcommand};
use holographic_memory::core::format::{
self, plan_upgrade, scan, upgrade_store, verify_path, Severity, VerifyOptions,
};
#[derive(Parser)]
#[command(
name = "hms-format",
about = "Verify and upgrade the persisted format of an HMS store, offline",
after_help = "Exit codes: 0 ok, 10 recoverable, 20 corrupt, 30 unsupported version, 64 usage, 74 I/O"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Verify {
path: PathBuf,
#[arg(long)]
passphrase_env: Option<String>,
#[arg(long)]
deep: bool,
#[arg(long)]
json: bool,
},
Plan {
path: PathBuf,
#[arg(long)]
json: bool,
},
Upgrade {
source: PathBuf,
destination: PathBuf,
#[arg(long)]
dry_run: bool,
#[arg(long)]
json: bool,
},
Versions,
Dump {
path: PathBuf,
#[arg(long)]
frame: Option<usize>,
},
}
fn main() -> ExitCode {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(error) => {
let code = if error.use_stderr() { 64 } else { 0 };
let _ = error.print();
return ExitCode::from(code);
}
};
match run(cli.command) {
Ok(code) => ExitCode::from(code),
Err(error) => {
eprintln!("hms-format: {error:#}");
ExitCode::from(74)
}
}
}
fn run(command: Command) -> Result<u8> {
match command {
Command::Verify {
path,
passphrase_env,
deep,
json,
} => {
let report = verify_path(
&path,
&VerifyOptions {
passphrase_env,
deep,
},
)?;
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
print_report(&report, 0);
}
Ok(report.exit_code as u8)
}
Command::Plan { path, json } => {
let plan = plan_upgrade(&path)?;
if json {
println!("{}", serde_json::to_string_pretty(&plan)?);
} else {
print_plan(&plan);
}
Ok(0)
}
Command::Upgrade {
source,
destination,
dry_run,
json,
} => {
let report = upgrade_store(&source, &destination, dry_run)?;
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
print_plan(&report.plan);
match (&report.destination, dry_run) {
(Some(destination), _) => {
println!("upgraded {} frames into {destination}", report.frames)
}
(None, true) => println!("dry run: nothing written"),
(None, false) => println!("nothing to do"),
}
}
Ok(0)
}
Command::Versions => {
println!("{}", serde_json::to_string_pretty(&format::CURRENT)?);
Ok(0)
}
Command::Dump { path, frame } => {
dump(&path, frame)?;
Ok(0)
}
}
}
fn print_plan(plan: &format::UpgradePlan) {
println!(
"{}: storage format {} (this build writes {})",
plan.source, plan.storage_format, plan.target_storage_format
);
if plan.up_to_date {
println!("up to date");
}
for step in &plan.steps {
println!(
" {} ({} {} -> {}): {}",
step.id, step.artefact, step.from, step.to, step.description
);
}
}
fn print_report(report: &format::Report, depth: usize) {
let pad = " ".repeat(depth);
println!(
"{pad}{}: {:?} (exit {})",
report.path, report.verdict, report.exit_code
);
let v = &report.versions;
println!(
"{pad} storage format {:?}, schema {:?}, encoder {:?}, encrypted {:?}, dimensions {:?}",
v.storage_format, v.store_schema, v.text_encoder, v.encrypted, v.dimensions
);
println!(
"{pad} {} segments, {} frames, {} mutations, documents {:?}, vectors {:?}, stamped {:?}",
report.segments,
report.frames,
report.mutations,
report.live_documents,
report.live_vectors,
report.memory_stamped_documents
);
if !report.checks.is_empty() {
println!("{pad} passed: {}", report.checks.join(", "));
}
for finding in &report.findings {
let tag = match finding.severity {
Severity::Info => "info",
Severity::Warning => "warning",
Severity::Error => "ERROR",
Severity::Unsupported => "UNSUPPORTED",
};
let at = finding.offset.map(|o| format!("@{o}")).unwrap_or_default();
println!(
"{pad} [{tag}] {} {}{at}: {}",
finding.code, finding.file, finding.message
);
}
for tenant in &report.tenants {
println!("{pad} tenant {}", tenant.name);
print_report(&tenant.report, depth + 2);
}
}
fn dump(path: &std::path::Path, only: Option<usize>) -> Result<()> {
let arena = path.join("vectors_data.bin");
let generation = holographic_memory::core::format::verify::resolve_generation(&arena)?;
let mut index = 0usize;
for segment in 0.. {
let file = generation.join(format!("seg_{segment}.bin"));
if !file.exists() {
break;
}
let map = std::fs::read(&file)?;
let scan = scan::scan_segment(&map, true, |frame, payload| {
if only.is_none_or(|n| n == index) {
if only.is_some() {
match scan::transaction_json(payload) {
Some(Ok(json)) => println!("{}", String::from_utf8_lossy(&json)),
_ => println!("<{} bytes, not a plaintext transaction>", payload.len()),
}
} else {
let kinds = match scan::transaction_json(payload) {
Some(Ok(json)) => serde_json::from_slice::<Vec<serde_json::Value>>(&json)
.map(|ms| {
ms.iter()
.filter_map(|m| {
m.as_object().and_then(|o| o.keys().next().cloned())
})
.collect::<Vec<_>>()
.join(",")
})
.unwrap_or_default(),
_ => "(opaque)".to_owned(),
};
println!(
"frame {index} seg {segment} @{} raw {} stored {} crc {:08x} seq {} {kinds}",
frame.offset, frame.raw_len, frame.comp_len, frame.crc, frame.seq
);
}
}
index += 1;
});
if only.is_none() {
println!(
"segment {segment} ends: {:?} at {}",
scan.end, scan.end_offset
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_every_subcommand() {
for args in [
vec!["hms-format", "verify", "store"],
vec![
"hms-format",
"verify",
"store",
"--deep",
"--json",
"--passphrase-env",
"P",
],
vec!["hms-format", "plan", "store"],
vec!["hms-format", "upgrade", "a", "b", "--dry-run"],
vec!["hms-format", "versions"],
vec!["hms-format", "dump", "store", "--frame", "3"],
] {
Cli::try_parse_from(args).expect("valid command");
}
}
}