Skip to main content

act_store/
layout.rs

1//! OCI image-layout directory mechanics. Knows files and blobs, not components.
2
3use std::io;
4use std::path::{Path, PathBuf};
5
6use sha2::{Digest, Sha256};
7
8pub fn index_path(root: &Path) -> PathBuf {
9    root.join("index.json")
10}
11
12pub fn oci_layout_path(root: &Path) -> PathBuf {
13    root.join("oci-layout")
14}
15
16pub fn blobs_dir(root: &Path) -> PathBuf {
17    root.join("blobs").join("sha256")
18}
19
20/// Path to a blob given its hex digest (no `sha256:` prefix).
21pub fn blob_path(root: &Path, hex: &str) -> PathBuf {
22    blobs_dir(root).join(hex)
23}
24
25/// Create the layout skeleton. Idempotent.
26pub fn init(root: &Path) -> io::Result<()> {
27    std::fs::create_dir_all(blobs_dir(root))?;
28    let marker = oci_layout_path(root);
29    if !marker.exists() {
30        std::fs::write(&marker, b"{\"imageLayoutVersion\":\"1.0.0\"}")?;
31    }
32    Ok(())
33}
34
35/// Lowercase hex of the sha256 of `bytes`.
36pub fn sha256_hex(bytes: &[u8]) -> String {
37    let digest = Sha256::digest(bytes);
38    let mut s = String::with_capacity(64);
39    for b in digest {
40        s.push_str(&format!("{b:02x}"));
41    }
42    s
43}
44
45pub fn has_blob(root: &Path, hex: &str) -> bool {
46    blob_path(root, hex).is_file()
47}
48
49pub fn read_blob(root: &Path, hex: &str) -> io::Result<Vec<u8>> {
50    std::fs::read(blob_path(root, hex))
51}
52
53/// Write `bytes` as a blob; returns its hex digest. Atomic (temp + rename);
54/// a no-op if the blob already exists (content-addressed dedup).
55pub fn write_blob(root: &Path, bytes: &[u8]) -> io::Result<String> {
56    let hex = sha256_hex(bytes);
57    let dest = blob_path(root, &hex);
58    if dest.is_file() {
59        return Ok(hex);
60    }
61    std::fs::create_dir_all(blobs_dir(root))?;
62    let tmp = blobs_dir(root).join(format!(".{}.{}.tmp", hex, std::process::id()));
63    std::fs::write(&tmp, bytes)?;
64    match std::fs::rename(&tmp, &dest) {
65        Ok(()) => Ok(hex),
66        Err(e) => {
67            let _ = std::fs::remove_file(&tmp);
68            Err(e)
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use tempfile::TempDir;
77
78    #[test]
79    fn init_creates_layout_skeleton() {
80        let dir = TempDir::new().unwrap();
81        init(dir.path()).unwrap();
82        assert!(dir.path().join("oci-layout").is_file());
83        assert!(dir.path().join("blobs").join("sha256").is_dir());
84        let marker = std::fs::read_to_string(dir.path().join("oci-layout")).unwrap();
85        assert!(marker.contains("imageLayoutVersion"));
86    }
87
88    #[test]
89    fn init_is_idempotent() {
90        let dir = TempDir::new().unwrap();
91        init(dir.path()).unwrap();
92        init(dir.path()).unwrap();
93    }
94
95    #[test]
96    fn write_blob_is_content_addressed_and_dedups() {
97        let dir = TempDir::new().unwrap();
98        init(dir.path()).unwrap();
99        let hex1 = write_blob(dir.path(), b"hello").unwrap();
100        let hex2 = write_blob(dir.path(), b"hello").unwrap();
101        assert_eq!(hex1, hex2, "same bytes -> same digest");
102        assert_eq!(
103            hex1,
104            "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
105        );
106        assert!(blob_path(dir.path(), &hex1).is_file());
107        assert_eq!(read_blob(dir.path(), &hex1).unwrap(), b"hello");
108        assert!(has_blob(dir.path(), &hex1));
109        assert!(!has_blob(dir.path(), "deadbeef"));
110    }
111}