Skip to main content

znippy_common/plugins/native/
generic_native.rs

1//! The **no-package** handler: an arbitrary directory tree, archived as opaque
2//! blobs, with no package semantics claimed at all.
3//!
4//! ## Why a handler for "no handler"
5//!
6//! `--format` selects exactly one handler and defaults to `rust`, so before this
7//! module the only way to archive a tree that is not a package registry was to
8//! tell znippy it was a Rust one. That worked — `CargoPlugin::matches_path` is
9//! `path.ends_with(".crate")`, so nothing matched and no metadata was extracted —
10//! but it worked by accident, and it lied twice over: the archive recorded
11//! `pkg_type = 1`, so `znippy decompress --type rust` and every `RustView`
12//! consumer would claim a container-registry mirror as crates.io content.
13//!
14//! A `pkg_type` is a promise about what the rows mean. The honest promise for a
15//! tree of image layers, a build output, or a home directory is *none*, and
16//! [`GenericPlugin`] is the handler that makes it: its own `type_id`, no
17//! extensions claimed, no columns contributed, [`matches_path`] permanently
18//! `false` so [`extract_metadata`] is never even called.
19//!
20//! ## The use case it was added for
21//!
22//! A container-registry mirror tree — what `talosctl image cache-create` writes,
23//! and the same shape a Nexus/registry mirror has:
24//!
25//! ```text
26//! <root>/blob/<sha256-hex>                             1 KB … 500 MB, ALREADY gzip/zstd
27//! <root>/manifests/<host>/<repo>/reference/<tag>       small JSON
28//! <root>/manifests/<host>/<repo>/digest/<sha256>       small JSON
29//! ```
30//!
31//! Every property of that tree argues for a no-op handler. The blob filenames are
32//! bare hex with **no extension**, so no extension-keyed handler could match one
33//! anyway; the blobs are content-addressed, so a metadata extractor has nothing
34//! to add that the name does not already say; and the payload is already
35//! compressed, which is a question for `precompressed`'s magic-byte probe, not
36//! for a package parser.
37//!
38//! Tier A by rule P-5 (`.nornir/plugins-design.md` §f3): it needs `std` and
39//! `arrow` and nothing else — in fact it needs less than any other handler here.
40
41use crate::plugin::{ArchiveTypePlugin, ExtensionRow, Claim, HandlerSpec};
42
43/// Archive any tree as opaque blobs. Extracts no metadata, claims no extension,
44/// contributes no Arrow column.
45///
46/// The `type_id` matters even though nothing is extracted: it is what stops a
47/// generic archive from being indistinguishable from a `cargo` one, and what
48/// makes `znippy decompress --type generic` mean something.
49pub struct GenericPlugin;
50
51impl GenericPlugin {
52    pub fn new() -> Self {
53        Self
54    }
55}
56
57impl Default for GenericPlugin {
58    fn default() -> Self {
59        Self::new()
60    }
61}
62
63/// The generic handler. `type_id` 26 is the first free discriminant above the
64/// skeletons (4-24) and media (25), below the out-of-crate handlers (40-42).
65pub static GENERIC_SPEC: HandlerSpec = HandlerSpec {
66    name: "generic",
67    // `none` is what a user types when they mean "no package semantics";
68    // `raw`/`plain`/`blob` are what they type when they mean the same thing
69    // about the payload.
70    aliases: &["none", "raw", "plain", "blob"],
71    type_id: 26,
72    ecosystem: "none — an arbitrary file tree",
73    // Deliberately empty: claiming an extension is exactly the promise this
74    // handler exists NOT to make — and with none, the derived `matches_path` is
75    // permanently `false`, so `extract_metadata` is never reached.
76    extensions: &[],
77    claim: Claim::Extensions,
78    description: "Any directory tree as opaque blobs — no metadata extracted, \
79                  no package type claimed. The honest choice for a \
80                  container-registry mirror, a build output or a data dump.",
81    fields: &[],
82    commands: &[],
83};
84
85impl ArchiveTypePlugin for GenericPlugin {
86    fn spec(&self) -> &HandlerSpec {
87        &GENERIC_SPEC
88    }
89
90    /// Unreachable through the registry (nothing matches),
91    /// and `None` if a caller invokes it directly: there is no metadata to claim.
92    fn extract_metadata(&self, _path: &str, _data: &[u8]) -> Option<ExtensionRow> {
93        None
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    /// The handler's contract is what it *refuses* to do, so that is what is
102    /// asserted. Every path shape from the registry-mirror tree — bare hex with no
103    /// extension, a `host:port` segment, an `@` in a repo name, a `+` in a tag, a
104    /// non-ASCII tag — must be claimed by nothing and extract nothing.
105    #[test]
106    fn nothing_is_claimed_and_nothing_is_extracted() {
107        let p = GenericPlugin::new();
108        for path in [
109            "blob/9f2c1e0b7a4d3f8c6e5a2b9d0c7f4e1a8b3d6c9e2f0a1b2c3d4e5f6a7b8c9d0e",
110            "manifests/registry.k8s.io:5000/kube-apiserver/reference/v1.30.0+build1",
111            "manifests/ghcr.io/siderolabs@sha256/digest/sha256:abcdef",
112            "manifests/ünïcödé-täg.json",
113            // And the things a package handler WOULD claim: generic must not.
114            "serde-1.0.200.crate",
115            "guava-33.0.jar",
116        ] {
117            assert!(!p.matches_path(path), "generic must claim nothing: {path}");
118            assert!(p.extract_metadata(path, b"whatever").is_none());
119        }
120        assert!(p.schema_fields().is_empty(), "no columns means no schema change");
121        assert!(p.meta().extensions.is_empty(), "claiming an extension is the promise we avoid");
122    }
123
124    /// The aliases are the user-facing surface — a `--format none` that resolves
125    /// to nothing is the bug this guards. Resolution itself lives in
126    /// `znippy-cli::handlers::find_handler`; what is assertable here is that the
127    /// names are declared and lower-case (that lookup is `eq_ignore_ascii_case`,
128    /// but a mixed-case alias in the register would still read wrong in
129    /// `znippy handlers`).
130    #[test]
131    fn the_no_package_aliases_are_declared() {
132        let m = GenericPlugin::new().meta();
133        for want in ["none", "raw", "plain", "blob"] {
134            assert!(m.aliases.iter().any(|a| a == want), "missing alias {want}");
135        }
136        assert!(m.aliases.iter().all(|a| a.chars().all(|c| c.is_ascii_lowercase())));
137        assert_eq!(m.name, "generic");
138        assert_eq!(m.type_id, 26);
139    }
140}