Files
moss-kernel/libkernel/src/fs/filesystems/ext4/mod.rs
2026-03-02 11:53:06 -08:00

565 lines
18 KiB
Rust

//! EXT4 Filesystem Driver
#![allow(dead_code)]
#![allow(unused_variables)]
#![allow(unused_imports)]
use crate::error::FsError;
use crate::fs::path::Path;
use crate::fs::pathbuf::PathBuf;
use crate::fs::{DirStream, Dirent};
use crate::proc::ids::{Gid, Uid};
use crate::sync::mutex::Mutex;
use crate::{
CpuOps,
error::{KernelError, Result},
fs::{
FileType, Filesystem, Inode, InodeId,
attr::{FileAttr, FilePermissions},
blk::buffer::BlockBuffer,
},
};
use alloc::string::ToString;
use alloc::vec::Vec;
use alloc::{
boxed::Box,
sync::{Arc, Weak},
};
use async_trait::async_trait;
use core::error::Error;
use core::marker::PhantomData;
use core::num::NonZeroU32;
use core::time::Duration;
use ext4_view::{
AsyncIterator, AsyncSkip, Ext4, Ext4Read, Ext4Write, File, FollowSymlinks,
InodeCreationOptions, InodeFlags, InodeMode, Metadata, ReadDir, get_dir_entry_inode_by_name,
write_at,
};
use log::error;
#[async_trait]
impl Ext4Read for BlockBuffer {
async fn read(
&self,
start_byte: u64,
dst: &mut [u8],
) -> core::result::Result<(), Box<dyn Error + Send + Sync + 'static>> {
Ok(self.read_at(start_byte, dst).await?)
}
}
#[async_trait]
impl Ext4Write for BlockBuffer {
async fn write(
&self,
start_byte: u64,
src: &[u8],
) -> core::result::Result<(), Box<dyn Error + Send + Sync + 'static>> {
Ok(self.write_at(start_byte, src).await?)
}
}
impl From<ext4_view::Ext4Error> for KernelError {
fn from(err: ext4_view::Ext4Error) -> Self {
match err {
ext4_view::Ext4Error::NotFound => KernelError::Fs(FsError::NotFound),
ext4_view::Ext4Error::NotADirectory => KernelError::Fs(FsError::NotADirectory),
ext4_view::Ext4Error::AlreadyExists => KernelError::Fs(FsError::AlreadyExists),
ext4_view::Ext4Error::Corrupt(c) => {
error!("Corrupt EXT4 filesystem: {c}, likely a bug");
KernelError::Fs(FsError::InvalidFs)
}
e => {
error!("Unmapped EXT4 error: {e:?}");
KernelError::Other("EXT4 error")
}
}
}
}
impl From<ext4_view::FileType> for FileType {
fn from(ft: ext4_view::FileType) -> Self {
match ft {
ext4_view::FileType::BlockDevice => todo!(),
ext4_view::FileType::CharacterDevice => todo!(),
ext4_view::FileType::Directory => FileType::Directory,
ext4_view::FileType::Fifo => FileType::Fifo,
ext4_view::FileType::Regular => FileType::File,
ext4_view::FileType::Socket => FileType::Socket,
ext4_view::FileType::Symlink => FileType::Symlink,
}
}
}
impl From<Metadata> for FileAttr {
fn from(meta: Metadata) -> Self {
FileAttr {
size: meta.size_in_bytes,
file_type: meta.file_type.into(),
permissions: FilePermissions::from_bits_truncate(meta.mode.bits()),
uid: Uid::new(meta.uid),
gid: Gid::new(meta.gid),
atime: meta.atime,
ctime: meta.ctime,
mtime: meta.mtime,
nlinks: meta.links_count as u32,
..Default::default()
}
}
}
pub struct ReadDirWrapper {
inner: AsyncSkip<ReadDir>,
fs_id: u64,
current_off: u64,
}
impl ReadDirWrapper {
pub fn new(inner: ReadDir, fs_id: u64, start_offset: u64) -> Self {
Self {
inner: inner.skip(start_offset as usize),
fs_id,
current_off: start_offset,
}
}
}
#[async_trait]
impl DirStream for ReadDirWrapper {
async fn next_entry(&mut self) -> Result<Option<Dirent>> {
match self.inner.next().await {
Some(entry) => {
let entry = entry?;
self.current_off += 1;
Ok(Some(Dirent {
id: InodeId::from_fsid_and_inodeid(self.fs_id, entry.inode.get() as u64),
name: entry.file_name().as_str().unwrap().to_string(),
file_type: entry.file_type()?.into(),
offset: self.current_off,
}))
}
None => Ok(None),
}
}
}
pub struct Ext4Inode<CPU: CpuOps> {
fs_ref: Weak<Ext4Filesystem<CPU>>,
id: NonZeroU32,
inner: Mutex<ext4_view::Inode, CPU>,
path: ext4_view::PathBuf,
}
#[async_trait]
impl<CPU> Inode for Ext4Inode<CPU>
where
CPU: CpuOps + Send + Sync,
{
fn id(&self) -> InodeId {
let fs = self.fs_ref.upgrade().unwrap();
InodeId::from_fsid_and_inodeid(fs.id(), self.id.get() as u64)
}
async fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
let inner = self.inner.lock().await;
// Must be a regular file.
if inner.file_type() != ext4_view::FileType::Regular {
return Err(KernelError::NotSupported);
}
let file_size = inner.size_in_bytes();
// Past EOF = nothing to read.
if offset >= file_size {
return Ok(0);
}
// Do not read past the end of the file.
let to_read = core::cmp::min(buf.len() as u64, file_size - offset) as usize;
let fs = self.fs_ref.upgrade().unwrap();
let mut file = File::open_inode(&fs.inner, inner.clone())?;
file.seek_to(offset).await?;
// `ext4_view::File::read_bytes` may return fewer bytes than requested
// if the read crosses a block boundary. Loop until we've filled
// `to_read` bytes or hit EOF.
let mut total_read = 0;
while total_read < to_read {
let bytes_read = file.read_bytes(&mut buf[total_read..to_read]).await?;
if bytes_read == 0 {
break; // EOF
}
total_read += bytes_read;
}
Ok(total_read)
}
async fn write_at(&self, offset: u64, buf: &[u8]) -> Result<usize> {
let mut inner = self.inner.lock().await;
// Must be a regular file.
if inner.file_type() != ext4_view::FileType::Regular {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
let total_written = write_at(&fs.inner, &mut inner, buf, offset).await?;
Ok(total_written)
}
async fn truncate(&self, size: u64) -> Result<()> {
let inner = self.inner.lock().await;
if inner.file_type() != ext4_view::FileType::Regular {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
let mut file = File::open_inode(&fs.inner, inner.clone())?;
file.truncate(size).await?;
Ok(())
}
async fn getattr(&self) -> Result<FileAttr> {
let inner = self.inner.lock().await;
let mut attrs: FileAttr = inner.metadata().into();
let fs = self.fs_ref.upgrade().ok_or(FsError::InvalidFs)?;
attrs.id = InodeId::from_fsid_and_inodeid(fs.id(), self.id.get() as u64);
Ok(attrs)
}
async fn setattr(&self, attr: FileAttr) -> Result<()> {
let mut inner = self.inner.lock().await;
inner.set_atime(attr.atime);
inner.set_ctime(attr.ctime);
inner.set_mtime(attr.mtime);
inner.set_gid(attr.gid.into());
inner.set_uid(attr.uid.into());
inner.set_links_count(attr.nlinks as u16);
let fs = self.fs_ref.upgrade().ok_or(FsError::InvalidFs)?;
inner.write(&fs.inner).await?;
Ok(())
}
async fn lookup(&self, name: &str) -> Result<Arc<dyn Inode>> {
let fs = self.fs_ref.upgrade().unwrap();
let inner = self.inner.lock().await;
let child_inode = get_dir_entry_inode_by_name(
&fs.inner,
&inner,
ext4_view::DirEntryName::try_from(name)
.map_err(|_| KernelError::Fs(FsError::InvalidInput))?,
)
.await?;
let child_path = self.path.join(name);
Ok(Arc::new(Ext4Inode::<CPU> {
fs_ref: self.fs_ref.clone(),
id: child_inode.index,
inner: Mutex::new(child_inode),
path: child_path,
}))
}
async fn create(
&self,
name: &str,
file_type: FileType,
permissions: FilePermissions,
) -> Result<Arc<dyn Inode>> {
let fs = self.fs_ref.upgrade().unwrap();
let mut inner = self.inner.lock().await;
let mut new_inode = if matches!(file_type, FileType::File) {
fs.inner
.create_inode(InodeCreationOptions {
file_type: ext4_view::FileType::Regular,
mode: InodeMode::S_IFREG | InodeMode::from_bits(permissions.bits()).unwrap(),
uid: 0,
gid: 0,
time: Default::default(),
flags: InodeFlags::empty(),
})
.await?
} else if matches!(file_type, FileType::Directory) {
let old_links_count = inner.links_count();
inner.set_links_count(old_links_count + 1);
let mut inode = fs
.inner
.create_inode(InodeCreationOptions {
file_type: ext4_view::FileType::Directory,
mode: InodeMode::S_IFDIR | InodeMode::from_bits(permissions.bits()).unwrap(),
uid: 0,
gid: 0,
time: Default::default(),
flags: InodeFlags::empty(),
})
.await?;
ext4_view::init_directory(&fs.inner, &mut inode, inner.index).await?;
inode
} else {
return Err(KernelError::NotSupported);
};
fs.inner
.link(&inner, name.to_string(), &mut new_inode)
.await?;
let new_path = self.path.join(name);
Ok(Arc::new(Ext4Inode::<CPU> {
fs_ref: self.fs_ref.clone(),
id: new_inode.index,
inner: Mutex::new(new_inode),
path: new_path,
}))
}
async fn link(&self, name: &str, inode: Arc<dyn Inode>) -> Result<()> {
let inner = self.inner.lock().await;
if inner.file_type() != ext4_view::FileType::Directory {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
// TODO: This forces the other inode out of sync
// Check fs ids match
if inode.id().fs_id() != fs.id() {
return Err(KernelError::Fs(FsError::CrossDevice));
}
let mut other_inode = ext4_view::Inode::read(
&fs.inner,
(inode.id().inode_id() as u32).try_into().unwrap(),
)
.await?;
let file_type = other_inode.file_type();
fs.inner
.link(&inner, name.to_string(), &mut other_inode)
.await?;
Ok(())
}
async fn unlink(&self, name: &str) -> Result<()> {
let inner = self.inner.lock().await;
if inner.file_type() != ext4_view::FileType::Directory {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
let child_inode = get_dir_entry_inode_by_name(
&fs.inner,
&inner,
ext4_view::DirEntryName::try_from(name)
.map_err(|_| KernelError::Fs(FsError::InvalidInput))?,
)
.await?;
fs.inner
.unlink(&inner, name.to_string(), child_inode)
.await?;
Ok(())
}
async fn readdir(&self, start_offset: u64) -> Result<Box<dyn DirStream>> {
let inner = self.inner.lock().await;
if inner.file_type() != ext4_view::FileType::Directory {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
Ok(Box::new(ReadDirWrapper::new(
ReadDir::new(fs.inner.clone(), &inner, self.path.clone())?,
fs.id(),
start_offset,
)))
}
async fn readlink(&self) -> Result<PathBuf> {
let inner = self.inner.lock().await;
if inner.file_type() != ext4_view::FileType::Symlink {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
// Conversion has to ensure path is valid UTF-8 (O(n) time).
Ok(inner
.symlink_target(&fs.inner)
.await
.map(|p| PathBuf::from(p.to_str().unwrap()))?)
}
async fn rename_from(
&self,
old_parent: Arc<dyn Inode>,
old_name: &str,
new_name: &str,
no_replace: bool,
) -> Result<()> {
if old_name == new_name && old_parent.id().inode_id() == self.id().inode_id() {
return Ok(());
}
let old_parent_id = old_parent.id();
let fs = self.fs_ref.upgrade().unwrap();
let old_parent_inode = ext4_view::Inode::read(
&fs.inner,
(old_parent_id.inode_id() as u32).try_into().unwrap(),
)
.await?;
let inner = self.inner.lock().await;
// inode being moved (source)
let mut child_inode = get_dir_entry_inode_by_name(
&fs.inner,
&old_parent_inode,
ext4_view::DirEntryName::try_from(old_name)
.map_err(|_| KernelError::Fs(FsError::InvalidInput))?,
)
.await?;
// Check if destination exists and handle overwrite constraints.
let dst_lookup = get_dir_entry_inode_by_name(
&fs.inner,
&inner,
ext4_view::DirEntryName::try_from(new_name)
.map_err(|_| KernelError::Fs(FsError::InvalidInput))?,
)
.await;
if no_replace && dst_lookup.is_ok() {
return Err(KernelError::Fs(FsError::AlreadyExists));
}
if let Ok(target_inode) = dst_lookup {
let target_kind = target_inode.file_type();
let source_kind = child_inode.file_type();
if target_kind == ext4_view::FileType::Directory {
let target_is_empty = fs
.inner
.read_dir(&self.path.join(new_name))
.await?
.all(|e| {
let Ok(entry) = e else {
// If we fail to read the directory, be conservative and treat it as non-empty.
return false;
};
let name = entry.file_name().as_str().unwrap();
name == "." || name == ".."
})
.await;
if !target_is_empty {
return Err(KernelError::Fs(FsError::DirectoryNotEmpty));
}
if source_kind != ext4_view::FileType::Directory {
return Err(KernelError::Fs(FsError::IsADirectory));
}
} else if source_kind == ext4_view::FileType::Directory {
// Can't replace non-directory with a directory.
return Err(KernelError::Fs(FsError::NotADirectory));
}
// Overwrite: remove destination entry first.
fs.inner
.unlink(&inner, new_name.to_string(), target_inode)
.await?;
}
// Link into destination parent, then unlink from source parent.
fs.inner
.link(&inner, new_name.to_string(), &mut child_inode)
.await?;
fs.inner
.unlink(&old_parent_inode, old_name.to_string(), child_inode)
.await?;
Ok(())
}
async fn symlink(&self, name: &str, target: &Path) -> Result<()> {
let inner = self.inner.lock().await;
if inner.file_type() != ext4_view::FileType::Directory {
return Err(KernelError::NotSupported);
}
let fs = self.fs_ref.upgrade().unwrap();
fs.inner
.symlink(
&inner,
name.to_string(),
ext4_view::PathBuf::new(target.as_str().as_bytes()),
0,
0,
Duration::from_secs(0),
)
.await?;
Ok(())
}
async fn sync(&self) -> Result<()> {
let mut inner = self.inner.lock().await;
let fs = self.fs_ref.upgrade().ok_or(FsError::InvalidFs)?;
inner.write(&fs.inner).await?;
Ok(())
}
}
/// An EXT4 filesystem instance.
///
/// For now this struct only stores the underlying block buffer and an ID
/// assigned by the VFS when the filesystem is mounted.
pub struct Ext4Filesystem<CPU: CpuOps> {
inner: Ext4,
id: u64,
this: Weak<Ext4Filesystem<CPU>>,
dev: Arc<BlockBuffer>,
_phantom_data: PhantomData<CPU>,
}
impl<CPU> Ext4Filesystem<CPU>
where
CPU: CpuOps + Send + Sync,
{
/// Construct a new EXT4 filesystem instance.
pub async fn new(dev: BlockBuffer, id: u64) -> Result<Arc<Self>> {
let dev_arc = Arc::new(dev);
let inner =
Ext4::load_with_writer(Box::new(dev_arc.clone()), Some(Box::new(dev_arc.clone())))
.await?;
Ok(Arc::new_cyclic(|weak| Self {
inner,
id,
this: weak.clone(),
dev: dev_arc,
_phantom_data: PhantomData,
}))
}
}
#[async_trait]
impl<CPU> Filesystem for Ext4Filesystem<CPU>
where
CPU: CpuOps + Send + Sync,
{
fn id(&self) -> u64 {
self.id
}
fn magic(&self) -> u64 {
// TODO: retrieve magic from superblock instead of hardcoding
0xef53 // EXT4 magic number
}
/// Returns the root inode of the mounted EXT4 filesystem.
async fn root_inode(&self) -> Result<Arc<dyn Inode>> {
let root = self.inner.read_root_inode().await?;
Ok(Arc::new(Ext4Inode::<CPU> {
fs_ref: self.this.clone(),
id: root.index,
inner: Mutex::new(root),
path: ext4_view::PathBuf::new("/"),
}))
}
/// Flushes any dirty data to the underlying block device. The current
/// stub implementation simply forwards the request to `BlockBuffer::sync`.
async fn sync(&self) -> Result<()> {
self.dev.sync().await?;
Ok(())
}
}