1use hd_cas::ContentHash;
2use hd_engine::{Dag, Node};
3
4use crate::overlay::Overlay;
5
6#[derive(Debug, thiserror::Error)]
7pub enum ProjectedError {
8 #[error("path not found: {0}")]
9 NotFound(String),
10 #[error("not a file: {0}")]
11 NotAFile(String),
12 #[error("not a directory: {0}")]
13 NotADirectory(String),
14 #[error("CAS error: {0}")]
15 Store(#[from] hd_cas::store::StoreError),
16}
17
18pub struct ProjectedFs {
21 dag: Dag,
22 root: ContentHash,
23 overlay: Overlay,
24}
25
26impl ProjectedFs {
27 pub fn new(dag: Dag, root: ContentHash) -> Self {
28 ProjectedFs {
29 dag,
30 root,
31 overlay: Overlay::new(),
32 }
33 }
34
35 pub fn read_file(&self, path: &str) -> Result<Vec<u8>, ProjectedError> {
37 if self.overlay.is_deleted(path) {
39 return Err(ProjectedError::NotFound(path.to_string()));
40 }
41 if let Some(data) = self.overlay.read(path) {
42 return Ok(data.to_vec());
43 }
44
45 let node = self.dag.query(&self.root, path)
47 .ok_or_else(|| ProjectedError::NotFound(path.to_string()))?;
48
49 match node {
50 Node::File { manifest_hash, .. } => {
51 let manifest = self.dag.store().get_manifest(manifest_hash)?;
52 let mut data = Vec::with_capacity(manifest.size as usize);
53 for chunk_hash in &manifest.chunks {
54 let chunk = self.dag.store().get_chunk(chunk_hash)?;
55 data.extend_from_slice(&chunk);
56 }
57 Ok(data)
58 }
59 _ => Err(ProjectedError::NotAFile(path.to_string())),
60 }
61 }
62
63 pub fn list_dir(&self, path: &str) -> Result<Vec<String>, ProjectedError> {
65 if path.is_empty() {
66 let root_node = self.dag.get(&self.root)
68 .ok_or_else(|| ProjectedError::NotFound("root".to_string()))?;
69 match root_node {
70 Node::Env { children, .. } => {
71 let mut entries = Vec::new();
72 for child_hash in children {
73 if let Some(child) = self.dag.get(child_hash) {
74 if let Some(name) = node_name(child) {
75 entries.push(name);
76 }
77 }
78 }
79 Ok(entries)
80 }
81 _ => Err(ProjectedError::NotADirectory(path.to_string())),
82 }
83 } else {
84 let node = self.dag.query(&self.root, path)
85 .ok_or_else(|| ProjectedError::NotFound(path.to_string()))?;
86 match node {
87 Node::Dir { children, .. } => {
88 Ok(children.iter().map(|(name, _)| name.clone()).collect())
89 }
90 _ => Err(ProjectedError::NotADirectory(path.to_string())),
91 }
92 }
93 }
94
95 pub fn exists(&self, path: &str) -> bool {
97 if self.overlay.is_deleted(path) {
98 return false;
99 }
100 if self.overlay.read(path).is_some() {
101 return true;
102 }
103 self.dag.query(&self.root, path).is_some()
104 }
105
106 pub fn overlay_mut(&mut self) -> &mut Overlay {
108 &mut self.overlay
109 }
110
111 pub fn overlay(&self) -> &Overlay {
113 &self.overlay
114 }
115
116 pub fn root(&self) -> &ContentHash {
118 &self.root
119 }
120
121 pub fn set_root(&mut self, root: ContentHash) {
123 self.root = root;
124 }
125
126 pub fn dag(&self) -> &Dag {
128 &self.dag
129 }
130}
131
132fn node_name(node: &Node) -> Option<String> {
134 match node {
135 Node::Dir { path, .. } => {
136 Some(path.rsplit('/').next().unwrap_or(path).to_string())
137 }
138 Node::File { path, .. } => {
139 Some(path.rsplit('/').next().unwrap_or(path).to_string())
140 }
141 _ => None,
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use hd_cas::ContentStore;
149 use tempfile::TempDir;
150
151 fn setup() -> (ProjectedFs, TempDir) {
152 let dir = TempDir::new().unwrap();
153 let store = ContentStore::open(dir.path()).unwrap();
154
155 let file_data = b"fn main() { println!(\"hello\"); }";
157 let manifest_hash = store.put_file_from_bytes(file_data, 0o644).unwrap();
158
159 let lib_data = b"pub fn greet() {}";
160 let lib_manifest_hash = store.put_file_from_bytes(lib_data, 0o644).unwrap();
161
162 let mut dag = Dag::new(store);
164 let main_node = Node::file("src/main.rs", manifest_hash);
165 let main_hash = dag.insert(main_node).unwrap();
166 let lib_node = Node::file("src/lib.rs", lib_manifest_hash);
167 let lib_hash = dag.insert(lib_node).unwrap();
168 let src_dir = Node::dir("src", vec![
169 ("main.rs".into(), main_hash),
170 ("lib.rs".into(), lib_hash),
171 ]);
172 let src_hash = dag.insert(src_dir).unwrap();
173 let env = Node::env("testapp", vec![src_hash]);
174 let env_hash = dag.insert(env).unwrap();
175
176 let fs = ProjectedFs::new(dag, env_hash);
177 (fs, dir)
178 }
179
180 #[test]
181 fn read_file_from_dag() {
182 let (fs, _dir) = setup();
183 let data = fs.read_file("src/main.rs").unwrap();
184 assert_eq!(data, b"fn main() { println!(\"hello\"); }");
185 }
186
187 #[test]
188 fn read_nonexistent_file() {
189 let (fs, _dir) = setup();
190 assert!(fs.read_file("missing.rs").is_err());
191 }
192
193 #[test]
194 fn list_directory() {
195 let (fs, _dir) = setup();
196 let entries = fs.list_dir("src").unwrap();
197 assert_eq!(entries.len(), 2);
198 assert!(entries.contains(&"main.rs".to_string()));
199 assert!(entries.contains(&"lib.rs".to_string()));
200 }
201
202 #[test]
203 fn list_root() {
204 let (fs, _dir) = setup();
205 let entries = fs.list_dir("").unwrap();
206 assert!(entries.contains(&"src".to_string()));
207 }
208
209 #[test]
210 fn overlay_overrides_dag() {
211 let (mut fs, _dir) = setup();
212 fs.overlay_mut().write("src/main.rs", b"// modified".to_vec());
213 let data = fs.read_file("src/main.rs").unwrap();
214 assert_eq!(data, b"// modified");
215 }
216
217 #[test]
218 fn overlay_adds_new_file() {
219 let (mut fs, _dir) = setup();
220 fs.overlay_mut().write("src/new.rs", b"// new file".to_vec());
221 let data = fs.read_file("src/new.rs").unwrap();
222 assert_eq!(data, b"// new file");
223 }
224
225 #[test]
226 fn overlay_deletes_dag_file() {
227 let (mut fs, _dir) = setup();
228 fs.overlay_mut().delete("src/main.rs");
229 assert!(fs.read_file("src/main.rs").is_err());
230 }
231
232 #[test]
233 fn file_exists() {
234 let (fs, _dir) = setup();
235 assert!(fs.exists("src/main.rs"));
236 assert!(fs.exists("src"));
237 assert!(!fs.exists("missing"));
238 }
239}