Skip to main content

varve_core/
kind.rs

1//! Payload kinds (REQ-KIND-001) — what a layer entry *is*.
2//!
3//! A tool binary is just bytes with an exec bit; a crate, a WIT package, a
4//! Zephyr module, an SDK, a wasm component are also just bytes. The kind
5//! selects which export adapter and store layout apply — it does NOT change
6//! how bytes are verified (every kind is a signed digest checked against the
7//! trust root, exactly as a tool binary is; DD-003).
8//!
9//! Back-compat: an entry with no kind annotation is a `tool` (pre-kind layers,
10//! as an unstamped platform means any-platform). An *unknown* kind is a hard
11//! error WHERE THE KIND IS ACTED ON — `collect_verified_crates` refuses to
12//! export a payload it cannot classify, rather than mishandle it.
13//!
14//! Scope, stated precisely because an earlier version of this comment claimed
15//! more than the code does: `install` and `verify_installed` do NOT consult the
16//! kind. They check the signed digest of every entry, which is what makes the
17//! bytes trustworthy, and that check is kind-independent by design (DD-003). So
18//! a layer deposited by a newer varve, carrying a kind this build has never
19//! heard of, installs and verifies normally; only the adapters that must DO
20//! something kind-specific refuse it. Consumers of `kind()` must therefore
21//! handle `Err` on real installed layers — `sbom` labels such an entry rather
22//! than dropping it. Whether install should refuse outright is tracked in
23//! varve#49.
24
25use std::fmt;
26use std::str::FromStr;
27
28/// The annotation carrying an entry's payload kind.
29pub const ANN_KIND: &str = "eu.pulseengine.varve.kind";
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
32pub enum PayloadKind {
33    /// An executable dispatched by `varve run` / shims (the original kind).
34    #[default]
35    Tool,
36    /// A Rust `.crate` tarball, consumed via `export-cargo`.
37    Crate,
38    /// A WIT interface package (`wit/` + `wit/deps/`).
39    Wit,
40    /// A Zephyr module directory (`zephyr/module.yml`).
41    ZephyrModule,
42    /// A C/C++ SDK tree (headers + libs + a cmake package).
43    Sdk,
44    /// A WebAssembly component.
45    WasmComponent,
46    /// A VS Code extension package (`.vsix`), consumed via `export-vsix`
47    /// (REQ-VSIX-001). A `.vsix` is a single zip file, so it needs no
48    /// tree-shaped store — and it is DATA handed to `code`, never executed
49    /// by varve, so it is not dispatchable and carries no execute bit.
50    Vsix,
51    /// Another LAYER, composed into this one (REQ-COMPOSE-001). The digest is
52    /// that layer's signed manifest; it is not a blob to lay down.
53    Layer,
54}
55
56impl PayloadKind {
57    /// Is a payload of this kind dispatched BY NAME (REQ-STORE-002 clause 1)?
58    ///
59    /// Only a `tool` is: `varve which`, `varve run` and the argv[0] shims all
60    /// resolve a bare name, so a name must resolve to exactly one binary and
61    /// the identity of a tool is (name, platform). Every other kind is held,
62    /// not dispatched — its identity is (name, version, platform), because
63    /// several versions of one crate is the ordinary shape of a dependency
64    /// graph. A `layer` is not laid down at all; it answers `false` because it
65    /// is certainly not dispatched by name.
66    pub fn is_dispatchable(self) -> bool {
67        matches!(self, PayloadKind::Tool)
68    }
69
70    /// The canonical wire string, as written in the signed annotation.
71    pub fn as_str(self) -> &'static str {
72        match self {
73            PayloadKind::Tool => "tool",
74            PayloadKind::Crate => "crate",
75            PayloadKind::Wit => "wit",
76            PayloadKind::ZephyrModule => "zephyr-module",
77            PayloadKind::Sdk => "sdk",
78            PayloadKind::WasmComponent => "wasm-component",
79            PayloadKind::Vsix => "vsix",
80            PayloadKind::Layer => "layer",
81        }
82    }
83}
84
85impl fmt::Display for PayloadKind {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str(self.as_str())
88    }
89}
90
91/// An unrecognised payload kind — varve refuses it rather than guess.
92#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
93#[error(
94    "unknown payload kind '{0}': this varve does not know how to handle it \
95     (expected one of tool, crate, wit, zephyr-module, sdk, wasm-component, vsix)"
96)]
97pub struct UnknownKind(pub String);
98
99impl FromStr for PayloadKind {
100    type Err = UnknownKind;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        match s {
104            "tool" => Ok(PayloadKind::Tool),
105            "crate" => Ok(PayloadKind::Crate),
106            "wit" => Ok(PayloadKind::Wit),
107            "zephyr-module" => Ok(PayloadKind::ZephyrModule),
108            "sdk" => Ok(PayloadKind::Sdk),
109            "wasm-component" => Ok(PayloadKind::WasmComponent),
110            "vsix" => Ok(PayloadKind::Vsix),
111            "layer" => Ok(PayloadKind::Layer),
112            other => Err(UnknownKind(other.to_string())),
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    /// Every variant, in one place, so the tests below cannot silently skip a
122    /// newly added kind — which is exactly how `layer` and `vsix` reached the
123    /// enum with the round-trip test still listing six.
124    const ALL_KINDS: &[PayloadKind] = &[
125        PayloadKind::Tool,
126        PayloadKind::Crate,
127        PayloadKind::Wit,
128        PayloadKind::ZephyrModule,
129        PayloadKind::Sdk,
130        PayloadKind::WasmComponent,
131        PayloadKind::Vsix,
132        PayloadKind::Layer,
133    ];
134
135    /// Position of a kind in `ALL_KINDS`. The match is EXHAUSTIVE on purpose:
136    /// a new variant that is not added to `ALL_KINDS` fails to COMPILE here,
137    /// so the round-trip and dispatchability tests always cover every kind.
138    fn index_in_all_kinds(k: PayloadKind) -> usize {
139        match k {
140            PayloadKind::Tool => 0,
141            PayloadKind::Crate => 1,
142            PayloadKind::Wit => 2,
143            PayloadKind::ZephyrModule => 3,
144            PayloadKind::Sdk => 4,
145            PayloadKind::WasmComponent => 5,
146            PayloadKind::Vsix => 6,
147            PayloadKind::Layer => 7,
148        }
149    }
150
151    // rivet: verifies REQ-KIND-001
152    #[test]
153    fn the_kind_list_the_other_tests_iterate_holds_every_variant() {
154        for (i, k) in ALL_KINDS.iter().enumerate() {
155            assert_eq!(
156                index_in_all_kinds(*k),
157                i,
158                "ALL_KINDS is out of step with the enum at {k}"
159            );
160        }
161    }
162
163    // rivet: verifies REQ-KIND-001, REQ-VSIX-001
164    #[test]
165    fn every_kind_round_trips_through_its_wire_string() {
166        for k in ALL_KINDS {
167            assert_eq!(k.as_str().parse::<PayloadKind>().unwrap(), *k);
168        }
169        // Clause 1: the wire string a deposit spec writes is `vsix`, spelled
170        // out rather than left to whatever `as_str` happens to return — the
171        // annotation is SIGNED, so renaming it silently breaks every layer
172        // already deposited.
173        assert_eq!(PayloadKind::Vsix.as_str(), "vsix");
174        assert_eq!("vsix".parse::<PayloadKind>().unwrap(), PayloadKind::Vsix);
175        assert_eq!(PayloadKind::Vsix.to_string(), "vsix");
176    }
177
178    // rivet: verifies REQ-KIND-001
179    #[test]
180    fn an_unknown_kind_is_refused_not_guessed() {
181        let err = "quantum-blob".parse::<PayloadKind>().unwrap_err();
182        assert_eq!(err, UnknownKind("quantum-blob".into()));
183        // The refusal has to say what WOULD have worked, or the depositor who
184        // wrote `kind = "vscode"` has nothing to correct it to.
185        assert!(
186            err.to_string().contains("vsix"),
187            "the hint must list every kind this varve accepts: {err}"
188        );
189    }
190
191    // rivet: verifies REQ-KIND-001
192    #[test]
193    fn the_default_kind_is_tool_for_back_compat() {
194        assert_eq!(PayloadKind::default(), PayloadKind::Tool);
195    }
196
197    // rivet: verifies REQ-STORE-002, REQ-VSIX-001
198    #[test]
199    fn only_a_tool_is_dispatched_by_name() {
200        // Clause 1: the identity rule follows dispatchability. A tool resolves
201        // by bare name through `varve run`/`which`/the shims, so one name must
202        // mean one binary. Nothing else is dispatched, so nothing else may be
203        // keyed by name alone — that is what let two versions of one crate
204        // overwrite each other.
205        assert!(PayloadKind::Tool.is_dispatchable());
206        for held in ALL_KINDS.iter().filter(|k| **k != PayloadKind::Tool) {
207            assert!(
208                !held.is_dispatchable(),
209                "{held} is not dispatched by name and must not be keyed by one"
210            );
211        }
212        // REQ-VSIX-001 clauses 2 and 4 both hang off this one answer: it is
213        // what denies a `.vsix` the execute bit in `lay_down_payloads` and
214        // what gives it a (name, version) identity, so two versions of one
215        // extension coexist. Asserted by name, not only through the loop.
216        assert!(
217            !PayloadKind::Vsix.is_dispatchable(),
218            "a .vsix is data handed to `code`, never a binary varve dispatches"
219        );
220    }
221}