use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand, ValueEnum};
use fstool::block::{BlockDevice, FileBackend};
use fstool::fs::ext::{Ext, FsKind};
#[derive(Parser, Debug)]
#[command(
name = "fstool",
version,
about = "Build and inspect disk-image filesystems (ext2/3/4, MBR, GPT)."
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
ExtBuild {
#[arg(value_name = "SRC_DIR")]
src_dir: PathBuf,
#[arg(short = 'o', long = "output", value_name = "IMAGE")]
output: PathBuf,
#[arg(long, value_enum, default_value_t = ExtKindArg::Ext2)]
kind: ExtKindArg,
#[arg(long, default_value_t = 1024)]
block_size: u32,
#[arg(long)]
sparse: bool,
},
FatBuild {
#[arg(value_name = "SRC_DIR")]
src_dir: PathBuf,
#[arg(short = 'o', long = "output", value_name = "IMAGE")]
output: PathBuf,
#[arg(long, value_name = "SIZE")]
size: String,
#[arg(long, default_value = "NO NAME")]
label: String,
#[arg(long, default_value_t = 0)]
volume_id: u32,
},
Ls {
#[arg(value_name = "IMAGE")]
image: PathBuf,
#[arg(value_name = "PATH", default_value = "/")]
path: String,
},
Cat {
#[arg(value_name = "IMAGE")]
image: PathBuf,
#[arg(value_name = "PATH")]
path: String,
},
Info {
#[arg(value_name = "IMAGE")]
image: PathBuf,
},
Build {
#[arg(value_name = "SPEC")]
spec: PathBuf,
#[arg(short = 'o', long = "output", value_name = "IMAGE")]
output: PathBuf,
},
Add {
#[arg(value_name = "IMAGE")]
image: PathBuf,
#[arg(value_name = "HOST_SRC")]
host_src: PathBuf,
#[arg(value_name = "FS_DEST")]
fs_dest: String,
},
Rm {
#[arg(value_name = "IMAGE")]
image: PathBuf,
#[arg(value_name = "FS_PATH")]
fs_path: String,
},
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum ExtKindArg {
Ext2,
Ext3,
Ext4,
}
impl From<ExtKindArg> for FsKind {
fn from(a: ExtKindArg) -> Self {
match a {
ExtKindArg::Ext2 => FsKind::Ext2,
ExtKindArg::Ext3 => FsKind::Ext3,
ExtKindArg::Ext4 => FsKind::Ext4,
}
}
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("fstool: {e}");
ExitCode::from(1)
}
}
}
fn run(cli: Cli) -> fstool::Result<()> {
match cli.command {
Command::ExtBuild {
src_dir,
output,
kind,
block_size,
sparse,
} => ext_build(&src_dir, &output, kind.into(), block_size, sparse),
Command::FatBuild {
src_dir,
output,
size,
label,
volume_id,
} => fat_build(&src_dir, &output, &size, &label, volume_id),
Command::Ls { image, path } => ls(&image, &path),
Command::Cat { image, path } => cat(&image, &path),
Command::Info { image } => info(&image),
Command::Build { spec, output } => build(&spec, &output),
Command::Add {
image,
host_src,
fs_dest,
} => add(&image, &host_src, &fs_dest),
Command::Rm { image, fs_path } => rm(&image, &fs_path),
}
}
fn rm(image: &std::path::Path, fs_path: &str) -> fstool::Result<()> {
let mut dev = FileBackend::open(image)?;
let mut ext = Ext::open(&mut dev)?;
ext.remove_path(&mut dev, fs_path)?;
ext.flush(&mut dev)?;
dev.sync()?;
eprintln!("removed {fs_path}");
Ok(())
}
fn add(image: &std::path::Path, host_src: &std::path::Path, fs_dest: &str) -> fstool::Result<()> {
use fstool::fs::{FileMeta, FileSource, Filesystem};
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::symlink_metadata(host_src)?;
let mut dev = FileBackend::open(image)?;
let mut ext = Ext::open(&mut dev)?;
let dest = std::path::Path::new(fs_dest);
if meta.is_dir() {
let fmeta = FileMeta {
mode: (meta.permissions().mode() & 0o7777) as u16,
..FileMeta::default()
};
ext.create_dir(&mut dev, dest, fmeta)?;
let dir_ino = ext.path_to_inode(&mut dev, fs_dest)?;
ext.populate_from_host_dir(&mut dev, dir_ino, host_src)?;
} else if meta.is_file() {
let fmeta = FileMeta {
mode: (meta.permissions().mode() & 0o7777) as u16,
..FileMeta::default()
};
ext.create_file(
&mut dev,
dest,
FileSource::HostPath(host_src.to_path_buf()),
fmeta,
)?;
} else {
return Err(fstool::Error::InvalidArgument(format!(
"add: {} is neither a regular file nor a directory",
host_src.display()
)));
}
ext.flush(&mut dev)?;
dev.sync()?;
eprintln!("added {} → {fs_dest}", host_src.display());
Ok(())
}
fn build(spec_path: &std::path::Path, output: &std::path::Path) -> fstool::Result<()> {
let spec = fstool::spec::Spec::parse_file(spec_path)?;
fstool::spec::build(&spec, output)?;
eprintln!("built {} from {}", output.display(), spec_path.display());
Ok(())
}
fn fat_build(
src_dir: &std::path::Path,
output: &std::path::Path,
size: &str,
label: &str,
volume_id: u32,
) -> fstool::Result<()> {
use fstool::fs::fat::Fat32;
let bytes = fstool::spec::parse_size(size)?;
let total_sectors: u32 = (bytes / 512).try_into().map_err(|_| {
fstool::Error::InvalidArgument("fat-build: --size doesn't fit in a u32 sector count".into())
})?;
let label_bytes = fat32_label_bytes(label);
let mut dev = FileBackend::create(output, bytes)?;
Fat32::build_from_host_dir(&mut dev, total_sectors, src_dir, volume_id, label_bytes)?;
dev.sync()?;
eprintln!(
"wrote {} ({} bytes, fat32, label {:?})",
output.display(),
bytes,
label
);
Ok(())
}
fn fat32_label_bytes(label: &str) -> [u8; 11] {
let mut out = [b' '; 11];
let upper = label.to_ascii_uppercase();
for (i, &b) in upper.as_bytes().iter().take(11).enumerate() {
out[i] = if b.is_ascii() && b >= 0x20 && b != 0x7F {
b
} else {
b'_'
};
}
out
}
fn ext_build(
src_dir: &std::path::Path,
output: &std::path::Path,
kind: FsKind,
block_size: u32,
sparse: bool,
) -> fstool::Result<()> {
use fstool::fs::ext::BuildPlan;
let mut plan = BuildPlan::new(block_size, kind);
plan.scan_host_path(src_dir)?;
let mut opts = plan.to_format_opts();
opts.sparse = sparse;
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(output, size)?;
let mut ext = Ext::format_with(&mut dev, &opts)?;
ext.populate_from_host_dir(&mut dev, 2, src_dir)?;
ext.flush(&mut dev)?;
dev.sync()?;
eprintln!(
"wrote {} ({} bytes, {:?}{}, {} inodes, {} blocks)",
output.display(),
size,
kind,
if sparse { ", sparse" } else { "" },
opts.inodes_count,
opts.blocks_count
);
Ok(())
}
fn ls(image: &std::path::Path, path: &str) -> fstool::Result<()> {
let mut dev = FileBackend::open(image)?;
let ext = Ext::open(&mut dev)?;
let ino = ext.path_to_inode(&mut dev, path)?;
let entries = ext.list_inode(&mut dev, ino)?;
let mut out = std::io::stdout().lock();
for e in &entries {
let _ = writeln!(out, "{}\t{:?}\t{}", e.inode, e.kind, e.name);
}
Ok(())
}
fn cat(image: &std::path::Path, path: &str) -> fstool::Result<()> {
let mut dev = FileBackend::open(image)?;
let ext = Ext::open(&mut dev)?;
let ino = ext.path_to_inode(&mut dev, path)?;
let mut reader = ext.open_file_reader(&mut dev, ino)?;
let mut out = std::io::stdout().lock();
let mut buf = [0u8; 64 * 1024];
use std::io::Read;
loop {
let n = reader.read(&mut buf).map_err(fstool::Error::from)?;
if n == 0 {
break;
}
out.write_all(&buf[..n]).map_err(fstool::Error::from)?;
}
Ok(())
}
fn info(image: &std::path::Path) -> fstool::Result<()> {
let mut dev = FileBackend::open(image)?;
let ext = Ext::open(&mut dev)?;
let sb = &ext.sb;
println!("fs kind: {:?}", ext.kind);
println!("block size: {}", sb.block_size());
println!("blocks total: {}", sb.blocks_count);
println!("blocks free: {}", sb.free_blocks_count);
println!("inodes total: {}", sb.inodes_count);
println!("inodes free: {}", sb.free_inodes_count);
println!("blocks per group: {}", sb.blocks_per_group);
println!("inodes per group: {}", sb.inodes_per_group);
println!("groups: {}", ext.layout.num_groups());
println!("first data block: {}", sb.first_data_block);
println!("revision: {}", sb.rev_level);
println!("first inode: {}", sb.first_ino);
println!("inode size: {}", sb.inode_size);
println!(
"feature flags: compat={:#010x} incompat={:#010x} ro_compat={:#010x}",
sb.feature_compat, sb.feature_incompat, sb.feature_ro_compat
);
println!("uuid: {}", format_uuid(&sb.uuid));
println!();
println!("/ listing:");
let entries = ext.list_inode(&mut dev, 2)?;
for e in &entries {
println!(" {:>6} {:?} {}", e.inode, e.kind, e.name);
}
Ok(())
}
fn format_uuid(bytes: &[u8; 16]) -> String {
let mut s = String::with_capacity(36);
for (i, b) in bytes.iter().enumerate() {
s.push_str(&format!("{b:02x}"));
if matches!(i, 3 | 5 | 7 | 9) {
s.push('-');
}
}
s
}