znippy-common/src/plugins/native/generic_native.rs
//! The **no-package** handler: an arbitrary directory tree, archived as opaque
//! blobs, with no package semantics claimed at all.
//!
//! ## Why a handler for "no handler"
//!
//! `--format` selects exactly one handler and defaults to `rust`, so before this
//! module the only way to archive a tree that is not a package registry was to
//! tell znippy it was a Rust one. That worked — `CargoPlugin::matches_path` is
//! `path.ends_with(".crate")`, so nothing matched and no metadata was extracted —
//! but it worked by accident, and it lied twice over: the archive recorded
//! `pkg_type = 1`, so `znippy decompress --type rust` and every `RustView`
//! consumer would claim a container-registry mirror as crates.io content.
//!
//! A `pkg_type` is a promise about what the rows mean. The honest promise for a
//! tree of image layers, a build output, or a home directory is *none*, and
//! [`GenericPlugin`] is the handler that makes it: its own `type_id`, no
//! extensions claimed, no columns contributed, [`matches_path`] permanently
//! `false` so [`extract_metadata`] is never even called.
//!
//! ## The use case it was added for
//!
//! A container-registry mirror tree — what `talosctl image cache-create` writes,
//! and the same shape a Nexus/registry mirror has:
//!
//! ```text
//! <root>/blob/<sha256-hex> 1 KB … 500 MB, ALREADY gzip/zstd
//! <root>/manifests/<host>/<repo>/reference/<tag> small JSON
//! <root>/manifests/<host>/<repo>/digest/<sha256> small JSON
//! ```
//!
//! Every property of that tree argues for a no-op handler. The blob filenames are
//! bare hex with **no extension**, so no extension-keyed handler could match one
//! anyway; the blobs are content-addressed, so a metadata extractor has nothing
//! to add that the name does not already say; and the payload is already
//! compressed, which is a question for `precompressed`'s magic-byte probe, not
//! for a package parser.
//!
//! Tier A by rule P-5 (`.nornir/plugins-design.md` §f3): it needs `std` and
//! `arrow` and nothing else — in fact it needs less than any other handler here.
use crate::plugin::{ArchiveTypePlugin, ExtensionRow, Claim, HandlerSpec};
/// Archive any tree as opaque blobs. Extracts no metadata, claims no extension,
/// contributes no Arrow column.
///
/// The `type_id` matters even though nothing is extracted: it is what stops a
/// generic archive from being indistinguishable from a `cargo` one, and what
/// makes `znippy decompress --type generic` mean something.
pub struct GenericPlugin;
impl GenericPlugin {
pub fn new() -> Self {
Self
}
}
impl Default for GenericPlugin {
fn default() -> Self {
Self::new()
}
}
/// The generic handler. `type_id` 26 is the first free discriminant above the
/// skeletons (4-24) and media (25), below the out-of-crate handlers (40-42).
pub static GENERIC_SPEC: HandlerSpec = HandlerSpec {
name: "generic",
// `none` is what a user types when they mean "no package semantics";
// `raw`/`plain`/`blob` are what they type when they mean the same thing
// about the payload.
aliases: &["none", "raw", "plain", "blob"],
type_id: 26,
ecosystem: "none — an arbitrary file tree",
// Deliberately empty: claiming an extension is exactly the promise this
// handler exists NOT to make — and with none, the derived `matches_path` is
// permanently `false`, so `extract_metadata` is never reached.
extensions: &[],
claim: Claim::Extensions,
description: "Any directory tree as opaque blobs — no metadata extracted, \
no package type claimed. The honest choice for a \
container-registry mirror, a build output or a data dump.",
fields: &[],
commands: &[],
};
impl ArchiveTypePlugin for GenericPlugin {
fn spec(&self) -> &HandlerSpec {
&GENERIC_SPEC
}
/// Unreachable through the registry (nothing matches),
/// and `None` if a caller invokes it directly: there is no metadata to claim.
fn extract_metadata(&self, _path: &str, _data: &[u8]) -> Option<ExtensionRow> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The handler's contract is what it *refuses* to do, so that is what is
/// asserted. Every path shape from the registry-mirror tree — bare hex with no
/// extension, a `host:port` segment, an `@` in a repo name, a `+` in a tag, a
/// non-ASCII tag — must be claimed by nothing and extract nothing.
#[test]
fn nothing_is_claimed_and_nothing_is_extracted() {
let p = GenericPlugin::new();
for path in [
"blob/9f2c1e0b7a4d3f8c6e5a2b9d0c7f4e1a8b3d6c9e2f0a1b2c3d4e5f6a7b8c9d0e",
"manifests/registry.k8s.io:5000/kube-apiserver/reference/v1.30.0+build1",
"manifests/ghcr.io/siderolabs@sha256/digest/sha256:abcdef",
"manifests/ünïcödé-täg.json",
// And the things a package handler WOULD claim: generic must not.
"serde-1.0.200.crate",
"guava-33.0.jar",
] {
assert!(!p.matches_path(path), "generic must claim nothing: {path}");
assert!(p.extract_metadata(path, b"whatever").is_none());
}
assert!(p.schema_fields().is_empty(), "no columns means no schema change");
assert!(p.meta().extensions.is_empty(), "claiming an extension is the promise we avoid");
}
/// The aliases are the user-facing surface — a `--format none` that resolves
/// to nothing is the bug this guards. Resolution itself lives in
/// `znippy-cli::handlers::find_handler`; what is assertable here is that the
/// names are declared and lower-case (that lookup is `eq_ignore_ascii_case`,
/// but a mixed-case alias in the register would still read wrong in
/// `znippy handlers`).
#[test]
fn the_no_package_aliases_are_declared() {
let m = GenericPlugin::new().meta();
for want in ["none", "raw", "plain", "blob"] {
assert!(m.aliases.iter().any(|a| a == want), "missing alias {want}");
}
assert!(m.aliases.iter().all(|a| a.chars().all(|c| c.is_ascii_lowercase())));
assert_eq!(m.name, "generic");
assert_eq!(m.type_id, 26);
}
}