Skip to main content

git_async/
file_system.rs

1//! Traits and error types for interacting with files and directories
2//!
3//! The purpose of this module is to specify what consumers of this library must
4//! implement in order for the filesystem calls in `git-async` to work.
5//!
6//! The [`FileSystem`] trait is a wrapper trait which has associated types for
7//! objects which implement [`File`] and [`Directory`]. The correct way to
8//! implement this is to use a zero-sized struct for the parent, and real
9//! objects for [`File`] and [`Directory`]. This structure is to avoid generic
10//! proliferation as much as possible.
11//!
12//! For example:
13//!
14//! ```
15//! # use git_async::file_system::{File, Directory, FileSystem, FileSystemError, Offset, DirEntry};
16//! struct MyFile {
17//!     /* path, handle, etc. */
18//! }
19//!
20//! impl File for MyFile {
21//!     /* methods */
22//!     # async fn read_all(&mut self) -> Result<Vec<u8>, FileSystemError> { unimplemented! () }
23//!     # async fn read_segment(&mut self, _: Offset, _: &mut [u8]) -> Result<usize, FileSystemError> { unimplemented! () }
24//! }
25//!
26//! struct MyDirectory {
27//!     /* path, handle, etc. */
28//! }
29//! # impl Clone for MyDirectory { fn clone(&self) -> Self { unimplemented!() } }
30//!
31//! impl Directory<MyFile> for MyDirectory {
32//!     /* methods */
33//!     # async fn open_subdir(&self, _: &[u8]) -> Result<Self, FileSystemError> { unimplemented!() }
34//!     # async fn list_dir(&self) -> Result<Vec<DirEntry>, FileSystemError> { unimplemented!() }
35//!     # async fn open_file(&self, _: &[u8]) -> Result<MyFile, FileSystemError> { unimplemented!() }
36//! }
37//!
38//! struct MyFS;
39//!
40//! impl FileSystem for MyFS {
41//!     type Directory = MyDirectory;
42//!     type File = MyFile;
43//! }
44//! ```
45//!
46//! The simplest way of implementing these would be to use [`std::fs`], but this
47//! nullifies the async capabilities of this crate. If you are using Tokio for
48//! example, you should use primitives from
49//! [`tokio::fs`](https://docs.rs/tokio/latest/tokio/fs/). If you are using
50//! `git-async` in a browser, you may want to use the [web filesystem
51//! API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API).
52
53use alloc::{boxed::Box, vec::Vec};
54use core::{any::Any, future::Future};
55
56/// Represents a directory entry
57///
58/// Names are represented as [`Vec<u8>`] to work on platforms with non-Unicode
59/// filename encodings.
60///
61/// Note that the names in this struct are **names** only, not full paths.
62pub enum DirEntry {
63    #[expect(missing_docs)]
64    File(Vec<u8>),
65    #[expect(missing_docs)]
66    Directory(Vec<u8>),
67}
68
69/// A wrapper trait encapsulating filesystem objects
70///
71/// See the [module documentation](`self`) for further details.
72pub trait FileSystem: 'static {
73    /// A type for files
74    type File: File;
75    /// A type for a directory, which contains files
76    type Directory: Directory<Self::File>;
77}
78
79/// An error encountered when doing file or directory operations.
80///
81/// Rather than make everything generic over the type of errors, we use
82/// [`Box<dyn Any>`] to hold platform-native errors.
83#[derive(Debug)]
84pub enum FileSystemError {
85    /// The requested file was not found
86    NotFound(Box<dyn Any>),
87    /// Any other kind of error
88    Other(Box<dyn Any>),
89}
90
91/// A trait for directories and their operations
92///
93/// A simple implementation might just use a [`std::path::PathBuf`]. On
94/// platforms which implement directory handles, this should encapsulate the
95/// handle.
96pub trait Directory<File>: Sized + Clone {
97    /// Open a subdirectory of this directory
98    fn open_subdir(&self, name: &[u8]) -> impl Future<Output = Result<Self, FileSystemError>>;
99
100    /// List the entries in this directory
101    fn list_dir(&self) -> impl Future<Output = Result<Vec<DirEntry>, FileSystemError>>;
102
103    /// Open a file (for reading)
104    fn open_file(&self, name: &[u8]) -> impl Future<Output = Result<File, FileSystemError>>;
105}
106
107/// An offset within a file; a newtype wrapper around a [`u64`]
108#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
109pub struct Offset(pub u64);
110
111impl core::ops::Add<u64> for Offset {
112    type Output = Self;
113    fn add(self, rhs: u64) -> Self::Output {
114        Self(self.0 + rhs)
115    }
116}
117impl core::ops::Sub<u64> for Offset {
118    type Output = Self;
119    fn sub(self, rhs: u64) -> Self::Output {
120        Self(self.0 - rhs)
121    }
122}
123impl core::ops::Div<u64> for Offset {
124    type Output = Self;
125
126    fn div(self, rhs: u64) -> Self::Output {
127        Self(self.0 / rhs)
128    }
129}
130impl core::ops::Mul<u64> for Offset {
131    type Output = Self;
132    fn mul(self, rhs: u64) -> Self::Output {
133        Self(self.0 * rhs)
134    }
135}
136
137/// A trait for reading files
138///
139/// A simple (non-async) implementation would hold a [`std::fs::File`] handle.
140pub trait File: Sized {
141    /// Read everything in the file
142    ///
143    /// This should be idempotent with any other reads. If the platform requires
144    /// it, implementors should seek to the start of the file before reading.
145    fn read_all(&mut self) -> impl Future<Output = Result<Vec<u8>, FileSystemError>>;
146
147    /// Read a segment of the file into the destination buffer. If less data is
148    /// available than the size of the buffer, then only the first `n` bytes of
149    /// the buffer are modified. Therefore implementors should take care not to
150    /// error on EOF conditions.
151    ///
152    /// Successful reads return the number of bytes read.
153    fn read_segment(
154        &mut self,
155        offset: Offset,
156        dest: &mut [u8],
157    ) -> impl Future<Output = Result<usize, FileSystemError>>;
158}
159
160pub(crate) type PathComponent = Vec<u8>;
161pub(crate) type Path = Vec<PathComponent>;
162enum SearchPath {
163    File(Path),
164    Directory(Path),
165}
166
167pub(crate) async fn search_for_files<F: File, D: Directory<F>>(
168    root: &D,
169) -> Result<Vec<Path>, FileSystemError> {
170    use SearchPath::*;
171    let mut out: Vec<Path> = Vec::new();
172    let mut stack: Vec<SearchPath> = Vec::new();
173    stack.push(Directory(Vec::new()));
174    while let Some(this) = stack.pop() {
175        match this {
176            File(path) => out.push(path),
177            Directory(dir) => {
178                let mut dir_handle = root.clone();
179                for component in &dir {
180                    dir_handle = dir_handle.open_subdir(component).await?;
181                }
182                let entries = dir_handle.list_dir().await?;
183                let new_stack_entries = entries.into_iter().map(|entry| {
184                    let mut new_path = dir.clone();
185                    match entry {
186                        DirEntry::File(name) => {
187                            new_path.push(name);
188                            File(new_path)
189                        }
190                        DirEntry::Directory(name) => {
191                            new_path.push(name);
192                            Directory(new_path)
193                        }
194                    }
195                });
196                stack.extend(new_stack_entries);
197            }
198        }
199    }
200    Ok(out)
201}
202
203#[cfg(test)]
204mod tests {
205    use crate::test::{directory::TestRepoDirectory, repo::TestDirectory};
206
207    use super::*;
208    use futures::executor::block_on;
209    use std::{
210        fs::{OpenOptions, create_dir},
211        io::{self, Write},
212        path::PathBuf,
213        sync::Arc,
214    };
215    use tempfile::TempDir;
216
217    #[test]
218    fn test_search_for_files() {
219        fn touch(path: impl AsRef<std::path::Path>) -> io::Result<()> {
220            let mut f = OpenOptions::new()
221                .create(true)
222                .truncate(true)
223                .write(true)
224                .open(path)?;
225            f.flush()?;
226            Ok(())
227        }
228        let dir = TempDir::new().unwrap();
229        touch(dir.path().join("file-a")).unwrap();
230        touch(dir.path().join("file-b")).unwrap();
231        create_dir(dir.path().join("dir-a")).unwrap();
232        touch(dir.path().join("dir-a").join("file-c")).unwrap();
233        create_dir(dir.path().join("dir-a").join("dir-b")).unwrap();
234        touch(dir.path().join("dir-a").join("dir-b").join("file-d")).unwrap();
235        let mut expected: Vec<Path> = vec![
236            vec![b"file-a".to_vec()],
237            vec![b"file-b".to_vec()],
238            vec![b"dir-a".to_vec(), b"file-c".to_vec()],
239            vec![b"dir-a".to_vec(), b"dir-b".to_vec(), b"file-d".to_vec()],
240        ];
241        expected.sort();
242        let dir = TestRepoDirectory {
243            root: TestDirectory::Temp(Arc::new(dir)),
244            sub_path: PathBuf::new(),
245        };
246        let mut paths = block_on(search_for_files(&dir)).unwrap();
247        paths.sort();
248        assert_eq!(paths, expected);
249    }
250}