Skip to main content

varve_core/
store.rs

1//! The core — the local content-addressed store (REQ-COEXIST-001, DD-006).
2//!
3//! ```text
4//! <root>/core/sha256-<hex>/          # one layer, keyed by its manifest digest
5//!   layer.json                       # the layer manifest, kept for verify/archive
6//!   bin/<tool>                       # the tools — dispatched by name
7//!   payloads/<name>/<version>        # everything else — held by name AND version
8//! ```
9//!
10//! The split is REQ-STORE-002. A tool is dispatched by name (`varve which`,
11//! `varve run`, the argv[0] shims), so a name must resolve to exactly one
12//! binary and `bin/<name>` is right. A crate, a WIT package, an SDK or a wasm
13//! component is not dispatched at all, and a dependency graph ordinarily holds
14//! several versions of one name — laying those down by name alone made the
15//! second entry silently overwrite the first's bytes.
16//!
17//! Keyed by manifest digest, so layers coexist by construction: installing
18//! August cannot disturb July, and selecting either costs no download.
19//! The store is written at lay-down time and read-only ever after — nothing
20//! in resolution or listing mutates it (REQ-SCOPE-001's read-only half).
21
22use std::collections::BTreeMap;
23use std::path::{Path, PathBuf};
24
25use serde::Deserialize;
26use sha2::{Digest, Sha256};
27
28use crate::layer::LayerId;
29use crate::manifest::ManifestEntry;
30
31/// The directory holding non-dispatchable payloads, keyed by name AND version.
32pub const PAYLOAD_DIR: &str = "payloads";
33
34/// One payload to lay down. What it *is* decides where it lands: a dispatchable
35/// payload is placed by name, everything else by name and version.
36#[derive(Debug, Clone, Copy)]
37pub struct Payload<'a> {
38    pub name: &'a str,
39    /// The version from the signed manifest. Absent only for pre-kind layers
40    /// and for tools, which are not keyed by version.
41    pub version: Option<&'a str>,
42    /// Dispatched by name — see `PayloadKind::is_dispatchable`.
43    pub dispatchable: bool,
44    pub bytes: &'a [u8],
45}
46
47impl<'a> Payload<'a> {
48    /// A dispatchable tool binary: `bin/<name>`, the original layout.
49    pub fn tool(name: &'a str, bytes: &'a [u8]) -> Self {
50        Payload {
51            name,
52            version: None,
53            dispatchable: true,
54            bytes,
55        }
56    }
57}
58
59/// Is this manifest entry dispatched by name (REQ-STORE-002 clause 1)?
60///
61/// An entry with no kind annotation is a `tool` (pre-kind layers), so it is.
62/// An entry whose kind THIS build does not recognise is not: varve cannot
63/// dispatch what it cannot classify, and placing an unknown kind by name alone
64/// is exactly the overwrite this requirement exists to prevent — a newer
65/// varve's versioned kind must not collapse onto one path here.
66pub fn entry_is_dispatchable(entry: &ManifestEntry) -> bool {
67    matches!(entry.kind(), Ok(kind) if kind.is_dispatchable())
68}
69
70/// The version a manifest entry declares, if any.
71pub fn entry_version(entry: &ManifestEntry) -> Option<&str> {
72    entry
73        .annotations
74        .get("eu.pulseengine.tool.version")
75        .map(String::as_str)
76}
77
78/// Refuse a name or version that is not a single, safe path component. These
79/// strings come out of a signed manifest, but "signed" means "attributable",
80/// not "benign": a realm root signing `../../evil` must not be able to place
81/// bytes outside the layer it is laying down.
82fn safe_component(what: &'static str, value: &str) -> Result<(), StoreError> {
83    let bad = |why: &str| {
84        Err(StoreError::UnsafeComponent {
85            what,
86            value: value.to_string(),
87            why: why.to_string(),
88        })
89    };
90    if value.is_empty() {
91        return bad("empty");
92    }
93    if value == "." || value == ".." {
94        return bad("a relative path element");
95    }
96    if let Some(c) = value
97        .chars()
98        .find(|c| matches!(c, '/' | '\\' | '\0') || c.is_control())
99    {
100        return bad(&format!("contains {c:?}"));
101    }
102    Ok(())
103}
104
105/// Where a payload lives inside a layer root, relative to it (clause 2).
106///
107/// Dispatchable → `bin/<name>`, unchanged and byte-compatible with every layer
108/// installed before this requirement. Non-dispatchable with a version →
109/// `payloads/<name>/<version>`, so `serde@1.0.200` and `serde@1.0.210` are two
110/// files, not one file written twice. A non-dispatchable payload with NO
111/// version keeps the legacy `bin/<name>` place — that is what a pre-kind layer
112/// looks like on disk, and the collision guard in `lay_down` still refuses to
113/// let two of them land on one path.
114pub fn payload_rel_path(
115    dispatchable: bool,
116    name: &str,
117    version: Option<&str>,
118) -> Result<PathBuf, StoreError> {
119    safe_component("payload name", name)?;
120    match (dispatchable, version) {
121        (false, Some(version)) => {
122            safe_component("payload version", version)?;
123            Ok(PathBuf::from(PAYLOAD_DIR).join(name).join(version))
124        }
125        _ => Ok(PathBuf::from("bin").join(name)),
126    }
127}
128
129/// The minimal slice of a layer manifest the store needs: the annotations
130/// carrying the layer identity. Everything else is preserved verbatim in
131/// `layer.json` for later verification and archiving.
132#[derive(Debug, Clone, Deserialize)]
133struct ManifestEnvelope {
134    #[serde(default)]
135    annotations: BTreeMap<String, String>,
136}
137
138/// One layer present in the core.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct InstalledLayer {
141    /// `sha256:<hex>` of the manifest bytes — the store key.
142    pub digest: String,
143    /// The layer identity from the manifest annotations.
144    pub layer: LayerId,
145    /// The channel annotation (`qualified` / `rolling`), verbatim.
146    pub channel: String,
147    /// Root directory of this layer in the core.
148    pub root: PathBuf,
149}
150
151/// The core store rooted at a directory (defaults to `~/.varve` in the CLI;
152/// injectable here so tests and future tools own their roots).
153#[derive(Debug, Clone)]
154pub struct Store {
155    root: PathBuf,
156}
157
158/// Store failures. Note what is *absent*: there is no variant for "fell back
159/// to another layer" — the API cannot express fallback.
160#[derive(Debug, thiserror::Error)]
161pub enum StoreError {
162    #[error("io error at {path}")]
163    Io {
164        path: String,
165        #[source]
166        source: std::io::Error,
167    },
168    #[error("{path}: layer.json is not a valid layer manifest: {reason}")]
169    BadManifest { path: String, reason: String },
170    #[error("{what} {value:?} is not a usable path component ({why}) — refusing to lay it down")]
171    UnsafeComponent {
172        what: &'static str,
173        value: String,
174        why: String,
175    },
176    #[error(
177        "two payloads of this layer both claim {path} ('{first}' and '{second}') — refusing to \
178         lay one down over the other. A layer may hold several versions of one name, but each \
179         must be a distinct payload; two entries with one identity would mean the wrong bytes \
180         land under the right name."
181    )]
182    Collision {
183        path: String,
184        first: String,
185        second: String,
186    },
187}
188
189impl Store {
190    pub fn at(root: impl Into<PathBuf>) -> Self {
191        Store { root: root.into() }
192    }
193
194    pub fn root(&self) -> &Path {
195        &self.root
196    }
197
198    /// Lay a layer down in the core from its manifest bytes and tool
199    /// binaries. Returns the manifest digest (`sha256:<hex>`) — the store key.
200    ///
201    /// The dispatchable-only convenience over [`Store::lay_down_payloads`],
202    /// which is the single writer.
203    pub fn lay_down(
204        &self,
205        manifest_bytes: &[u8],
206        tools: &[(&str, &[u8])],
207    ) -> Result<String, StoreError> {
208        let payloads: Vec<Payload<'_>> = tools
209            .iter()
210            .map(|(name, bytes)| Payload::tool(name, bytes))
211            .collect();
212        self.lay_down_payloads(manifest_bytes, &payloads)
213    }
214
215    /// Lay a layer down in the core from its manifest bytes and its payloads.
216    /// Returns the manifest digest (`sha256:<hex>`) — the store key.
217    ///
218    /// This is the write path shared by the installer and by tests; nothing
219    /// else writes to the core. Two payloads that would land on ONE path are
220    /// refused (`StoreError::Collision`) rather than written in turn: relaxing
221    /// the deposit-time identity check without this would turn a clean error
222    /// into silent data loss — the second entry's bytes under the first's name,
223    /// with verification then failing on the *other* entry and nothing
224    /// explaining why (REQ-STORE-002).
225    pub fn lay_down_payloads(
226        &self,
227        manifest_bytes: &[u8],
228        payloads: &[Payload<'_>],
229    ) -> Result<String, StoreError> {
230        let digest = manifest_digest(manifest_bytes);
231        let entry = self.core_dir().join(digest.replace(':', "-"));
232        let io = |path: &Path, source: std::io::Error| StoreError::Io {
233            path: path.display().to_string(),
234            source,
235        };
236
237        // Resolve every destination BEFORE writing anything: an unusable name
238        // or a collision must leave the core untouched, not half-written.
239        let mut placed: BTreeMap<PathBuf, String> = BTreeMap::new();
240        let mut plan: Vec<(PathBuf, &Payload<'_>)> = Vec::new();
241        for payload in payloads {
242            let rel = payload_rel_path(payload.dispatchable, payload.name, payload.version)?;
243            let who = match payload.version {
244                Some(v) => format!("{}@{v}", payload.name),
245                None => payload.name.to_string(),
246            };
247            if let Some(first) = placed.get(&rel) {
248                return Err(StoreError::Collision {
249                    path: rel.display().to_string(),
250                    first: first.clone(),
251                    second: who,
252                });
253            }
254            placed.insert(rel.clone(), who);
255            plan.push((rel, payload));
256        }
257
258        let bin = entry.join("bin");
259        std::fs::create_dir_all(&bin).map_err(|e| io(&bin, e))?;
260        let manifest_path = entry.join("layer.json");
261        std::fs::write(&manifest_path, manifest_bytes).map_err(|e| io(&manifest_path, e))?;
262        for (rel, payload) in plan {
263            let path = entry.join(rel);
264            if let Some(parent) = path.parent() {
265                std::fs::create_dir_all(parent).map_err(|e| io(parent, e))?;
266            }
267            std::fs::write(&path, payload.bytes).map_err(|e| io(&path, e))?;
268            #[cfg(unix)]
269            {
270                use std::os::unix::fs::PermissionsExt;
271                // Only what is dispatched gets the execute bit. A `.crate`
272                // tarball or a WIT package is data varve hands to another tool.
273                let mode = if payload.dispatchable { 0o755 } else { 0o644 };
274                std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
275                    .map_err(|e| io(&path, e))?;
276            }
277        }
278        Ok(digest)
279    }
280
281    /// Every layer present in the core, in stable (digest) order.
282    pub fn list(&self) -> Result<Vec<InstalledLayer>, StoreError> {
283        let core = self.core_dir();
284        if !core.exists() {
285            return Ok(Vec::new());
286        }
287        let mut names: Vec<String> = std::fs::read_dir(&core)
288            .map_err(|e| StoreError::Io {
289                path: core.display().to_string(),
290                source: e,
291            })?
292            .filter_map(|e| e.ok())
293            .filter(|e| e.path().is_dir())
294            .map(|e| e.file_name().to_string_lossy().into_owned())
295            .filter(|n| n.starts_with("sha256-"))
296            .collect();
297        names.sort();
298        names
299            .into_iter()
300            .map(|name| self.read_entry(&name.replacen('-', ":", 1)))
301            .collect()
302    }
303
304    /// Look up a layer by manifest digest (`sha256:<hex>`).
305    /// The varve root this store lives under: itself, or the parent of a realm
306    /// partition (`<root>/realms/<fingerprint>`).
307    pub fn varve_root(&self) -> std::path::PathBuf {
308        let root = self.root();
309        if root
310            .parent()
311            .and_then(|p| p.file_name())
312            .is_some_and(|n| n == "realms")
313        {
314            root.parent()
315                .and_then(|p| p.parent())
316                .map(|p| p.to_path_buf())
317                .unwrap_or_else(|| root.to_path_buf())
318        } else {
319            root.to_path_buf()
320        }
321    }
322
323    /// Every partition under this varve root: the top-level core first, then
324    /// each realm partition in a stable order, paired with the realm
325    /// fingerprint that names it (`None` for the top-level core, which is not
326    /// realm-scoped).
327    ///
328    /// `find_anywhere` had this enumeration inline and private, so a caller
329    /// that needed to WALK the store rather than look one digest up had no way
330    /// to do it — which is how `verify --all` came to check only the pinned
331    /// realm's partition while its `--help` promised every installed layer
332    /// (REQ-VERIFYALL-001, varve#84).
333    pub fn partitions(&self) -> Vec<(Option<String>, Store)> {
334        let root = self.varve_root();
335        let mut out: Vec<(Option<String>, Store)> = vec![(None, Store::at(&root))];
336        if let Ok(rd) = std::fs::read_dir(root.join("realms")) {
337            let mut parts: Vec<std::path::PathBuf> =
338                rd.filter_map(|e| e.ok()).map(|e| e.path()).collect();
339            parts.sort();
340            for p in parts {
341                let fp = p.file_name().map(|n| n.to_string_lossy().into_owned());
342                out.push((fp, Store::at(p)));
343            }
344        }
345        out
346    }
347
348    /// Find a layer by digest in ANY partition under this varve root — the
349    /// top-level core or any realm's. A digest is content-addressed, so where
350    /// it happens to live does not change what it is; a cross-realm composition
351    /// include is installed under the INCLUDED realm's fingerprint, not the
352    /// including project's, and looking only in one partition reported it as
353    /// missing while `list` showed it installed (REQ-STORE-001).
354    ///
355    /// Locating a layer is not accepting it: the caller still verifies it
356    /// against the trust root of the realm that vouches for it.
357    pub fn find_anywhere(
358        &self,
359        digest: &str,
360    ) -> Result<Option<(Store, InstalledLayer)>, StoreError> {
361        if let Some(entry) = self.get(digest)? {
362            return Ok(Some((self.clone(), entry)));
363        }
364        let root = self.varve_root();
365        // The top-level core, then every realm partition, in a stable order.
366        let mut candidates = vec![Store::at(&root)];
367        if let Ok(rd) = std::fs::read_dir(root.join("realms")) {
368            let mut parts: Vec<std::path::PathBuf> =
369                rd.filter_map(|e| e.ok()).map(|e| e.path()).collect();
370            parts.sort();
371            candidates.extend(parts.into_iter().map(Store::at));
372        }
373        for candidate in candidates {
374            if candidate.root() == self.root() {
375                continue;
376            }
377            if let Some(entry) = candidate.get(digest)? {
378                return Ok(Some((candidate, entry)));
379            }
380        }
381        Ok(None)
382    }
383
384    pub fn get(&self, digest: &str) -> Result<Option<InstalledLayer>, StoreError> {
385        let entry = self.core_dir().join(digest.replace(':', "-"));
386        if !entry.join("layer.json").is_file() {
387            return Ok(None);
388        }
389        self.read_entry(digest).map(Some)
390    }
391
392    /// Path of one tool's binary within an installed layer, if present.
393    /// Dispatch is by name, so this takes a bare name — see `entry_path` for
394    /// payloads that are held rather than dispatched.
395    pub fn tool_path(&self, layer: &InstalledLayer, tool: &str) -> Option<PathBuf> {
396        let path = layer.root.join("bin").join(tool);
397        path.is_file().then_some(path)
398    }
399
400    /// Path of the bytes one MANIFEST ENTRY refers to within an installed
401    /// layer, if present. This is the read side of `lay_down_payloads`, and
402    /// every consumer of a layer's bytes (`verify`, `archive`, the export
403    /// adapters) goes through it so the two cannot drift.
404    ///
405    /// Backward compatibility: a layer installed BEFORE REQ-STORE-002 holds its
406    /// crates at `bin/<name>`, so a versioned payload that is not at its
407    /// versioned path falls back there. Such a layer can hold only one version
408    /// per name — the old deposit check made sure of it — so the fallback is
409    /// unambiguous.
410    pub fn entry_path(&self, layer: &InstalledLayer, entry: &ManifestEntry) -> Option<PathBuf> {
411        let name = entry.annotations.get("eu.pulseengine.tool")?;
412        let dispatchable = entry_is_dispatchable(entry);
413        let rel = payload_rel_path(dispatchable, name, entry_version(entry)).ok()?;
414        let path = layer.root.join(rel);
415        if path.is_file() {
416            return Some(path);
417        }
418        (!dispatchable)
419            .then(|| layer.root.join("bin").join(name))
420            .filter(|legacy| legacy.is_file())
421    }
422
423    fn core_dir(&self) -> PathBuf {
424        self.root.join("core")
425    }
426
427    fn read_entry(&self, digest: &str) -> Result<InstalledLayer, StoreError> {
428        let root = self.core_dir().join(digest.replace(':', "-"));
429        let manifest_path = root.join("layer.json");
430        let bad = |reason: String| StoreError::BadManifest {
431            path: manifest_path.display().to_string(),
432            reason,
433        };
434        let bytes = std::fs::read(&manifest_path).map_err(|source| StoreError::Io {
435            path: manifest_path.display().to_string(),
436            source,
437        })?;
438        let envelope: ManifestEnvelope =
439            serde_json::from_slice(&bytes).map_err(|e| bad(e.to_string()))?;
440        let layer_str = envelope
441            .annotations
442            .get("eu.pulseengine.varve.layer")
443            .ok_or_else(|| bad("missing eu.pulseengine.varve.layer annotation".into()))?;
444        let layer: LayerId = layer_str
445            .parse()
446            .map_err(|e: crate::layer::LayerIdError| bad(e.to_string()))?;
447        let channel = envelope
448            .annotations
449            .get("eu.pulseengine.varve.channel")
450            .cloned()
451            .unwrap_or_default();
452        Ok(InstalledLayer {
453            digest: digest.to_string(),
454            layer,
455            channel,
456            root,
457        })
458    }
459}
460
461/// Compute the store key for manifest bytes: `sha256:<hex>`.
462impl Store {
463    /// Every tool name the layer's SIGNED manifest carries, whatever this host
464    /// laid down. Used to tell "the pin asks for something that was never in
465    /// this layer" apart from "the install is incomplete" — the two need
466    /// opposite advice, and conflating them produced an error whose stated fix
467    /// could not work.
468    pub fn manifest_tool_names(&self, layer: &InstalledLayer) -> Result<Vec<String>, StoreError> {
469        let payload = std::fs::read(layer.root.join("layer.json")).map_err(|e| StoreError::Io {
470            path: layer.root.join("layer.json").display().to_string(),
471            source: e,
472        })?;
473        let json: serde_json::Value = match serde_json::from_slice(&payload) {
474            Ok(j) => j,
475            Err(_) => return Ok(Vec::new()),
476        };
477        Ok(json["manifests"]
478            .as_array()
479            .map(|es| {
480                es.iter()
481                    .filter_map(|e| e["annotations"]["eu.pulseengine.tool"].as_str())
482                    .map(str::to_string)
483                    .collect()
484            })
485            .unwrap_or_default())
486    }
487}
488
489pub fn manifest_digest(bytes: &[u8]) -> String {
490    format!("sha256:{}", hex::encode(Sha256::digest(bytes)))
491}
492
493#[cfg(test)]
494pub(crate) mod fixtures {
495    /// A minimal, valid layer manifest for tests.
496    pub fn manifest(layer: &str, channel: &str) -> Vec<u8> {
497        format!(
498            r#"{{
499  "schemaVersion": 2,
500  "mediaType": "application/vnd.oci.image.index.v1+json",
501  "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
502  "annotations": {{
503    "eu.pulseengine.varve.layer": "{layer}",
504    "eu.pulseengine.varve.channel": "{channel}"
505  }},
506  "manifests": []
507}}"#
508        )
509        .into_bytes()
510    }
511
512    /// A layer from before channel annotations existed. The resolver's channel
513    /// guard must exempt it: a pre-channel layer states no channel, so it
514    /// contradicts no pin.
515    pub fn manifest_without_channel(layer: &str) -> Vec<u8> {
516        format!(
517            r#"{{
518  "schemaVersion": 2,
519  "mediaType": "application/vnd.oci.image.index.v1+json",
520  "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
521  "annotations": {{
522    "eu.pulseengine.varve.layer": "{layer}"
523  }},
524  "manifests": []
525}}"#
526        )
527        .into_bytes()
528    }
529}
530
531#[cfg(test)]
532mod partition_tests {
533    use super::*;
534
535    // rivet: verifies REQ-STORE-001
536    #[test]
537    fn a_layer_is_found_in_any_partition_under_the_same_root() {
538        // A cross-realm composition include lives under the INCLUDED realm's
539        // fingerprint, not the including project's. Looking in one partition
540        // reported it missing while `list` showed it installed — `verify`,
541        // `which` and `run` disagreed with `list`, and the corrective advice
542        // failed (REQ-STORE-001).
543        let tmp = tempfile::tempdir().unwrap();
544        let root = tmp.path();
545        let mine = Store::at(root.join("realms").join("aaaa"));
546        let theirs = Store::at(root.join("realms").join("bbbb"));
547        let digest = theirs
548            .lay_down(
549                &fixtures::manifest("2026.08.0", "qualified"),
550                &[("btool", b"b")],
551            )
552            .unwrap();
553
554        // My own partition does not have it…
555        assert!(mine.get(&digest).unwrap().is_none());
556        // …but it is installed under this varve root, and locating it says so.
557        let (owner, entry) = mine.find_anywhere(&digest).unwrap().expect("found");
558        assert_eq!(entry.digest, digest);
559        assert_eq!(owner.root(), theirs.root(), "found in the owning partition");
560        // The tool resolves through the partition that actually holds it.
561        assert!(owner.tool_path(&entry, "btool").is_some());
562    }
563
564    // rivet: verifies REQ-STORE-001
565    #[test]
566    fn the_varve_root_is_recovered_from_a_realm_partition() {
567        let tmp = tempfile::tempdir().unwrap();
568        let root = tmp.path();
569        assert_eq!(
570            Store::at(root.join("realms").join("ffff")).varve_root(),
571            root.to_path_buf()
572        );
573        // A non-partition store is its own root.
574        assert_eq!(Store::at(root).varve_root(), root.to_path_buf());
575    }
576}
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581
582    fn store() -> (tempfile::TempDir, Store) {
583        let tmp = tempfile::tempdir().unwrap();
584        let store = Store::at(tmp.path().join("varve-root"));
585        (tmp, store)
586    }
587
588    // rivet: verifies REQ-COEXIST-001
589    #[test]
590    fn two_layers_coexist_and_are_independently_addressable() {
591        let (_tmp, store) = store();
592        let july = fixtures::manifest("2026.07.0", "qualified");
593        let august = fixtures::manifest("2026.08.0", "qualified");
594        let d_july = store.lay_down(&july, &[("synth", b"july-synth")]).unwrap();
595        let d_august = store
596            .lay_down(&august, &[("synth", b"august-synth")])
597            .unwrap();
598        assert_ne!(d_july, d_august);
599
600        let listed = store.list().unwrap();
601        assert_eq!(listed.len(), 2);
602
603        let july_entry = store.get(&d_july).unwrap().unwrap();
604        let august_entry = store.get(&d_august).unwrap().unwrap();
605        assert_eq!(july_entry.layer.to_string(), "2026.07.0");
606        assert_eq!(august_entry.layer.to_string(), "2026.08.0");
607
608        // The same tool name resolves to different bytes per layer — the
609        // wohl-on-July-while-relay-on-August afternoon, on one machine.
610        let july_synth = store.tool_path(&july_entry, "synth").unwrap();
611        let august_synth = store.tool_path(&august_entry, "synth").unwrap();
612        assert_eq!(std::fs::read(july_synth).unwrap(), b"july-synth");
613        assert_eq!(std::fs::read(august_synth).unwrap(), b"august-synth");
614    }
615
616    // rivet: verifies REQ-COEXIST-001
617    #[test]
618    fn store_key_is_the_manifest_digest() {
619        let (_tmp, store) = store();
620        let bytes = fixtures::manifest("2026.07.0", "qualified");
621        let digest = store.lay_down(&bytes, &[]).unwrap();
622        assert_eq!(digest, manifest_digest(&bytes));
623        let entry = store.get(&digest).unwrap().unwrap();
624        assert!(
625            entry
626                .root
627                .ends_with(format!("core/{}", digest.replace(':', "-"))),
628            "entry rooted at digest-keyed dir, got {}",
629            entry.root.display()
630        );
631        // layer.json preserved verbatim for verify/archive.
632        assert_eq!(std::fs::read(entry.root.join("layer.json")).unwrap(), bytes);
633    }
634
635    // rivet: verifies REQ-PIN-001
636    #[test]
637    fn missing_layer_is_none_not_an_invention() {
638        let (_tmp, store) = store();
639        assert_eq!(
640            store
641                .get("sha256:0000000000000000000000000000000000000000000000000000000000000000")
642                .unwrap(),
643            None
644        );
645        assert_eq!(store.list().unwrap(), vec![]);
646    }
647
648    /// A manifest entry as a signed layer carries it.
649    fn entry(name: &str, version: Option<&str>, kind: Option<&str>) -> ManifestEntry {
650        let mut annotations = BTreeMap::new();
651        annotations.insert("eu.pulseengine.tool".to_string(), name.to_string());
652        if let Some(v) = version {
653            annotations.insert("eu.pulseengine.tool.version".to_string(), v.to_string());
654        }
655        if let Some(k) = kind {
656            annotations.insert(crate::kind::ANN_KIND.to_string(), k.to_string());
657        }
658        ManifestEntry {
659            digest: manifest_digest(name.as_bytes()),
660            annotations,
661        }
662    }
663
664    fn held<'a>(name: &'a str, version: &'a str, bytes: &'a [u8]) -> Payload<'a> {
665        Payload {
666            name,
667            version: Some(version),
668            dispatchable: false,
669            bytes,
670        }
671    }
672
673    // rivet: verifies REQ-STORE-002
674    #[test]
675    fn two_versions_of_one_name_are_two_files_neither_overwriting_the_other() {
676        // Clause 2. `lay_down` wrote every payload to `bin/<name>`, so relaxing
677        // the deposit check alone would have made the second serde silently
678        // overwrite the first: the WRONG BYTES land under the right name, and
679        // verification then fails on the OTHER entry with nothing explaining
680        // why. A clean error turned into silent data loss.
681        let (_tmp, store) = store();
682        let manifest = fixtures::manifest("2026.08.0", "qualified");
683        let digest = store
684            .lay_down_payloads(
685                &manifest,
686                &[
687                    held("serde", "1.0.200", b"serde-200-bytes"),
688                    held("serde", "1.0.210", b"serde-210-bytes"),
689                ],
690            )
691            .unwrap();
692        let layer = store.get(&digest).unwrap().unwrap();
693
694        // Each version is its OWN file, holding its OWN bytes.
695        let two_hundred = layer.root.join("payloads/serde/1.0.200");
696        let two_ten = layer.root.join("payloads/serde/1.0.210");
697        assert_eq!(std::fs::read(&two_hundred).unwrap(), b"serde-200-bytes");
698        assert_eq!(std::fs::read(&two_ten).unwrap(), b"serde-210-bytes");
699        // …and nothing landed under the bare name, where one would have won.
700        assert!(!layer.root.join("bin/serde").exists());
701
702        // Both are reachable FROM THEIR MANIFEST ENTRIES — the lookup every
703        // consumer uses, so `verify`, `archive` and `export-cargo` each see the
704        // version they asked for rather than whichever landed last.
705        assert_eq!(
706            store.entry_path(&layer, &entry("serde", Some("1.0.200"), Some("crate"))),
707            Some(two_hundred)
708        );
709        assert_eq!(
710            store.entry_path(&layer, &entry("serde", Some("1.0.210"), Some("crate"))),
711            Some(two_ten)
712        );
713    }
714
715    // rivet: verifies REQ-STORE-002
716    #[test]
717    fn two_payloads_claiming_one_path_are_refused_before_anything_is_written() {
718        // The guard that makes the relaxation safe. deposit refuses two tools
719        // under one name, but deposit is not the only producer: install accepts
720        // ANY manifest a realm root signed, including one built by other
721        // software. If two entries ever reach one path, the store must say so
722        // loudly rather than write one over the other.
723        let (_tmp, store) = store();
724        let manifest = fixtures::manifest("2026.08.0", "qualified");
725        let err = store
726            .lay_down_payloads(
727                &manifest,
728                &[
729                    Payload::tool("synth", b"first-bytes"),
730                    Payload::tool("synth", b"second-bytes"),
731                ],
732            )
733            .unwrap_err();
734        assert!(
735            matches!(&err, StoreError::Collision { path, .. } if path.contains("synth")),
736            "got: {err}"
737        );
738        // And the core is untouched: destinations are resolved before any byte
739        // is written, so a colliding layer never half-lands.
740        assert!(store.list().unwrap().is_empty(), "nothing may be laid down");
741
742        // The same collision through the versionless legacy placement.
743        let err = store
744            .lay_down_payloads(
745                &manifest,
746                &[
747                    Payload {
748                        name: "wit-pkg",
749                        version: None,
750                        dispatchable: false,
751                        bytes: b"a",
752                    },
753                    Payload {
754                        name: "wit-pkg",
755                        version: None,
756                        dispatchable: false,
757                        bytes: b"b",
758                    },
759                ],
760            )
761            .unwrap_err();
762        assert!(matches!(err, StoreError::Collision { .. }), "got: {err}");
763    }
764
765    // rivet: verifies REQ-STORE-002
766    #[test]
767    fn a_name_or_version_that_escapes_the_layer_is_refused() {
768        // The names now compose a PATH, and they come out of a manifest.
769        // "Signed" means attributable, not benign: a realm root must not be
770        // able to place bytes outside the layer it is laying down.
771        let (_tmp, store) = store();
772        let manifest = fixtures::manifest("2026.08.0", "qualified");
773        for (name, version) in [
774            ("../../escape", Some("1.0.0")),
775            ("serde", Some("../../escape")),
776            ("a/b", Some("1.0.0")),
777            ("..", Some("1.0.0")),
778            ("", Some("1.0.0")),
779            ("serde", Some("")),
780        ] {
781            let err = store
782                .lay_down_payloads(&manifest, &[held(name, version.unwrap(), b"x")])
783                .unwrap_err();
784            assert!(
785                matches!(err, StoreError::UnsafeComponent { .. }),
786                "{name:?}@{version:?} must be refused, got: {err}"
787            );
788        }
789        assert!(store.list().unwrap().is_empty());
790        // The escape did not happen by any other route either.
791        assert!(!store.root().join("escape").exists());
792    }
793
794    // rivet: verifies REQ-STORE-002
795    #[test]
796    fn a_layer_installed_before_this_change_still_resolves_its_crate() {
797        // Backward compatibility, stated as a test rather than a hope: a layer
798        // laid down by an older varve holds its crate at `bin/<name>`, and
799        // `verify`/`archive`/`export-cargo` must still find it there. Such a
800        // layer can hold only ONE version per name — the old deposit check made
801        // sure of it — so the fallback is unambiguous.
802        let (_tmp, store) = store();
803        let manifest = fixtures::manifest("2026.08.0", "qualified");
804        let digest = store
805            .lay_down(&manifest, &[("legacy-crate", b"old-layout-bytes")])
806            .unwrap();
807        let layer = store.get(&digest).unwrap().unwrap();
808        let path = store
809            .entry_path(&layer, &entry("legacy-crate", Some("0.1.0"), Some("crate")))
810            .expect("a pre-REQ-STORE-002 layer must keep resolving");
811        assert_eq!(std::fs::read(path).unwrap(), b"old-layout-bytes");
812    }
813
814    // rivet: verifies REQ-STORE-002
815    #[test]
816    fn a_tool_keeps_bin_and_a_held_payload_never_borrows_it() {
817        // Dispatch is by name, so `bin/<name>` is not merely retained for
818        // compatibility — it is the contract `varve which`, `varve run` and the
819        // argv[0] shims resolve through. A held payload must never satisfy a
820        // dispatch lookup by landing there.
821        let (_tmp, store) = store();
822        let manifest = fixtures::manifest("2026.08.0", "qualified");
823        let digest = store
824            .lay_down_payloads(
825                &manifest,
826                &[
827                    Payload::tool("synth", b"synth-binary"),
828                    held("serde", "1.0.200", b"serde-crate"),
829                ],
830            )
831            .unwrap();
832        let layer = store.get(&digest).unwrap().unwrap();
833        assert_eq!(
834            std::fs::read(store.tool_path(&layer, "synth").unwrap()).unwrap(),
835            b"synth-binary"
836        );
837        assert_eq!(store.tool_path(&layer, "serde"), None, "not dispatchable");
838        #[cfg(unix)]
839        {
840            use std::os::unix::fs::PermissionsExt;
841            let mode =
842                |p: std::path::PathBuf| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
843            assert_eq!(mode(layer.root.join("bin/synth")), 0o755);
844            assert_eq!(
845                mode(layer.root.join("payloads/serde/1.0.200")),
846                0o644,
847                "a .crate tarball is data, not something to execute"
848            );
849        }
850    }
851
852    // rivet: verifies REQ-STORE-002
853    #[test]
854    fn an_unrecognised_kind_is_held_by_version_never_dispatched_by_name() {
855        // A layer deposited by a NEWER varve carries kinds this build has never
856        // heard of, and installs and verifies normally (DD-003, kind.rs). If an
857        // unknown kind were placed by name alone, two versions of it would
858        // overwrite each other — the very loss this requirement forbids, one
859        // release later.
860        let e = entry("future", Some("2.0.0"), Some("quantum-blob"));
861        assert!(!entry_is_dispatchable(&e));
862        assert_eq!(
863            payload_rel_path(false, "future", Some("2.0.0")).unwrap(),
864            PathBuf::from("payloads/future/2.0.0")
865        );
866        // …while an entry with NO kind annotation is a tool, as pre-kind
867        // layers require.
868        assert!(entry_is_dispatchable(&entry("synth", Some("0.45.0"), None)));
869        assert_eq!(
870            payload_rel_path(true, "synth", Some("0.45.0")).unwrap(),
871            PathBuf::from("bin/synth")
872        );
873    }
874
875    // rivet: verifies REQ-SDK-001
876    #[test]
877    fn a_tree_shaped_payload_is_held_by_name_and_version_like_any_other() {
878        // REQ-SDK-001 clause 1, and the correction the clause needed. A tree
879        // payload is held as the single archive its producer SIGNED — one blob,
880        // one digest, at `payloads/<name>/<version>` exactly like a `.crate` or
881        // a `.vsix`. It is not unpacked here, and that is the design rather than
882        // a shortfall:
883        //
884        //   * `verify` and `archive` re-hash ONE file at `entry_path` against
885        //     ONE signed digest, so an unpacked store entry would be neither
886        //     verifiable nor archivable — the opposite of clause 2;
887        //   * relocation can only ever SHORTEN a path, and the store path is
888        //     ~90 characters before any content, so an SDK could not be
889        //     relocated INTO the store even if it were unpacked there.
890        //
891        // The tree is materialised where it is used, by `sdkexport`.
892        let (_tmp, store) = store();
893        let manifest = fixtures::manifest("2026.08.0", "qualified");
894        let digest = store
895            .lay_down_payloads(
896                &manifest,
897                &[
898                    held("poky-cortexa53", "4.0.15", b"sdk-tarball-4.0.15"),
899                    held("poky-cortexa53", "5.0.2", b"sdk-tarball-5.0.2"),
900                    held("zephyr-hal", "3.6.0", b"zephyr-module-tarball"),
901                ],
902            )
903            .unwrap();
904        let layer = store.get(&digest).unwrap().unwrap();
905
906        // Two SDK versions coexist, neither overwriting the other — a project
907        // migrating between Yocto releases holds both.
908        for (version, want) in [
909            ("4.0.15", &b"sdk-tarball-4.0.15"[..]),
910            ("5.0.2", &b"sdk-tarball-5.0.2"[..]),
911        ] {
912            let path = store
913                .entry_path(&layer, &entry("poky-cortexa53", Some(version), Some("sdk")))
914                .expect("an sdk resolves from its manifest entry like any held payload");
915            assert_eq!(
916                path,
917                layer
918                    .root
919                    .join(format!("payloads/poky-cortexa53/{version}"))
920            );
921            assert_eq!(std::fs::read(&path).unwrap(), want);
922        }
923        // An SDK is data handed to a compiler, never something varve dispatches.
924        assert!(!crate::kind::PayloadKind::Sdk.is_dispatchable());
925        assert_eq!(store.tool_path(&layer, "poky-cortexa53"), None);
926        assert!(!layer.root.join("bin/poky-cortexa53").exists());
927        // The other tree kinds are held the same way — nothing about `sdk` is
928        // special in the store, which is what "like any other held payload"
929        // means.
930        assert!(
931            store
932                .entry_path(
933                    &layer,
934                    &entry("zephyr-hal", Some("3.6.0"), Some("zephyr-module"))
935                )
936                .is_some()
937        );
938    }
939
940    // rivet: verifies REQ-PIN-001
941    #[test]
942    fn missing_tool_in_an_installed_layer_is_none() {
943        let (_tmp, store) = store();
944        let bytes = fixtures::manifest("2026.07.0", "qualified");
945        let digest = store.lay_down(&bytes, &[("rivet", b"r")]).unwrap();
946        let entry = store.get(&digest).unwrap().unwrap();
947        assert!(store.tool_path(&entry, "rivet").is_some());
948        assert_eq!(store.tool_path(&entry, "synth"), None);
949    }
950}