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:
@@ -1,9 +1,10 @@
|
|||||||
# Red Bear OS shared device-service wiring
|
# Red Bear OS shared device-service wiring
|
||||||
#
|
#
|
||||||
# Shared by profiles that ship the firmware/input/Wi-Fi control compatibility stack.
|
# Shared by profiles that ship the firmware/input/Wi-Fi/FAT control compatibility stack.
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
redbear-quirks = {}
|
redbear-quirks = {}
|
||||||
|
fatd = {}
|
||||||
|
|
||||||
[[files]]
|
[[files]]
|
||||||
path = "/lib/firmware"
|
path = "/lib/firmware"
|
||||||
@@ -67,3 +68,18 @@ requires_weak = [
|
|||||||
cmd = "evdevd"
|
cmd = "evdevd"
|
||||||
type = "oneshot_async"
|
type = "oneshot_async"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[[files]]
|
||||||
|
path = "/usr/lib/init.d/15_fatd.service"
|
||||||
|
data = """
|
||||||
|
[unit]
|
||||||
|
description = "FAT filesystem scheme daemon"
|
||||||
|
requires_weak = [
|
||||||
|
"00_pcid-spawner.service",
|
||||||
|
]
|
||||||
|
|
||||||
|
[service]
|
||||||
|
cmd = "fatd"
|
||||||
|
args = ["disk/live-virtio", "fat-live"]
|
||||||
|
type = { scheme = "fat-live" }
|
||||||
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[source]
|
||||||
|
path = "source"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
template = "custom"
|
||||||
|
script = """
|
||||||
|
# Build and install fatd scheme daemon
|
||||||
|
COOKBOOK_CARGO_PATH=fatd cookbook_cargo
|
||||||
|
|
||||||
|
# Build and install fat-mkfs tool
|
||||||
|
COOKBOOK_CARGO_PATH=fat-mkfs cookbook_cargo
|
||||||
|
|
||||||
|
# Build and install fat-label tool
|
||||||
|
COOKBOOK_CARGO_PATH=fat-label cookbook_cargo
|
||||||
|
|
||||||
|
# Build and install fat-check tool
|
||||||
|
COOKBOOK_CARGO_PATH=fat-check cookbook_cargo
|
||||||
|
"""
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"fat-blockdev",
|
||||||
|
"fatd",
|
||||||
|
"fat-mkfs",
|
||||||
|
"fat-label",
|
||||||
|
"fat-check",
|
||||||
|
]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
fatfs = "0.3.6"
|
||||||
|
fscommon = "0.1.1"
|
||||||
|
redox_syscall = "0.7.3"
|
||||||
|
redox-scheme = "0.11.0"
|
||||||
|
libredox = "0.1.13"
|
||||||
|
redox-path = "0.3.0"
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
libc = "0.2"
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "fat-check"
|
||||||
|
description = "FAT filesystem checker (fsck.fat equivalent)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fat-check"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fat-blockdev = { path = "../fat-blockdev" }
|
||||||
|
fatfs.workspace = true
|
||||||
|
fscommon.workspace = true
|
||||||
|
log.workspace = true
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "fat-label"
|
||||||
|
description = "Read and write FAT volume labels (fatlabel equivalent)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fat-label"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fat-blockdev = { path = "../fat-blockdev" }
|
||||||
|
fatfs.workspace = true
|
||||||
|
fscommon.workspace = true
|
||||||
|
log.workspace = true
|
||||||
@@ -0,0 +1,436 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::io::{Read, Seek, SeekFrom, Write};
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use fat_blockdev::FileDisk;
|
||||||
|
use fatfs::FsOptions;
|
||||||
|
|
||||||
|
fn usage() -> ! {
|
||||||
|
eprintln!("Usage: fat-label [-s <label>|--set <label>] <device>");
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn printable_label(label: &str) -> String {
|
||||||
|
if label.is_empty() {
|
||||||
|
"(no label)".to_string()
|
||||||
|
} else {
|
||||||
|
label.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_volume_label_char(byte: u8) -> bool {
|
||||||
|
matches!(
|
||||||
|
byte,
|
||||||
|
0x00..=0x1F
|
||||||
|
| 0x7F
|
||||||
|
| b'"'
|
||||||
|
| b'*'
|
||||||
|
| b'+'
|
||||||
|
| b','
|
||||||
|
| b'.'
|
||||||
|
| b'/'
|
||||||
|
| b':'
|
||||||
|
| b';'
|
||||||
|
| b'<'
|
||||||
|
| b'='
|
||||||
|
| b'>'
|
||||||
|
| b'?'
|
||||||
|
| b'['
|
||||||
|
| b'\\'
|
||||||
|
| b']'
|
||||||
|
| b'|'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_label(label: &str) -> [u8; 11] {
|
||||||
|
if !label.is_ascii() {
|
||||||
|
eprintln!("fat-label: label must contain only ASCII characters");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if label.len() > 11 {
|
||||||
|
eprintln!("fat-label: label too long (max 11 chars)");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = label.to_ascii_uppercase();
|
||||||
|
if let Some(invalid) = label.bytes().find(|byte| invalid_volume_label_char(*byte)) {
|
||||||
|
eprintln!("fat-label: invalid character '{}' in label", invalid as char);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bytes = [b' '; 11];
|
||||||
|
for (index, byte) in label.bytes().enumerate() {
|
||||||
|
bytes[index] = byte;
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label_string(bytes: &[u8; 11]) -> String {
|
||||||
|
String::from_utf8_lossy(bytes)
|
||||||
|
.trim_end_matches(' ')
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_volume_label(device: &str) -> String {
|
||||||
|
let disk = FileDisk::open(device).unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to open {device}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let buf_disk = fscommon::BufStream::new(disk);
|
||||||
|
let fs = fatfs::FileSystem::new(buf_disk, FsOptions::new()).unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to mount {device}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.volume_label()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_volume_label(device: &str, label: [u8; 11]) {
|
||||||
|
let mut disk = FileDisk::open(device).unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to open {device}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut boot_sector = [0u8; 512];
|
||||||
|
disk.seek(SeekFrom::Start(0))
|
||||||
|
.and_then(|_| disk.read_exact(&mut boot_sector))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to read BPB from {device}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if boot_sector[510] != 0x55 || boot_sector[511] != 0xAA {
|
||||||
|
eprintln!(
|
||||||
|
"fat-label: invalid boot sector signature {:02X} {:02X}",
|
||||||
|
boot_sector[510], boot_sector[511]
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let root_entry_count = u16::from_le_bytes([boot_sector[17], boot_sector[18]]);
|
||||||
|
let fat_size_32 = u32::from_le_bytes([
|
||||||
|
boot_sector[36],
|
||||||
|
boot_sector[37],
|
||||||
|
boot_sector[38],
|
||||||
|
boot_sector[39],
|
||||||
|
]);
|
||||||
|
let label_offset = if root_entry_count == 0 && fat_size_32 != 0 {
|
||||||
|
71
|
||||||
|
} else {
|
||||||
|
43
|
||||||
|
};
|
||||||
|
|
||||||
|
disk.seek(SeekFrom::Start(label_offset))
|
||||||
|
.and_then(|_| disk.write_all(&label))
|
||||||
|
.and_then(|_| disk.flush())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to write BPB volume label to {device}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
drop(disk);
|
||||||
|
|
||||||
|
update_root_dir_label(device, label, &boot_sector);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_root_dir_label(device: &str, label: [u8; 11], boot_sector: &[u8; 512]) {
|
||||||
|
let root_entry_count = u16::from_le_bytes([boot_sector[17], boot_sector[18]]) as u32;
|
||||||
|
let bytes_per_sector = u16::from_le_bytes([boot_sector[11], boot_sector[12]]) as u32;
|
||||||
|
let sectors_per_cluster = boot_sector[13] as u32;
|
||||||
|
let reserved_sectors = u16::from_le_bytes([boot_sector[14], boot_sector[15]]) as u32;
|
||||||
|
let num_fats = boot_sector[16] as u32;
|
||||||
|
let fat_size_16 = u16::from_le_bytes([boot_sector[22], boot_sector[23]]) as u32;
|
||||||
|
let fat_size_32 = u32::from_le_bytes([boot_sector[36], boot_sector[37], boot_sector[38], boot_sector[39]]);
|
||||||
|
let fat_size = if fat_size_32 != 0 && root_entry_count == 0 { fat_size_32 } else { fat_size_16 };
|
||||||
|
let root_dir_sectors = (root_entry_count * 32).div_ceil(bytes_per_sector);
|
||||||
|
let first_root_dir_sector = reserved_sectors + num_fats * fat_size;
|
||||||
|
let root_cluster = u32::from_le_bytes([boot_sector[44], boot_sector[45], boot_sector[46], boot_sector[47]]);
|
||||||
|
|
||||||
|
let mut disk = FileDisk::open(device).unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to reopen {device} for root dir update: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_fat32 = root_entry_count == 0 && fat_size_32 != 0;
|
||||||
|
|
||||||
|
let mut new_entry = [0u8; 32];
|
||||||
|
new_entry[0..11].copy_from_slice(&label);
|
||||||
|
new_entry[11] = 0x08;
|
||||||
|
|
||||||
|
if is_fat32 {
|
||||||
|
let first_data_sector = first_root_dir_sector + root_dir_sectors;
|
||||||
|
let cluster_offset = |cluster: u32| -> u64 {
|
||||||
|
let first_sector = first_data_sector + (cluster - 2) * sectors_per_cluster;
|
||||||
|
u64::from(first_sector) * u64::from(bytes_per_sector)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut found = false;
|
||||||
|
let mut first_free_offset: Option<u64> = None;
|
||||||
|
let mut cluster = root_cluster;
|
||||||
|
let cluster_size = (bytes_per_sector * sectors_per_cluster) as usize;
|
||||||
|
loop {
|
||||||
|
let offset = cluster_offset(cluster);
|
||||||
|
let mut buf = vec![0u8; cluster_size];
|
||||||
|
disk.seek(SeekFrom::Start(offset))
|
||||||
|
.and_then(|_| disk.read_exact(&mut buf))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to read root dir cluster: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (i, chunk) in buf.chunks_exact(32).enumerate() {
|
||||||
|
if chunk[0] == 0x00 {
|
||||||
|
if first_free_offset.is_none() {
|
||||||
|
first_free_offset = Some(offset + (i as u64 * 32));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if chunk[0] == 0xE5 {
|
||||||
|
if first_free_offset.is_none() {
|
||||||
|
first_free_offset = Some(offset + (i as u64 * 32));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if chunk[11] == 0x08 {
|
||||||
|
let entry_offset = offset + (i as u64 * 32);
|
||||||
|
disk.seek(SeekFrom::Start(entry_offset))
|
||||||
|
.and_then(|_| disk.write_all(&label))
|
||||||
|
.and_then(|_| disk.flush())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to write root dir label entry: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fat_offset = cluster as usize * 4;
|
||||||
|
let fat_byte_offset = reserved_sectors as u64 * bytes_per_sector as u64;
|
||||||
|
let mut fat_entry = [0u8; 4];
|
||||||
|
disk.seek(SeekFrom::Start(fat_byte_offset + fat_offset as u64))
|
||||||
|
.and_then(|_| disk.read_exact(&mut fat_entry))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to read FAT entry: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let next = u32::from_le_bytes(fat_entry) & 0x0FFF_FFFF;
|
||||||
|
if next >= 0x0FFF_FFF8 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cluster = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if let Some(free_offset) = first_free_offset {
|
||||||
|
disk.seek(SeekFrom::Start(free_offset))
|
||||||
|
.and_then(|_| disk.write_all(&new_entry))
|
||||||
|
.and_then(|_| disk.flush())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to create root dir label entry: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
eprintln!("fat-label: warning: root directory full, BPB label updated but no root-dir entry created");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let root_dir_offset = u64::from(first_root_dir_sector) * u64::from(bytes_per_sector);
|
||||||
|
let root_dir_size = (root_entry_count * 32) as usize;
|
||||||
|
let mut buf = vec![0u8; root_dir_size];
|
||||||
|
disk.seek(SeekFrom::Start(root_dir_offset))
|
||||||
|
.and_then(|_| disk.read_exact(&mut buf))
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to read root dir: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut found = false;
|
||||||
|
let mut first_free_offset: Option<u64> = None;
|
||||||
|
|
||||||
|
for (i, chunk) in buf.chunks_exact(32).enumerate() {
|
||||||
|
if chunk[0] == 0x00 {
|
||||||
|
if first_free_offset.is_none() {
|
||||||
|
first_free_offset = Some(root_dir_offset + (i as u64 * 32));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if chunk[0] == 0xE5 {
|
||||||
|
if first_free_offset.is_none() {
|
||||||
|
first_free_offset = Some(root_dir_offset + (i as u64 * 32));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if chunk[11] == 0x08 {
|
||||||
|
let entry_offset = root_dir_offset + (i as u64 * 32);
|
||||||
|
disk.seek(SeekFrom::Start(entry_offset))
|
||||||
|
.and_then(|_| disk.write_all(&label))
|
||||||
|
.and_then(|_| disk.flush())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to write root dir label entry: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
if let Some(free_offset) = first_free_offset {
|
||||||
|
disk.seek(SeekFrom::Start(free_offset))
|
||||||
|
.and_then(|_| disk.write_all(&new_entry))
|
||||||
|
.and_then(|_| disk.flush())
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-label: failed to create root dir label entry: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
eprintln!("fat-label: warning: root directory full, BPB label updated but no root-dir entry created");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Test invalid_volume_label_char
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_volume_label_char_valid_range() {
|
||||||
|
// A-Z, 0-9 are valid
|
||||||
|
assert!(!invalid_volume_label_char(b'A'));
|
||||||
|
assert!(!invalid_volume_label_char(b'Z'));
|
||||||
|
assert!(!invalid_volume_label_char(b'0'));
|
||||||
|
assert!(!invalid_volume_label_char(b'9'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_volume_label_char_invalid_chars() {
|
||||||
|
// Control chars 0x00-0x1F
|
||||||
|
assert!(invalid_volume_label_char(0x00));
|
||||||
|
assert!(invalid_volume_label_char(0x1F));
|
||||||
|
assert!(invalid_volume_label_char(0x7F));
|
||||||
|
// Invalid symbols
|
||||||
|
assert!(invalid_volume_label_char(b'"'));
|
||||||
|
assert!(invalid_volume_label_char(b'*'));
|
||||||
|
assert!(invalid_volume_label_char(b'+'));
|
||||||
|
assert!(invalid_volume_label_char(b','));
|
||||||
|
assert!(invalid_volume_label_char(b'.'));
|
||||||
|
assert!(invalid_volume_label_char(b'/'));
|
||||||
|
assert!(invalid_volume_label_char(b':'));
|
||||||
|
assert!(invalid_volume_label_char(b';'));
|
||||||
|
assert!(invalid_volume_label_char(b'<'));
|
||||||
|
assert!(invalid_volume_label_char(b'='));
|
||||||
|
assert!(invalid_volume_label_char(b'>'));
|
||||||
|
assert!(invalid_volume_label_char(b'?'));
|
||||||
|
assert!(invalid_volume_label_char(b'['));
|
||||||
|
assert!(invalid_volume_label_char(b'\\'));
|
||||||
|
assert!(invalid_volume_label_char(b']'));
|
||||||
|
assert!(invalid_volume_label_char(b'|'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test label_string
|
||||||
|
#[test]
|
||||||
|
fn test_label_string_trims_trailing_spaces() {
|
||||||
|
let bytes = *b"TEST ";
|
||||||
|
let result = label_string(&bytes);
|
||||||
|
assert_eq!(result, "TEST");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_label_string_no_trailing_spaces() {
|
||||||
|
let bytes = *b"NOTRIMMED ";
|
||||||
|
let result = label_string(&bytes);
|
||||||
|
assert_eq!(result, "NOTRIMMED");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_label_string_all_spaces() {
|
||||||
|
let bytes = *b" ";
|
||||||
|
let result = label_string(&bytes);
|
||||||
|
assert_eq!(result, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test printable_label
|
||||||
|
#[test]
|
||||||
|
fn test_printable_label_empty() {
|
||||||
|
let result = printable_label("");
|
||||||
|
assert_eq!(result, "(no label)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_printable_label_non_empty() {
|
||||||
|
let result = printable_label("MYDISK");
|
||||||
|
assert_eq!(result, "MYDISK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = env::args().skip(1);
|
||||||
|
let mut new_label = None;
|
||||||
|
let mut device = None;
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-s" | "--set" => {
|
||||||
|
if new_label.is_some() {
|
||||||
|
eprintln!("fat-label: volume label already specified");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
new_label = Some(args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("fat-label: {} requires a label argument", arg);
|
||||||
|
process::exit(1);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
_ if arg.starts_with('-') => {
|
||||||
|
eprintln!("fat-label: unknown option '{arg}'");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if device.is_some() {
|
||||||
|
eprintln!("fat-label: unexpected extra argument '{arg}'");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
device = Some(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let device = device.unwrap_or_else(|| usage());
|
||||||
|
|
||||||
|
match new_label {
|
||||||
|
Some(label) => {
|
||||||
|
let old_label = read_volume_label(&device);
|
||||||
|
let label_bytes = normalize_label(&label);
|
||||||
|
write_volume_label(&device, label_bytes);
|
||||||
|
|
||||||
|
let new_label = read_volume_label(&device);
|
||||||
|
let expected_label = label_string(&label_bytes);
|
||||||
|
if new_label != expected_label {
|
||||||
|
eprintln!(
|
||||||
|
"fat-label: verification failed: expected '{}', got '{}'",
|
||||||
|
printable_label(&expected_label),
|
||||||
|
printable_label(&new_label)
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("old label: {}", printable_label(&old_label));
|
||||||
|
println!("new label: {}", printable_label(&new_label));
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let label = read_volume_label(&device);
|
||||||
|
if label.is_empty() {
|
||||||
|
println!("(no label)");
|
||||||
|
} else {
|
||||||
|
println!("{label}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "fat-mkfs"
|
||||||
|
description = "Create FAT filesystems (mkfs.fat equivalent)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fat-mkfs"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fat-blockdev = { path = "../fat-blockdev" }
|
||||||
|
fatfs.workspace = true
|
||||||
|
log.workspace = true
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::process;
|
||||||
|
|
||||||
|
use fat_blockdev::FileDisk;
|
||||||
|
use fatfs::{FatType, FormatVolumeOptions};
|
||||||
|
|
||||||
|
fn usage() -> ! {
|
||||||
|
eprintln!("Usage: fat-mkfs [options] <device>");
|
||||||
|
eprintln!("Options:");
|
||||||
|
eprintln!(" -F <12|16|32> FAT type (default: auto)");
|
||||||
|
eprintln!(" -n <label> Volume label (max 11 chars)");
|
||||||
|
eprintln!(" -s <size> File size in bytes (for file-backed images)");
|
||||||
|
eprintln!(" -c <sectors> Sectors per cluster (must be power of 2)");
|
||||||
|
process::exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_fat_type(s: &str) -> Option<FatType> {
|
||||||
|
match s {
|
||||||
|
"12" => Some(FatType::Fat12),
|
||||||
|
"16" => Some(FatType::Fat16),
|
||||||
|
"32" => Some(FatType::Fat32),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut args = env::args().skip(1).peekable();
|
||||||
|
let mut fat_type: Option<FatType> = None;
|
||||||
|
let mut label: Option<String> = None;
|
||||||
|
let mut file_size: Option<u64> = None;
|
||||||
|
let mut cluster_sectors: Option<u8> = None;
|
||||||
|
|
||||||
|
while let Some(arg) = args.next() {
|
||||||
|
match arg.as_str() {
|
||||||
|
"-F" => {
|
||||||
|
let val = args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("fat-mkfs: -F requires an argument (12, 16, or 32)");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
fat_type = Some(parse_fat_type(&val).unwrap_or_else(|| {
|
||||||
|
eprintln!("fat-mkfs: invalid FAT type '{val}', use 12, 16, or 32");
|
||||||
|
process::exit(1);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
"-n" => {
|
||||||
|
let val = args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("fat-mkfs: -n requires a label argument");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
if val.len() > 11 {
|
||||||
|
eprintln!("fat-mkfs: volume label too long (max 11 chars)");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
label = Some(val.to_uppercase());
|
||||||
|
}
|
||||||
|
"-s" => {
|
||||||
|
let val = args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("fat-mkfs: -s requires a size argument");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
file_size = Some(val.parse::<u64>().unwrap_or_else(|_| {
|
||||||
|
eprintln!("fat-mkfs: invalid size '{val}'");
|
||||||
|
process::exit(1);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
"-c" => {
|
||||||
|
let val = args.next().unwrap_or_else(|| {
|
||||||
|
eprintln!("fat-mkfs: -c requires a sectors argument");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
let sc = val.parse::<u8>().unwrap_or_else(|_| {
|
||||||
|
eprintln!("fat-mkfs: invalid sectors '{val}'");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
if sc == 0 || (sc & (sc - 1)) != 0 {
|
||||||
|
eprintln!("fat-mkfs: sectors per cluster must be a power of 2 and greater than 0");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
cluster_sectors = Some(sc);
|
||||||
|
}
|
||||||
|
other if other.starts_with('-') => {
|
||||||
|
eprintln!("fat-mkfs: unknown option '{other}'");
|
||||||
|
usage();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let path = arg;
|
||||||
|
if let Some(size) = file_size {
|
||||||
|
let file = std::fs::File::create(&path)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-mkfs: failed to create {path}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
// Pre-zero to avoid sparse file issues on some hosts
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
|
let mut writer = BufWriter::new(file);
|
||||||
|
let zeros = [0u8; 65536];
|
||||||
|
let mut remaining = size as usize;
|
||||||
|
while remaining > 0 {
|
||||||
|
let chunk_size = remaining.min(zeros.len());
|
||||||
|
writer.write_all(&zeros[..chunk_size]).unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-mkfs: failed to zero-fill {path}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
remaining -= chunk_size;
|
||||||
|
}
|
||||||
|
drop(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&path)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-mkfs: failed to open {path}: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut options = FormatVolumeOptions::new();
|
||||||
|
if let Some(ft) = fat_type {
|
||||||
|
options = options.fat_type(ft);
|
||||||
|
}
|
||||||
|
if let Some(ref lbl) = label {
|
||||||
|
let mut label_bytes = [b' '; 11];
|
||||||
|
for (i, b) in lbl.bytes().take(11).enumerate() {
|
||||||
|
label_bytes[i] = b;
|
||||||
|
}
|
||||||
|
options = options.volume_label(label_bytes);
|
||||||
|
}
|
||||||
|
if let Some(sc) = cluster_sectors {
|
||||||
|
options = options.bytes_per_cluster(u32::from(sc) * 512);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut disk = FileDisk::new(file);
|
||||||
|
fatfs::format_volume(&mut disk, options)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
eprintln!("fat-mkfs: format failed: {e}");
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let type_str = match fat_type {
|
||||||
|
Some(FatType::Fat12) => "FAT12",
|
||||||
|
Some(FatType::Fat16) => "FAT16",
|
||||||
|
Some(FatType::Fat32) => "FAT32",
|
||||||
|
None => "auto-detected",
|
||||||
|
};
|
||||||
|
let cluster_str = if let Some(sc) = cluster_sectors {
|
||||||
|
format!(", cluster {} sectors", sc)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
eprintln!("fat-mkfs: formatted {path} as {type_str}{cluster_str}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usage();
|
||||||
|
}
|
||||||
@@ -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
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../local/recipes/core/fatd
|
||||||
Reference in New Issue
Block a user