use std::fs::File; use { super::Cmd, clap::{Arg, ArgAction, ArgGroup, Command, ValueHint}, std::{error::Error, fmt, fs, 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> { let size = if let Some(file) = matches.get_one::("reference") { let num: i64 = fs::metadata(file)?.len().try_into()?; Size { operator: Operator::Equal, num, } } else if let Some(s) = matches.get_one::("size") { parse_size(s)? } else { unreachable!(); }; matches .get_many::("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>(()) })?; Ok(()) } fn path(&self) -> Option { Some(shitbox::Path::UsrBin) } } #[derive(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 for ParseSizeError { fn from(_value: ParseIntError) -> Self { Self::ParseIntError } } impl Error for ParseSizeError {} fn parse_size(size: &str) -> Result { if size.is_empty() { return Err(ParseSizeError::EmptySize); } let mut operator: Option = None; let mut num = vec![]; let mut multiplier: Option = 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_digit(10) => { 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, }), } }