Skip to main content

verdant_runtime/
tree.rs

1use crate::cas::{CasError, ManifestEntry, TreeBlobStore, TreeManifest};
2use std::os::unix::fs::MetadataExt;
3use std::path::{Path, PathBuf};
4use std::{fs, io};
5
6#[derive(Debug, thiserror::Error)]
7pub enum TreeError {
8    #[error("io error: {0}")]
9    Io(#[from] io::Error),
10    #[error("cas error: {0}")]
11    Cas(#[from] CasError),
12    #[error("blob missing for {path}")]
13    BlobMissing { path: PathBuf },
14}
15
16pub fn capture_tree(blobs: &TreeBlobStore, root: &Path) -> Result<TreeManifest, TreeError> {
17    let mut entries: Vec<ManifestEntry> = Vec::new();
18    collect_entries(blobs, root, root, &mut entries)?;
19    entries.sort_by(|a, b| a.path.cmp(&b.path));
20    Ok(TreeManifest::new(entries, vec![]))
21}
22
23fn collect_entries(
24    blobs: &TreeBlobStore,
25    root: &Path,
26    dir: &Path,
27    entries: &mut Vec<ManifestEntry>,
28) -> Result<(), TreeError> {
29    for entry in fs::read_dir(dir)? {
30        let entry = entry?;
31        let path = entry.path();
32        let file_type = entry.file_type()?;
33
34        if file_type.is_symlink() {
35            // symlinks out of scope for M4
36            continue;
37        }
38
39        if file_type.is_dir() {
40            collect_entries(blobs, root, &path, entries)?;
41        } else if file_type.is_file() {
42            let metadata = fs::metadata(&path)?;
43            let content = fs::read(&path)?;
44            let key = blobs.put_file_blob(&content)?;
45            let mode = (metadata.mode() & 0o7777) as u32;
46            let mtime_secs = metadata.mtime();
47            let mtime_nanos = metadata.mtime_nsec() as u32;
48            let rel = path
49                .strip_prefix(root)
50                .expect("path must be under root")
51                .to_path_buf();
52            entries.push(ManifestEntry::entry_for(
53                rel,
54                key,
55                mode,
56                (mtime_secs, mtime_nanos),
57            ));
58        }
59    }
60    Ok(())
61}
62
63pub fn restore_tree(
64    blobs: &TreeBlobStore,
65    manifest: &TreeManifest,
66    dest: &Path,
67) -> Result<(), TreeError> {
68    for entry in &manifest.entries {
69        let target = dest.join(&entry.path);
70        if let Some(parent) = target.parent() {
71            fs::create_dir_all(parent)?;
72        }
73
74        let bytes =
75            blobs
76                .get_file_blob(&entry.content_hash)?
77                .ok_or_else(|| TreeError::BlobMissing {
78                    path: entry.path.clone(),
79                })?;
80
81        fs::write(&target, &bytes)?;
82
83        // Set permissions before mtime so the permission change doesn't bump mtime.
84        use std::os::unix::fs::PermissionsExt;
85        fs::set_permissions(&target, fs::Permissions::from_mode(entry.mode))?;
86
87        // Set mtime after writing and after permissions, because both operations
88        // would otherwise reset it.
89        let ft = filetime::FileTime::from_unix_time(entry.mtime_secs, entry.mtime_nanos);
90        filetime::set_file_mtime(&target, ft)?;
91    }
92
93    for deleted in &manifest.deleted {
94        let target = dest.join(deleted);
95        if target.exists() {
96            fs::remove_file(&target)?;
97        }
98    }
99
100    Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106    use crate::cas::TreeBlobStore;
107    use crate::store::FileStore;
108    use filetime::FileTime;
109    use std::path::PathBuf;
110    use tempfile::TempDir;
111
112    fn setup_store() -> (TempDir, FileStore) {
113        let dir = TempDir::new().unwrap();
114        let store = FileStore::open(dir.path()).unwrap();
115        (dir, store)
116    }
117
118    #[test]
119    fn round_trip_fidelity() {
120        let (_store_dir, store) = setup_store();
121        let blobs = TreeBlobStore::new(&store);
122
123        let src_dir = TempDir::new().unwrap();
124        let nested = src_dir.path().join("sub/dir");
125        fs::create_dir_all(&nested).unwrap();
126
127        let file_a = src_dir.path().join("top.txt");
128        fs::write(&file_a, b"hello top").unwrap();
129
130        let file_b = nested.join("deep.bin");
131        fs::write(&file_b, b"\x00\x01\x02\x03").unwrap();
132
133        // Set a precise mtime with non-zero nanoseconds on file_a.
134        let known_mtime = FileTime::from_unix_time(1_700_000_000, 123_456_789);
135        filetime::set_file_mtime(&file_a, known_mtime).unwrap();
136
137        // Capture permissions before capture so we can verify restoration.
138        use std::os::unix::fs::PermissionsExt;
139        let mode_a = fs::metadata(&file_a).unwrap().permissions().mode() & 0o7777;
140        let mode_b = fs::metadata(&file_b).unwrap().permissions().mode() & 0o7777;
141        let mtime_b = {
142            use std::os::unix::fs::MetadataExt;
143            let md = fs::metadata(&file_b).unwrap();
144            FileTime::from_unix_time(md.mtime(), md.mtime_nsec() as u32)
145        };
146
147        let manifest = capture_tree(&blobs, src_dir.path()).unwrap();
148        assert_eq!(manifest.entries.len(), 2);
149        assert!(manifest.deleted.is_empty());
150
151        let dest_dir = TempDir::new().unwrap();
152        restore_tree(&blobs, &manifest, dest_dir.path()).unwrap();
153
154        // Verify contents.
155        let restored_a = fs::read(dest_dir.path().join("top.txt")).unwrap();
156        assert_eq!(restored_a, b"hello top");
157        let restored_b = fs::read(dest_dir.path().join("sub/dir/deep.bin")).unwrap();
158        assert_eq!(restored_b, b"\x00\x01\x02\x03");
159
160        // Verify permissions.
161        let restored_mode_a = fs::metadata(dest_dir.path().join("top.txt"))
162            .unwrap()
163            .permissions()
164            .mode()
165            & 0o7777;
166        let restored_mode_b = fs::metadata(dest_dir.path().join("sub/dir/deep.bin"))
167            .unwrap()
168            .permissions()
169            .mode()
170            & 0o7777;
171        assert_eq!(restored_mode_a, mode_a);
172        assert_eq!(restored_mode_b, mode_b);
173
174        // Verify mtimes including nanoseconds.
175        use std::os::unix::fs::MetadataExt;
176        let md_a = fs::metadata(dest_dir.path().join("top.txt")).unwrap();
177        assert_eq!(md_a.mtime(), 1_700_000_000);
178        assert_eq!(md_a.mtime_nsec() as u32, 123_456_789);
179
180        let md_b = fs::metadata(dest_dir.path().join("sub/dir/deep.bin")).unwrap();
181        assert_eq!(
182            FileTime::from_unix_time(md_b.mtime(), md_b.mtime_nsec() as u32),
183            mtime_b
184        );
185    }
186
187    #[test]
188    fn deletion_removes_file_from_dest() {
189        let (_store_dir, store) = setup_store();
190        let blobs = TreeBlobStore::new(&store);
191
192        let dest_dir = TempDir::new().unwrap();
193        let target = dest_dir.path().join("to_delete.txt");
194        fs::write(&target, b"present").unwrap();
195        assert!(target.exists());
196
197        let manifest = TreeManifest::new(vec![], vec![PathBuf::from("to_delete.txt")]);
198        restore_tree(&blobs, &manifest, dest_dir.path()).unwrap();
199
200        assert!(!target.exists(), "deleted path must be removed from dest");
201    }
202
203    #[test]
204    fn missing_blob_returns_error() {
205        let (_store_dir, store) = setup_store();
206        let blobs = TreeBlobStore::new(&store);
207        let dest_dir = TempDir::new().unwrap();
208
209        let phantom_key = crate::store::Key::from_bytes(b"never-stored");
210        let entry =
211            ManifestEntry::entry_for(PathBuf::from("ghost.txt"), phantom_key, 0o644, (0, 0));
212        let manifest = TreeManifest::new(vec![entry], vec![]);
213        let err = restore_tree(&blobs, &manifest, dest_dir.path())
214            .expect_err("must error on missing blob");
215        assert!(
216            matches!(err, TreeError::BlobMissing { .. }),
217            "unexpected error variant: {err:?}"
218        );
219    }
220}