use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::{Path, PathBuf};
use znippy_common::{VerifyReport, list_archive_contents, verify_archive_integrity};
use znippy_common::plugin::PluginRegistry;
use znippy_common::plugins::wasm_loader::WasmPlugin;
use znippy_compress::compress_dir;
use znippy_decompress::{decompress_archive, decompress_archive_filtered};
pub mod handlers;
#[inline]
fn functional_status(component: &str, check: &str, ok: bool, detail: &str) {
#[cfg(feature = "testmatrix")]
nornir_testmatrix::functional_status(component, check, ok, detail);
#[cfg(not(feature = "testmatrix"))]
{
let _ = (component, check, ok, detail);
}
}
#[derive(Parser)]
#[command(name = "znippy")]
#[command(about = "Znippy: fast archive format with per-file compression", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Compress {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long)]
no_skip: bool,
#[arg(long, default_value = "rust")]
format: String,
#[arg(long, default_value = "arrow-ipc")]
meta_format: String,
#[arg(long)]
warehouse: Option<PathBuf>,
#[arg(long)]
plugin: Option<PathBuf>,
#[arg(long, default_value_t = 1)]
plugin_type_id: i8,
#[arg(long, value_name = "KEY")]
sign: Option<PathBuf>,
#[arg(long, value_name = "CERT")]
sign_cert: Option<PathBuf>,
#[arg(long, default_value = "p256")]
sign_alg: String,
},
Append {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
add: PathBuf,
#[arg(short, long, default_value_t = 3)]
level: i32,
},
Decompress {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long = "type")]
pkg_type: Option<String>,
#[arg(long)]
repo: Option<String>,
},
List {
#[arg(short, long)]
input: PathBuf,
},
Get {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
path: String,
#[arg(short, long)]
output: Option<PathBuf>,
},
Verify {
#[arg(short, long)]
input: PathBuf,
#[arg(long)]
signed: bool,
#[arg(long, value_name = "CA")]
root: Vec<PathBuf>,
},
Seal {
#[arg(short, long)]
input: PathBuf,
#[arg(long)]
warehouse: PathBuf,
#[arg(long)]
namespace: Option<String>,
#[arg(short, long)]
output: PathBuf,
},
Handlers,
Run {
format: String,
cmd: String,
args: Vec<String>,
},
}
fn build_meta_sink(
meta_format: &str,
warehouse: Option<PathBuf>,
output: &std::path::Path,
) -> Result<Option<znippy_common::MetaSinkFactory>> {
match meta_format {
"arrow-ipc" => Ok(None),
"iceberg" => {
#[cfg(feature = "iceberg")]
{
let wh = warehouse.ok_or_else(|| {
anyhow::anyhow!("--warehouse <DIR> is required for --meta-format iceberg")
})?;
let namespace = output
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "znippy".to_string());
println!(
"π§ Metadata β Iceberg table (namespace `{namespace}`) in {}",
wh.display()
);
Ok(Some(Box::new(move |_file, _off| {
Box::new(znippy_iceberg::IcebergSink::new(wh, namespace))
as Box<dyn znippy_common::ArchiveMetaSink>
})))
}
#[cfg(not(feature = "iceberg"))]
{
let _ = (warehouse, output);
anyhow::bail!(
"iceberg metadata backend not compiled in; rebuild znippy-cli with `--features iceberg`"
)
}
}
other => anyhow::bail!("unknown --meta-format '{other}' (expected arrow-ipc|iceberg)"),
}
}
#[cfg(feature = "sign")]
fn build_signer(
sign: &Option<PathBuf>,
sign_cert: &Option<PathBuf>,
sign_alg: &str,
) -> Result<Option<Box<dyn znippy_common::sign::ArchiveSigner + Send>>> {
let Some(key_path) = sign else { return Ok(None) };
let cert_path = sign_cert
.as_ref()
.ok_or_else(|| anyhow::anyhow!("--sign requires --sign-cert <CERT> (DER signer certificate)"))?;
let alg = znippy_common::sign::SigAlg::from_name(sign_alg)?;
let key = std::fs::read(key_path)?;
let cert = std::fs::read(cert_path)?;
Ok(Some(znippy_common::sign::signer_from_pkcs8(alg, &key, &cert)?))
}
#[cfg(feature = "sign")]
fn sign_meta_factory(
signer: Box<dyn znippy_common::sign::ArchiveSigner + Send>,
) -> znippy_common::MetaSinkFactory {
Box::new(move |file, off| {
Box::new(znippy_common::ArrowIpcSink::new(file, off).with_signer(signer))
as Box<dyn znippy_common::ArchiveMetaSink>
})
}
#[cfg(feature = "sign")]
fn run_signed_verify(input: &std::path::Path, roots: &[PathBuf]) -> Result<()> {
anyhow::ensure!(
!roots.is_empty(),
"--signed requires at least one --root <CA> (DER trusted root)"
);
let ders: Vec<Vec<u8>> = roots.iter().map(std::fs::read).collect::<std::io::Result<_>>()?;
let store = znippy_common::sign::CertStore::from_der_certs(&ders)?;
let report = znippy_common::sign::verify_archive(input, &store)?;
println!("\nπ Provenans verifierad:");
println!("βοΈ Signerad av (CN): {}", report.signer.common_name);
println!("πͺͺ Subjekt: {}", report.signer.subject);
println!("π¦ Verifierade artefakter: {}", report.artifacts_verified);
functional_status(
"znippy-cli/verify-signed",
"provenance_chain",
true,
&format!(
"CMS chained to root; signer={}, artifacts={}",
report.signer.common_name, report.artifacts_verified
),
);
Ok(())
}
fn collect_files(root: &Path, dir: &Path, out: &mut Vec<(String, Vec<u8>)>) -> Result<()> {
let mut entries: Vec<_> = std::fs::read_dir(dir)?
.collect::<std::io::Result<Vec<_>>>()?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
let ft = entry.file_type()?;
if ft.is_dir() {
collect_files(root, &path, out)?;
} else if ft.is_file() {
let rel = path
.strip_prefix(root)
.unwrap_or(&path)
.to_string_lossy()
.into_owned();
let bytes = std::fs::read(&path)?;
out.push((rel, bytes));
}
}
Ok(())
}
pub fn run() -> Result<()> {
env_logger::init();
let cli = Cli::parse();
match cli.command {
Commands::Compress {
input,
output,
no_skip,
format,
meta_format,
warehouse,
plugin,
plugin_type_id,
sign,
sign_cert,
sign_alg,
} => {
let registry = match plugin {
Some(wasm_path) => {
let wp = WasmPlugin::load(&wasm_path.to_string_lossy(), "wasm-plugin", plugin_type_id)?;
PluginRegistry::with_plugin(Box::new(wp))
}
None => {
let handler = handlers::find_handler(&format)?;
println!("π Handler: {} (type_id {})", handler.meta().name, handler.type_id());
PluginRegistry::with_plugin(handler)
}
};
#[allow(unused_mut)]
let mut sink_factory = build_meta_sink(&meta_format, warehouse, &output)?;
#[cfg(feature = "sign")]
{
if let Some(signer) = build_signer(&sign, &sign_cert, &sign_alg)? {
anyhow::ensure!(
sink_factory.is_none(),
"--sign is only supported with --meta-format arrow-ipc"
);
println!("π Signering aktiverad ({sign_alg})");
sink_factory = Some(sign_meta_factory(signer));
functional_status(
"znippy-cli/compress-sign",
"signer_loaded",
true,
&format!("detached CMS provenance armed ({sign_alg})"),
);
}
}
#[cfg(not(feature = "sign"))]
{
let _ = &sign_alg;
anyhow::ensure!(
sign.is_none() && sign_cert.is_none(),
"signing not compiled in; rebuild znippy-cli with `--features sign`"
);
}
let report = compress_dir(&input, &output, no_skip, Some(®istry), None, sink_factory)?;
println!("\nβ
Komprimering klar:");
println!("π Totalt antal filer: {}", report.total_files);
println!("π Totalt antal chunks: {}", report.chunks);
println!("π Totalt antal kataloger: {}", report.total_dirs);
println!("π¦ Filer komprimerade: {}", report.compressed_files);
println!(
"π Filer ej komprimerade: {}",
report.uncompressed_files
);
println!("π₯ Totalt inlΓ€sta bytes: {}", report.total_bytes_in);
println!("π€ Totalt skrivna bytes: {}", report.total_bytes_out);
println!("π Bytes som komprimerades: {}", report.compressed_bytes);
println!(
"π Bytes ej komprimerade: {}",
report.uncompressed_bytes
);
println!(
"π Komprimeringsgrad: {:.2}%",
report.compression_ratio
);
}
Commands::Append { input, add, level } => {
let mut files = Vec::new();
collect_files(&add, &add, &mut files)?;
let file_count = files.len();
anyhow::ensure!(
file_count > 0,
"inga filer att lΓ€gga till hittades under {}",
add.display()
);
let report = znippy_common::append_files(&input, &files, level)?;
println!("\nβ
Append klar:");
println!("π¦ Arkiv: {}", input.display());
println!("π Filer tillagda: {}", file_count);
println!("β Nya rader: {}", report.rows_added);
println!("π Rader innan: {}", report.rows_before);
println!("π Blob-append-offset: {}", report.blob_append_offset);
println!("π€ Nya blob-bytes: {}", report.blob_bytes_added);
println!("πΎ Slutlig arkivstorlek: {}", report.sealed_total_bytes);
functional_status(
"znippy-cli/append",
"native_append",
report.rows_added >= file_count as u64,
&format!(
"appended {file_count} files ({} new rows) into {}",
report.rows_added,
input.display()
),
);
}
Commands::Decompress { input, output, pkg_type, repo } => {
let filter = znippy_common::IndexFilter {
pkg_type: match &pkg_type {
Some(name) => Some(handlers::find_handler(name)?.type_id()),
None => None,
},
repo: repo.clone(),
};
let report: VerifyReport = if filter.is_empty() {
decompress_archive(&input, &output)?
} else {
println!(
"π Selective extract: type={} repo={}",
pkg_type.as_deref().unwrap_or("*"),
repo.as_deref().unwrap_or("*"),
);
decompress_archive_filtered(&input, &output, &filter)?
};
println!("\nβ
Dekomprimering och verifiering klar:");
println!("π Totala filer: {}", report.total_files);
println!("π Verifierade filer: {}", report.verified_files);
println!("π₯ chunks: {}", report.chunks);
println!("β Korrupta filer: {}", report.corrupt_files);
println!("π₯ Totala bytes: {}", report.total_bytes);
println!("π€ Verifierade bytes: {}", report.verified_bytes);
println!("β οΈ Korrupta bytes: {}", report.corrupt_bytes);
if report.corrupt_files > 0 || report.corrupt_bytes > 0 {
anyhow::bail!(
"dekomprimering misslyckades: {} korrupta filer, {} korrupta bytes β utdata Γ€r ofullstΓ€ndig/otillfΓΆrlitlig",
report.corrupt_files,
report.corrupt_bytes
);
}
}
Commands::List { input } => {
list_archive_contents(&input)?;
}
Commands::Get { input, path, output } => {
let reader = znippy_common::ArchiveReader::open(&input)?;
let data = reader.read_file(&path)?;
match output {
Some(dest) => {
std::fs::write(&dest, &data)?;
eprintln!("π€ {} ({} bytes) β {}", path, data.len(), dest.display());
}
None => {
use std::io::Write;
std::io::stdout().write_all(&data)?;
}
}
}
Commands::Verify { input, signed, root } => {
let report: VerifyReport = verify_archive_integrity(&input)?;
functional_status(
"znippy-cli/format-version-guard",
"on_disk_version_supported",
true,
&format!(
"reader max v{}",
znippy_common::index::ZNIPPY_FORMAT_VERSION
),
);
let integrity_ok = report.corrupt_files == 0 && report.corrupt_bytes == 0;
functional_status(
"znippy-cli/verify",
"integrity_checksum",
integrity_ok,
&format!(
"{} verified, {} corrupt files",
report.verified_files, report.corrupt_files
),
);
println!("\nπ Verifiering klar:");
println!("π Totala filer: {}", report.total_files);
println!("π Verifierade filer: {}", report.verified_files);
println!("β Korrupta filer: {}", report.corrupt_files);
println!("π₯ Totala bytes: {}", report.total_bytes);
println!("π€ Verifierade bytes: {}", report.verified_bytes);
println!("β οΈ Korrupta bytes: {}", report.corrupt_bytes);
if signed {
#[cfg(feature = "sign")]
run_signed_verify(&input, &root)?;
#[cfg(not(feature = "sign"))]
{
let _ = &root;
anyhow::bail!(
"signature verification not compiled in; rebuild znippy-cli with `--features sign`"
);
}
}
}
Commands::Seal { input, warehouse, namespace, output } => {
#[cfg(feature = "iceberg")]
{
let ns = namespace.unwrap_or_else(|| {
input
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "znippy".to_string())
});
println!(
"π§βπ¦ Sealing iceberg archive (namespace `{ns}`) in {} β {}",
warehouse.display(),
output.display()
);
let report = znippy_iceberg::seal(&input, &warehouse, &ns, &output)?;
println!("\nβ
Sealed (static native .znippy):");
println!("π Filer: {}", report.files);
println!("π§± Chunk-rader: {}", report.rows);
println!(
"π€ Blob-bytes Γ₯teranvΓ€nda: {} (ingen omkomprimering)",
report.blob_bytes_copied
);
println!("π¦ Sealad total storlek: {}", report.sealed_total_bytes);
println!(
"π Metadata-svans + footer: {} bytes",
report.sealed_total_bytes - report.blob_bytes_copied
);
}
#[cfg(not(feature = "iceberg"))]
{
let _ = (input, warehouse, namespace, output);
anyhow::bail!(
"iceberg backend not compiled in; rebuild znippy-cli with `--features iceberg`"
);
}
}
Commands::Handlers => {
handlers::print_catalog();
}
Commands::Run { format, cmd, args } => {
let handler = handlers::find_handler(&format)?;
let dispatch = handler.run_command(&cmd, &args);
functional_status(
"znippy-cli/run-dispatch",
"handler_command",
dispatch.is_ok(),
&format!("handler `{}` cmd `{}`", handler.meta().name, cmd),
);
dispatch?;
}
}
Ok(())
}
#[cfg(all(test, feature = "sign"))]
mod sign_tests {
use super::*;
use znippy_common::sign;
#[test]
fn compress_sign_then_verify_signed_round_trip() {
let dir = tempfile::tempdir().unwrap();
let input = dir.path().join("src");
std::fs::create_dir_all(&input).unwrap();
std::fs::write(input.join("a.txt"), b"hello znippy provenance").unwrap();
std::fs::write(input.join("b.txt"), vec![7u8; 4096]).unwrap();
let (ca_key, ca_der) = sign::dev::mint_ca("Znippy CLI Test CA").unwrap();
let signer = sign::dev::new_p256_signer(&ca_key, &ca_der, "cli-signer").unwrap();
let factory = sign_meta_factory(Box::new(signer));
let registry = PluginRegistry::with_plugin(handlers::find_handler("rust").unwrap());
let output = dir.path().join("out");
compress_dir(&input, &output, false, Some(®istry), None, Some(factory)).unwrap();
let archive = output.with_extension("znippy");
assert!(archive.exists());
let ca_path = dir.path().join("ca.der");
std::fs::write(&ca_path, &ca_der).unwrap();
run_signed_verify(&archive, &[ca_path]).unwrap();
let (_other_key, other_der) = sign::dev::mint_ca("Rogue CA").unwrap();
let rogue_path = dir.path().join("rogue.der");
std::fs::write(&rogue_path, &other_der).unwrap();
assert!(run_signed_verify(&archive, &[rogue_path]).is_err());
assert!(run_signed_verify(&archive, &[]).is_err());
}
}