Red Bear OS — microkernel OS in Rust, based on Redox
Derivative of Redox OS (https://www.redox-os.org) adding: - AMD GPU driver (amdgpu) via LinuxKPI compat layer - ext4 filesystem support (ext4d scheme daemon) - ACPI fixes for AMD bare metal (x2APIC, DMAR, IVRS, MCFG) - Custom branding (hostname, os-release, boot identity) Build system is full upstream Redox with RBOS overlay in local/. Patches for kernel, base, and relibc are symlinked from local/patches/ and protected from make clean/distclean. Custom recipes live in local/recipes/ with symlinks into the recipes/ search path. Build: make all CONFIG_NAME=redbear-full Sync: ./local/scripts/sync-upstream.sh
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
script = """
|
||||
# Build and install ext4d scheme daemon
|
||||
COOKBOOK_CARGO_PATH=ext4d cookbook_cargo
|
||||
|
||||
# Build and install ext4-mkfs tool
|
||||
COOKBOOK_CARGO_PATH=ext4-mkfs cookbook_cargo
|
||||
"""
|
||||
@@ -0,0 +1,3 @@
|
||||
[build]
|
||||
target-dir = "target"
|
||||
# Target will be set by cookbook's COOKBOOK_TARGET
|
||||
@@ -0,0 +1,22 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"ext4-blockdev",
|
||||
"ext4d",
|
||||
"ext4-mkfs",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
rsext4 = "0.3"
|
||||
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,16 @@
|
||||
[package]
|
||||
name = "ext4-blockdev"
|
||||
description = "BlockDevice trait implementations for rsext4 on Redox OS"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
rsext4.workspace = true
|
||||
redox_syscall = { workspace = true, optional = true }
|
||||
libredox = { workspace = true, optional = true }
|
||||
log.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["redox"]
|
||||
redox = ["dep:redox_syscall", "dep:libredox"]
|
||||
@@ -0,0 +1,100 @@
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::Path;
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
use rsext4::bmalloc::AbsoluteBN;
|
||||
use rsext4::disknode::Ext4Timestamp;
|
||||
use rsext4::{BlockDevice, Ext4Error, Ext4Result};
|
||||
|
||||
pub struct FileDisk {
|
||||
file: File,
|
||||
total_blocks: u64,
|
||||
block_size: u32,
|
||||
}
|
||||
|
||||
impl FileDisk {
|
||||
pub fn open<P: AsRef<Path>>(path: P, block_size: u32) -> std::io::Result<Self> {
|
||||
let file = OpenOptions::new().read(true).write(true).open(path)?;
|
||||
let len = file.metadata()?.len();
|
||||
Ok(Self {
|
||||
file,
|
||||
total_blocks: len / block_size as u64,
|
||||
block_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create<P: AsRef<Path>>(path: P, size: u64, block_size: u32) -> std::io::Result<Self> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
file.set_len(size)?;
|
||||
Ok(Self {
|
||||
file,
|
||||
total_blocks: size / block_size as u64,
|
||||
block_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockDevice for FileDisk {
|
||||
fn read(&mut self, buffer: &mut [u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
|
||||
let offset = block_id.raw() * self.block_size as u64;
|
||||
self.file
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
let total = count as usize * self.block_size as usize;
|
||||
if buffer.len() < total {
|
||||
return Err(Ext4Error::invalid_input());
|
||||
}
|
||||
self.file
|
||||
.read_exact(&mut buffer[..total])
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, buffer: &[u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
|
||||
let offset = block_id.raw() * self.block_size as u64;
|
||||
self.file
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
let total = count as usize * self.block_size as usize;
|
||||
if buffer.len() < total {
|
||||
return Err(Ext4Error::invalid_input());
|
||||
}
|
||||
self.file
|
||||
.write_all(&buffer[..total])
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open(&mut self) -> Ext4Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Ext4Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn total_blocks(&self) -> u64 {
|
||||
self.total_blocks
|
||||
}
|
||||
|
||||
fn block_size(&self) -> u32 {
|
||||
self.block_size
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Ext4Result<()> {
|
||||
self.file.sync_data().map_err(|_| Ext4Error::io())
|
||||
}
|
||||
|
||||
fn current_time(&self) -> Ext4Result<Ext4Timestamp> {
|
||||
let dur = std::time::SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
Ok(Ext4Timestamp::new(dur.as_secs() as i64, dur.subsec_nanos()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
pub mod file_disk;
|
||||
|
||||
#[cfg(feature = "redox")]
|
||||
pub mod redox_disk;
|
||||
|
||||
pub use file_disk::FileDisk;
|
||||
|
||||
#[cfg(feature = "redox")]
|
||||
pub use redox_disk::RedoxDisk;
|
||||
|
||||
pub use rsext4::bmalloc::AbsoluteBN;
|
||||
pub use rsext4::disknode::Ext4Timestamp;
|
||||
pub use rsext4::{BlockDevice, Ext4Error, Ext4Result};
|
||||
@@ -0,0 +1,93 @@
|
||||
use rsext4::bmalloc::AbsoluteBN;
|
||||
use rsext4::disknode::Ext4Timestamp;
|
||||
use rsext4::{BlockDevice, Ext4Error, Ext4Result};
|
||||
|
||||
pub struct RedoxDisk {
|
||||
fd: usize,
|
||||
total_blocks: u64,
|
||||
block_size: u32,
|
||||
}
|
||||
|
||||
impl RedoxDisk {
|
||||
pub fn open(disk_path: &str, block_size: u32) -> 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)?;
|
||||
let total_blocks = stat.st_size / block_size as u64;
|
||||
Ok(Self {
|
||||
fd,
|
||||
total_blocks,
|
||||
block_size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockDevice for RedoxDisk {
|
||||
fn read(&mut self, buffer: &mut [u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
|
||||
let offset = block_id.raw() * self.block_size as u64;
|
||||
let total = count as usize * self.block_size as usize;
|
||||
if buffer.len() < total {
|
||||
return Err(Ext4Error::invalid_input());
|
||||
}
|
||||
syscall::call::lseek(self.fd, offset as isize, syscall::flag::SEEK_SET)
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
let mut read_total = 0;
|
||||
while read_total < total {
|
||||
let n = syscall::call::read(self.fd, &mut buffer[read_total..total])
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
if n == 0 {
|
||||
return Err(Ext4Error::io());
|
||||
}
|
||||
read_total += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write(&mut self, buffer: &[u8], block_id: AbsoluteBN, count: u32) -> Ext4Result<()> {
|
||||
let offset = block_id.raw() * self.block_size as u64;
|
||||
let total = count as usize * self.block_size as usize;
|
||||
if buffer.len() < total {
|
||||
return Err(Ext4Error::invalid_input());
|
||||
}
|
||||
syscall::call::lseek(self.fd, offset as isize, syscall::flag::SEEK_SET)
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
let mut written_total = 0;
|
||||
while written_total < total {
|
||||
let n = syscall::call::write(self.fd, &buffer[written_total..total])
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
if n == 0 {
|
||||
return Err(Ext4Error::io());
|
||||
}
|
||||
written_total += n;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open(&mut self) -> Ext4Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close(&mut self) -> Ext4Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn total_blocks(&self) -> u64 {
|
||||
self.total_blocks
|
||||
}
|
||||
|
||||
fn block_size(&self) -> u32 {
|
||||
self.block_size
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Ext4Result<()> {
|
||||
syscall::call::fsync(self.fd).map_err(|_| Ext4Error::io())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_time(&self) -> Ext4Result<Ext4Timestamp> {
|
||||
let mut ts = syscall::data::TimeSpec::default();
|
||||
syscall::call::clock_gettime(syscall::flag::CLOCK_REALTIME, &mut ts)
|
||||
.map_err(|_| Ext4Error::io())?;
|
||||
Ok(Ext4Timestamp::new(ts.tv_sec, ts.tv_nsec as u32))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "ext4-mkfs"
|
||||
description = "Create ext4 filesystems (mkfs for Redox OS)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ext4-mkfs"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ext4-blockdev = { path = "../ext4-blockdev" }
|
||||
rsext4.workspace = true
|
||||
log.workspace = true
|
||||
env_logger.workspace = true
|
||||
@@ -0,0 +1,40 @@
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
use ext4_blockdev::FileDisk;
|
||||
use rsext4::{mkfs, Jbd2Dev};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: ext4-mkfs <image> [size_in_mb]");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let path = &args[1];
|
||||
let size_mb: u64 = if args.len() > 2 {
|
||||
args[2].parse().unwrap_or(100)
|
||||
} else {
|
||||
100
|
||||
};
|
||||
let block_size = 4096u32;
|
||||
let size = size_mb * 1024 * 1024;
|
||||
|
||||
let disk = FileDisk::create(path, size, block_size).unwrap_or_else(|e| {
|
||||
eprintln!("ext4-mkfs: failed to create {}: {}", path, e);
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk, false);
|
||||
|
||||
mkfs(&mut jbd).unwrap_or_else(|e| {
|
||||
eprintln!("ext4-mkfs: failed to format: {}", e);
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
eprintln!(
|
||||
"ext4-mkfs: created ext4 filesystem on {} ({}MB)",
|
||||
path, size_mb
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
use ext4_blockdev::FileDisk;
|
||||
use rsext4::{
|
||||
api, dir, entries::DirEntryIterator, loopfile, mkdir, mkfile, mkfs, mount as ext4_mount,
|
||||
umount, Jbd2Dev,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn roundtrip_mkfs_mount_read_write_remount() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let path = "/tmp/test-ext4-roundtrip.img";
|
||||
let size: u64 = 100 * 1024 * 1024; // 100MB
|
||||
let block_size = 4096u32;
|
||||
|
||||
// Step 1: Create and format
|
||||
println!("=== Step 1: Create ext4 image ===");
|
||||
let disk = FileDisk::create(path, size, block_size).expect("create disk");
|
||||
let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk, false);
|
||||
mkfs(&mut jbd).expect("mkfs");
|
||||
println!("Formatted {} ({}MB)", path, size / (1024 * 1024));
|
||||
|
||||
// Step 2: Mount
|
||||
println!("\n=== Step 2: Mount ===");
|
||||
let disk = FileDisk::open(path, block_size).expect("open for mount");
|
||||
let mut jbd = Jbd2Dev::initial_jbd2dev(0, disk, true);
|
||||
let mut fs = ext4_mount(&mut jbd).expect("mount");
|
||||
println!(
|
||||
"Mounted: {} blocks, {} free",
|
||||
fs.superblock.blocks_count(),
|
||||
fs.statfs().free_blocks
|
||||
);
|
||||
|
||||
// Step 3: Create directory
|
||||
println!("\n=== Step 3: Create directory /testdir ===");
|
||||
mkdir(&mut jbd, &mut fs, "/testdir").expect("mkdir");
|
||||
println!("Created /testdir");
|
||||
|
||||
// Step 4: Create file
|
||||
println!("\n=== Step 4: Create file /testdir/hello.txt ===");
|
||||
mkfile(&mut jbd, &mut fs, "/testdir/hello.txt", None, None).expect("mkfile");
|
||||
println!("Created /testdir/hello.txt");
|
||||
|
||||
// Step 5: Open and write
|
||||
println!("\n=== Step 5: Write data ===");
|
||||
let mut file = api::open(&mut jbd, &mut fs, "/testdir/hello.txt", false).expect("open file");
|
||||
let data = b"Hello from Red Bear OS ext4!\n";
|
||||
api::write_at(&mut jbd, &mut fs, &mut file, data).expect("write");
|
||||
println!("Wrote {} bytes to /testdir/hello.txt", data.len());
|
||||
|
||||
// Step 6: Read back
|
||||
println!("\n=== Step 6: Read back ===");
|
||||
api::lseek(&mut file, 0).expect("seek to 0");
|
||||
let read_data = api::read_at(&mut jbd, &mut fs, &mut file, data.len()).expect("read");
|
||||
let read_str = std::str::from_utf8(&read_data).expect("utf8");
|
||||
println!("Read back: {:?}", read_str.trim());
|
||||
assert_eq!(
|
||||
data,
|
||||
&read_data[..data.len()],
|
||||
"read data matches written data"
|
||||
);
|
||||
|
||||
// Step 7: List root directory
|
||||
println!("\n=== Step 7: List root directory ===");
|
||||
let (_, root_inode) = dir::get_inode_with_num(&mut fs, &mut jbd, "/")
|
||||
.expect("get root inode")
|
||||
.expect("root inode found");
|
||||
|
||||
let mut root_copy = root_inode;
|
||||
let blocks = loopfile::resolve_inode_block_allextend(&mut fs, &mut jbd, &mut root_copy)
|
||||
.expect("resolve root blocks");
|
||||
let block_size_usize = fs.superblock.block_size() as usize;
|
||||
for (&_logical, &phys) in blocks.iter() {
|
||||
let cached = fs
|
||||
.datablock_cache
|
||||
.get_or_load(&mut jbd, phys)
|
||||
.expect("cache load");
|
||||
for (entry, _) in DirEntryIterator::new(&cached.data[..block_size_usize]) {
|
||||
if let Some(name) = entry.name_str() {
|
||||
if !name.is_empty() && name != "." && name != ".." {
|
||||
println!(" /{} (inode={})", name, entry.inode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: List /testdir
|
||||
println!("\n=== Step 8: List /testdir ===");
|
||||
let (_, dir_inode) = dir::get_inode_with_num(&mut fs, &mut jbd, "/testdir")
|
||||
.expect("get testdir inode")
|
||||
.expect("testdir found");
|
||||
|
||||
let mut dir_copy = dir_inode;
|
||||
let dir_blocks = loopfile::resolve_inode_block_allextend(&mut fs, &mut jbd, &mut dir_copy)
|
||||
.expect("resolve testdir blocks");
|
||||
for (&_logical, &phys) in dir_blocks.iter() {
|
||||
let cached = fs
|
||||
.datablock_cache
|
||||
.get_or_load(&mut jbd, phys)
|
||||
.expect("cache load dir");
|
||||
for (entry, _) in DirEntryIterator::new(&cached.data[..block_size_usize]) {
|
||||
if let Some(name) = entry.name_str() {
|
||||
if !name.is_empty() && name != "." && name != ".." {
|
||||
println!(" /testdir/{} (inode={})", name, entry.inode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 9: Stat filesystem
|
||||
println!("\n=== Step 9: Filesystem stats ===");
|
||||
let stats = fs.statfs();
|
||||
println!(" block_size: {}", stats.block_size);
|
||||
println!(" total_blocks: {}", stats.total_blocks);
|
||||
println!(" free_blocks: {}", stats.free_blocks);
|
||||
println!(" total_inodes: {}", stats.total_inodes);
|
||||
println!(" free_inodes: {}", stats.free_inodes);
|
||||
|
||||
// Step 10: Sync and unmount
|
||||
println!("\n=== Step 10: Sync + Unmount ===");
|
||||
fs.sync_filesystem(&mut jbd).expect("sync");
|
||||
umount(fs, &mut jbd).expect("umount");
|
||||
println!("Synced and unmounted cleanly");
|
||||
|
||||
// Step 11: Re-mount and verify data persists
|
||||
println!("\n=== Step 11: Re-mount and verify persistence ===");
|
||||
let disk2 = FileDisk::open(path, block_size).expect("reopen");
|
||||
let mut jbd2 = Jbd2Dev::initial_jbd2dev(0, disk2, true);
|
||||
let mut fs2 = ext4_mount(&mut jbd2).expect("remount");
|
||||
|
||||
let mut file2 =
|
||||
api::open(&mut jbd2, &mut fs2, "/testdir/hello.txt", false).expect("reopen file");
|
||||
let read_data2 = api::read_at(&mut jbd2, &mut fs2, &mut file2, data.len()).expect("reread");
|
||||
assert_eq!(
|
||||
data,
|
||||
&read_data2[..data.len()],
|
||||
"data persists after remount"
|
||||
);
|
||||
let read_str2 = std::str::from_utf8(&read_data2).expect("utf8");
|
||||
println!("After remount, read: {:?}", read_str2.trim());
|
||||
|
||||
fs2.sync_filesystem(&mut jbd2).expect("sync2");
|
||||
umount(fs2, &mut jbd2).expect("umount2");
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "ext4d"
|
||||
description = "ext4 filesystem scheme daemon for Redox OS"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ext4d"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ext4-blockdev = { path = "../ext4-blockdev" }
|
||||
rsext4.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", "ext4-blockdev/redox", "dep:env_logger"]
|
||||
@@ -0,0 +1,96 @@
|
||||
use rsext4::{api::OpenFile, bmalloc::InodeNumber, disknode::Ext4Inode};
|
||||
use syscall::flag::{O_ACCMODE, O_RDONLY, O_RDWR, O_WRONLY};
|
||||
|
||||
pub enum Handle {
|
||||
File(FileHandle),
|
||||
Directory(DirectoryHandle),
|
||||
SchemeRoot,
|
||||
}
|
||||
|
||||
pub struct FileHandle {
|
||||
path: String,
|
||||
pub file: OpenFile,
|
||||
flags: usize,
|
||||
}
|
||||
|
||||
pub struct DirectoryHandle {
|
||||
path: String,
|
||||
inode_num: InodeNumber,
|
||||
inode: Ext4Inode,
|
||||
flags: usize,
|
||||
}
|
||||
|
||||
impl FileHandle {
|
||||
pub fn new(path: String, file: OpenFile, flags: usize) -> Self {
|
||||
Self { path, file, flags }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn inode_num(&self) -> InodeNumber {
|
||||
self.file.inode_num
|
||||
}
|
||||
|
||||
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 set_path(&mut self, path: String) {
|
||||
self.path = path;
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectoryHandle {
|
||||
pub fn new(path: String, inode_num: InodeNumber, inode: Ext4Inode, flags: usize) -> Self {
|
||||
Self {
|
||||
path,
|
||||
inode_num,
|
||||
inode,
|
||||
flags,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn inode_num(&self) -> InodeNumber {
|
||||
self.inode_num
|
||||
}
|
||||
|
||||
pub fn inode(&self) -> &Ext4Inode {
|
||||
&self.inode
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> usize {
|
||||
self.flags
|
||||
}
|
||||
}
|
||||
|
||||
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,196 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, Read, Write},
|
||||
os::unix::io::{FromRawFd, RawFd},
|
||||
process,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use ext4_blockdev::FileDisk;
|
||||
#[cfg(target_os = "redox")]
|
||||
use ext4_blockdev::RedoxDisk;
|
||||
use rsext4::{Jbd2Dev, mount as ext4_mount};
|
||||
|
||||
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!("ext4d: failed to enter null namespace: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn capability_mode() {}
|
||||
|
||||
fn usage() {
|
||||
eprintln!("ext4d [--no-daemon|-d] <disk_path> <mountpoint>");
|
||||
}
|
||||
|
||||
fn fail_usage(message: &str) -> ! {
|
||||
eprintln!("ext4d: {message}");
|
||||
usage();
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_mount(disk_path: &str, mountpoint: &str) -> Result<(), String> {
|
||||
let disk = RedoxDisk::open(disk_path, 4096)
|
||||
.map_err(|err| format!("failed to open {disk_path}: {err}"))?;
|
||||
let mut journal = Jbd2Dev::initial_jbd2dev(0, disk, true);
|
||||
let filesystem = ext4_mount(&mut journal)
|
||||
.map_err(|err| format!("failed to mount ext4 on {disk_path}: {err}"))?;
|
||||
|
||||
mount::mount(filesystem, journal, mountpoint, |mounted_path| {
|
||||
capability_mode();
|
||||
log::info!("mounted ext4 filesystem on {disk_path} to {mounted_path}");
|
||||
})
|
||||
.map_err(|err| format!("failed to serve scheme {mountpoint}: {err}"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn run_mount(disk_path: &str, mountpoint: &str) -> Result<(), String> {
|
||||
let disk = FileDisk::open(disk_path, 4096)
|
||||
.map_err(|err| format!("failed to open {disk_path}: {err}"))?;
|
||||
let mut journal = Jbd2Dev::initial_jbd2dev(0, disk, true);
|
||||
let filesystem = ext4_mount(&mut journal)
|
||||
.map_err(|err| format!("failed to mount ext4 on {disk_path}: {err}"))?;
|
||||
|
||||
mount::mount(filesystem, journal, mountpoint, |mounted_path| {
|
||||
capability_mode();
|
||||
log::info!("mounted ext4 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!("ext4d: 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!("ext4d: failed to read child status: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
process::exit(i32::from(response[0]));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("ext4d: failed to fork: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("running ext4d in foreground");
|
||||
process::exit(daemon(&disk_path, &mountpoint, None));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use redox_scheme::{
|
||||
RequestKind, Response, SignalBehavior, Socket,
|
||||
scheme::{SchemeState, SchemeSync, register_sync_scheme},
|
||||
};
|
||||
use rsext4::{BlockDevice, Ext4FileSystem, Jbd2Dev};
|
||||
|
||||
use crate::{IS_UMT, scheme::Ext4Scheme};
|
||||
|
||||
pub fn mount<D, T, F>(
|
||||
filesystem: Ext4FileSystem,
|
||||
journal: Jbd2Dev<D>,
|
||||
mountpoint: &str,
|
||||
callback: F,
|
||||
) -> syscall::error::Result<T>
|
||||
where
|
||||
D: BlockDevice,
|
||||
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 = Ext4Scheme::new(scheme_name, mounted_path.clone(), filesystem, journal);
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use redox_scheme::{CallerCtx, OpenResult, SendFdRequest, scheme::SchemeSync};
|
||||
use rsext4::{
|
||||
BlockDevice, Ext4Error, Ext4FileSystem, Jbd2Dev, api, delete_dir, delete_file, dir,
|
||||
disknode::Ext4Inode,
|
||||
entries::{DirEntryIterator, Ext4DirEntry2},
|
||||
loopfile, mkdir, mkfile, truncate, umount,
|
||||
};
|
||||
use syscall::{
|
||||
data::{Stat, StatVfs},
|
||||
dirent::{DirEntry, DirentBuf, DirentKind},
|
||||
error::{
|
||||
EACCES, EBADF, EEXIST, EINVAL, EISDIR, ENOENT, ENOTDIR, ENOTEMPTY, EPERM, Error, Result,
|
||||
},
|
||||
flag::{
|
||||
AT_REMOVEDIR, EventFlags, F_GETFD, F_GETFL, F_SETFD, F_SETFL, O_ACCMODE, O_CREAT,
|
||||
O_DIRECTORY, O_EXCL, O_RDONLY, O_TRUNC, O_WRONLY,
|
||||
},
|
||||
schemev2::NewFdFlags,
|
||||
};
|
||||
|
||||
use crate::handle::{DirectoryHandle, FileHandle, Handle};
|
||||
|
||||
const PERM_EXEC: u16 = 0o1;
|
||||
const PERM_WRITE: u16 = 0o2;
|
||||
const PERM_READ: u16 = 0o4;
|
||||
|
||||
struct Lookup {
|
||||
path: String,
|
||||
inode_num: rsext4::bmalloc::InodeNumber,
|
||||
inode: Ext4Inode,
|
||||
}
|
||||
|
||||
pub struct Ext4Scheme<D: BlockDevice> {
|
||||
mounted_path: String,
|
||||
fs: Ext4FileSystem,
|
||||
journal: Jbd2Dev<D>,
|
||||
next_id: AtomicUsize,
|
||||
handles: BTreeMap<usize, Handle>,
|
||||
}
|
||||
|
||||
impl<D: BlockDevice> Ext4Scheme<D> {
|
||||
pub fn new(
|
||||
_scheme_name: String,
|
||||
mounted_path: String,
|
||||
fs: Ext4FileSystem,
|
||||
journal: Jbd2Dev<D>,
|
||||
) -> Self {
|
||||
Self {
|
||||
mounted_path,
|
||||
fs,
|
||||
journal,
|
||||
next_id: AtomicUsize::new(1),
|
||||
handles: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup(self) -> Result<()> {
|
||||
let Ext4Scheme {
|
||||
mut fs,
|
||||
mut journal,
|
||||
..
|
||||
} = self;
|
||||
|
||||
fs.sync_filesystem(&mut journal).map_err(ext4_error)?;
|
||||
umount(fs, &mut journal).map_err(ext4_error)
|
||||
}
|
||||
|
||||
fn insert_handle(&mut self, handle: Handle) -> usize {
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
self.handles.insert(id, handle);
|
||||
id
|
||||
}
|
||||
|
||||
fn root_lookup(&mut self) -> Result<Lookup> {
|
||||
let (inode_num, inode) = dir::get_inode_with_num(&mut self.fs, &mut self.journal, "/")
|
||||
.map_err(ext4_error)?
|
||||
.ok_or(Error::new(ENOENT))?;
|
||||
|
||||
Ok(Lookup {
|
||||
path: String::new(),
|
||||
inode_num,
|
||||
inode,
|
||||
})
|
||||
}
|
||||
|
||||
fn make_ext4_path(path: &str) -> String {
|
||||
if path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
format!("/{path}")
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_path(path: &str) -> String {
|
||||
let mut components = Vec::new();
|
||||
|
||||
for component in path.split('/') {
|
||||
match component {
|
||||
"" | "." => {}
|
||||
".." => {
|
||||
let _ = components.pop();
|
||||
}
|
||||
part => components.push(part),
|
||||
}
|
||||
}
|
||||
|
||||
components.join("/")
|
||||
}
|
||||
|
||||
fn join_path(base: &str, path: &str) -> String {
|
||||
if path.starts_with('/') {
|
||||
return Self::normalize_path(path);
|
||||
}
|
||||
|
||||
if base.is_empty() {
|
||||
Self::normalize_path(path)
|
||||
} else if path.is_empty() {
|
||||
base.to_string()
|
||||
} else {
|
||||
Self::normalize_path(&format!("{base}/{path}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn dirfd_base_path(&self, dirfd: usize, path: &str) -> Result<String> {
|
||||
if path.starts_with('/') {
|
||||
return Ok(Self::normalize_path(path));
|
||||
}
|
||||
|
||||
match self.handles.get(&dirfd) {
|
||||
Some(Handle::SchemeRoot) => Ok(Self::normalize_path(path)),
|
||||
Some(Handle::Directory(handle)) => Ok(Self::join_path(handle.path(), path)),
|
||||
Some(Handle::File(_)) => Err(Error::new(ENOTDIR)),
|
||||
None => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
|
||||
fn split_parent_child(path: &str) -> Result<(String, String)> {
|
||||
let normalized = Self::normalize_path(path);
|
||||
if normalized.is_empty() {
|
||||
return Err(Error::new(EPERM));
|
||||
}
|
||||
|
||||
match normalized.rsplit_once('/') {
|
||||
Some((parent, child)) if !child.is_empty() => {
|
||||
Ok((parent.to_string(), child.to_string()))
|
||||
}
|
||||
None => Ok((String::new(), normalized)),
|
||||
_ => Err(Error::new(EINVAL)),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_permission(inode: &Ext4Inode, ctx: &CallerCtx, perm: u16) -> bool {
|
||||
if ctx.uid == 0 {
|
||||
return true;
|
||||
}
|
||||
|
||||
let mode = inode.permissions();
|
||||
let granted = if ctx.uid == inode.uid() {
|
||||
(mode >> 6) & 0o7
|
||||
} else if ctx.gid == inode.gid() {
|
||||
(mode >> 3) & 0o7
|
||||
} else {
|
||||
mode & 0o7
|
||||
};
|
||||
|
||||
granted & perm == perm
|
||||
}
|
||||
|
||||
fn require_permission(inode: &Ext4Inode, ctx: &CallerCtx, perm: u16) -> Result<()> {
|
||||
if Self::check_permission(inode, ctx, perm) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(EACCES))
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup_path(&mut self, path: &str, ctx: &CallerCtx) -> Result<Option<Lookup>> {
|
||||
let normalized = Self::normalize_path(path);
|
||||
if normalized.is_empty() {
|
||||
return self.root_lookup().map(Some);
|
||||
}
|
||||
|
||||
let mut current = self.root_lookup()?;
|
||||
for component in normalized.split('/') {
|
||||
if !current.inode.is_dir() {
|
||||
return Err(Error::new(ENOTDIR));
|
||||
}
|
||||
|
||||
Self::require_permission(¤t.inode, ctx, PERM_EXEC)?;
|
||||
|
||||
let next_path = if current.path.is_empty() {
|
||||
component.to_string()
|
||||
} else {
|
||||
format!("{}/{}", current.path, component)
|
||||
};
|
||||
|
||||
let Some((inode_num, inode)) = dir::get_inode_with_num(
|
||||
&mut self.fs,
|
||||
&mut self.journal,
|
||||
&Self::make_ext4_path(&next_path),
|
||||
)
|
||||
.map_err(ext4_error)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
current = Lookup {
|
||||
path: next_path,
|
||||
inode_num,
|
||||
inode,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Some(current))
|
||||
}
|
||||
|
||||
fn lookup_existing(&mut self, path: &str, ctx: &CallerCtx) -> Result<Lookup> {
|
||||
self.lookup_path(path, ctx)?.ok_or(Error::new(ENOENT))
|
||||
}
|
||||
|
||||
fn lookup_parent(&mut self, path: &str, ctx: &CallerCtx) -> Result<(Lookup, String)> {
|
||||
let (parent_path, child) = Self::split_parent_child(path)?;
|
||||
let parent = self.lookup_existing(&parent_path, ctx)?;
|
||||
if !parent.inode.is_dir() {
|
||||
return Err(Error::new(ENOTDIR));
|
||||
}
|
||||
Self::require_permission(&parent.inode, ctx, PERM_EXEC | PERM_WRITE)?;
|
||||
Ok((parent, child))
|
||||
}
|
||||
|
||||
fn stat_from_lookup(&self, lookup: &Lookup, stat: &mut Stat) {
|
||||
*stat = Stat::default();
|
||||
stat.st_dev = 0;
|
||||
stat.st_ino = u64::from(lookup.inode_num.raw());
|
||||
stat.st_mode = lookup.inode.i_mode;
|
||||
stat.st_nlink = u32::from(lookup.inode.i_links_count);
|
||||
stat.st_uid = lookup.inode.uid();
|
||||
stat.st_gid = lookup.inode.gid();
|
||||
stat.st_size = lookup.inode.size();
|
||||
stat.st_blksize = self.fs.superblock.block_size() as u32;
|
||||
stat.st_blocks = lookup.inode.blocks_count();
|
||||
|
||||
let inode_size = self.fs.superblock.inode_size();
|
||||
let atime = lookup.inode.atime_ts(inode_size);
|
||||
let mtime = lookup.inode.mtime_ts(inode_size);
|
||||
let ctime = lookup.inode.ctime_ts(inode_size);
|
||||
|
||||
stat.st_atime = atime.sec.max(0) as u64;
|
||||
stat.st_atime_nsec = atime.nsec;
|
||||
stat.st_mtime = mtime.sec.max(0) as u64;
|
||||
stat.st_mtime_nsec = mtime.nsec;
|
||||
stat.st_ctime = ctime.sec.max(0) as u64;
|
||||
stat.st_ctime_nsec = ctime.nsec;
|
||||
}
|
||||
|
||||
fn refresh_file_handle(&mut self, id: usize) -> Result<()> {
|
||||
let (path, offset) = match self.handles.get(&id) {
|
||||
Some(Handle::File(handle)) => (handle.path().to_string(), handle.file.offset),
|
||||
_ => return Err(Error::new(EBADF)),
|
||||
};
|
||||
|
||||
let file = api::open(
|
||||
&mut self.journal,
|
||||
&mut self.fs,
|
||||
&Self::make_ext4_path(&path),
|
||||
false,
|
||||
)
|
||||
.map_err(ext4_error)?;
|
||||
|
||||
let mut file = file;
|
||||
api::lseek(&mut file, offset).map_err(ext4_error)?;
|
||||
|
||||
match self.handles.get_mut(&id) {
|
||||
Some(Handle::File(handle)) => {
|
||||
handle.file = file;
|
||||
handle.set_path(path);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
|
||||
fn dirent_kind_from_file_type(file_type: u8) -> DirentKind {
|
||||
match file_type {
|
||||
Ext4DirEntry2::EXT4_FT_DIR => DirentKind::Directory,
|
||||
Ext4DirEntry2::EXT4_FT_REG_FILE => DirentKind::Regular,
|
||||
Ext4DirEntry2::EXT4_FT_CHRDEV => DirentKind::CharDev,
|
||||
Ext4DirEntry2::EXT4_FT_BLKDEV => DirentKind::BlockDev,
|
||||
Ext4DirEntry2::EXT4_FT_SYMLINK => DirentKind::Symlink,
|
||||
Ext4DirEntry2::EXT4_FT_SOCK => DirentKind::Socket,
|
||||
_ => DirentKind::Unspecified,
|
||||
}
|
||||
}
|
||||
|
||||
fn directory_entries(
|
||||
&mut self,
|
||||
_path: &str,
|
||||
inode: &Ext4Inode,
|
||||
) -> Result<Vec<(u64, u64, String, DirentKind)>> {
|
||||
let mut inode_copy = *inode;
|
||||
let blocks = loopfile::resolve_inode_block_allextend(
|
||||
&mut self.fs,
|
||||
&mut self.journal,
|
||||
&mut inode_copy,
|
||||
)
|
||||
.map_err(ext4_error)?;
|
||||
|
||||
let block_size = self.fs.superblock.block_size() as usize;
|
||||
let mut entries = Vec::new();
|
||||
let mut opaque = 1u64;
|
||||
|
||||
for &phys in blocks.values() {
|
||||
let cached = self
|
||||
.fs
|
||||
.datablock_cache
|
||||
.get_or_load(&mut self.journal, phys)
|
||||
.map_err(ext4_error)?;
|
||||
for (entry, _) in DirEntryIterator::new(&cached.data[..block_size]) {
|
||||
let Some(name) = entry.name_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let kind = match name {
|
||||
"." | ".." => DirentKind::Directory,
|
||||
_ => Self::dirent_kind_from_file_type(entry.file_type),
|
||||
};
|
||||
|
||||
entries.push((u64::from(entry.inode), opaque, name.to_string(), kind));
|
||||
opaque = opaque.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn create_directory_handle(&mut self, lookup: Lookup, flags: usize) -> OpenResult {
|
||||
let id = self.insert_handle(Handle::Directory(DirectoryHandle::new(
|
||||
lookup.path,
|
||||
lookup.inode_num,
|
||||
lookup.inode,
|
||||
flags,
|
||||
)));
|
||||
|
||||
OpenResult::ThisScheme {
|
||||
number: id,
|
||||
flags: NewFdFlags::POSITIONED,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_file_handle(
|
||||
&mut self,
|
||||
path: String,
|
||||
file: api::OpenFile,
|
||||
flags: usize,
|
||||
) -> OpenResult {
|
||||
let id = self.insert_handle(Handle::File(FileHandle::new(path, file, flags)));
|
||||
|
||||
OpenResult::ThisScheme {
|
||||
number: id,
|
||||
flags: NewFdFlags::POSITIONED,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_lookup_for_stat(&mut self, id: usize, ctx: &CallerCtx) -> Result<Lookup> {
|
||||
let path = match self.handles.get(&id) {
|
||||
Some(Handle::SchemeRoot) => None,
|
||||
Some(Handle::Directory(handle)) => Some(handle.path().to_string()),
|
||||
Some(Handle::File(handle)) => Some(handle.path().to_string()),
|
||||
None => return Err(Error::new(EBADF)),
|
||||
};
|
||||
|
||||
match path {
|
||||
Some(path) => self.lookup_existing(&path, ctx),
|
||||
None => self.root_lookup(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_regular_file_access(handle: &FileHandle, write: bool) -> Result<()> {
|
||||
if write && !handle.can_write() {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
if !write && !handle.can_read() {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: BlockDevice> SchemeSync for Ext4Scheme<D> {
|
||||
fn scheme_root(&mut self) -> Result<usize> {
|
||||
Ok(self.insert_handle(Handle::SchemeRoot))
|
||||
}
|
||||
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
ctx: &CallerCtx,
|
||||
) -> Result<OpenResult> {
|
||||
let resolved_path = self.dirfd_base_path(dirfd, path)?;
|
||||
|
||||
match self.lookup_path(&resolved_path, ctx)? {
|
||||
Some(lookup) => {
|
||||
if flags & (O_CREAT | O_EXCL) == O_CREAT | O_EXCL {
|
||||
return Err(Error::new(EEXIST));
|
||||
}
|
||||
|
||||
if lookup.inode.is_dir() {
|
||||
if flags & O_ACCMODE != O_RDONLY {
|
||||
return Err(Error::new(EISDIR));
|
||||
}
|
||||
Self::require_permission(&lookup.inode, ctx, PERM_READ)?;
|
||||
return Ok(self.create_directory_handle(lookup, flags));
|
||||
}
|
||||
|
||||
if flags & O_DIRECTORY == O_DIRECTORY {
|
||||
return Err(Error::new(ENOTDIR));
|
||||
}
|
||||
|
||||
if flags & O_ACCMODE != O_WRONLY {
|
||||
Self::require_permission(&lookup.inode, ctx, PERM_READ)?;
|
||||
}
|
||||
if flags & O_ACCMODE != O_RDONLY {
|
||||
Self::require_permission(&lookup.inode, ctx, PERM_WRITE)?;
|
||||
}
|
||||
|
||||
let ext4_path = Self::make_ext4_path(&resolved_path);
|
||||
if flags & O_TRUNC == O_TRUNC {
|
||||
truncate(&mut self.journal, &mut self.fs, &ext4_path, 0).map_err(ext4_error)?;
|
||||
}
|
||||
|
||||
let file = api::open(&mut self.journal, &mut self.fs, &ext4_path, false)
|
||||
.map_err(ext4_error)?;
|
||||
Ok(self.create_file_handle(resolved_path, file, flags))
|
||||
}
|
||||
None => {
|
||||
if flags & O_CREAT != O_CREAT {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
|
||||
let (_parent, _name) = self.lookup_parent(&resolved_path, ctx)?;
|
||||
let ext4_path = Self::make_ext4_path(&resolved_path);
|
||||
|
||||
if flags & O_DIRECTORY == O_DIRECTORY {
|
||||
mkdir(&mut self.journal, &mut self.fs, &ext4_path).map_err(ext4_error)?;
|
||||
let lookup = self.lookup_existing(&resolved_path, ctx)?;
|
||||
Ok(self.create_directory_handle(lookup, flags))
|
||||
} else {
|
||||
mkfile(&mut self.journal, &mut self.fs, &ext4_path, None, None)
|
||||
.map_err(ext4_error)?;
|
||||
let file = api::open(&mut self.journal, &mut self.fs, &ext4_path, false)
|
||||
.map_err(ext4_error)?;
|
||||
Ok(self.create_file_handle(resolved_path, file, flags))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &mut [u8],
|
||||
offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
match self.handles.get_mut(&id) {
|
||||
Some(Handle::File(handle)) => {
|
||||
Self::ensure_regular_file_access(handle, false)?;
|
||||
api::lseek(&mut handle.file, offset).map_err(ext4_error)?;
|
||||
let data =
|
||||
api::read_at(&mut self.journal, &mut self.fs, &mut handle.file, buf.len())
|
||||
.map_err(ext4_error)?;
|
||||
let count = data.len();
|
||||
buf[..count].copy_from_slice(&data);
|
||||
Ok(count)
|
||||
}
|
||||
Some(Handle::Directory(_)) | Some(Handle::SchemeRoot) => Err(Error::new(EISDIR)),
|
||||
None => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &[u8],
|
||||
offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
match self.handles.get_mut(&id) {
|
||||
Some(Handle::File(handle)) => {
|
||||
Self::ensure_regular_file_access(handle, true)?;
|
||||
api::lseek(&mut handle.file, offset).map_err(ext4_error)?;
|
||||
api::write_at(&mut self.journal, &mut self.fs, &mut handle.file, buf)
|
||||
.map_err(ext4_error)?;
|
||||
Ok(buf.len())
|
||||
}
|
||||
Some(Handle::Directory(_)) | Some(Handle::SchemeRoot) => Err(Error::new(EISDIR)),
|
||||
None => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
|
||||
fn fsize(&mut self, id: usize, ctx: &CallerCtx) -> Result<u64> {
|
||||
Ok(self.handle_lookup_for_stat(id, ctx)?.inode.size())
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, id: usize, cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result<usize> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
match cmd {
|
||||
F_GETFL => Ok(handle.flags().unwrap_or(O_RDONLY)),
|
||||
F_GETFD => Ok(0),
|
||||
F_SETFL | F_SETFD => Ok(0),
|
||||
_ => Err(Error::new(EINVAL)),
|
||||
}
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
|
||||
if self.handles.contains_key(&id) {
|
||||
Err(Error::new(EPERM))
|
||||
} else {
|
||||
Err(Error::new(EBADF))
|
||||
}
|
||||
}
|
||||
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
let Some(path) = handle.path() else {
|
||||
return Err(Error::new(EBADF));
|
||||
};
|
||||
|
||||
let full_path = if path.is_empty() {
|
||||
self.mounted_path.clone()
|
||||
} else {
|
||||
format!("{}/{}", self.mounted_path, path)
|
||||
};
|
||||
|
||||
let bytes = full_path.as_bytes();
|
||||
let count = bytes.len().min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[..count]);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat, ctx: &CallerCtx) -> Result<()> {
|
||||
let lookup = self.handle_lookup_for_stat(id, ctx)?;
|
||||
self.stat_from_lookup(&lookup, stat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fstatvfs(&mut self, id: usize, stat: &mut StatVfs, _ctx: &CallerCtx) -> Result<()> {
|
||||
if !self.handles.contains_key(&id) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
|
||||
let stats = self.fs.statfs();
|
||||
stat.f_bsize = stats.block_size as u32;
|
||||
stat.f_blocks = stats.total_blocks;
|
||||
stat.f_bfree = stats.free_blocks;
|
||||
stat.f_bavail = stats.free_blocks;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn getdents<'buf>(
|
||||
&mut self,
|
||||
id: usize,
|
||||
mut buf: DirentBuf<&'buf mut [u8]>,
|
||||
opaque_offset: u64,
|
||||
) -> Result<DirentBuf<&'buf mut [u8]>> {
|
||||
let (path, inode) = match self.handles.get(&id) {
|
||||
Some(Handle::Directory(handle)) => (handle.path().to_string(), *handle.inode()),
|
||||
Some(Handle::SchemeRoot) => {
|
||||
let lookup = self.root_lookup()?;
|
||||
(lookup.path, lookup.inode)
|
||||
}
|
||||
Some(Handle::File(_)) => return Err(Error::new(ENOTDIR)),
|
||||
None => return Err(Error::new(EBADF)),
|
||||
};
|
||||
|
||||
let entries = self.directory_entries(&path, &inode)?;
|
||||
for (inode, next_opaque_id, name, kind) in entries {
|
||||
if next_opaque_id <= opaque_offset {
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.entry(DirEntry {
|
||||
inode,
|
||||
next_opaque_id,
|
||||
name: &name,
|
||||
kind,
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
|
||||
if !self.handles.contains_key(&id) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
|
||||
self.fs
|
||||
.sync_filesystem(&mut self.journal)
|
||||
.map_err(ext4_error)
|
||||
}
|
||||
|
||||
fn ftruncate(&mut self, id: usize, len: u64, _ctx: &CallerCtx) -> Result<()> {
|
||||
let path = match self.handles.get(&id) {
|
||||
Some(Handle::File(handle)) => handle.path().to_string(),
|
||||
Some(Handle::Directory(_)) | Some(Handle::SchemeRoot) => {
|
||||
return Err(Error::new(EISDIR));
|
||||
}
|
||||
None => return Err(Error::new(EBADF)),
|
||||
};
|
||||
|
||||
truncate(
|
||||
&mut self.journal,
|
||||
&mut self.fs,
|
||||
&Self::make_ext4_path(&path),
|
||||
len,
|
||||
)
|
||||
.map_err(ext4_error)?;
|
||||
self.refresh_file_handle(id)
|
||||
}
|
||||
|
||||
fn unlinkat(&mut self, dirfd: usize, path: &str, flags: usize, ctx: &CallerCtx) -> Result<()> {
|
||||
let resolved_path = self.dirfd_base_path(dirfd, path)?;
|
||||
let lookup = self.lookup_existing(&resolved_path, ctx)?;
|
||||
let (_parent, _name) = self.lookup_parent(&resolved_path, ctx)?;
|
||||
let ext4_path = Self::make_ext4_path(&resolved_path);
|
||||
|
||||
if flags & AT_REMOVEDIR == AT_REMOVEDIR {
|
||||
if !lookup.inode.is_dir() {
|
||||
return Err(Error::new(ENOTDIR));
|
||||
}
|
||||
|
||||
let entries = self.directory_entries(&lookup.path, &lookup.inode)?;
|
||||
if entries
|
||||
.into_iter()
|
||||
.any(|(_, _, name, _)| name != "." && name != "..")
|
||||
{
|
||||
return Err(Error::new(ENOTEMPTY));
|
||||
}
|
||||
|
||||
delete_dir(&mut self.fs, &mut self.journal, &ext4_path).map_err(ext4_error)
|
||||
} else {
|
||||
if lookup.inode.is_dir() {
|
||||
return Err(Error::new(EISDIR));
|
||||
}
|
||||
|
||||
delete_file(&mut self.fs, &mut self.journal, &ext4_path).map_err(ext4_error)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_close(&mut self, id: usize) {
|
||||
let _ = self.handles.remove(&id);
|
||||
}
|
||||
|
||||
fn on_sendfd(&mut self, _sendfd_request: &SendFdRequest) -> Result<usize> {
|
||||
Err(Error::new(EPERM))
|
||||
}
|
||||
|
||||
fn inode(&self, id: usize) -> Result<usize> {
|
||||
match self.handles.get(&id) {
|
||||
Some(Handle::File(handle)) => Ok(handle.inode_num().raw() as usize),
|
||||
Some(Handle::Directory(handle)) => Ok(handle.inode_num().raw() as usize),
|
||||
Some(Handle::SchemeRoot) => Ok(2),
|
||||
None => Err(Error::new(EBADF)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ext4_error(err: Ext4Error) -> Error {
|
||||
Error::new(err.code.as_i32())
|
||||
}
|
||||
Reference in New Issue
Block a user