In short: the FUSE adapter speaks in paths, and asks a VirtualFilesystem for attributes,
bytes, directory entries or a link target. The only implementation, PathMappedVfs, turns
every one of those calls into two steps: ask the PathMapper for the
VirtualFile at that path, then call one method on it. A VirtualFile is
created fresh for each request and dropped afterwards. Nothing in this layer caches.
1. VirtualFilesystem
VirtualFilesystem is described as the "middle-layer
filesystem abstraction representing inode-based VFS operations", between the FUSE layer and the
path mapper. It is an async_trait, and every method takes a relative &Path
(the root is the empty path) and returns Result<_, JjError>.
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
get_attributes | (&self, path: &Path) → Result<FileAttributes, JjError> | Size, type and timestamps for lookup and getattr. | vfs.rs |
read | (&self, path: &Path, offset: u64, size: u32) → Result<Box<[u8]>, JjError> | Up to size bytes starting at offset. Fewer bytes means end of file. | vfs.rs |
read_directory | (&self, path: &Path) → Result<DirectoryStream, JjError> | The directory's entries as a stream. | vfs.rs |
read_link | (&self, path: &Path) → Result<PathBuf, JjError> | A symlink's target. | vfs.rs |
There is no write, create, rename or remove method. The trait itself makes the mount read-only, not just the mount option.
2. PathMapper
PathMapper "represents the VFS mountpoint structure by
mapping a given absolute path to a VirtualFile". It has a single method,
async fn get_entry(&self, path: &Path) → Result<Box<dyn VirtualFile>, JjError>,
and requires Send + Sync. Despite the doc's "absolute", every caller passes a path relative
to the mount root. The only implementation is AllCommitsPathMapper, described on
the mount's namespace.
The point of the trait is that another layout (for example one showing a single workspace) would only
need a new mapper.
3. VirtualFile
VirtualFile is "a file in our virtual file system. This
can either be a normal file you can read from or for example a directory". It "is not meant to be
created by the user, but instead returned by a PathMapper". Three of its five methods have
default implementations that fail, so each implementor only overrides what makes sense for it.
| Item | Type/Signature | Default | Source |
|---|---|---|---|
read | (&self) → Result<Pin<Box<dyn AsyncRead + Send>>, JjError> | Err(NotAFile) | virtual_file.rs |
list | (&self) → Result<DirectoryStream, JjError> | Err(NotADirectory) | virtual_file.rs |
read_link | (&self) → Result<PathBuf, JjError> | Err(NotASymlink) | virtual_file.rs |
attributes | (&self) → Result<FileAttributes, JjError> | required | virtual_file.rs |
file_type | (&self) → Result<FileType, JjError> | required | virtual_file.rs |
Note that read takes no offset: it hands back a stream positioned at the start of the file.
Seeking is the caller's job (see serving reads).
| Implementor | Overrides | Explained on |
|---|---|---|
StaticDirectory | list, attributes, file_type | namespace |
CommitsDirectory | list, attributes, file_type | namespace |
WorkspacesDirectory | list, attributes, file_type | namespace |
CommitTreeFile | all five | commits as directory trees |
4. The data types
| Item | Type/Signature | Meaning | Source |
|---|---|---|---|
FileType | enum { File, Directory, Symlink } | The three kinds the file system can show. JjFuse maps them to fuser's RegularFile, Directory and Symlink. | virtual_file.rs |
FileAttributes | { size: u64, file_type, created: SystemTime, modified: SystemTime } | Everything a VirtualFile reports about itself. Permissions, ownership and link counts are filled in later by FileAttributes::to_fuse. | virtual_file.rs |
DirectoryEntry | { name: String, file_type: FileType } | One listing entry. No size or inode: the FUSE layer allocates inodes itself as it emits entries. | virtual_file.rs |
DirectoryStream | BoxStream<'static, DirectoryEntry> | A boxed, owned stream of entries, so a listing could in principle be produced lazily. Every implementor at this pin builds a Vec first and streams it. | virtual_file.rs |
5. PathMappedVfs
PathMappedVfs<P: PathMapper> holds the mapper in
an Arc and implements each VirtualFilesystem method in two lines: resolve, then
delegate. get_attributes calls attributes, read_directory calls
list, read_link calls read_link, and read calls
read and then cuts the requested window out of the stream. Errors from either step pass
through unchanged, and I/O errors become JjError::IO, which FUSE reports as
EIO.
Serving reads
FUSE asks for a byte window (offset, size), but a VirtualFile only
offers a stream from the beginning. PathMappedVfs::read
bridges the two with futures::io::AsyncReadExt:
- Open the stream with
virtual_file.read(). - Wrap it in
take(offset)and copy that intofutures::io::sink(), which reads and discards the firstoffsetbytes ("TODO: handle proper seek()"). - Unwrap the original reader with
into_inner(), wrap it intake(size), andread_to_endinto a buffer pre-sized tosize. - Return the buffer as
Box<[u8]>.
If the file ends before offset, step 2 simply stops early and step 3 returns nothing, which
the kernel reads as end of file. The cost is that every read starts from byte 0: reading a file in
chunks of c bytes costs about n²/2c bytes of decoding for a file of n bytes, on
top of the full file read that getattr already does to find the size.
6. Tests
vfs.rs tests PathMappedVfs against an in-memory fake, without jj
(source). MockVirtualFile is a file, a directory of
named children, or a symlink, and MockPathMapper walks a tree of them. The tree is
file.txt, symlink.txt → file.txt and dir/nested.txt.
| Test | Asserts |
|---|---|
test_read_root_file | Reading 11 bytes from offset 0 returns hello world. |
test_read_nested_file | The same through a subdirectory. |
test_list_directory | Listing dir yields one File entry, nested.txt. |
test_list_directory_not_a_directory | Listing a file fails with NotADirectory. |
test_list_directory_not_found | Listing a missing path fails with NotFound. |
test_read_link | The symlink's target is file.txt. |
test_read_link_not_a_symlink | Reading a file as a link fails with NotASymlink. |
Every read in the suite starts at offset 0, including the mounted integration test's 5-byte partial
read (source), so no test exercises the
offset-skipping step of read with a non-zero offset.