use anyhow::{anyhow, bail, Result};
use std::path::Path;
use rsext4::{
entries::DirEntryIterator,
file::{delete_dir, delete_file, read_file, rename, truncate, write_file},
loopfile::{get_file_inode, resolve_inode_block_allextend},
mkfs, Ext4FileSystem, Jbd2Dev, BLOCK_SIZE,
};
use rsext4::disknode::Ext4Inode;
use super::super::io::PartitionBlockDev;
use super::super::types::{DirEntry, PartitionTarget};
use super::super::utils::{iter_path_components, normalize_image_path};
use super::FsOps;
pub struct Ext4Ops<'a> {
jbd: &'a mut Jbd2Dev<PartitionBlockDev>,
fs: &'a mut Ext4FileSystem,
}
pub fn mkfs_ext4(disk: &Path, target: &PartitionTarget, label: Option<&str>) -> Result<()> {
if label.is_some() {
eprintln!("ext4 label not supported, ignoring --label");
}
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(disk)
.map_err(|e| anyhow!("failed to open disk {}: {e}", disk.display()))?;
let block_size = BLOCK_SIZE as u64;
let usable = target.size_bytes - (target.size_bytes % block_size);
if usable < block_size * 16 {
bail!("partition too small for ext4");
}
let total_blocks = usable / block_size;
let dev = PartitionBlockDev::new(file, target.offset_bytes, total_blocks, BLOCK_SIZE as u32);
let mut jbd = Jbd2Dev::initial_jbd2dev(0, dev, false);
mkfs(&mut jbd).map_err(|e| anyhow!("mkfs ext4 failed: {e:?}"))?;
jbd.cantflush()
.map_err(|e| anyhow!("flush failed: {e:?}"))?;
Ok(())
}
pub fn with_ext4<R>(
disk: &Path,
target: &PartitionTarget,
f: impl for<'a> FnOnce(Ext4Ops<'a>) -> Result<R>,
) -> Result<R> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(disk)
.map_err(|e| anyhow!("failed to open disk {}: {e}", disk.display()))?;
let block_size = BLOCK_SIZE as u64;
let usable = target.size_bytes - (target.size_bytes % block_size);
if usable < block_size * 2 {
bail!("partition too small for ext4");
}
let total_blocks = usable / block_size;
let dev = PartitionBlockDev::new(file, target.offset_bytes, total_blocks, BLOCK_SIZE as u32);
let mut jbd = Jbd2Dev::initial_jbd2dev(0, dev, false);
let mut fs = Ext4FileSystem::mount(&mut jbd)
.map_err(|e| anyhow!("mount ext4 failed: {e:?}"))?;
let result = f(Ext4Ops { jbd: &mut jbd, fs: &mut fs })?;
fs.umount(&mut jbd)
.map_err(|e| anyhow!("umount failed: {e:?}"))?;
jbd.cantflush()
.map_err(|e| anyhow!("flush failed: {e:?}"))?;
Ok(result)
}
impl<'a> Ext4Ops<'a> {
fn get_dir_entries(&mut self, inode: &mut Ext4Inode) -> Result<Vec<(u32, String, bool)>> {
let blocks = resolve_inode_block_allextend(self.fs, self.jbd, inode)
.map_err(|e| anyhow!("resolve dir blocks failed: {e:?}"))?;
let mut entries = Vec::new();
for phys in blocks.values() {
let mut raw_entries = Vec::new();
{
let cached = self
.fs
.datablock_cache
.get_or_load(self.jbd, *phys)
.map_err(|e| anyhow!("load block failed: {e:?}"))?;
let data = &cached.data[..BLOCK_SIZE];
let iter = DirEntryIterator::new(data);
for (entry, _) in iter {
if entry.is_dot() || entry.is_dotdot() {
continue;
}
if entry.inode == 0 {
continue;
}
let name = entry
.name_str()
.map(|s| s.to_string())
.unwrap_or_else(|| String::from_utf8_lossy(entry.name).to_string());
raw_entries.push((entry.inode, name));
}
}
for (inode_num, name) in raw_entries {
let child_inode = self
.fs
.get_inode_by_num(self.jbd, inode_num)
.map_err(|e| anyhow!("inode read failed: {e:?}"))?;
entries.push((inode_num, name, child_inode.is_dir()));
}
}
Ok(entries)
}
fn resolve_path(&mut self, path: &str) -> Result<Ext4Inode> {
if path == "/" {
let (_, root) = get_file_inode(self.fs, self.jbd, "/")
.map_err(|e| anyhow!("root lookup failed: {e:?}"))?
.ok_or_else(|| anyhow!("root not found"))?;
return Ok(root);
}
let mut current_inode = {
let (_, root) = get_file_inode(self.fs, self.jbd, "/")
.map_err(|e| anyhow!("root lookup failed: {e:?}"))?
.ok_or_else(|| anyhow!("root not found"))?;
root
};
let normalized = normalize_image_path(path);
let parts: Vec<&str> = normalized.split('/').filter(|s| !s.is_empty()).collect();
for part in parts {
if !current_inode.is_dir() {
bail!("not a directory");
}
let entries = self.get_dir_entries(&mut current_inode)?;
let mut found_inode_num = None;
for (inum, name, _) in entries {
if name == part {
found_inode_num = Some(inum);
break;
}
}
match found_inode_num {
Some(num) => {
current_inode = self
.fs
.get_inode_by_num(self.jbd, num)
.map_err(|e| anyhow!("inode read failed: {e:?}"))?;
}
None => bail!("path not found: {}", path),
}
}
Ok(current_inode)
}
}
impl FsOps for Ext4Ops<'_> {
fn list_dir(&mut self, path: &str) -> Result<Vec<DirEntry>> {
let mut inode = self.resolve_path(path)?;
if !inode.is_dir() {
bail!("not a directory");
}
let entries = self.get_dir_entries(&mut inode)?;
let mut res = Vec::new();
for (_, name, is_dir) in entries {
res.push(DirEntry { name, is_dir });
}
res.sort_by(|a, b| a.name.cmp(&b.name));
Ok(res)
}
fn read_file(&mut self, path: &str, offset: u64, bytes: Option<usize>) -> Result<Vec<u8>> {
let _ = self.resolve_path(path)?;
let data = read_file(self.jbd, self.fs, path)
.map_err(|e| anyhow!("read failed: {e:?}"))?
.ok_or_else(|| anyhow!("file not found (read)"))?;
let start = offset as usize;
if start >= data.len() {
return Ok(Vec::new());
}
let end = bytes.map_or(data.len(), |n| (start + n).min(data.len()));
Ok(data[start..end].to_vec())
}
fn write_file(&mut self, path: &str, data: &[u8], force: bool) -> Result<()> {
let exists = match self.resolve_path(path) {
Ok(_) => true,
Err(_) => false, };
if exists {
if !force {
bail!("destination exists, use -f to overwrite");
}
truncate(self.jbd, self.fs, path, 0).map_err(|e| anyhow!("truncate failed: {e:?}"))?;
} else {
rsext4::mkfile(self.jbd, self.fs, path, None, None)
.ok_or_else(|| anyhow!("mkfile failed for path: {}", path))?;
}
write_file(self.jbd, self.fs, path, 0, data)
.map_err(|e| anyhow!("write failed: {e:?}"))?;
Ok(())
}
fn mkdir(&mut self, path: &str, parents: bool) -> Result<()> {
if parents {
for p in iter_path_components(path) {
let _ = rsext4::mkdir(self.jbd, self.fs, &p);
}
return Ok(());
}
rsext4::mkdir(self.jbd, self.fs, path).ok_or_else(|| anyhow!("mkdir failed"))?;
Ok(())
}
fn rm(&mut self, path: &str, recursive: bool) -> Result<()> {
let inode = self.resolve_path(path)?;
if inode.is_dir() {
if !recursive {
bail!("directory requires -r");
}
delete_dir(self.fs, self.jbd, path);
return Ok(());
}
delete_file(self.fs, self.jbd, path);
Ok(())
}
fn mv(&mut self, src: &str, dst: &str, force: bool) -> Result<()> {
if !force
&& self.resolve_path(dst).is_ok()
{
bail!("destination exists, use -f to overwrite");
}
rename(self.jbd, self.fs, src, dst).map_err(|e| anyhow!("rename failed: {e:?}"))?;
Ok(())
}
fn is_dir(&mut self, path: &str) -> Result<bool> {
let inode = self.resolve_path(path)?;
Ok(inode.is_dir())
}
}