use std::{
ffi::OsStr,
fs,
io,
path::{Path, PathBuf},
};
use indexmap::IndexMap;
pub trait FileSource {
fn read_file(&self, path: &Path) -> io::Result<String>;
fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>>;
}
pub struct DiskFileSource;
impl FileSource for DiskFileSource {
fn read_file(&self, path: &Path) -> io::Result<String> {
fs::read_to_string(path)
}
fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>> {
let mut files = Vec::new();
walk_dir_recursive(dir, exclude, &mut files)?;
files.sort();
Ok(files)
}
}
fn walk_dir_recursive(dir: &Path, exclude: &Path, files: &mut Vec<PathBuf>) -> io::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
walk_dir_recursive(&path, exclude, files)?;
} else if path != exclude && path.extension() == Some(OsStr::new("leo")) {
files.push(path);
}
}
Ok(())
}
#[derive(Default)]
pub struct InMemoryFileSource {
files: IndexMap<PathBuf, String>,
}
impl InMemoryFileSource {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, path: PathBuf, contents: String) {
self.files.insert(path, contents);
}
}
impl FileSource for InMemoryFileSource {
fn read_file(&self, path: &Path) -> io::Result<String> {
self.files.get(path).cloned().ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, path.display().to_string()))
}
fn list_leo_files(&self, dir: &Path, exclude: &Path) -> io::Result<Vec<PathBuf>> {
let mut files = Vec::with_capacity(self.files.len());
for path in self.files.keys() {
if path.starts_with(dir) && path != exclude && path.extension() == Some(OsStr::new("leo")) {
files.push(path.clone());
}
}
files.sort();
Ok(files)
}
}
#[cfg(test)]
mod tests {
use super::{DiskFileSource, FileSource, InMemoryFileSource};
use std::{
env,
fs,
io,
path::{Path, PathBuf},
process,
time::{SystemTime, UNIX_EPOCH},
};
fn unique_temp_dir() -> PathBuf {
let nanos =
SystemTime::now().duration_since(UNIX_EPOCH).expect("system clock should be after unix epoch").as_nanos();
env::temp_dir().join(format!("leo_file_source_{}_{}", process::id(), nanos))
}
#[test]
fn in_memory_read_file() {
let mut source = InMemoryFileSource::new();
source.set(PathBuf::from("/src/main.leo"), "program test.aleo { }".into());
let content = source.read_file(Path::new("/src/main.leo")).unwrap();
assert_eq!(content, "program test.aleo { }");
}
#[test]
fn in_memory_read_file_not_found() {
let source = InMemoryFileSource::new();
let err = source.read_file(Path::new("/nonexistent.leo")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn in_memory_list_leo_files() {
let mut source = InMemoryFileSource::new();
source.set(PathBuf::from("/src/main.leo"), String::new());
source.set(PathBuf::from("/src/utils.leo"), String::new());
source.set(PathBuf::from("/src/alpha.leo"), String::new());
source.set(PathBuf::from("/src/data.json"), String::new());
source.set(PathBuf::from("/other/lib.leo"), String::new());
let files = source.list_leo_files(Path::new("/src"), Path::new("/src/main.leo")).unwrap();
assert_eq!(files, vec![PathBuf::from("/src/alpha.leo"), PathBuf::from("/src/utils.leo")]);
}
#[test]
fn disk_read_file() {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
let content = DiskFileSource.read_file(&path).unwrap();
assert!(content.contains("leo-span"));
}
#[test]
fn disk_read_file_not_found() {
let err = DiskFileSource.read_file(Path::new("/nonexistent_path_12345.leo")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn disk_list_leo_files() {
let tmp = unique_temp_dir();
let nested = tmp.join("nested");
let excluded = tmp.join("excluded.leo");
let result = (|| -> io::Result<Vec<PathBuf>> {
fs::create_dir_all(&nested)?;
fs::write(tmp.join("b.leo"), "")?;
fs::write(tmp.join("a.leo"), "")?;
fs::write(&excluded, "")?;
fs::write(tmp.join("not_leo.txt"), "")?;
fs::write(nested.join("nested.leo"), "")?;
DiskFileSource.list_leo_files(&tmp, &excluded)
})();
let _ = fs::remove_dir_all(&tmp);
let files = result.unwrap();
assert_eq!(files, vec![tmp.join("a.leo"), tmp.join("b.leo"), nested.join("nested.leo")]);
}
#[test]
fn disk_list_leo_files_propagates_errors() {
let err = DiskFileSource.list_leo_files(Path::new("/nonexistent_dir_12345"), Path::new("")).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::NotFound);
}
#[test]
fn in_memory_deterministic_ordering() {
let mut source = InMemoryFileSource::new();
source.set(PathBuf::from("/src/z.leo"), String::new());
source.set(PathBuf::from("/src/m.leo"), String::new());
source.set(PathBuf::from("/src/a.leo"), String::new());
let files = source.list_leo_files(Path::new("/src"), Path::new("/src/none.leo")).unwrap();
assert_eq!(files, vec![PathBuf::from("/src/a.leo"), PathBuf::from("/src/m.leo"), PathBuf::from("/src/z.leo")]);
}
}