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#[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}