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
121#[cfg(test)]
122mod tests {
123 use super::{DiskFileSource, FileSource, InMemoryFileSource};
124
125 use std::{
126 env,
127 fs,
128 io,
129 path::{Path, PathBuf},
130 process,
131 time::{SystemTime, UNIX_EPOCH},
132 };
133
134 fn unique_temp_dir() -> PathBuf {
135 let nanos =
136 SystemTime::now().duration_since(UNIX_EPOCH).expect("system clock should be after unix epoch").as_nanos();
137 env::temp_dir().join(format!("leo_file_source_{}_{}", process::id(), nanos))
138 }
139
140 #[test]
141 fn in_memory_read_file() {
142 let mut source = InMemoryFileSource::new();
143 source.set(PathBuf::from("/src/main.leo"), "program test.aleo { }".into());
144
145 let content = source.read_file(Path::new("/src/main.leo")).unwrap();
146 assert_eq!(content, "program test.aleo { }");
147 }
148
149 #[test]
150 fn in_memory_read_file_not_found() {
151 let source = InMemoryFileSource::new();
152
153 let err = source.read_file(Path::new("/nonexistent.leo")).unwrap_err();
154 assert_eq!(err.kind(), io::ErrorKind::NotFound);
155 }
156
157 #[test]
158 fn in_memory_list_leo_files() {
159 let mut source = InMemoryFileSource::new();
160 source.set(PathBuf::from("/src/main.leo"), String::new());
161 source.set(PathBuf::from("/src/utils.leo"), String::new());
162 source.set(PathBuf::from("/src/alpha.leo"), String::new());
163 source.set(PathBuf::from("/src/data.json"), String::new());
164 source.set(PathBuf::from("/other/lib.leo"), String::new());
165
166 let files = source.list_leo_files(Path::new("/src"), Path::new("/src/main.leo")).unwrap();
167 assert_eq!(files, vec![PathBuf::from("/src/alpha.leo"), PathBuf::from("/src/utils.leo")]);
168 }
169
170 #[test]
171 fn disk_read_file() {
172 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
173
174 let content = DiskFileSource.read_file(&path).unwrap();
175 assert!(content.contains("leo-span"));
176 }
177
178 #[test]
179 fn disk_read_file_not_found() {
180 let err = DiskFileSource.read_file(Path::new("/nonexistent_path_12345.leo")).unwrap_err();
181 assert_eq!(err.kind(), io::ErrorKind::NotFound);
182 }
183
184 #[test]
185 fn disk_list_leo_files() {
186 let tmp = unique_temp_dir();
187 let nested = tmp.join("nested");
188 let excluded = tmp.join("excluded.leo");
189 let result = (|| -> io::Result<Vec<PathBuf>> {
190 fs::create_dir_all(&nested)?;
191 fs::write(tmp.join("b.leo"), "")?;
192 fs::write(tmp.join("a.leo"), "")?;
193 fs::write(&excluded, "")?;
194 fs::write(tmp.join("not_leo.txt"), "")?;
195 fs::write(nested.join("nested.leo"), "")?;
196
197 DiskFileSource.list_leo_files(&tmp, &excluded)
198 })();
199
200 let _ = fs::remove_dir_all(&tmp);
201
202 let files = result.unwrap();
203 assert_eq!(files, vec![tmp.join("a.leo"), tmp.join("b.leo"), nested.join("nested.leo")]);
204 }
205
206 #[test]
207 fn disk_list_leo_files_propagates_errors() {
208 let err = DiskFileSource.list_leo_files(Path::new("/nonexistent_dir_12345"), Path::new("")).unwrap_err();
209 assert_eq!(err.kind(), io::ErrorKind::NotFound);
210 }
211
212 #[test]
213 fn in_memory_deterministic_ordering() {
214 let mut source = InMemoryFileSource::new();
215 source.set(PathBuf::from("/src/z.leo"), String::new());
216 source.set(PathBuf::from("/src/m.leo"), String::new());
217 source.set(PathBuf::from("/src/a.leo"), String::new());
218
219 let files = source.list_leo_files(Path::new("/src"), Path::new("/src/none.leo")).unwrap();
220 assert_eq!(files, vec![PathBuf::from("/src/a.leo"), PathBuf::from("/src/m.leo"), PathBuf::from("/src/z.leo")]);
221 }
222}