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,
},
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,
},
}
#[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,
} => ext_build(&src_dir, &output, kind.into(), block_size),
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),
}
}
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 ext_build(
src_dir: &std::path::Path,
output: &std::path::Path,
kind: FsKind,
block_size: u32,
) -> fstool::Result<()> {
use fstool::fs::ext::BuildPlan;
let mut plan = BuildPlan::new(block_size, kind);
plan.scan_host_path(src_dir)?;
let opts = plan.to_format_opts();
let size = opts.blocks_count as u64 * opts.block_size as u64;
let mut dev = FileBackend::create(output, size)?;
Ext::build_from_host_dir(&mut dev, src_dir, kind, block_size)?;
dev.sync()?;
eprintln!(
"wrote {} ({} bytes, {:?}, {} inodes, {} blocks)",
output.display(),
size,
kind,
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
}