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}