shitbox/corebox/commands/truncate/mod.rs

288 lines
9.6 KiB
Rust

use {
super::Cmd,
clap::{Arg, ArgAction, ArgGroup, Command, ValueHint},
std::{error::Error, fmt, fs::{self, File}, num::ParseIntError, path::PathBuf},
};
#[derive(Debug, Default)]
pub struct Truncate;
impl Cmd for Truncate {
fn cli(&self) -> clap::Command {
Command::new("truncate")
.about("truncate or extend the length of files")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
Arg::new("create")
.help("do not create files if they do not exist")
.long_help(
"Do not create files if they do not exist. The truncate \
utility does not treat this as an error. No error messages are \
displayed and the exit value is not affected.",
)
.short('c')
.long("no-create")
.action(ArgAction::SetFalse),
Arg::new("reference")
.help("truncate or extend files to the length of RFILE")
.short('r')
.long("reference")
.value_name("RFILE")
.value_hint(ValueHint::FilePath)
.num_args(1),
Arg::new("size")
.help("set or adjust the file size by SIZE bytes")
.long_help(
"If the size argument is preceded by a plus sign (+), files will \
be extended by this number of bytes. If the size argument is \
preceded by a dash (-), file lengths will be reduced by no more \
than this number of bytes, to a minimum length of zero bytes. If \
the size argument is preceded by a percent sign (%), files will be \
round up to a multiple of this number of bytes. If the size argument \
is preceded by a slash sign (/), files will be round down to a \
multiple of this number of bytes, to a minimum length of zero bytes. \
Otherwise, the size argument specifies an absolute length to which all \
files should be extended or reduced as appropriate.\n\nThe size argument \
may be suffixed with one of K, M, G or T (either upper or lower case) to \
indicate a multiple of Kilobytes, Megabytes, Gigabytes or Terabytes \
respectively.",
)
.short('s')
.long("size")
.allow_hyphen_values(true)
.value_name("SIZE")
.num_args(1),
Arg::new("file").value_name("FILE").num_args(1..),
])
.group(
ArgGroup::new("args")
.args(["reference", "size"])
.required(true),
)
}
fn run(&self, matches: &clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let size = if let Some(file) = matches.get_one::<String>("reference") {
let num: i64 = fs::metadata(file)?.len().try_into()?;
Size {
operator: Operator::Equal,
num,
}
} else if let Some(s) = matches.get_one::<String>("size") {
parse_size(s)?
} else {
unreachable!();
};
matches
.get_many::<String>("file")
.unwrap()
.try_for_each(|file| {
let path = PathBuf::from(file);
if !matches.get_flag("create") && !path.exists() {
return Ok(());
}
let fd = File::options().write(true).create(true).open(&path)?;
let current: i64 = fd.metadata()?.len().try_into()?;
let len = match size.operator {
Operator::Equal => size.num,
Operator::Add => size.num + current,
Operator::Remove => current - size.num,
Operator::ModUp => {
if current % size.num == 0 {
current
} else {
let fraction = current / size.num;
(fraction + 1) * size.num
}
}
Operator::ModDown => {
if current % size.num == 0 {
current
} else {
let fraction = current / size.num;
fraction * size.num
}
}
};
unistd::ftruncate(&fd, len)?;
Ok::<(), Box<dyn Error>>(())
})?;
Ok(())
}
fn path(&self) -> Option<shitbox::Path> {
Some(shitbox::Path::UsrBin)
}
}
#[derive(Debug, PartialEq)]
enum Operator {
Equal,
Add,
Remove,
ModUp,
ModDown,
}
struct Size {
operator: Operator,
num: i64,
}
#[repr(i64)]
enum Multiplier {
Kilo = 1024,
Mega = 1024 * 1024,
Giga = 1024 * 1024 * 1024,
Tera = 1024 * 1024 * 1024 * 1024,
}
#[derive(Debug)]
pub enum ParseSizeError {
EmptySize,
InvalidChar,
ParseIntError,
}
impl fmt::Display for ParseSizeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}
impl From<ParseIntError> for ParseSizeError {
fn from(_value: ParseIntError) -> Self {
Self::ParseIntError
}
}
impl Error for ParseSizeError {}
fn parse_size(size: &str) -> Result<Size, ParseSizeError> {
if size.is_empty() {
return Err(ParseSizeError::EmptySize);
}
let mut operator: Option<Operator> = None;
let mut num = vec![];
let mut multiplier: Option<Multiplier> = None;
size.chars().try_for_each(|c| {
match c {
'+' => {
if operator.is_some() || !num.is_empty() || multiplier.is_some() {
return Err(ParseSizeError::InvalidChar);
} else {
operator = Some(Operator::Add);
}
}
'-' => {
if operator.is_some() || !num.is_empty() || multiplier.is_some() {
return Err(ParseSizeError::InvalidChar);
} else {
operator = Some(Operator::Remove);
}
}
'%' => {
if operator.is_some() || !num.is_empty() || multiplier.is_some() {
return Err(ParseSizeError::InvalidChar);
} else {
operator = Some(Operator::ModUp);
}
}
'/' => {
if operator.is_some() || !num.is_empty() || multiplier.is_some() {
return Err(ParseSizeError::InvalidChar);
} else {
operator = Some(Operator::ModDown);
}
}
'k' | 'K' => {
if multiplier.is_some() || num.is_empty() {
return Err(ParseSizeError::InvalidChar);
} else {
multiplier = Some(Multiplier::Kilo);
}
}
'm' | 'M' => {
if multiplier.is_some() || num.is_empty() {
return Err(ParseSizeError::InvalidChar);
} else {
multiplier = Some(Multiplier::Mega);
}
}
'g' | 'G' => {
if multiplier.is_some() || num.is_empty() {
return Err(ParseSizeError::InvalidChar);
} else {
multiplier = Some(Multiplier::Giga);
}
}
't' | 'T' => {
if multiplier.is_some() || num.is_empty() {
return Err(ParseSizeError::InvalidChar);
} else {
multiplier = Some(Multiplier::Tera);
}
}
ch if ch.is_ascii_digit() => {
if multiplier.is_some() {
return Err(ParseSizeError::InvalidChar);
} else {
num.push(ch as u8);
}
}
_ => return Err(ParseSizeError::InvalidChar),
}
Ok(())
})?;
let mut num: i64 = String::from_utf8(num).unwrap().parse()?;
if let Some(m) = multiplier {
match m {
Multiplier::Kilo => num *= 1024,
Multiplier::Mega => num *= 1024 * 1024,
Multiplier::Giga => num *= 1024 * 1024 * 1024,
Multiplier::Tera => num *= 1024 * 1024 * 1024 * 1024,
}
}
match operator {
Some(operator) => Ok(Size { operator, num }),
None => Ok(Size {
operator: Operator::Equal,
num,
}),
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parse_size_equal() {
let size = parse_size("10k").unwrap();
assert_eq!(size.num, 10240);
assert_eq!(size.operator, Operator::Equal);
}
#[test]
fn parse_size_add() {
let size = parse_size("+4M").unwrap();
assert_eq!(size.num, 4194304);
assert_eq!(size.operator, Operator::Add);
}
#[test]
fn parse_size_remove() {
let size = parse_size("-2G").unwrap();
assert_eq!(size.num, 2147483648);
assert_eq!(size.operator, Operator::Remove);
}
#[test]
fn parse_size_error() {
let size = parse_size("10.5");
assert!(size.is_err());
}
}