use async_trait::async_trait;
use std::ffi::OsString;
use super::generated::truncate_args::truncate_command;
use super::{Builtin, Context, resolve_path};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Truncate;
const MAX_SIZE_BYTES: u64 = 1 << 32;
#[async_trait]
impl Builtin for Truncate {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let argv: Vec<OsString> = std::iter::once(OsString::from("truncate"))
.chain(ctx.args.iter().map(OsString::from))
.collect();
let cmd = truncate_command().help_template("Usage: {usage}\n{about}\n\n{all-args}\n");
let matches = match cmd.try_get_matches_from(argv) {
Ok(m) => m,
Err(e) => {
let kind = e.kind();
let rendered = e.render().to_string();
if matches!(
kind,
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
) {
return Ok(ExecResult::ok(rendered));
}
return Ok(ExecResult::err(rendered, 2));
}
};
if matches.get_flag("io-blocks") {
return Ok(ExecResult::err(
"truncate: --io-blocks not yet implemented in bashkit\n".to_string(),
1,
));
}
let no_create = matches.get_flag("no-create");
let target_spec: Option<TargetSize> =
if let Some(rfile) = matches.get_one::<String>("reference") {
let path = resolve_path(ctx.cwd, rfile);
let meta = match ctx.fs.stat(&path).await {
Ok(m) => m,
Err(e) => {
return Ok(ExecResult::err(
format!(
"truncate: cannot stat reference '{}': {}\n",
rfile,
error_message(&e)
),
1,
));
}
};
Some(TargetSize::Absolute(meta.size))
} else if let Some(spec) = matches.get_one::<String>("size") {
match parse_size(spec) {
Ok(t) => Some(t),
Err(e) => return Ok(ExecResult::err(format!("truncate: {e}\n"), 1)),
}
} else {
None
};
let Some(target_spec) = target_spec else {
return Ok(ExecResult::err(
"truncate: you must specify either --size or --reference\n".to_string(),
1,
));
};
let files: Vec<String> = matches
.get_many::<OsString>("files")
.map(|vs| vs.map(|v| v.to_string_lossy().into_owned()).collect())
.unwrap_or_default();
for file in &files {
let path = resolve_path(ctx.cwd, file);
let exists = ctx.fs.exists(&path).await.unwrap_or(false);
if !exists && no_create {
continue;
}
let current = if exists {
match ctx.fs.read_file(&path).await {
Ok(b) => b,
Err(e) => {
return Ok(ExecResult::err(
format!(
"truncate: cannot open '{}' for reading: {}\n",
file,
error_message(&e)
),
1,
));
}
}
} else {
Vec::new()
};
let new_len = match target_spec.resolve(current.len() as u64) {
Ok(n) => n,
Err(e) => return Ok(ExecResult::err(format!("truncate: {e}\n"), 1)),
};
if new_len > MAX_SIZE_BYTES {
return Ok(ExecResult::err(
format!("truncate: target size {new_len} exceeds VFS limit\n"),
1,
));
}
let mut next = current;
let new_len_usize = new_len as usize;
if next.len() > new_len_usize {
next.truncate(new_len_usize);
} else {
next.resize(new_len_usize, 0);
}
if let Err(e) = ctx.fs.write_file(&path, &next).await {
return Ok(ExecResult::err(
format!("truncate: cannot write '{}': {}\n", file, error_message(&e)),
1,
));
}
}
Ok(ExecResult::ok(String::new()))
}
}
fn error_message(e: &crate::error::Error) -> String {
e.to_string()
}
#[derive(Debug, Clone, Copy)]
enum TargetSize {
Absolute(u64),
ExtendBy(u64),
ReduceBy(u64),
AtMost(u64),
AtLeast(u64),
RoundDownTo(u64),
RoundUpTo(u64),
}
impl TargetSize {
fn resolve(self, current: u64) -> std::result::Result<u64, String> {
Ok(match self {
TargetSize::Absolute(n) => n,
TargetSize::ExtendBy(n) => current.checked_add(n).ok_or("size overflow")?,
TargetSize::ReduceBy(n) => current.saturating_sub(n),
TargetSize::AtMost(n) => current.min(n),
TargetSize::AtLeast(n) => current.max(n),
TargetSize::RoundDownTo(n) => {
if n == 0 {
return Err("round-down multiple cannot be zero".into());
}
(current / n) * n
}
TargetSize::RoundUpTo(n) => {
if n == 0 {
return Err("round-up multiple cannot be zero".into());
}
let rem = current % n;
if rem == 0 {
current
} else {
current.checked_add(n - rem).ok_or("size overflow")?
}
}
})
}
}
fn parse_size(raw: &str) -> std::result::Result<TargetSize, String> {
let (op, rest) = match raw.chars().next() {
Some('+') => (Some(b'+'), &raw[1..]),
Some('-') => (Some(b'-'), &raw[1..]),
Some('<') => (Some(b'<'), &raw[1..]),
Some('>') => (Some(b'>'), &raw[1..]),
Some('/') => (Some(b'/'), &raw[1..]),
Some('%') => (Some(b'%'), &raw[1..]),
_ => (None, raw),
};
let n = parse_size_number(rest).ok_or_else(|| format!("invalid number in size '{raw}'"))?;
Ok(match op {
None => TargetSize::Absolute(n),
Some(b'+') => TargetSize::ExtendBy(n),
Some(b'-') => TargetSize::ReduceBy(n),
Some(b'<') => TargetSize::AtMost(n),
Some(b'>') => TargetSize::AtLeast(n),
Some(b'/') => TargetSize::RoundDownTo(n),
Some(b'%') => TargetSize::RoundUpTo(n),
_ => unreachable!(),
})
}
fn parse_size_number(raw: &str) -> Option<u64> {
let split = raw
.char_indices()
.find(|(_, c)| !c.is_ascii_digit())
.map(|(i, _)| i)
.unwrap_or(raw.len());
let (digits, unit) = raw.split_at(split);
if digits.is_empty() {
return None;
}
let n: u64 = digits.parse().ok()?;
let mul = match unit {
"" | "B" => 1,
"K" => 1024,
"KB" => 1000,
"M" => 1024 * 1024,
"MB" => 1000 * 1000,
"G" => 1024u64.pow(3),
"GB" => 1000u64.pow(3),
"T" => 1024u64.pow(4),
"TB" => 1000u64.pow(4),
"P" => 1024u64.pow(5),
"PB" => 1000u64.pow(5),
"E" => 1024u64.pow(6),
"EB" => 1000u64.pow(6),
_ => return None,
};
n.checked_mul(mul)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_size_plain_number() {
assert!(matches!(
parse_size("100").unwrap(),
TargetSize::Absolute(100)
));
}
#[test]
fn parse_size_with_units() {
assert!(matches!(
parse_size("1K").unwrap(),
TargetSize::Absolute(1024)
));
assert!(matches!(
parse_size("2KB").unwrap(),
TargetSize::Absolute(2000)
));
assert!(matches!(
parse_size("1M").unwrap(),
TargetSize::Absolute(1_048_576)
));
}
#[test]
fn parse_size_relative_ops() {
assert!(matches!(
parse_size("+100").unwrap(),
TargetSize::ExtendBy(100)
));
assert!(matches!(
parse_size("-50").unwrap(),
TargetSize::ReduceBy(50)
));
assert!(matches!(
parse_size("<200").unwrap(),
TargetSize::AtMost(200)
));
assert!(matches!(
parse_size(">300").unwrap(),
TargetSize::AtLeast(300)
));
assert!(matches!(
parse_size("/16").unwrap(),
TargetSize::RoundDownTo(16)
));
assert!(matches!(
parse_size("%16").unwrap(),
TargetSize::RoundUpTo(16)
));
}
#[test]
fn parse_size_rejects_garbage() {
assert!(parse_size("abc").is_err());
assert!(parse_size("100Q").is_err());
assert!(parse_size("").is_err());
}
#[test]
fn target_resolve_absolute() {
assert_eq!(TargetSize::Absolute(100).resolve(50).unwrap(), 100);
}
#[test]
fn target_resolve_extend_and_reduce() {
assert_eq!(TargetSize::ExtendBy(10).resolve(50).unwrap(), 60);
assert_eq!(TargetSize::ReduceBy(20).resolve(50).unwrap(), 30);
assert_eq!(TargetSize::ReduceBy(100).resolve(50).unwrap(), 0);
}
#[test]
fn target_resolve_at_most_at_least() {
assert_eq!(TargetSize::AtMost(40).resolve(50).unwrap(), 40);
assert_eq!(TargetSize::AtMost(60).resolve(50).unwrap(), 50);
assert_eq!(TargetSize::AtLeast(40).resolve(50).unwrap(), 50);
assert_eq!(TargetSize::AtLeast(60).resolve(50).unwrap(), 60);
}
#[test]
fn target_resolve_round_to_multiple() {
assert_eq!(TargetSize::RoundDownTo(16).resolve(50).unwrap(), 48);
assert_eq!(TargetSize::RoundUpTo(16).resolve(50).unwrap(), 64);
assert_eq!(TargetSize::RoundDownTo(16).resolve(48).unwrap(), 48);
assert_eq!(TargetSize::RoundUpTo(16).resolve(48).unwrap(), 48);
assert!(TargetSize::RoundDownTo(0).resolve(10).is_err());
assert!(TargetSize::RoundUpTo(0).resolve(10).is_err());
}
}