use alloc::{boxed::Box, vec::Vec};
use core::{any::Any, future::Future};
pub enum DirEntry {
#[expect(missing_docs)]
File(Vec<u8>),
#[expect(missing_docs)]
Directory(Vec<u8>),
}
pub trait FileSystem: 'static {
type File: File;
type Directory: Directory<Self::File>;
}
#[derive(Debug)]
pub enum FileSystemError {
NotFound(Box<dyn Any>),
Other(Box<dyn Any>),
}
pub trait Directory<File>: Sized + Clone {
fn open_subdir(&self, name: &[u8]) -> impl Future<Output = Result<Self, FileSystemError>>;
fn list_dir(&self) -> impl Future<Output = Result<Vec<DirEntry>, FileSystemError>>;
fn open_file(&self, name: &[u8]) -> impl Future<Output = Result<File, FileSystemError>>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Offset(pub u64);
impl core::ops::Add<u64> for Offset {
type Output = Self;
fn add(self, rhs: u64) -> Self::Output {
Self(self.0 + rhs)
}
}
impl core::ops::Sub<u64> for Offset {
type Output = Self;
fn sub(self, rhs: u64) -> Self::Output {
Self(self.0 - rhs)
}
}
impl core::ops::Div<u64> for Offset {
type Output = Self;
fn div(self, rhs: u64) -> Self::Output {
Self(self.0 / rhs)
}
}
impl core::ops::Mul<u64> for Offset {
type Output = Self;
fn mul(self, rhs: u64) -> Self::Output {
Self(self.0 * rhs)
}
}
pub trait File: Sized {
fn read_all(&mut self) -> impl Future<Output = Result<Vec<u8>, FileSystemError>>;
fn read_segment(
&mut self,
offset: Offset,
dest: &mut [u8],
) -> impl Future<Output = Result<usize, FileSystemError>>;
}
pub(crate) type PathComponent = Vec<u8>;
pub(crate) type Path = Vec<PathComponent>;
enum SearchPath {
File(Path),
Directory(Path),
}
pub(crate) async fn search_for_files<F: File, D: Directory<F>>(
root: &D,
) -> Result<Vec<Path>, FileSystemError> {
use SearchPath::*;
let mut out: Vec<Path> = Vec::new();
let mut stack: Vec<SearchPath> = Vec::new();
stack.push(Directory(Vec::new()));
while let Some(this) = stack.pop() {
match this {
File(path) => out.push(path),
Directory(dir) => {
let mut dir_handle = root.clone();
for component in &dir {
dir_handle = dir_handle.open_subdir(component).await?;
}
let entries = dir_handle.list_dir().await?;
let new_stack_entries = entries.into_iter().map(|entry| {
let mut new_path = dir.clone();
match entry {
DirEntry::File(name) => {
new_path.push(name);
File(new_path)
}
DirEntry::Directory(name) => {
new_path.push(name);
Directory(new_path)
}
}
});
stack.extend(new_stack_entries);
}
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use crate::test::{directory::TestRepoDirectory, repo::TestDirectory};
use super::*;
use futures::executor::block_on;
use std::{
fs::{OpenOptions, create_dir},
io::{self, Write},
path::PathBuf,
sync::Arc,
};
use tempfile::TempDir;
#[test]
fn test_search_for_files() {
fn touch(path: impl AsRef<std::path::Path>) -> io::Result<()> {
let mut f = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(path)?;
f.flush()?;
Ok(())
}
let dir = TempDir::new().unwrap();
touch(dir.path().join("file-a")).unwrap();
touch(dir.path().join("file-b")).unwrap();
create_dir(dir.path().join("dir-a")).unwrap();
touch(dir.path().join("dir-a").join("file-c")).unwrap();
create_dir(dir.path().join("dir-a").join("dir-b")).unwrap();
touch(dir.path().join("dir-a").join("dir-b").join("file-d")).unwrap();
let mut expected: Vec<Path> = vec![
vec![b"file-a".to_vec()],
vec![b"file-b".to_vec()],
vec![b"dir-a".to_vec(), b"file-c".to_vec()],
vec![b"dir-a".to_vec(), b"dir-b".to_vec(), b"file-d".to_vec()],
];
expected.sort();
let dir = TestRepoDirectory {
root: TestDirectory::Temp(Arc::new(dir)),
sub_path: PathBuf::new(),
};
let mut paths = block_on(search_for_files(&dir)).unwrap();
paths.sort();
assert_eq!(paths, expected);
}
}