1use 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
20pub fn blob_path(root: &Path, hex: &str) -> PathBuf {
22 blobs_dir(root).join(hex)
23}
24
25pub 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
35pub 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
53pub 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}