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}