1use std::{
18 ffi::OsStr,
19 fs,
20 io,
21 path::{Path, PathBuf},
22};
23
24use indexmap::IndexMap;
25
26pub trait FileSource {
46 fn read_file(&self, path: &Path) -> io::Result<String>;
48
49 fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>>;
51}
52
53pub struct DiskFileSource;
55
56impl FileSource for DiskFileSource {
57 fn read_file(&self, path: &Path) -> io::Result<String> {
58 fs::read_to_string(path)
59 }
60
61 fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>> {
62 let mut files = Vec::new();
63 walk_dir_recursive(dir, exclude, &mut files)?;
64 files.sort();
65 Ok(files)
66 }
67}
68
69fn walk_dir_recursive(dir: &Path, exclude: &Path, files: &mut Vec<PathBuf>) -> io::Result<()> {
71 for entry in fs::read_dir(dir)? {
72 let entry = entry?;
73 let path = entry.path();
74
75 if path.is_dir() {
76 walk_dir_recursive(&path, exclude, files)?;
77 } else if path != exclude && path.extension() == Some(OsStr::new("leo")) {
78 files.push(path);
79 }
80 }
81
82 Ok(())
83}
84
85#[derive(Default)]
87pub struct InMemoryFileSource {
88 files: IndexMap<PathBuf, String>,
89}
90
91impl InMemoryFileSource {
92 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn set(&mut self, path: PathBuf, contents: String) {
99 self.files.insert(path, contents);
100 }
101}
102
103impl FileSource for InMemoryFileSource {
104 fn read_file(&self, path: &Path) -> io::Result<String> {
105 self.files.get(path).cloned().ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, path.display().to_string()))
106 }
107
108 fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>> {
109 let mut files = Vec::with_capacity(self.files.len());
110 for path in self.files.keys() {
111 if path.starts_with(dir) && path != exclude && path.extension() == Some(OsStr::new("leo")) {
112 files.push(path.clone());
113 }
114 }
115
116 files.sort();
117 Ok(files)
118 }
119}
120
121pub struct OverlayFileSource<'a, F> {
123 overlay_path: PathBuf,
124 overlay_contents: String,
125 fallback: &'a F,
126}
127
128impl<'a, F> OverlayFileSource<'a, F> {
129 pub fn new(overlay_path: PathBuf, overlay_contents: String, fallback: &'a F) -> Self {
131 Self { overlay_path, overlay_contents, fallback }
132 }
133}
134
135impl<F: FileSource> FileSource for OverlayFileSource<'_, F> {
136 fn read_file(&self, path: &Path) -> io::Result<String> {
137 if path == self.overlay_path { Ok(self.overlay_contents.clone()) } else { self.fallback.read_file(path) }
138 }
139
140 fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>> {
141 let mut files = self.fallback.list_leo_files(dir, exclude)?;
142
143 if self.overlay_path.starts_with(dir)
144 && self.overlay_path.extension() == Some(OsStr::new("leo"))
145 && self.overlay_path != exclude
146 && !files.iter().any(|path| path == &self.overlay_path)
147 {
148 files.push(self.overlay_path.clone());
149 files.sort();
150 }
151
152 Ok(files)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::{DiskFileSource, FileSource, InMemoryFileSource, OverlayFileSource};
159
160 use std::{
161 env,
162 fs,
163 io,
164 path::{Path, PathBuf},
165 process,
166 time::{SystemTime, UNIX_EPOCH},
167 };
168
169 fn unique_temp_dir() -> PathBuf {
170 let nanos =
171 SystemTime::now().duration_since(UNIX_EPOCH).expect("system clock should be after unix epoch").as_nanos();
172 env::temp_dir().join(format!("leo_file_source_{}_{}", process::id(), nanos))
173 }
174
175 #[test]
176 fn in_memory_read_file() {
177 let mut source = InMemoryFileSource::new();
178 source.set(PathBuf::from("/src/main.leo"), "program test.aleo { }".into());
179
180 let content = source.read_file(Path::new("/src/main.leo")).unwrap();
181 assert_eq!(content, "program test.aleo { }");
182 }
183
184 #[test]
185 fn in_memory_read_file_not_found() {
186 let source = InMemoryFileSource::new();
187
188 let err = source.read_file(Path::new("/nonexistent.leo")).unwrap_err();
189 assert_eq!(err.kind(), io::ErrorKind::NotFound);
190 }
191
192 #[test]
193 fn in_memory_list_leo_files() {
194 let mut source = InMemoryFileSource::new();
195 source.set(PathBuf::from("/src/main.leo"), String::new());
196 source.set(PathBuf::from("/src/utils.leo"), String::new());
197 source.set(PathBuf::from("/src/alpha.leo"), String::new());
198 source.set(PathBuf::from("/src/data.json"), String::new());
199 source.set(PathBuf::from("/other/lib.leo"), String::new());
200
201 let files = source.list_leo_files(Path::new("/src"), Path::new("/src/main.leo")).unwrap();
202 assert_eq!(files, vec![PathBuf::from("/src/alpha.leo"), PathBuf::from("/src/utils.leo")]);
203 }
204
205 #[test]
206 fn disk_read_file() {
207 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
208
209 let content = DiskFileSource.read_file(&path).unwrap();
210 assert!(content.contains("leo-span"));
211 }
212
213 #[test]
214 fn disk_read_file_not_found() {
215 let err = DiskFileSource.read_file(Path::new("/nonexistent_path_12345.leo")).unwrap_err();
216 assert_eq!(err.kind(), io::ErrorKind::NotFound);
217 }
218
219 #[test]
220 fn disk_list_leo_files() {
221 let tmp = unique_temp_dir();
222 let nested = tmp.join("nested");
223 let excluded = tmp.join("excluded.leo");
224 let result = (|| -> io::Result<Vec<PathBuf>> {
225 fs::create_dir_all(&nested)?;
226 fs::write(tmp.join("b.leo"), "")?;
227 fs::write(tmp.join("a.leo"), "")?;
228 fs::write(&excluded, "")?;
229 fs::write(tmp.join("not_leo.txt"), "")?;
230 fs::write(nested.join("nested.leo"), "")?;
231
232 DiskFileSource.list_leo_files(&tmp, &excluded)
233 })();
234
235 let _ = fs::remove_dir_all(&tmp);
236
237 let files = result.unwrap();
238 assert_eq!(files, vec![tmp.join("a.leo"), tmp.join("b.leo"), nested.join("nested.leo")]);
239 }
240
241 #[test]
242 fn disk_list_leo_files_propagates_errors() {
243 let err = DiskFileSource.list_leo_files(Path::new("/nonexistent_dir_12345"), Path::new("")).unwrap_err();
244 assert_eq!(err.kind(), io::ErrorKind::NotFound);
245 }
246
247 #[test]
248 fn in_memory_deterministic_ordering() {
249 let mut source = InMemoryFileSource::new();
250 source.set(PathBuf::from("/src/z.leo"), String::new());
251 source.set(PathBuf::from("/src/m.leo"), String::new());
252 source.set(PathBuf::from("/src/a.leo"), String::new());
253
254 let files = source.list_leo_files(Path::new("/src"), Path::new("/src/none.leo")).unwrap();
255 assert_eq!(files, vec![PathBuf::from("/src/a.leo"), PathBuf::from("/src/m.leo"), PathBuf::from("/src/z.leo")]);
256 }
257
258 #[test]
259 fn overlay_source_reads_overlay_before_fallback() {
260 let mut fallback = InMemoryFileSource::new();
261 fallback.set(PathBuf::from("/src/main.leo"), "disk".into());
262 let overlay = OverlayFileSource::new(PathBuf::from("/src/main.leo"), "memory".into(), &fallback);
263
264 let content = overlay.read_file(Path::new("/src/main.leo")).unwrap();
265 assert_eq!(content, "memory");
266 }
267
268 #[test]
269 fn overlay_source_merges_overlay_file_into_listings() {
270 let mut fallback = InMemoryFileSource::new();
271 fallback.set(PathBuf::from("/src/utils.leo"), String::new());
272 let overlay = OverlayFileSource::new(PathBuf::from("/src/main.leo"), String::new(), &fallback);
273
274 let files = overlay.list_leo_files(Path::new("/src"), Path::new("/src/none.leo")).unwrap();
275 assert_eq!(files, vec![PathBuf::from("/src/main.leo"), PathBuf::from("/src/utils.leo")]);
276 }
277}