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;
pub const GIT_HASH: &str = env!("ZNIPPY_GIT_HASH");
pub const VERSION_LINE: &str =
concat!("v", env!("CARGO_PKG_VERSION"), " (", env!("ZNIPPY_GIT_HASH"), ")");
#[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(version = VERSION_LINE)]
#[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,
#[arg(long = "meta", value_name = "PATH=KEY=VALUE")]
meta: Vec<String>,
#[arg(long = "meta-archive", value_name = "KEY=VALUE")]
meta_archive: Vec<String>,
},
Meta {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
key: Option<String>,
#[arg(short, long)]
prefix: Option<String>,
#[arg(long)]
paths_only: bool,
},
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)"),
}
}
fn compress_reporting(
input: &PathBuf,
output: &PathBuf,
no_skip: bool,
registry: &PluginRegistry,
sink_factory: Option<znippy_common::MetaSinkFactory>,
) -> Result<znippy_common::CompressionReport> {
match compress_dir(input, output, no_skip, Some(registry), None, sink_factory) {
Ok(report) => {
let ok = report.total_files > 0
&& report.chunks > 0
&& report.files_failed == 0;
functional_status(
"znippy-cli/compress",
"archive_written",
ok,
&format!(
"{} files ({} failed), {} chunks, {:.2}% ratio → {}",
report.total_files,
report.files_failed,
report.chunks,
report.compression_ratio,
output.display()
),
);
if report.files_failed > 0 {
eprintln!(
"⚠️ {} av {} filer kunde inte läsas och utelämnades ur arkivet",
report.files_failed, report.total_files
);
}
Ok(report)
}
Err(e) => {
functional_status(
"znippy-cli/compress",
"archive_written",
false,
&format!("compress failed: {e}"),
);
Err(e)
}
}
}
fn verify_reporting(input: &Path) -> Result<VerifyReport> {
let report = match verify_archive_integrity(input) {
Ok(r) => r,
Err(e) => {
functional_status(
"znippy-cli/verify",
"integrity_checksum",
false,
&format!("verify failed: {e}"),
);
return Err(e);
}
};
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
),
);
Ok(report)
}
fn decompress_reporting(
input: &PathBuf,
output: &PathBuf,
filter: &znippy_common::IndexFilter,
pkg_type: Option<&str>,
repo: Option<&str>,
) -> Result<VerifyReport> {
let result = if filter.is_empty() {
decompress_archive(input, output)
} else {
println!(
"🔎 Selective extract: type={} repo={}",
pkg_type.unwrap_or("*"),
repo.unwrap_or("*"),
);
decompress_archive_filtered(input, output, filter)
};
let report = match result {
Ok(r) => r,
Err(e) => {
functional_status(
"znippy-cli/decompress",
"reconstruct_verify",
false,
&format!("decompress failed: {e}"),
);
return Err(e);
}
};
let integrity_ok = report.corrupt_files == 0 && report.corrupt_bytes == 0;
functional_status(
"znippy-cli/decompress",
"reconstruct_verify",
integrity_ok,
&format!(
"{} verified, {} corrupt files, {} corrupt bytes",
report.verified_files, report.corrupt_files, report.corrupt_bytes
),
);
Ok(report)
}
#[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 load = (|| -> Result<Box<dyn znippy_common::sign::ArchiveSigner + Send>> {
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(znippy_common::sign::signer_from_pkcs8(alg, &key, &cert)?)
})();
match load {
Ok(signer) => Ok(Some(signer)),
Err(e) => {
functional_status(
"znippy-cli/compress-sign",
"signer_loaded",
false,
&format!("signer load failed ({sign_alg}): {e}"),
);
Err(e)
}
}
}
#[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<()> {
match run_signed_verify_inner(input, roots) {
Ok(()) => Ok(()),
Err(e) => {
functional_status(
"znippy-cli/verify-signed",
"provenance_chain",
false,
&format!("provenance verify failed: {e}"),
);
Err(e)
}
}
}
#[cfg(feature = "sign")]
pub fn provenance_is_verified(artifacts_verified: usize) -> bool {
artifacts_verified > 0
}
#[cfg(feature = "sign")]
fn run_signed_verify_inner(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.id.common_name);
println!("🪪 Subjekt: {}", report.signer.id.subject);
println!("🔑 Fingeravtryck (SHA-256): {}", report.signer.id.fingerprint_hex());
println!("📦 Verifierade artefakter: {}", report.artifacts_verified);
let ok = provenance_is_verified(report.artifacts_verified);
if !ok {
eprintln!(
"⚠️ arkivsignaturen kedjar till en betrodd rot, men NOLL artefakter \
verifierades — arkivet är antingen förseglat utan filer eller saknar \
sin per-artefakt-sektion"
);
}
functional_status(
"znippy-cli/verify-signed",
"provenance_chain",
ok,
&format!(
"CMS chained to root; signer={}, fp={}, artifacts={}",
report.signer.id.common_name,
report.signer.id.fingerprint_hex(),
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(())
}
fn parse_meta_args(
entry_args: &[String],
archive_args: &[String],
) -> Result<Option<znippy_common::MetaTable>> {
if entry_args.is_empty() && archive_args.is_empty() {
return Ok(None);
}
let mut table = znippy_common::MetaTable::new();
for raw in entry_args {
let (path, rest) = raw
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--meta {raw:?}: expected PATH=KEY=VALUE"))?;
let (key, value) = rest
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--meta {raw:?}: expected PATH=KEY=VALUE"))?;
table.insert(path, key, parse_meta_value(value)?);
}
for raw in archive_args {
let (key, value) = raw
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--meta-archive {raw:?}: expected KEY=VALUE"))?;
table.insert_archive(key, parse_meta_value(value)?);
}
Ok(Some(table))
}
fn parse_meta_value(raw: &str) -> Result<znippy_common::MetaValue> {
use znippy_common::MetaValue;
if let Some(file) = raw.strip_prefix('@') {
let bytes = std::fs::read(file)
.map_err(|e| anyhow::anyhow!("--meta value @{file}: {e}"))?;
return Ok(MetaValue::Bytes(bytes));
}
Ok(match raw {
"true" => MetaValue::Bool(true),
"false" => MetaValue::Bool(false),
_ => {
if let Ok(i) = raw.parse::<i64>() {
MetaValue::I64(i)
} else if let Ok(f) = raw.parse::<f64>() {
MetaValue::F64(f)
} else {
MetaValue::Str(raw.to_string())
}
}
})
}
fn run_meta_search(
input: &Path,
key: Option<&str>,
prefix: Option<&str>,
paths_only: bool,
) -> Result<()> {
use std::io::Write;
use znippy_common::{ArchiveMeta, MetaValue, read_archive_meta};
let meta = read_archive_meta(input)?;
let index = match &meta {
ArchiveMeta::NoMetadata => {
if !paths_only {
eprintln!(
"ℹ️ {} carries NO metadata index — nothing was searched. \
(That is not the same as searching and finding nothing.)",
input.display()
);
}
functional_status(
"znippy-cli/meta",
"no_metadata_reported_distinctly",
true,
"archive has no __znippy_meta__ section; reported as NoMetadata, exit 2",
);
std::io::stdout().flush().ok();
std::process::exit(2);
}
ArchiveMeta::Index(i) => i,
};
let hits: &[znippy_common::MetaEntry] = match (key, prefix) {
(Some(k), _) => index.find_by_key(k),
(None, Some(p)) => index.find_by_prefix(p),
(None, None) => index.find_by_prefix(""),
};
if paths_only {
for h in hits {
if let Some(p) = h.path() {
println!("{p}");
}
}
} else {
println!(
"🔎 {} — metadata index: {} rows, {} distinct keys",
input.display(),
index.len(),
index.keys().len()
);
if hits.is_empty() {
println!(" (searched — no row matches)");
}
for h in hits {
let scope = h.path().unwrap_or("<archive>");
let shown = match &h.value {
MetaValue::Str(v) => format!("{v:?}"),
MetaValue::I64(v) => v.to_string(),
MetaValue::F64(v) => v.to_string(),
MetaValue::Bool(v) => v.to_string(),
MetaValue::Bytes(b) => format!("<{} bytes>", b.len()),
};
println!(" {scope} {} = {shown}", h.key);
}
}
functional_status(
"znippy-cli/meta",
"index_searched_without_payload_read",
true,
&format!("{} rows in index, {} hits", index.len(), hits.len()),
);
std::io::stdout().flush().ok();
if hits.is_empty() {
std::process::exit(1);
}
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_reporting(&input, &output, no_skip, ®istry, sink_factory)?;
if report.files_failed == 0 {
println!("\n✅ Komprimering klar:");
} else {
println!("\n⚠️ Komprimering klar med fel:");
}
println!("📁 Totalt antal filer: {}", report.total_files);
println!("📁 Totalt antal chunks: {}", report.chunks);
println!("❌ Filer som misslyckades: {}", report.files_failed);
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, meta, meta_archive } => {
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 meta_table = parse_meta_args(&meta, &meta_archive)?;
let meta_rows = meta_table.as_ref().map_or(0, |t| t.len());
let report =
znippy_common::append_files_with_meta(&input, &files, level, meta_table)?;
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!("♻️ Ersatta rader: {}", report.rows_replaced);
println!("📍 Blob-append-offset: {}", report.blob_append_offset);
println!("📤 Nya blob-bytes: {}", report.blob_bytes_added);
println!("💾 Slutlig arkivstorlek: {}", report.sealed_total_bytes);
if meta_rows > 0 {
println!("🔎 Metadata-rader tillagda: {meta_rows}");
}
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 = decompress_reporting(
&input,
&output,
&filter,
pkg_type.as_deref(),
repo.as_deref(),
)?;
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::Meta { input, key, prefix, paths_only } => {
return run_meta_search(&input, key.as_deref(), prefix.as_deref(), paths_only);
}
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_reporting(&input)?;
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 report.corrupt_files > 0 || report.corrupt_bytes > 0 {
anyhow::bail!(
"verifiering misslyckades: {} korrupta filer, {} korrupta bytes — arkivet är skadat",
report.corrupt_files,
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(test)]
mod meta_cli_tests {
use super::*;
use znippy_common::MetaValue;
#[test]
fn cli_meta_values_are_typed_by_shape_predictably() {
assert_eq!(parse_meta_value("12").unwrap(), MetaValue::I64(12));
assert_eq!(parse_meta_value("-3").unwrap(), MetaValue::I64(-3));
assert_eq!(parse_meta_value("1.5").unwrap(), MetaValue::F64(1.5));
assert_eq!(parse_meta_value("true").unwrap(), MetaValue::Bool(true));
assert_eq!(parse_meta_value("false").unwrap(), MetaValue::Bool(false));
assert_eq!(parse_meta_value("wasi-p2").unwrap(), MetaValue::Str("wasi-p2".into()));
assert_eq!(parse_meta_value("1.0.2").unwrap(), MetaValue::Str("1.0.2".into()));
assert_eq!(parse_meta_value("").unwrap(), MetaValue::Str(String::new()));
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("m.wasm");
std::fs::write(&f, b"\0asm\x01\0\0\0").unwrap();
assert_eq!(
parse_meta_value(&format!("@{}", f.display())).unwrap(),
MetaValue::Bytes(b"\0asm\x01\0\0\0".to_vec())
);
assert!(parse_meta_value("@/nonexistent/x.wasm").is_err());
}
#[test]
fn absent_meta_flags_are_none_not_an_empty_table() {
assert!(parse_meta_args(&[], &[]).unwrap().is_none(), "no flags must mean NO section");
let t = parse_meta_args(
&["app/x.wasm=build-thing=@/dev/null".into()],
&["producer=znippy".into()],
)
.unwrap()
.expect("flags given → a table");
assert_eq!(t.len(), 2);
assert_eq!(t.rows()[0].path(), Some("app/x.wasm"));
assert_eq!(t.rows()[1].path(), None, "--meta-archive is archive-scoped");
assert!(parse_meta_args(&["nokey".into()], &[]).is_err());
assert!(parse_meta_args(&["path=keyonly".into()], &[]).is_err());
assert!(parse_meta_args(&[], &["novalue".into()]).is_err());
}
}
#[cfg(all(test, feature = "testmatrix"))]
fn fs_test_lock() -> std::sync::MutexGuard<'static, ()> {
use std::sync::{Mutex, OnceLock};
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
#[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());
}
#[cfg(feature = "testmatrix")]
#[test]
fn build_signer_missing_cert_emits_red_row() {
let _guard = super::fs_test_lock();
let _ = nornir_testmatrix::drain_functional_rows();
let dir = tempfile::tempdir().unwrap();
let key = dir.path().join("k.pkcs8");
std::fs::write(&key, b"not-a-real-key").unwrap();
let out = build_signer(&Some(key), &None, "p256");
assert!(out.is_err(), "missing --sign-cert must be an error");
let rows = nornir_testmatrix::drain_functional_rows();
let red = rows
.iter()
.find(|r| r.suite == "znippy-cli/compress-sign" && r.test_name == "signer_loaded")
.expect("a signer_loaded row was emitted");
assert_eq!(red.status, "fail", "broken signer config is a RED row");
}
}
#[cfg(all(test, feature = "testmatrix"))]
mod functional_status_tests {
use super::*;
fn drained_status(suite: &str, check: &str) -> Option<String> {
nornir_testmatrix::drain_functional_rows()
.into_iter()
.filter(|r| r.suite == suite && r.test_name == check)
.next_back()
.map(|r| r.status)
}
#[test]
fn green_roundtrip_then_red_on_corruption() {
let _guard = super::fs_test_lock();
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"), vec![b'a'; 64 * 1024]).unwrap();
std::fs::write(input.join("b.txt"), b"znippy functional status coverage").unwrap();
let registry = PluginRegistry::with_plugin(handlers::find_handler("rust").unwrap());
let output = dir.path().join("out");
let archive = output.with_extension("znippy");
let _ = nornir_testmatrix::drain_functional_rows();
compress_reporting(&input, &output, false, ®istry, None).unwrap();
assert_eq!(
drained_status("znippy-cli/compress", "archive_written").as_deref(),
Some("pass"),
"clean compress records a GREEN row"
);
let _ = nornir_testmatrix::drain_functional_rows();
let vr = verify_reporting(&archive).unwrap();
assert_eq!(vr.corrupt_files, 0, "clean archive has no corrupt files");
assert_eq!(
drained_status("znippy-cli/verify", "integrity_checksum").as_deref(),
Some("pass"),
"clean verify records a GREEN row"
);
let _ = nornir_testmatrix::drain_functional_rows();
let out_clean = dir.path().join("extract_clean");
let filter = znippy_common::IndexFilter { pkg_type: None, repo: None };
decompress_reporting(&archive, &out_clean, &filter, None, None).unwrap();
assert_eq!(
drained_status("znippy-cli/decompress", "reconstruct_verify").as_deref(),
Some("pass"),
"clean decompress records a GREEN row"
);
let mut bytes = std::fs::read(&archive).unwrap();
let flip = 16.min(bytes.len() - 1);
bytes[flip] ^= 0xFF;
std::fs::write(&archive, &bytes).unwrap();
let _ = nornir_testmatrix::drain_functional_rows();
let _ = verify_reporting(&archive);
assert_eq!(
drained_status("znippy-cli/verify", "integrity_checksum").as_deref(),
Some("fail"),
"corrupt verify records a RED row"
);
let _ = nornir_testmatrix::drain_functional_rows();
let out_bad = dir.path().join("extract_bad");
let _ = decompress_reporting(&archive, &out_bad, &filter, None, None);
assert_eq!(
drained_status("znippy-cli/decompress", "reconstruct_verify").as_deref(),
Some("fail"),
"corrupt decompress records a RED row"
);
}
}
#[cfg(all(test, feature = "sign"))]
mod provenance_green_tests {
#[test]
fn zero_verified_artifacts_is_not_a_verified_chain() {
assert!(
!super::provenance_is_verified(0),
"an archive with a valid ROOT signature but zero verified artifacts is not a \
verified provenance chain — this is the hardcoded-true surface the compress \
false-green fix already retired"
);
assert!(super::provenance_is_verified(1), "one verified artifact IS a chain");
assert!(super::provenance_is_verified(9_999));
}
}