Implementing VFS Backends
Guide to creating custom filesystem implementations.
FileSystem Trait
All filesystems implement this trait:
pub trait FileSystem {
// File operations
fn open(&mut self, path: &str, opts: OpenOptions) -> io::Result<FileHandle>;
fn close(&mut self, handle: FileHandle) -> io::Result<()>;
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> io::Result<usize>;
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> io::Result<usize>;
fn seek(&mut self, handle: FileHandle, pos: SeekFrom) -> io::Result<u64>;
fn truncate(&mut self, handle: FileHandle, len: u64) -> io::Result<()>;
// Metadata
fn metadata(&self, path: &str) -> io::Result<Metadata>;
fn fstat(&self, handle: FileHandle) -> io::Result<Metadata>;
fn exists(&self, path: &str) -> bool;
// Directory operations
fn create_dir(&mut self, path: &str) -> io::Result<()>;
fn read_dir(&self, path: &str) -> io::Result<Vec<DirEntry>>;
fn remove_file(&mut self, path: &str) -> io::Result<()>;
fn remove_dir(&mut self, path: &str) -> io::Result<()>;
// Links
fn symlink(&mut self, target: &str, link: &str) -> io::Result<()>;
fn read_link(&self, path: &str) -> io::Result<String>;
fn link(&mut self, src: &str, dst: &str) -> io::Result<()>;
// Permissions
fn chmod(&mut self, path: &str, mode: u32) -> io::Result<()>;
fn chown(&mut self, path: &str, uid: u32, gid: u32) -> io::Result<()>;
// Timestamps
fn utimes(&mut self, path: &str, atime: f64, mtime: f64) -> io::Result<()>;
fn set_clock(&mut self, now: f64);
// Rename
fn rename(&mut self, from: &str, to: &str) -> io::Result<()>;
}
Minimal Implementation
use axeberg::vfs::{FileSystem, FileHandle, OpenOptions, Metadata, DirEntry};
use std::io::{self, SeekFrom};
use std::collections::HashMap;
pub struct SimpleFs {
files: HashMap<String, Vec<u8>>,
handles: slab::Slab<OpenHandle>,
}
struct OpenHandle {
path: String,
position: usize,
writable: bool,
}
impl SimpleFs {
pub fn new() -> Self {
Self {
files: HashMap::new(),
handles: slab::Slab::new(),
}
}
}
impl FileSystem for SimpleFs {
fn open(&mut self, path: &str, opts: OpenOptions) -> io::Result<FileHandle> {
let path = normalize_path(path);
// Create if needed
if opts.create && !self.files.contains_key(&path) {
self.files.insert(path.clone(), Vec::new());
}
// Check exists
if !self.files.contains_key(&path) {
return Err(io::Error::new(io::ErrorKind::NotFound, "file not found"));
}
// Truncate if requested
if opts.truncate {
self.files.insert(path.clone(), Vec::new());
}
let handle = self.handles.insert(OpenHandle {
path,
position: 0,
writable: opts.write,
});
Ok(FileHandle(handle))
}
fn close(&mut self, handle: FileHandle) -> io::Result<()> {
self.handles.try_remove(handle.0)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "bad handle"))?;
Ok(())
}
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> io::Result<usize> {
let h = self.handles.get_mut(handle.0)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "bad handle"))?;
let data = self.files.get(&h.path)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file gone"))?;
let available = data.len().saturating_sub(h.position);
let to_read = buf.len().min(available);
buf[..to_read].copy_from_slice(&data[h.position..h.position + to_read]);
h.position += to_read;
Ok(to_read)
}
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> io::Result<usize> {
let h = self.handles.get_mut(handle.0)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "bad handle"))?;
if !h.writable {
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "not writable"));
}
let path = h.path.clone();
let pos = h.position;
let data = self.files.get_mut(&path)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "file gone"))?;
// Extend if needed
if pos + buf.len() > data.len() {
data.resize(pos + buf.len(), 0);
}
data[pos..pos + buf.len()].copy_from_slice(buf);
self.handles.get_mut(handle.0).unwrap().position += buf.len();
Ok(buf.len())
}
// ... implement remaining methods
}
Metadata Structure
pub struct Metadata {
pub file_type: FileType,
pub size: u64,
pub mode: u32, // Unix permissions (0o755, etc.)
pub uid: u32,
pub gid: u32,
pub nlink: u32, // Link count
pub atime: f64, // Access time (ms since epoch)
pub mtime: f64, // Modification time
pub ctime: f64, // Change time
}
pub enum FileType {
File,
Directory,
Symlink,
}
OpenOptions
pub struct OpenOptions {
pub read: bool,
pub write: bool,
pub create: bool,
pub truncate: bool,
pub append: bool,
}
impl OpenOptions {
pub const READ: Self = Self { read: true, write: false, create: false, truncate: false, append: false };
pub const WRITE: Self = Self { read: false, write: true, create: true, truncate: true, append: false };
pub const RDWR: Self = Self { read: true, write: true, create: false, truncate: false, append: false };
pub const APPEND: Self = Self { read: false, write: true, create: true, truncate: false, append: true };
}
Example: Read-Only Archive FS
pub struct ArchiveFs {
entries: HashMap<String, ArchiveEntry>,
handles: slab::Slab<ArchiveHandle>,
}
struct ArchiveEntry {
data: Vec<u8>,
metadata: Metadata,
}
impl ArchiveFs {
pub fn from_tar(data: &[u8]) -> io::Result<Self> {
let mut entries = HashMap::new();
// Parse tar archive
for entry in tar::Archive::new(data).entries()? {
let entry = entry?;
let path = entry.path()?.to_string_lossy().into_owned();
let mut data = Vec::new();
entry.read_to_end(&mut data)?;
entries.insert(path, ArchiveEntry {
data,
metadata: /* from tar header */,
});
}
Ok(Self { entries, handles: slab::Slab::new() })
}
}
impl FileSystem for ArchiveFs {
fn write(&mut self, _: FileHandle, _: &[u8]) -> io::Result<usize> {
Err(io::Error::new(io::ErrorKind::PermissionDenied, "read-only"))
}
// ... other methods
}
Integration
Register filesystem with kernel:
// In kernel initialization
let my_fs = MyCustomFs::new();
kernel.mount("/custom", Box::new(my_fs));
Or use with LayeredFs:
let base = ArchiveFs::from_tar(system_image)?;
let overlay = MemoryFs::new();
let fs = LayeredFs::new(base, overlay);
Testing
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_write() {
let mut fs = SimpleFs::new();
// Write
let h = fs.open("/test.txt", OpenOptions::WRITE).unwrap();
fs.write(h, b"hello").unwrap();
fs.close(h).unwrap();
// Read back
let h = fs.open("/test.txt", OpenOptions::READ).unwrap();
let mut buf = [0u8; 100];
let n = fs.read(h, &mut buf).unwrap();
assert_eq!(&buf[..n], b"hello");
}
}
Best Practices
- Normalize paths: Always normalize to absolute, canonical form
- Handle handles: Use slab or similar for O(1) handle lookup
- Atomic operations: Metadata changes should be atomic
- Error mapping: Map backend errors to appropriate io::ErrorKind
- Timestamps: Update atime/mtime/ctime appropriately
Related Documentation
- VFS - VFS overview
- Layered FS - Union filesystem
- Memory - Memory-backed storage