Skip to main content

verdant_runtime/
cas.rs

1use crate::store::{FileStore, Key, StoreError};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct ManifestEntry {
7    pub path: PathBuf,
8    pub content_hash: Key,
9    pub mode: u32,
10    pub mtime_secs: i64,
11    pub mtime_nanos: u32,
12}
13
14impl ManifestEntry {
15    pub fn entry_for(path: PathBuf, content_hash: Key, mode: u32, mtime: (i64, u32)) -> Self {
16        Self {
17            path,
18            content_hash,
19            mode,
20            mtime_secs: mtime.0,
21            mtime_nanos: mtime.1,
22        }
23    }
24}
25
26/// A directory the recorded run created, replayed after `deleted` and
27/// before `links` and `entries`.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct ManifestDir {
30    pub path: PathBuf,
31    pub mode: u32,
32    pub mtime_secs: i64,
33    pub mtime_nanos: u32,
34}
35
36/// A symlink the recorded run left, holding `target` as its link text.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38pub struct ManifestLink {
39    pub path: PathBuf,
40    pub target: PathBuf,
41    pub mtime_secs: i64,
42    pub mtime_nanos: u32,
43}
44
45/// A directory present before and after the recorded run, with the mode or
46/// mtime the run gave it; an attribute the run left alone is `None`.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct ManifestDirAttrs {
49    pub path: PathBuf,
50    pub mode: Option<u32>,
51    pub mtime: Option<(i64, u32)>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct TreeManifest {
56    pub entries: Vec<ManifestEntry>,
57    pub deleted: Vec<PathBuf>,
58    /// Absent from manifests written before directories were recorded.
59    #[serde(default)]
60    pub dirs: Vec<ManifestDir>,
61    /// Absent from manifests written before symlinks were recorded.
62    #[serde(default)]
63    pub links: Vec<ManifestLink>,
64    /// Absent from manifests written before directory attributes were
65    /// recorded.
66    #[serde(default)]
67    pub dir_attrs: Vec<ManifestDirAttrs>,
68}
69
70impl TreeManifest {
71    pub fn new(entries: Vec<ManifestEntry>, deleted: Vec<PathBuf>) -> Self {
72        Self {
73            entries,
74            deleted,
75            dirs: Vec::new(),
76            links: Vec::new(),
77            dir_attrs: Vec::new(),
78        }
79    }
80}
81
82#[derive(Debug, thiserror::Error)]
83pub enum CasError {
84    #[error("store error: {0}")]
85    Store(#[from] StoreError),
86    #[error("json error: {0}")]
87    Json(#[from] serde_json::Error),
88}
89
90pub struct TreeBlobStore<'a> {
91    store: &'a FileStore,
92}
93
94impl<'a> TreeBlobStore<'a> {
95    pub fn new(store: &'a FileStore) -> Self {
96        Self { store }
97    }
98
99    pub fn put_file_blob(&self, content: &[u8]) -> Result<Key, CasError> {
100        let key = Key::from_bytes(content);
101        if !self.store.contains(&key) {
102            self.store.persist(&key, content, "cas-blob", vec![])?;
103        }
104        Ok(key)
105    }
106
107    pub fn get_file_blob(&self, key: &Key) -> Result<Option<Vec<u8>>, CasError> {
108        Ok(self.store.lookup(key)?.map(|p| p.bytes))
109    }
110
111    pub fn put_manifest(&self, manifest: &TreeManifest) -> Result<Key, CasError> {
112        let bytes = serde_json::to_vec(manifest)?;
113        self.put_file_blob(&bytes)
114    }
115
116    pub fn get_manifest(&self, key: &Key) -> Result<Option<TreeManifest>, CasError> {
117        match self.store.lookup(key)? {
118            None => Ok(None),
119            Some(payload) => {
120                let manifest = serde_json::from_slice(&payload.bytes)?;
121                Ok(Some(manifest))
122            }
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::store::FileStore;
131    use std::path::PathBuf;
132    use tempfile::TempDir;
133
134    fn setup() -> (TempDir, FileStore) {
135        let dir = TempDir::new().unwrap();
136        let store = FileStore::open(dir.path()).unwrap();
137        (dir, store)
138    }
139
140    #[test]
141    fn manifest_round_trip() {
142        let (_dir, store) = setup();
143        let cas = TreeBlobStore::new(&store);
144
145        let hash_a = Key::from_bytes(b"file-a-content");
146        let hash_b = Key::from_bytes(b"file-b-content");
147        let hash_c = Key::from_bytes(b"file-c-content");
148
149        let entries = vec![
150            ManifestEntry::entry_for(
151                PathBuf::from("src/main.rs"),
152                hash_a,
153                0o644,
154                (1_700_000_000, 123_456_789),
155            ),
156            ManifestEntry::entry_for(
157                PathBuf::from("src/lib.rs"),
158                hash_b,
159                0o755,
160                (1_700_000_001, 0),
161            ),
162            ManifestEntry::entry_for(
163                PathBuf::from("build.rs"),
164                hash_c,
165                0o600,
166                (-86400, 999_999_999),
167            ),
168        ];
169        let deleted = vec![PathBuf::from("old/removed.rs")];
170        let manifest = TreeManifest::new(entries, deleted);
171
172        let key = cas.put_manifest(&manifest).unwrap();
173        let loaded = cas
174            .get_manifest(&key)
175            .unwrap()
176            .expect("manifest must exist");
177
178        assert_eq!(loaded, manifest);
179        assert_eq!(loaded.entries[0].mode, 0o644);
180        assert_eq!(loaded.entries[0].mtime_secs, 1_700_000_000);
181        assert_eq!(loaded.entries[0].mtime_nanos, 123_456_789);
182        assert_eq!(loaded.entries[2].mtime_secs, -86400);
183        assert_eq!(loaded.deleted.len(), 1);
184        assert_eq!(loaded.deleted[0], PathBuf::from("old/removed.rs"));
185    }
186
187    #[test]
188    fn blob_round_trip() {
189        let (_dir, store) = setup();
190        let cas = TreeBlobStore::new(&store);
191
192        let key = cas.put_file_blob(b"hello").unwrap();
193        let fetched = cas.get_file_blob(&key).unwrap();
194        assert_eq!(fetched, Some(b"hello".to_vec()));
195    }
196
197    #[test]
198    fn blob_dedup() {
199        let (_dir, store) = setup();
200        let cas = TreeBlobStore::new(&store);
201
202        let key1 = cas.put_file_blob(b"same content").unwrap();
203        let key2 = cas.put_file_blob(b"same content").unwrap();
204        assert_eq!(key1, key2);
205
206        let root = store.root();
207        let shard = &key1.as_str()[..2];
208        let shard_path = root.join(shard);
209        let count = std::fs::read_dir(&shard_path)
210            .unwrap()
211            .filter_map(|e| e.ok())
212            .filter(|e| e.file_name().to_string_lossy().ends_with(".payload"))
213            .count();
214        assert_eq!(
215            count, 1,
216            "dedup: only one payload file for identical content"
217        );
218    }
219
220    #[test]
221    fn get_missing_key_returns_none() {
222        let (_dir, store) = setup();
223        let cas = TreeBlobStore::new(&store);
224
225        let phantom_key = Key::from_bytes(b"never stored");
226        assert_eq!(cas.get_file_blob(&phantom_key).unwrap(), None);
227        assert!(cas.get_manifest(&phantom_key).unwrap().is_none());
228    }
229
230    #[test]
231    fn mtime_nanosecond_fidelity() {
232        let (_dir, store) = setup();
233        let cas = TreeBlobStore::new(&store);
234
235        let hash = Key::from_bytes(b"precision-test");
236        let entry = ManifestEntry::entry_for(
237            PathBuf::from("file.txt"),
238            hash,
239            0o644,
240            (1_000_000_000, 123_456_789),
241        );
242        let manifest = TreeManifest::new(vec![entry], vec![]);
243
244        let key = cas.put_manifest(&manifest).unwrap();
245        let loaded = cas.get_manifest(&key).unwrap().unwrap();
246
247        assert_eq!(loaded.entries[0].mtime_nanos, 123_456_789);
248        assert_eq!(loaded.entries[0].mtime_secs, 1_000_000_000);
249    }
250
251    #[test]
252    fn a_manifest_written_before_dirs_were_recorded_still_loads() {
253        let manifest: TreeManifest =
254            serde_json::from_str(r#"{"entries":[],"deleted":["gone"]}"#).unwrap();
255        assert_eq!(manifest.deleted, [PathBuf::from("gone")]);
256        assert!(manifest.dirs.is_empty());
257        assert!(manifest.links.is_empty());
258        assert!(manifest.dir_attrs.is_empty());
259    }
260}