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:
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "fatd"
|
||||
description = "FAT filesystem scheme daemon for Redox OS"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "fatd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
fat-blockdev = { path = "../fat-blockdev" }
|
||||
fatfs.workspace = true
|
||||
fscommon.workspace = true
|
||||
redox_syscall.workspace = true
|
||||
redox-scheme.workspace = true
|
||||
libredox = { workspace = true, optional = true }
|
||||
redox-path = { workspace = true, optional = true }
|
||||
log.workspace = true
|
||||
env_logger = { workspace = true, optional = true }
|
||||
libc.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["redox"]
|
||||
redox = ["dep:libredox", "dep:redox-path", "fat-blockdev/redox", "dep:env_logger"]
|
||||
@@ -0,0 +1,103 @@
|
||||
use syscall::flag::{O_ACCMODE, O_RDONLY, O_RDWR, O_WRONLY};
|
||||
|
||||
pub enum Handle {
|
||||
File(FileHandle),
|
||||
Directory(DirectoryHandle),
|
||||
SchemeRoot,
|
||||
}
|
||||
|
||||
pub struct FileHandle {
|
||||
path: String,
|
||||
offset: u64,
|
||||
flags: usize,
|
||||
}
|
||||
|
||||
pub struct DirectoryHandle {
|
||||
path: String,
|
||||
entries: Vec<(u64, String, u8)>,
|
||||
cursor: usize,
|
||||
flags: usize,
|
||||
}
|
||||
|
||||
impl FileHandle {
|
||||
pub fn new(path: String, flags: usize) -> Self {
|
||||
Self {
|
||||
path,
|
||||
offset: 0,
|
||||
flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn set_offset(&mut self, offset: u64) {
|
||||
self.offset = offset;
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> usize {
|
||||
self.flags
|
||||
}
|
||||
|
||||
pub fn can_read(&self) -> bool {
|
||||
matches!(self.flags & O_ACCMODE, O_RDONLY | O_RDWR)
|
||||
}
|
||||
|
||||
pub fn can_write(&self) -> bool {
|
||||
matches!(self.flags & O_ACCMODE, O_WRONLY | O_RDWR)
|
||||
}
|
||||
|
||||
pub fn update_path(&mut self, new_path: String) {
|
||||
self.path = new_path;
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryHandle {
|
||||
pub fn new(path: String, entries: Vec<(u64, String, u8)>, flags: usize) -> Self {
|
||||
Self {
|
||||
path,
|
||||
entries,
|
||||
cursor: 0,
|
||||
flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[(u64, String, u8)] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, cursor: usize) {
|
||||
self.cursor = cursor;
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> usize {
|
||||
self.flags
|
||||
}
|
||||
|
||||
pub fn update_path(&mut self, new_path: String) {
|
||||
self.path = new_path;
|
||||
}
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
pub fn path(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::File(handle) => Some(handle.path()),
|
||||
Self::Directory(handle) => Some(handle.path()),
|
||||
Self::SchemeRoot => Some(""),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::File(handle) => Some(handle.flags()),
|
||||
Self::Directory(handle) => Some(handle.flags()),
|
||||
Self::SchemeRoot => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, Read, Write},
|
||||
os::unix::io::{FromRawFd, RawFd},
|
||||
process,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
#[cfg(feature = "redox")]
|
||||
use fat_blockdev::RedoxDisk as SchemeDisk;
|
||||
#[cfg(not(feature = "redox"))]
|
||||
use fat_blockdev::FileDisk as SchemeDisk;
|
||||
|
||||
mod handle;
|
||||
mod mount;
|
||||
mod scheme;
|
||||
|
||||
pub static IS_UMT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
extern "C" fn unmount_handler(_signal: usize) {
|
||||
IS_UMT.store(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn install_sigterm_handler() -> io::Result<()> {
|
||||
unsafe {
|
||||
let mut action: libc::sigaction = std::mem::zeroed();
|
||||
if libc::sigemptyset(&mut action.sa_mask) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
action.sa_flags = 0;
|
||||
action.sa_sigaction = unmount_handler as usize;
|
||||
|
||||
if libc::sigaction(libc::SIGTERM, &action, std::ptr::null_mut()) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fork_process() -> io::Result<libc::pid_t> {
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(pid)
|
||||
}
|
||||
}
|
||||
|
||||
fn make_pipe() -> io::Result<[i32; 2]> {
|
||||
let mut pipes = [0; 2];
|
||||
if unsafe { libc::pipe(pipes.as_mut_ptr()) } != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(pipes)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn capability_mode() {
|
||||
if let Err(err) = libredox::call::setrens(0, 0) {
|
||||
log::error!("fatd: failed to enter null namespace: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn capability_mode() {}
|
||||
|
||||
fn usage() {
|
||||
eprintln!("fatd [--no-daemon|-d] <disk_path> <mountpoint>");
|
||||
}
|
||||
|
||||
fn fail_usage(message: &str) -> ! {
|
||||
eprintln!("fatd: {message}");
|
||||
usage();
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
fn run_mount(disk_path: &str, mountpoint: &str) -> Result<(), String> {
|
||||
let disk = SchemeDisk::open(disk_path).map_err(|err| format!("failed to open {disk_path}: {err}"))?;
|
||||
let disk = fscommon::BufStream::new(disk);
|
||||
let filesystem = fatfs::FileSystem::new(disk, fatfs::FsOptions::new())
|
||||
.map_err(|err| format!("failed to mount FAT on {disk_path}: {err}"))?;
|
||||
|
||||
mount::mount(filesystem, mountpoint, |mounted_path| {
|
||||
capability_mode();
|
||||
log::info!("mounted FAT filesystem on {disk_path} to {mounted_path}");
|
||||
})
|
||||
.map_err(|err| format!("failed to serve scheme {mountpoint}: {err}"))
|
||||
}
|
||||
|
||||
fn daemon(disk_path: &str, mountpoint: &str, mut status_pipe: Option<File>) -> i32 {
|
||||
IS_UMT.store(0, Ordering::SeqCst);
|
||||
|
||||
if let Err(err) = install_sigterm_handler() {
|
||||
log::error!("failed to install SIGTERM handler: {err}");
|
||||
if let Some(pipe) = status_pipe.as_mut() {
|
||||
let _ = pipe.write_all(&[1]);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
match run_mount(disk_path, mountpoint) {
|
||||
Ok(()) => {
|
||||
if let Some(pipe) = status_pipe.as_mut() {
|
||||
let _ = pipe.write_all(&[0]);
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{err}");
|
||||
if let Some(pipe) = status_pipe.as_mut() {
|
||||
let _ = pipe.write_all(&[1]);
|
||||
}
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(feature = "redox")]
|
||||
env_logger::init();
|
||||
|
||||
let mut daemonize = true;
|
||||
let mut disk_path: Option<String> = None;
|
||||
let mut mountpoint: Option<String> = None;
|
||||
|
||||
for arg in env::args().skip(1) {
|
||||
match arg.as_str() {
|
||||
"--no-daemon" | "-d" => daemonize = false,
|
||||
_ if disk_path.is_none() => disk_path = Some(arg),
|
||||
_ if mountpoint.is_none() => mountpoint = Some(arg),
|
||||
_ => fail_usage("too many arguments provided"),
|
||||
}
|
||||
}
|
||||
|
||||
let Some(disk_path) = disk_path else {
|
||||
fail_usage("no disk path provided");
|
||||
};
|
||||
let Some(mountpoint) = mountpoint else {
|
||||
fail_usage("no mountpoint provided");
|
||||
};
|
||||
|
||||
if daemonize {
|
||||
let pipes = match make_pipe() {
|
||||
Ok(pipes) => pipes,
|
||||
Err(err) => {
|
||||
eprintln!("fatd: failed to create pipe: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut read = unsafe { File::from_raw_fd(pipes[0] as RawFd) };
|
||||
let write = unsafe { File::from_raw_fd(pipes[1] as RawFd) };
|
||||
|
||||
match fork_process() {
|
||||
Ok(0) => {
|
||||
drop(read);
|
||||
process::exit(daemon(&disk_path, &mountpoint, Some(write)));
|
||||
}
|
||||
Ok(_pid) => {
|
||||
drop(write);
|
||||
let mut response = [1u8; 1];
|
||||
if let Err(err) = read.read_exact(&mut response) {
|
||||
eprintln!("fatd: failed to read child status: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
process::exit(i32::from(response[0]));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("fatd: failed to fork: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("running fatd in foreground");
|
||||
process::exit(daemon(&disk_path, &mountpoint, None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use fscommon::BufStream;
|
||||
use redox_scheme::{
|
||||
RequestKind, Response, SignalBehavior, Socket,
|
||||
scheme::{SchemeState, SchemeSync, register_sync_scheme},
|
||||
};
|
||||
|
||||
use crate::{IS_UMT, scheme::FatScheme};
|
||||
|
||||
pub fn mount<D, T, F>(
|
||||
filesystem: fatfs::FileSystem<BufStream<D>>,
|
||||
mountpoint: &str,
|
||||
callback: F,
|
||||
) -> syscall::error::Result<T>
|
||||
where
|
||||
D: Read + Write + Seek,
|
||||
F: FnOnce(&str) -> T,
|
||||
{
|
||||
let socket = Socket::create()?;
|
||||
|
||||
let scheme_name = mountpoint.to_string();
|
||||
let mounted_path = format!("/scheme/{mountpoint}");
|
||||
|
||||
let mut state = SchemeState::new();
|
||||
let mut scheme = FatScheme::new(scheme_name, mounted_path.clone(), filesystem);
|
||||
|
||||
register_sync_scheme(&socket, mountpoint, &mut scheme)?;
|
||||
|
||||
let result = callback(&mounted_path);
|
||||
|
||||
while IS_UMT.load(Ordering::SeqCst) == 0 {
|
||||
let request = match socket.next_request(SignalBehavior::Restart)? {
|
||||
None => break,
|
||||
Some(request) => match request.kind() {
|
||||
RequestKind::Call(request) => request,
|
||||
RequestKind::SendFd(sendfd_request) => {
|
||||
let response = Response::new(scheme.on_sendfd(&sendfd_request), sendfd_request);
|
||||
if !socket.write_response(response, SignalBehavior::Restart)? {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RequestKind::OnClose { id } => {
|
||||
scheme.on_close(id);
|
||||
state.on_close(id);
|
||||
continue;
|
||||
}
|
||||
RequestKind::OnDetach { id, pid } => {
|
||||
let Ok(inode) = scheme.inode(id) else {
|
||||
log::warn!("OnDetach received unknown handle id={id}");
|
||||
continue;
|
||||
};
|
||||
state.on_detach(id, inode, pid);
|
||||
continue;
|
||||
}
|
||||
_ => continue,
|
||||
},
|
||||
};
|
||||
|
||||
let response = request.handle_sync(&mut scheme, &mut state);
|
||||
if !socket.write_response(response, SignalBehavior::Restart)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
scheme.cleanup()?;
|
||||
Ok(result)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user