d248f7d17b
derive(Default) from applets
288 lines
9.6 KiB
Rust
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)]
|
|
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());
|
|
}
|
|
}
|
|
|