use fs_ext4::block_io::{BlockDevice, FileDevice};
use fs_ext4::mkfs::{format_filesystem, is_valid_block_size, MAX_BLOCK_SIZE, MIN_BLOCK_SIZE};
use std::process::ExitCode;
const IGNORED_FLAGS_WITH_ARG: &[&str] = &["-m", "-N", "-i", "-E", "-O", "-T"];
const IGNORED_BOOLEAN_FLAGS: &[&str] = &["-c"];
const USAGE: &str = "\
Usage: mkfs.ext4 [options] device
Options:
-L <label> Volume label (max 16 bytes UTF-8).
-b <size> Block size in bytes. Power of 2, 1024..=65536. Default: 4096.
-U <uuid> Volume UUID (32 hex chars, dashes optional). Default: random.
-F Force; format even if device looks in use. (Accepted; we do
not currently inspect for active mounts.)
-n Dry-run: parse args + open device but do not write.
-q Quiet (suppress non-error output).
--create-size <SIZE>
Non-standard extension (not in the conventional CLI): if device
doesn't exist, create it as a regular file of the given size first.
SIZE accepts K/M/G/T suffixes (1024-based). Refuses to apply
to existing block devices — only valid for image files. Use
when scripting test pipelines so you don't have to chain
truncate + mkfs.ext4. Without this flag the tool follows
the standard CLI convention exactly (file must pre-exist).
-V, --version Print version and exit.
-h, --help Print this help and exit.
Positional:
device Path to a block device or pre-sized regular file. The
file/device MUST already exist at the target size unless
--create-size is given. Pre-create with
truncate -s 64M out.img (Linux/macOS)
fsutil file createnew out.img 67108864 (Windows)
Unsupported flags from the standard CLI are accepted with a warning, and rejected
as errors otherwise. Two groups, because they parse differently: -m, -N, -i, -E,
-O and -T each consume the argument that follows them, while -c takes none. The
full feature set will land incrementally as the underlying crate grows.
";
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(msg) => {
eprintln!("mkfs.ext4: {msg}");
ExitCode::FAILURE
}
}
}
#[derive(Default, Debug)]
struct Opts {
label: Option<String>,
block_size: Option<u32>,
uuid: Option<[u8; 16]>,
force: bool,
dry_run: bool,
quiet: bool,
create_size: Option<u64>,
device: Option<String>,
warnings: Vec<String>,
}
fn run() -> Result<(), String> {
let opts = parse_args()?;
if !opts.quiet {
for warning in &opts.warnings {
eprintln!("mkfs.ext4: warning: {warning}");
}
}
let device = opts
.device
.as_deref()
.ok_or_else(|| format!("missing positional <device> argument\n\n{USAGE}"))?;
let block_size = opts.block_size.unwrap_or(fs_ext4::mkfs::DEFAULT_BLOCK_SIZE);
if let Some(n) = opts.create_size {
match std::fs::metadata(device) {
Ok(meta) => {
let ft = meta.file_type();
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if ft.is_block_device() || ft.is_char_device() {
return Err(format!(
"--create-size refuses to apply to {device}: looks like a real block/char device, \
not a regular file. Did you mean to leave --create-size off?"
));
}
}
if !ft.is_file() {
return Err(format!(
"--create-size: {device} exists but is not a regular file"
));
}
if !opts.quiet {
eprintln!(
"mkfs.ext4: --create-size: {device} already exists ({} bytes); leaving as-is",
meta.len()
);
}
}
Err(_) => {
let f = std::fs::File::create(device)
.map_err(|e| format!("--create-size: create {device}: {e}"))?;
f.set_len(n)
.map_err(|e| format!("--create-size: set_len({n}) on {device}: {e}"))?;
drop(f);
if !opts.quiet {
eprintln!("mkfs.ext4: --create-size: created {device} ({n} bytes)");
}
}
}
}
let dev =
FileDevice::open_rw(device).map_err(|e| format!("open {device} read-write: {e:?}"))?;
let size = dev.size_bytes();
if size == 0 {
return Err(format!(
"device {device} reports size 0 — pre-create with truncate / fsutil first"
));
}
if !opts.quiet {
eprintln!(
"mkfs.ext4: formatting {device} ({size} bytes, block_size={block_size}{})",
if opts.dry_run { ", dry-run" } else { "" }
);
}
if opts.dry_run {
if !opts.quiet {
eprintln!("mkfs.ext4: dry-run — no writes performed");
}
let _ = opts.force; return Ok(());
}
format_filesystem(&dev, opts.label.as_deref(), opts.uuid, size, block_size)
.map_err(|e| format!("format failed: {e:?}"))?;
dev.flush().map_err(|e| format!("flush failed: {e:?}"))?;
if !opts.quiet {
eprintln!("mkfs.ext4: {device} formatted successfully");
}
Ok(())
}
fn parse_args() -> Result<Opts, String> {
parse_args_from(std::env::args().skip(1))
}
fn parse_args_from(mut args: impl Iterator<Item = String>) -> Result<Opts, String> {
let mut opts = Opts::default();
while let Some(arg) = args.next() {
match arg.as_str() {
"-h" | "--help" => {
print!("{USAGE}");
std::process::exit(0);
}
"-V" | "--version" => {
println!("mkfs.ext4 (fs-ext4) {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
"-L" => {
let v = args
.next()
.ok_or_else(|| "-L requires a label argument".to_string())?;
if v.len() > 16 {
return Err(format!(
"label too long ({} bytes); ext4 max is 16 bytes UTF-8",
v.len()
));
}
opts.label = Some(v);
}
"-b" => {
let v = args
.next()
.ok_or_else(|| "-b requires a block size argument".to_string())?;
let n: u32 = v
.parse()
.map_err(|_| format!("-b: not a valid number: {v}"))?;
if !is_valid_block_size(n) {
return Err(format!(
"-b: block size must be a power of two in \
{MIN_BLOCK_SIZE}..={MAX_BLOCK_SIZE}, got {n}"
));
}
opts.block_size = Some(n);
}
"-U" => {
let v = args
.next()
.ok_or_else(|| "-U requires a UUID argument".to_string())?;
opts.uuid = Some(parse_uuid(&v)?);
}
"-F" => opts.force = true,
"-n" => opts.dry_run = true,
"-q" => opts.quiet = true,
"--create-size" => {
let v = args.next().ok_or_else(|| {
"--create-size requires a SIZE argument (e.g. 64M)".to_string()
})?;
opts.create_size = Some(parse_size(&v)?);
}
other if IGNORED_FLAGS_WITH_ARG.contains(&other) => {
let v = args
.next()
.ok_or_else(|| format!("{other} requires an argument"))?;
opts.warnings
.push(format!("{other} {v} not yet honored, ignoring"));
}
other if IGNORED_BOOLEAN_FLAGS.contains(&other) => {
opts.warnings
.push(format!("{other} not yet honored, ignoring"));
}
other if other.starts_with('-') => {
return Err(format!("unknown flag: {other}\n\n{USAGE}"));
}
_ => {
if opts.device.is_some() {
return Err(format!(
"extra positional argument: {arg} (only one device may be given)"
));
}
opts.device = Some(arg);
}
}
}
Ok(opts)
}
fn parse_size(s: &str) -> Result<u64, String> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err("--create-size: empty size argument".to_string());
}
let s = trimmed.strip_suffix(['B', 'b']).unwrap_or(trimmed);
let (num, mult): (&str, u64) = match s.chars().last() {
Some('K' | 'k') => (&s[..s.len() - 1], 1024),
Some('M' | 'm') => (&s[..s.len() - 1], 1024 * 1024),
Some('G' | 'g') => (&s[..s.len() - 1], 1024 * 1024 * 1024),
Some('T' | 't') => (&s[..s.len() - 1], 1024 * 1024 * 1024 * 1024),
Some(c) if c.is_ascii_digit() => (s, 1),
_ => return Err(format!("--create-size: unrecognised size suffix in {s:?}")),
};
let n: u64 = num
.parse()
.map_err(|_| format!("--create-size: not a valid number: {num:?}"))?;
n.checked_mul(mult)
.ok_or_else(|| format!("--create-size: {s} overflows u64"))
}
fn parse_uuid(s: &str) -> Result<[u8; 16], String> {
let cleaned: String = s.chars().filter(|c| *c != '-').collect();
if cleaned.len() != 32 {
return Err(format!(
"UUID must be 32 hex chars (with optional dashes), got {} chars",
cleaned.len()
));
}
let mut out = [0u8; 16];
for i in 0..16 {
out[i] = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16)
.map_err(|_| format!("UUID has non-hex character near position {}", i * 2))?;
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(argv: &[&str]) -> Result<Opts, String> {
parse_args_from(argv.iter().map(|s| (*s).to_string()))
}
#[test]
fn dash_c_is_boolean_and_leaves_the_device_alone() {
let opts = parse(&["-c", "/tmp/disk.img"]).expect("parse");
assert_eq!(opts.device.as_deref(), Some("/tmp/disk.img"));
assert_eq!(opts.warnings.len(), 1, "one ignored-flag warning");
assert!(opts.warnings[0].starts_with("-c "), "{:?}", opts.warnings);
}
#[test]
fn argument_taking_ignored_flags_consume_their_value() {
for &flag in IGNORED_FLAGS_WITH_ARG {
let opts = parse(&[flag, "1", "/tmp/disk.img"]).expect("parse");
assert_eq!(
opts.device.as_deref(),
Some("/tmp/disk.img"),
"{flag} should have eaten its own value, not the device"
);
assert_eq!(
opts.warnings,
vec![format!("{flag} 1 not yet honored, ignoring")]
);
}
}
#[test]
fn argument_taking_ignored_flag_without_a_value_is_an_error() {
let err = parse(&["-m"]).expect_err("should reject");
assert!(err.contains("-m requires an argument"), "{err}");
}
#[test]
fn out_of_range_block_size_is_rejected_at_parse_time() {
for bad in ["3000", "512", "131072", "0"] {
let err = parse(&["-b", bad, "/tmp/disk.img"])
.expect_err("out-of-range block size should be rejected");
assert!(
err.starts_with("-b: block size must be a power of two"),
"{bad}: {err}"
);
}
}
#[test]
fn in_range_block_sizes_parse() {
for good in ["1024", "4096", "65536"] {
let opts = parse(&["-b", good, "/tmp/disk.img"]).expect("parse");
assert_eq!(opts.block_size, Some(good.parse().unwrap()));
}
}
#[test]
fn quiet_is_independent_of_flag_order() {
let early = parse(&["-q", "-m", "1", "/tmp/disk.img"]).expect("parse");
let late = parse(&["-m", "1", "-q", "/tmp/disk.img"]).expect("parse");
assert!(early.quiet && late.quiet);
assert_eq!(early.warnings, late.warnings);
assert_eq!(early.warnings.len(), 1);
}
#[test]
fn unknown_flags_are_still_rejected() {
let err = parse(&["-Z", "/tmp/disk.img"]).expect_err("should reject");
assert!(err.starts_with("unknown flag: -Z"), "{err}");
}
}