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}