1use 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
31pub const PAYLOAD_DIR: &str = "payloads";
33
34#[derive(Debug, Clone, Copy)]
37pub struct Payload<'a> {
38 pub name: &'a str,
39 pub version: Option<&'a str>,
42 pub dispatchable: bool,
44 pub bytes: &'a [u8],
45}
46
47impl<'a> Payload<'a> {
48 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
59pub fn entry_is_dispatchable(entry: &ManifestEntry) -> bool {
67 matches!(entry.kind(), Ok(kind) if kind.is_dispatchable())
68}
69
70pub fn entry_version(entry: &ManifestEntry) -> Option<&str> {
72 entry
73 .annotations
74 .get("eu.pulseengine.tool.version")
75 .map(String::as_str)
76}
77
78fn 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
105pub 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#[derive(Debug, Clone, Deserialize)]
133struct ManifestEnvelope {
134 #[serde(default)]
135 annotations: BTreeMap<String, String>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct InstalledLayer {
141 pub digest: String,
143 pub layer: LayerId,
145 pub channel: String,
147 pub root: PathBuf,
149}
150
151#[derive(Debug, Clone)]
154pub struct Store {
155 root: PathBuf,
156}
157
158#[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 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 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 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 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 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 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 pub fn find_anywhere(
333 &self,
334 digest: &str,
335 ) -> Result<Option<(Store, InstalledLayer)>, StoreError> {
336 if let Some(entry) = self.get(digest)? {
337 return Ok(Some((self.clone(), entry)));
338 }
339 let root = self.varve_root();
340 let mut candidates = vec![Store::at(&root)];
342 if let Ok(rd) = std::fs::read_dir(root.join("realms")) {
343 let mut parts: Vec<std::path::PathBuf> =
344 rd.filter_map(|e| e.ok()).map(|e| e.path()).collect();
345 parts.sort();
346 candidates.extend(parts.into_iter().map(Store::at));
347 }
348 for candidate in candidates {
349 if candidate.root() == self.root() {
350 continue;
351 }
352 if let Some(entry) = candidate.get(digest)? {
353 return Ok(Some((candidate, entry)));
354 }
355 }
356 Ok(None)
357 }
358
359 pub fn get(&self, digest: &str) -> Result<Option<InstalledLayer>, StoreError> {
360 let entry = self.core_dir().join(digest.replace(':', "-"));
361 if !entry.join("layer.json").is_file() {
362 return Ok(None);
363 }
364 self.read_entry(digest).map(Some)
365 }
366
367 pub fn tool_path(&self, layer: &InstalledLayer, tool: &str) -> Option<PathBuf> {
371 let path = layer.root.join("bin").join(tool);
372 path.is_file().then_some(path)
373 }
374
375 pub fn entry_path(&self, layer: &InstalledLayer, entry: &ManifestEntry) -> Option<PathBuf> {
386 let name = entry.annotations.get("eu.pulseengine.tool")?;
387 let dispatchable = entry_is_dispatchable(entry);
388 let rel = payload_rel_path(dispatchable, name, entry_version(entry)).ok()?;
389 let path = layer.root.join(rel);
390 if path.is_file() {
391 return Some(path);
392 }
393 (!dispatchable)
394 .then(|| layer.root.join("bin").join(name))
395 .filter(|legacy| legacy.is_file())
396 }
397
398 fn core_dir(&self) -> PathBuf {
399 self.root.join("core")
400 }
401
402 fn read_entry(&self, digest: &str) -> Result<InstalledLayer, StoreError> {
403 let root = self.core_dir().join(digest.replace(':', "-"));
404 let manifest_path = root.join("layer.json");
405 let bad = |reason: String| StoreError::BadManifest {
406 path: manifest_path.display().to_string(),
407 reason,
408 };
409 let bytes = std::fs::read(&manifest_path).map_err(|source| StoreError::Io {
410 path: manifest_path.display().to_string(),
411 source,
412 })?;
413 let envelope: ManifestEnvelope =
414 serde_json::from_slice(&bytes).map_err(|e| bad(e.to_string()))?;
415 let layer_str = envelope
416 .annotations
417 .get("eu.pulseengine.varve.layer")
418 .ok_or_else(|| bad("missing eu.pulseengine.varve.layer annotation".into()))?;
419 let layer: LayerId = layer_str
420 .parse()
421 .map_err(|e: crate::layer::LayerIdError| bad(e.to_string()))?;
422 let channel = envelope
423 .annotations
424 .get("eu.pulseengine.varve.channel")
425 .cloned()
426 .unwrap_or_default();
427 Ok(InstalledLayer {
428 digest: digest.to_string(),
429 layer,
430 channel,
431 root,
432 })
433 }
434}
435
436impl Store {
438 pub fn manifest_tool_names(&self, layer: &InstalledLayer) -> Result<Vec<String>, StoreError> {
444 let payload = std::fs::read(layer.root.join("layer.json")).map_err(|e| StoreError::Io {
445 path: layer.root.join("layer.json").display().to_string(),
446 source: e,
447 })?;
448 let json: serde_json::Value = match serde_json::from_slice(&payload) {
449 Ok(j) => j,
450 Err(_) => return Ok(Vec::new()),
451 };
452 Ok(json["manifests"]
453 .as_array()
454 .map(|es| {
455 es.iter()
456 .filter_map(|e| e["annotations"]["eu.pulseengine.tool"].as_str())
457 .map(str::to_string)
458 .collect()
459 })
460 .unwrap_or_default())
461 }
462}
463
464pub fn manifest_digest(bytes: &[u8]) -> String {
465 format!("sha256:{}", hex::encode(Sha256::digest(bytes)))
466}
467
468#[cfg(test)]
469pub(crate) mod fixtures {
470 pub fn manifest(layer: &str, channel: &str) -> Vec<u8> {
472 format!(
473 r#"{{
474 "schemaVersion": 2,
475 "mediaType": "application/vnd.oci.image.index.v1+json",
476 "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
477 "annotations": {{
478 "eu.pulseengine.varve.layer": "{layer}",
479 "eu.pulseengine.varve.channel": "{channel}"
480 }},
481 "manifests": []
482}}"#
483 )
484 .into_bytes()
485 }
486
487 pub fn manifest_without_channel(layer: &str) -> Vec<u8> {
491 format!(
492 r#"{{
493 "schemaVersion": 2,
494 "mediaType": "application/vnd.oci.image.index.v1+json",
495 "artifactType": "application/vnd.pulseengine.varve.layer.v1+json",
496 "annotations": {{
497 "eu.pulseengine.varve.layer": "{layer}"
498 }},
499 "manifests": []
500}}"#
501 )
502 .into_bytes()
503 }
504}
505
506#[cfg(test)]
507mod partition_tests {
508 use super::*;
509
510 #[test]
512 fn a_layer_is_found_in_any_partition_under_the_same_root() {
513 let tmp = tempfile::tempdir().unwrap();
519 let root = tmp.path();
520 let mine = Store::at(root.join("realms").join("aaaa"));
521 let theirs = Store::at(root.join("realms").join("bbbb"));
522 let digest = theirs
523 .lay_down(
524 &fixtures::manifest("2026.08.0", "qualified"),
525 &[("btool", b"b")],
526 )
527 .unwrap();
528
529 assert!(mine.get(&digest).unwrap().is_none());
531 let (owner, entry) = mine.find_anywhere(&digest).unwrap().expect("found");
533 assert_eq!(entry.digest, digest);
534 assert_eq!(owner.root(), theirs.root(), "found in the owning partition");
535 assert!(owner.tool_path(&entry, "btool").is_some());
537 }
538
539 #[test]
541 fn the_varve_root_is_recovered_from_a_realm_partition() {
542 let tmp = tempfile::tempdir().unwrap();
543 let root = tmp.path();
544 assert_eq!(
545 Store::at(root.join("realms").join("ffff")).varve_root(),
546 root.to_path_buf()
547 );
548 assert_eq!(Store::at(root).varve_root(), root.to_path_buf());
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556
557 fn store() -> (tempfile::TempDir, Store) {
558 let tmp = tempfile::tempdir().unwrap();
559 let store = Store::at(tmp.path().join("varve-root"));
560 (tmp, store)
561 }
562
563 #[test]
565 fn two_layers_coexist_and_are_independently_addressable() {
566 let (_tmp, store) = store();
567 let july = fixtures::manifest("2026.07.0", "qualified");
568 let august = fixtures::manifest("2026.08.0", "qualified");
569 let d_july = store.lay_down(&july, &[("synth", b"july-synth")]).unwrap();
570 let d_august = store
571 .lay_down(&august, &[("synth", b"august-synth")])
572 .unwrap();
573 assert_ne!(d_july, d_august);
574
575 let listed = store.list().unwrap();
576 assert_eq!(listed.len(), 2);
577
578 let july_entry = store.get(&d_july).unwrap().unwrap();
579 let august_entry = store.get(&d_august).unwrap().unwrap();
580 assert_eq!(july_entry.layer.to_string(), "2026.07.0");
581 assert_eq!(august_entry.layer.to_string(), "2026.08.0");
582
583 let july_synth = store.tool_path(&july_entry, "synth").unwrap();
586 let august_synth = store.tool_path(&august_entry, "synth").unwrap();
587 assert_eq!(std::fs::read(july_synth).unwrap(), b"july-synth");
588 assert_eq!(std::fs::read(august_synth).unwrap(), b"august-synth");
589 }
590
591 #[test]
593 fn store_key_is_the_manifest_digest() {
594 let (_tmp, store) = store();
595 let bytes = fixtures::manifest("2026.07.0", "qualified");
596 let digest = store.lay_down(&bytes, &[]).unwrap();
597 assert_eq!(digest, manifest_digest(&bytes));
598 let entry = store.get(&digest).unwrap().unwrap();
599 assert!(
600 entry
601 .root
602 .ends_with(format!("core/{}", digest.replace(':', "-"))),
603 "entry rooted at digest-keyed dir, got {}",
604 entry.root.display()
605 );
606 assert_eq!(std::fs::read(entry.root.join("layer.json")).unwrap(), bytes);
608 }
609
610 #[test]
612 fn missing_layer_is_none_not_an_invention() {
613 let (_tmp, store) = store();
614 assert_eq!(
615 store
616 .get("sha256:0000000000000000000000000000000000000000000000000000000000000000")
617 .unwrap(),
618 None
619 );
620 assert_eq!(store.list().unwrap(), vec![]);
621 }
622
623 fn entry(name: &str, version: Option<&str>, kind: Option<&str>) -> ManifestEntry {
625 let mut annotations = BTreeMap::new();
626 annotations.insert("eu.pulseengine.tool".to_string(), name.to_string());
627 if let Some(v) = version {
628 annotations.insert("eu.pulseengine.tool.version".to_string(), v.to_string());
629 }
630 if let Some(k) = kind {
631 annotations.insert(crate::kind::ANN_KIND.to_string(), k.to_string());
632 }
633 ManifestEntry {
634 digest: manifest_digest(name.as_bytes()),
635 annotations,
636 }
637 }
638
639 fn held<'a>(name: &'a str, version: &'a str, bytes: &'a [u8]) -> Payload<'a> {
640 Payload {
641 name,
642 version: Some(version),
643 dispatchable: false,
644 bytes,
645 }
646 }
647
648 #[test]
650 fn two_versions_of_one_name_are_two_files_neither_overwriting_the_other() {
651 let (_tmp, store) = store();
657 let manifest = fixtures::manifest("2026.08.0", "qualified");
658 let digest = store
659 .lay_down_payloads(
660 &manifest,
661 &[
662 held("serde", "1.0.200", b"serde-200-bytes"),
663 held("serde", "1.0.210", b"serde-210-bytes"),
664 ],
665 )
666 .unwrap();
667 let layer = store.get(&digest).unwrap().unwrap();
668
669 let two_hundred = layer.root.join("payloads/serde/1.0.200");
671 let two_ten = layer.root.join("payloads/serde/1.0.210");
672 assert_eq!(std::fs::read(&two_hundred).unwrap(), b"serde-200-bytes");
673 assert_eq!(std::fs::read(&two_ten).unwrap(), b"serde-210-bytes");
674 assert!(!layer.root.join("bin/serde").exists());
676
677 assert_eq!(
681 store.entry_path(&layer, &entry("serde", Some("1.0.200"), Some("crate"))),
682 Some(two_hundred)
683 );
684 assert_eq!(
685 store.entry_path(&layer, &entry("serde", Some("1.0.210"), Some("crate"))),
686 Some(two_ten)
687 );
688 }
689
690 #[test]
692 fn two_payloads_claiming_one_path_are_refused_before_anything_is_written() {
693 let (_tmp, store) = store();
699 let manifest = fixtures::manifest("2026.08.0", "qualified");
700 let err = store
701 .lay_down_payloads(
702 &manifest,
703 &[
704 Payload::tool("synth", b"first-bytes"),
705 Payload::tool("synth", b"second-bytes"),
706 ],
707 )
708 .unwrap_err();
709 assert!(
710 matches!(&err, StoreError::Collision { path, .. } if path.contains("synth")),
711 "got: {err}"
712 );
713 assert!(store.list().unwrap().is_empty(), "nothing may be laid down");
716
717 let err = store
719 .lay_down_payloads(
720 &manifest,
721 &[
722 Payload {
723 name: "wit-pkg",
724 version: None,
725 dispatchable: false,
726 bytes: b"a",
727 },
728 Payload {
729 name: "wit-pkg",
730 version: None,
731 dispatchable: false,
732 bytes: b"b",
733 },
734 ],
735 )
736 .unwrap_err();
737 assert!(matches!(err, StoreError::Collision { .. }), "got: {err}");
738 }
739
740 #[test]
742 fn a_name_or_version_that_escapes_the_layer_is_refused() {
743 let (_tmp, store) = store();
747 let manifest = fixtures::manifest("2026.08.0", "qualified");
748 for (name, version) in [
749 ("../../escape", Some("1.0.0")),
750 ("serde", Some("../../escape")),
751 ("a/b", Some("1.0.0")),
752 ("..", Some("1.0.0")),
753 ("", Some("1.0.0")),
754 ("serde", Some("")),
755 ] {
756 let err = store
757 .lay_down_payloads(&manifest, &[held(name, version.unwrap(), b"x")])
758 .unwrap_err();
759 assert!(
760 matches!(err, StoreError::UnsafeComponent { .. }),
761 "{name:?}@{version:?} must be refused, got: {err}"
762 );
763 }
764 assert!(store.list().unwrap().is_empty());
765 assert!(!store.root().join("escape").exists());
767 }
768
769 #[test]
771 fn a_layer_installed_before_this_change_still_resolves_its_crate() {
772 let (_tmp, store) = store();
778 let manifest = fixtures::manifest("2026.08.0", "qualified");
779 let digest = store
780 .lay_down(&manifest, &[("legacy-crate", b"old-layout-bytes")])
781 .unwrap();
782 let layer = store.get(&digest).unwrap().unwrap();
783 let path = store
784 .entry_path(&layer, &entry("legacy-crate", Some("0.1.0"), Some("crate")))
785 .expect("a pre-REQ-STORE-002 layer must keep resolving");
786 assert_eq!(std::fs::read(path).unwrap(), b"old-layout-bytes");
787 }
788
789 #[test]
791 fn a_tool_keeps_bin_and_a_held_payload_never_borrows_it() {
792 let (_tmp, store) = store();
797 let manifest = fixtures::manifest("2026.08.0", "qualified");
798 let digest = store
799 .lay_down_payloads(
800 &manifest,
801 &[
802 Payload::tool("synth", b"synth-binary"),
803 held("serde", "1.0.200", b"serde-crate"),
804 ],
805 )
806 .unwrap();
807 let layer = store.get(&digest).unwrap().unwrap();
808 assert_eq!(
809 std::fs::read(store.tool_path(&layer, "synth").unwrap()).unwrap(),
810 b"synth-binary"
811 );
812 assert_eq!(store.tool_path(&layer, "serde"), None, "not dispatchable");
813 #[cfg(unix)]
814 {
815 use std::os::unix::fs::PermissionsExt;
816 let mode =
817 |p: std::path::PathBuf| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
818 assert_eq!(mode(layer.root.join("bin/synth")), 0o755);
819 assert_eq!(
820 mode(layer.root.join("payloads/serde/1.0.200")),
821 0o644,
822 "a .crate tarball is data, not something to execute"
823 );
824 }
825 }
826
827 #[test]
829 fn an_unrecognised_kind_is_held_by_version_never_dispatched_by_name() {
830 let e = entry("future", Some("2.0.0"), Some("quantum-blob"));
836 assert!(!entry_is_dispatchable(&e));
837 assert_eq!(
838 payload_rel_path(false, "future", Some("2.0.0")).unwrap(),
839 PathBuf::from("payloads/future/2.0.0")
840 );
841 assert!(entry_is_dispatchable(&entry("synth", Some("0.45.0"), None)));
844 assert_eq!(
845 payload_rel_path(true, "synth", Some("0.45.0")).unwrap(),
846 PathBuf::from("bin/synth")
847 );
848 }
849
850 #[test]
852 fn a_tree_shaped_payload_is_held_by_name_and_version_like_any_other() {
853 let (_tmp, store) = store();
868 let manifest = fixtures::manifest("2026.08.0", "qualified");
869 let digest = store
870 .lay_down_payloads(
871 &manifest,
872 &[
873 held("poky-cortexa53", "4.0.15", b"sdk-tarball-4.0.15"),
874 held("poky-cortexa53", "5.0.2", b"sdk-tarball-5.0.2"),
875 held("zephyr-hal", "3.6.0", b"zephyr-module-tarball"),
876 ],
877 )
878 .unwrap();
879 let layer = store.get(&digest).unwrap().unwrap();
880
881 for (version, want) in [
884 ("4.0.15", &b"sdk-tarball-4.0.15"[..]),
885 ("5.0.2", &b"sdk-tarball-5.0.2"[..]),
886 ] {
887 let path = store
888 .entry_path(&layer, &entry("poky-cortexa53", Some(version), Some("sdk")))
889 .expect("an sdk resolves from its manifest entry like any held payload");
890 assert_eq!(
891 path,
892 layer
893 .root
894 .join(format!("payloads/poky-cortexa53/{version}"))
895 );
896 assert_eq!(std::fs::read(&path).unwrap(), want);
897 }
898 assert!(!crate::kind::PayloadKind::Sdk.is_dispatchable());
900 assert_eq!(store.tool_path(&layer, "poky-cortexa53"), None);
901 assert!(!layer.root.join("bin/poky-cortexa53").exists());
902 assert!(
906 store
907 .entry_path(
908 &layer,
909 &entry("zephyr-hal", Some("3.6.0"), Some("zephyr-module"))
910 )
911 .is_some()
912 );
913 }
914
915 #[test]
917 fn missing_tool_in_an_installed_layer_is_none() {
918 let (_tmp, store) = store();
919 let bytes = fixtures::manifest("2026.07.0", "qualified");
920 let digest = store.lay_down(&bytes, &[("rivet", b"r")]).unwrap();
921 let entry = store.get(&digest).unwrap().unwrap();
922 assert!(store.tool_path(&entry, "rivet").is_some());
923 assert_eq!(store.tool_path(&entry, "synth"), None);
924 }
925}