Skip to main content

leo_span/
file_source.rs

1// Copyright (C) 2019-2026 Provable Inc.
2// This file is part of the Leo library.
3
4// The Leo library is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// The Leo library is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
16
17use std::{
18    ffi::OsStr,
19    fs,
20    io,
21    path::{Path, PathBuf},
22};
23
24use indexmap::IndexMap;
25
26/// Abstraction over where the compiler reads source files from.
27///
28/// The default implementation [`DiskFileSource`] reads from the real filesystem.
29/// Alternative implementations enable compilation and formatting from in-memory
30/// buffers without requiring disk I/O.
31///
32/// # Path contract
33///
34/// Callers must provide consistent paths. Path normalization is the caller's
35/// responsibility.
36///
37/// - `exclude` in [`FileSource::list_leo_files`] is compared by exact path
38///   equality, so it must match the listed path exactly.
39/// - [`InMemoryFileSource::read_file`] performs exact key lookup.
40///
41/// # Ordering
42///
43/// [`FileSource::list_leo_files`] must return paths in deterministic, sorted
44/// order to ensure reproducible module ordering.
45pub trait FileSource {
46    /// Read the contents of a file at the given path.
47    fn read_file(&self, path: &Path) -> io::Result<String>;
48
49    /// List all `.leo` files under `dir`, excluding `exclude`.
50    fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>>;
51}
52
53/// Reads source files from the real filesystem.
54pub 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
69/// Recursively walks `dir`, collecting `.leo` files and propagating I/O errors.
70fn 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/// Reads source files from in-memory buffers.
86#[derive(Default)]
87pub struct InMemoryFileSource {
88    files: IndexMap<PathBuf, String>,
89}
90
91impl InMemoryFileSource {
92    /// Creates a new empty in-memory file source.
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Inserts or replaces the contents of a file.
98    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/// Reads one overlay file from memory and falls back to another file source for everything else.
122pub 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    /// Creates a new overlay file source.
130    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}