Skip to main content

hd_mount/
overlay.rs

1use std::collections::{HashMap, HashSet};
2
3/// An in-memory overlay that captures writes on top of the immutable DAG.
4/// Reads check the overlay first; if not found, fall through to the DAG/CAS.
5pub struct Overlay {
6    /// Written/modified files: path -> data
7    files: HashMap<String, Vec<u8>>,
8    /// Deleted paths (mask out DAG entries)
9    deleted: HashSet<String>,
10}
11
12impl Overlay {
13    pub fn new() -> Self {
14        Overlay {
15            files: HashMap::new(),
16            deleted: HashSet::new(),
17        }
18    }
19
20    /// Write data to a path in the overlay.
21    pub fn write(&mut self, path: &str, data: Vec<u8>) {
22        self.deleted.remove(path);
23        self.files.insert(path.to_string(), data);
24    }
25
26    /// Read data from the overlay. Returns None if path is not in the overlay.
27    pub fn read(&self, path: &str) -> Option<&[u8]> {
28        if self.deleted.contains(path) {
29            return None;
30        }
31        self.files.get(path).map(|v| v.as_slice())
32    }
33
34    /// Mark a path as deleted in the overlay.
35    pub fn delete(&mut self, path: &str) {
36        self.files.remove(path);
37        self.deleted.insert(path.to_string());
38    }
39
40    /// Check if a path is explicitly deleted.
41    pub fn is_deleted(&self, path: &str) -> bool {
42        self.deleted.contains(path)
43    }
44
45    /// Return all modified paths (written + deleted).
46    pub fn modified_paths(&self) -> Vec<&str> {
47        let mut paths: Vec<&str> = self.files.keys().map(|s| s.as_str()).collect();
48        paths.extend(self.deleted.iter().map(|s| s.as_str()));
49        paths.sort();
50        paths
51    }
52
53    /// Clear all overlay state.
54    pub fn clear(&mut self) {
55        self.files.clear();
56        self.deleted.clear();
57    }
58
59    /// Check if the overlay has any modifications.
60    pub fn is_empty(&self) -> bool {
61        self.files.is_empty() && self.deleted.is_empty()
62    }
63}
64
65impl Default for Overlay {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn write_and_read_back() {
77        let mut overlay = Overlay::new();
78        overlay.write("/app/new.txt", b"hello".to_vec());
79        assert_eq!(overlay.read("/app/new.txt"), Some(b"hello".as_slice()));
80    }
81
82    #[test]
83    fn read_nonexistent_returns_none() {
84        let overlay = Overlay::new();
85        assert!(overlay.read("/missing").is_none());
86    }
87
88    #[test]
89    fn overwrite_replaces_data() {
90        let mut overlay = Overlay::new();
91        overlay.write("/file", b"v1".to_vec());
92        overlay.write("/file", b"v2".to_vec());
93        assert_eq!(overlay.read("/file"), Some(b"v2".as_slice()));
94    }
95
96    #[test]
97    fn delete_marks_as_deleted() {
98        let mut overlay = Overlay::new();
99        overlay.write("/file", b"data".to_vec());
100        overlay.delete("/file");
101        assert!(overlay.is_deleted("/file"));
102        assert!(overlay.read("/file").is_none());
103    }
104
105    #[test]
106    fn list_modified_paths() {
107        let mut overlay = Overlay::new();
108        overlay.write("/a", b"1".to_vec());
109        overlay.write("/b", b"2".to_vec());
110        overlay.delete("/c");
111        let paths = overlay.modified_paths();
112        assert_eq!(paths.len(), 3);
113    }
114
115    #[test]
116    fn clear_resets_state() {
117        let mut overlay = Overlay::new();
118        overlay.write("/file", b"data".to_vec());
119        overlay.clear();
120        assert!(overlay.read("/file").is_none());
121        assert!(overlay.modified_paths().is_empty());
122    }
123}