Add FAT12/16/32 scheme daemon, management tools, and build integration

5-crate Rust workspace implementing full VFAT support: fatd scheme daemon
(FSScheme with open/read/write/mkdir/unlink/rename/fstat), fat-mkfs (create
FAT12/16/32 with labels and cluster size), fat-label (read/write BPB + root-dir
volume labels), fat-check (verify + repair dirty flags, FSInfo, lost clusters,
orphaned LFN). 60 unit tests, 0 unwrap in production code. Included in all 5
redbear configs via redbear-device-services.toml.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-18 00:13:34 +01:00
parent f6ee40326b
commit 9d1954e0c4
19 changed files with 3703 additions and 1 deletions
@@ -0,0 +1,22 @@
[package]
name = "fat-blockdev"
description = "Block device adapter for fatfs crate on Redox OS"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
fatfs.workspace = true
log.workspace = true
[dependencies.redox_syscall]
workspace = true
optional = true
[dependencies.libredox]
workspace = true
optional = true
[features]
default = ["redox"]
redox = ["dep:redox_syscall", "dep:libredox"]
@@ -0,0 +1,72 @@
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::Path;
/// Block device adapter backed by a host file (Linux/macOS).
///
/// Implements `Read + Write + Seek` for use with the `fatfs` crate.
/// Wraps `std::fs::File` and reports total size from filesystem metadata.
pub struct FileDisk {
file: File,
size: u64,
}
impl FileDisk {
/// Open an existing file for read/write.
pub fn new(file: File) -> Self {
let size = file.metadata()
.map(|m| m.len())
.unwrap_or_else(|e| {
log::warn!("file_disk: metadata read failed, assuming zero size: {e}");
0
});
Self { file, size }
}
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = OpenOptions::new()
.read(true)
.write(true)
.open(path)?;
let size = file.metadata()?.len();
Ok(Self { file, size })
}
/// Create a new file of the given size, zero-filled.
pub fn create<P: AsRef<Path>>(path: P, size: u64) -> io::Result<Self> {
let file = File::create(&path)?;
file.set_len(size)?;
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)?;
Ok(Self { file, size })
}
/// Total size of the backing file in bytes.
pub fn size(&self) -> u64 {
self.size
}
}
impl Read for FileDisk {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file.read(buf)
}
}
impl Write for FileDisk {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
impl Seek for FileDisk {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.file.seek(pos)
}
}
@@ -0,0 +1,9 @@
mod file_disk;
pub use file_disk::FileDisk;
#[cfg(feature = "redox")]
mod redox_disk;
#[cfg(feature = "redox")]
pub use redox_disk::RedoxDisk;
@@ -0,0 +1,55 @@
use std::io::{self, Read, Seek, SeekFrom, Write};
pub struct RedoxDisk {
fd: usize,
size: u64,
}
impl RedoxDisk {
pub fn open(disk_path: &str) -> syscall::error::Result<Self> {
let fd = libredox::call::open(disk_path, libredox::flag::O_RDWR, 0)?;
let mut stat = syscall::data::Stat::default();
syscall::call::fstat(fd, &mut stat)?;
Ok(Self {
fd,
size: stat.st_size,
})
}
pub fn size(&self) -> u64 {
self.size
}
}
impl Read for RedoxDisk {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
syscall::call::read(self.fd, buf)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redox read: {e:?}")))
}
}
impl Write for RedoxDisk {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
syscall::call::write(self.fd, buf)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redox write: {e:?}")))
}
fn flush(&mut self) -> io::Result<()> {
syscall::call::fsync(self.fd)
.map(|_| ())
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redox flush: {e:?}")))
}
}
impl Seek for RedoxDisk {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let offset = match pos {
SeekFrom::Start(off) => off as isize,
SeekFrom::Current(off) => off as isize,
SeekFrom::End(off) => (self.size as isize) + (off as isize),
};
let result = syscall::call::lseek(self.fd, offset, syscall::flag::SEEK_SET)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("redox seek: {e:?}")))?;
Ok(result as u64)
}
}