use alloc::{
boxed::Box,
collections::{BTreeMap, BTreeSet},
sync::Arc,
};
use core::cell::OnceCell;
use axfs_ng_vfs::{
DirEntry, DirNode, Filesystem, FilesystemOps, Reference, StatFs, VfsResult, path::MAX_NAME_LEN,
};
use rsext4::{
Jbd2Dev, MountOptions, bmalloc::InodeNumber, error::Errno, superblock::Ext4Superblock,
};
use super::{Ext4Disk, Inode, util::into_vfs_err};
use crate::{
block::{BlockRegion, FsBlockDevice},
os::sync::{SleepMutex as Mutex, SleepMutexGuard as MutexGuard},
};
const EXT4_ROOT_INO: u32 = 2;
pub(crate) struct Ext4State {
pub fs: rsext4::Ext4FileSystem,
pub dev: Jbd2Dev<Ext4Disk>,
pub(crate) live_refs: BTreeMap<InodeNumber, usize>,
pub(crate) zero_link: BTreeSet<InodeNumber>,
}
impl Ext4State {
pub(crate) fn split(&mut self) -> (&mut rsext4::Ext4FileSystem, &mut Jbd2Dev<Ext4Disk>) {
let fs = &mut self.fs as *mut _;
let dev = &mut self.dev as *mut _;
unsafe { (&mut *fs, &mut *dev) }
}
pub(crate) fn inc_ref(&mut self, ino: InodeNumber) {
self.live_refs
.entry(ino)
.and_modify(|c| *c += 1)
.or_insert(1);
}
pub(crate) fn dec_ref(&mut self, ino: InodeNumber) -> bool {
use alloc::collections::btree_map::Entry;
match self.live_refs.entry(ino) {
Entry::Occupied(mut e) => {
let count = e.get_mut();
*count = count.saturating_sub(1);
if *count == 0 {
e.remove();
true
} else {
false
}
}
Entry::Vacant(_) => false,
}
}
pub(crate) fn mark_zero_link(&mut self, ino: InodeNumber) -> bool {
if self.live_refs.contains_key(&ino) {
self.zero_link.insert(ino);
false
} else {
true
}
}
pub(crate) fn is_zero_link(&self, ino: InodeNumber) -> bool {
self.zero_link.contains(&ino)
}
pub(crate) fn clear_zero_link(&mut self, ino: InodeNumber) {
self.zero_link.remove(&ino);
}
}
pub struct Ext4Filesystem {
inner: Mutex<Ext4State>,
root_dir: OnceCell<DirEntry>,
readonly: bool,
}
impl Ext4Filesystem {
pub fn new(dev: Box<dyn FsBlockDevice>, region: BlockRegion) -> VfsResult<Filesystem> {
Self::new_from_boxed(dev, region)
}
pub fn new_from_boxed(
dev: Box<dyn FsBlockDevice>,
region: BlockRegion,
) -> VfsResult<Filesystem> {
let disk = Ext4Disk::new(dev, region);
let mut dev = Jbd2Dev::initial_jbd2dev(0, disk, true);
let (fs, dev, readonly) = match rsext4::Ext4FileSystem::device_has_error_state(&mut dev) {
Ok(true) => {
warn!(
"ext4 filesystem is in error state; replaying journal then mounting read-only"
);
Self::mount_readonly_fallback(dev, true)?
}
Ok(false) => match rsext4::mount(&mut dev) {
Ok(fs) => (fs, dev, false),
Err(err) if err.code == Errno::EUCLEAN => {
warn!(
"ext4 journal replay failed with EUCLEAN; retrying read-only without \
journal replay"
);
Self::mount_readonly_fallback(dev, false)?
}
Err(err) => return Err(into_vfs_err(err)),
},
Err(err) if err.code == Errno::EUCLEAN => {
warn!(
"ext4 superblock check failed with EUCLEAN; retrying read-only without \
journal replay"
);
Self::mount_readonly_fallback(dev, false)?
}
Err(err) => return Err(into_vfs_err(err)),
};
let fs = Arc::new(Self {
inner: Mutex::new(Ext4State {
fs,
dev,
live_refs: BTreeMap::new(),
zero_link: BTreeSet::new(),
}),
root_dir: OnceCell::new(),
readonly,
});
let root_ino = InodeNumber::new(EXT4_ROOT_INO).unwrap();
fs.lock().inc_ref(root_ino);
let _ = fs.root_dir.set(DirEntry::new_dir(
|this| {
DirNode::new(Inode::new(
fs.clone(),
root_ino,
Some(this),
Some("/".into()),
))
},
Reference::root(),
));
Ok(Filesystem::new(fs))
}
fn mount_readonly_fallback(
mut dev: Jbd2Dev<Ext4Disk>,
replay_journal: bool,
) -> VfsResult<(rsext4::Ext4FileSystem, Jbd2Dev<Ext4Disk>, bool)> {
let fs = rsext4::mount_with_options(
&mut dev,
MountOptions {
readonly: true,
replay_journal,
},
)
.map_err(into_vfs_err)?;
Ok((fs, dev, true))
}
pub(crate) fn lock(&self) -> MutexGuard<'_, Ext4State> {
self.inner.lock()
}
pub(crate) fn sync_to_disk(&self) -> VfsResult<()> {
if self.readonly {
return Ok(());
}
let mut state = self.inner.lock();
let (fs, dev) = state.split();
fs.datablock_cache.flush_all(dev).map_err(into_vfs_err)?;
fs.bitmap_cache.flush_all(dev).map_err(into_vfs_err)?;
fs.inodetable_cache.flush_all(dev).map_err(into_vfs_err)?;
fs.superblock.s_state = Ext4Superblock::EXT4_VALID_FS;
fs.sync_superblock(dev).map_err(into_vfs_err)?;
fs.sync_group_descriptors(dev).map_err(into_vfs_err)?;
if dev.is_use_journal() {
dev.umount_commit();
}
dev.cantflush().map_err(into_vfs_err)
}
}
unsafe impl Send for Ext4Filesystem {}
unsafe impl Sync for Ext4Filesystem {}
impl FilesystemOps for Ext4Filesystem {
fn name(&self) -> &str {
"ext4"
}
fn is_readonly(&self) -> bool {
self.readonly
}
fn root_dir(&self) -> DirEntry {
self.root_dir.get().unwrap().clone()
}
fn stat(&self) -> VfsResult<StatFs> {
let state = self.lock();
let superblock = &state.fs.superblock;
let block_size = superblock.block_size();
let blocks = superblock.blocks_count();
let blocks_free = superblock.free_blocks_count();
Ok(StatFs {
fs_type: 0xef53,
block_size: block_size as _,
blocks,
blocks_free,
blocks_available: blocks_free,
file_count: superblock.s_inodes_count as _,
free_file_count: superblock.s_free_inodes_count as _,
name_length: MAX_NAME_LEN as _,
fragment_size: 0,
mount_flags: 0,
})
}
fn flush(&self) -> VfsResult<()> {
self.sync_to_disk()
}
}