use linuxutils_common::man::ManContent;
pub const MAN: ManContent = ManContent::empty();
use clap::Parser;
use rustix::{
fd::AsFd,
fs::{FallocateFlags, Mode, OFlags},
};
use std::{
io::{Read, Seek, SeekFrom},
process::ExitCode,
};
#[derive(Parser)]
#[command(
name = "fallocate",
about = "Preallocate or deallocate space to a file",
override_usage = "fallocate [options] <filename>"
)]
pub struct Args {
#[arg(short, long, value_name = "length")]
length: Option<String>,
#[arg(short, long, value_name = "offset", default_value = "0")]
offset: String,
#[arg(short = 'n', long)]
keep_size: bool,
#[arg(short = 'c', long, conflicts_with_all = ["punch_hole", "zero_range", "dig_holes", "insert_range", "posix"])]
collapse_range: bool,
#[arg(short = 'd', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "insert_range", "posix"])]
dig_holes: bool,
#[arg(short = 'i', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "dig_holes", "posix"])]
insert_range: bool,
#[arg(short = 'p', long, conflicts_with_all = ["collapse_range", "zero_range", "dig_holes", "insert_range", "posix"])]
punch_hole: bool,
#[arg(short = 'z', long, conflicts_with_all = ["collapse_range", "punch_hole", "dig_holes", "insert_range", "posix"])]
zero_range: bool,
#[arg(short = 'x', long, conflicts_with_all = ["collapse_range", "punch_hole", "zero_range", "dig_holes", "insert_range"])]
posix: bool,
#[arg(short, long)]
verbose: bool,
#[arg(required = true)]
filename: String,
}
fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty size string".to_string());
}
let num_end = s
.find(|c: char| !c.is_ascii_digit() && c != '.')
.unwrap_or(s.len());
let (num_str, suffix) = s.split_at(num_end);
let base: u64 = num_str
.parse()
.map_err(|e| format!("invalid number '{num_str}': {e}"))?;
let multiplier: u64 = match suffix {
"" | "B" => 1,
"K" | "KiB" => 1024,
"M" | "MiB" => 1024 * 1024,
"G" | "GiB" => 1024 * 1024 * 1024,
"T" | "TiB" => 1024u64 * 1024 * 1024 * 1024,
"P" | "PiB" => 1024u64 * 1024 * 1024 * 1024 * 1024,
"E" | "EiB" => 1024u64 * 1024 * 1024 * 1024 * 1024 * 1024,
"KB" => 1000,
"MB" => 1000 * 1000,
"GB" => 1000 * 1000 * 1000,
"TB" => 1000u64 * 1000 * 1000 * 1000,
"PB" => 1000u64 * 1000 * 1000 * 1000 * 1000,
"EB" => 1000u64 * 1000 * 1000 * 1000 * 1000 * 1000,
other => return Err(format!("unknown size suffix '{other}'")),
};
base.checked_mul(multiplier)
.ok_or_else(|| format!("size overflow: {s}"))
}
fn dig_holes(
fd: &std::fs::File,
offset: u64,
length: u64,
verbose: bool,
) -> Result<(), String> {
let file_size = fd
.metadata()
.map_err(|e| format!("failed to get file metadata: {e}"))?
.len();
let end = if length == 0 {
file_size
} else {
offset.saturating_add(length).min(file_size)
};
let mut pos = offset;
let mut buf = vec![0u8; 64 * 1024];
let mut reader = std::io::BufReader::new(fd);
reader
.seek(SeekFrom::Start(pos))
.map_err(|e| format!("seek failed: {e}"))?;
while pos < end {
let to_read = ((end - pos) as usize).min(buf.len());
let n = reader
.read(&mut buf[..to_read])
.map_err(|e| format!("read failed: {e}"))?;
if n == 0 {
break;
}
let chunk = &buf[..n];
let mut i = 0;
while i < chunk.len() {
if chunk[i] == 0 {
let zero_start = i;
while i < chunk.len() && chunk[i] == 0 {
i += 1;
}
let zero_len = i - zero_start;
if zero_len >= 4096 {
let hole_offset = pos + zero_start as u64;
let hole_len = zero_len as u64;
let flags =
FallocateFlags::PUNCH_HOLE | FallocateFlags::KEEP_SIZE;
if let Err(e) = rustix::fs::fallocate(
fd.as_fd(),
flags,
hole_offset,
hole_len,
) {
if verbose {
eprintln!(
"fallocate: punch hole at offset {hole_offset}, length {hole_len}: {e}"
);
}
} else if verbose {
eprintln!(
"fallocate: punched hole at offset {hole_offset}, length {hole_len}"
);
}
}
} else {
i += 1;
}
}
pos += n as u64;
}
Ok(())
}
pub fn run(args: Args) -> ExitCode {
let offset = match parse_size(&args.offset) {
Ok(v) => v,
Err(e) => {
eprintln!("fallocate: invalid offset: {e}");
return ExitCode::FAILURE;
}
};
let length = if args.dig_holes {
match &args.length {
Some(s) => match parse_size(s) {
Ok(v) => v,
Err(e) => {
eprintln!("fallocate: invalid length: {e}");
return ExitCode::FAILURE;
}
},
None => 0, }
} else {
match &args.length {
Some(s) => match parse_size(s) {
Ok(v) => v,
Err(e) => {
eprintln!("fallocate: invalid length: {e}");
return ExitCode::FAILURE;
}
},
None => {
eprintln!("fallocate: required argument --length not provided");
return ExitCode::FAILURE;
}
}
};
let oflags = if args.dig_holes {
OFlags::RDWR
} else {
OFlags::RDWR | OFlags::CREATE
};
let fd = match rustix::fs::open(
&args.filename,
oflags,
Mode::RUSR | Mode::WUSR | Mode::RGRP | Mode::ROTH,
) {
Ok(fd) => fd,
Err(e) => {
eprintln!("fallocate: {}: {e}", args.filename);
return ExitCode::FAILURE;
}
};
if args.dig_holes {
let file = std::fs::File::from(fd);
if let Err(e) = dig_holes(&file, offset, length, args.verbose) {
eprintln!("fallocate: {e}");
return ExitCode::FAILURE;
}
if args.verbose {
eprintln!("fallocate: {}: dig holes complete", args.filename);
}
return ExitCode::SUCCESS;
}
let flags = if args.punch_hole {
FallocateFlags::PUNCH_HOLE | FallocateFlags::KEEP_SIZE
} else if args.collapse_range {
FallocateFlags::COLLAPSE_RANGE
} else if args.insert_range {
FallocateFlags::INSERT_RANGE
} else if args.zero_range {
if args.keep_size {
FallocateFlags::ZERO_RANGE | FallocateFlags::KEEP_SIZE
} else {
FallocateFlags::ZERO_RANGE
}
} else if args.keep_size {
FallocateFlags::KEEP_SIZE
} else {
FallocateFlags::empty()
};
if args.verbose {
let mode = if args.punch_hole {
"punch hole"
} else if args.collapse_range {
"collapse range"
} else if args.insert_range {
"insert range"
} else if args.zero_range {
"zero range"
} else if args.posix {
"posix allocate"
} else {
"allocate"
};
eprintln!(
"fallocate: {}: {mode} offset {offset}, length {length}",
args.filename,
);
}
if let Err(e) = rustix::fs::fallocate(fd.as_fd(), flags, offset, length) {
eprintln!("fallocate: fallocate failed: {e}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_size_plain() {
assert_eq!(parse_size("1024").unwrap(), 1024);
}
#[test]
fn parse_size_k() {
assert_eq!(parse_size("1K").unwrap(), 1024);
}
#[test]
fn parse_size_kib() {
assert_eq!(parse_size("1KiB").unwrap(), 1024);
}
#[test]
fn parse_size_kb() {
assert_eq!(parse_size("1KB").unwrap(), 1000);
}
#[test]
fn parse_size_m() {
assert_eq!(parse_size("1M").unwrap(), 1024 * 1024);
}
#[test]
fn parse_size_mib() {
assert_eq!(parse_size("1MiB").unwrap(), 1024 * 1024);
}
#[test]
fn parse_size_mb() {
assert_eq!(parse_size("1MB").unwrap(), 1_000_000);
}
#[test]
fn parse_size_g() {
assert_eq!(parse_size("1G").unwrap(), 1024 * 1024 * 1024);
}
#[test]
fn parse_size_invalid_suffix() {
assert!(parse_size("1X").is_err());
}
#[test]
fn parse_size_empty() {
assert!(parse_size("").is_err());
}
}