mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-29 03:57:44 +00:00
Merge pull request #2247 from jfinkels/truncate-split-three-functions
truncate: re-organize truncate() into one function for each mode of operation
This commit is contained in:
commit
3d42d41a72
2 changed files with 239 additions and 103 deletions
|
@ -15,16 +15,40 @@ use std::fs::{metadata, OpenOptions};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[derive(Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
enum TruncateMode {
|
enum TruncateMode {
|
||||||
Absolute,
|
Absolute(u64),
|
||||||
Reference,
|
Extend(u64),
|
||||||
Extend,
|
Reduce(u64),
|
||||||
Reduce,
|
AtMost(u64),
|
||||||
AtMost,
|
AtLeast(u64),
|
||||||
AtLeast,
|
RoundDown(u64),
|
||||||
RoundDown,
|
RoundUp(u64),
|
||||||
RoundUp,
|
}
|
||||||
|
|
||||||
|
impl TruncateMode {
|
||||||
|
/// Compute a target size in bytes for this truncate mode.
|
||||||
|
///
|
||||||
|
/// `fsize` is the size of the reference file, in bytes.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let mode = TruncateMode::Extend(5);
|
||||||
|
/// let fsize = 10;
|
||||||
|
/// assert_eq!(mode.to_size(fsize), 15);
|
||||||
|
/// ```
|
||||||
|
fn to_size(&self, fsize: u64) -> u64 {
|
||||||
|
match self {
|
||||||
|
TruncateMode::Absolute(modsize) => *modsize,
|
||||||
|
TruncateMode::Extend(modsize) => fsize + modsize,
|
||||||
|
TruncateMode::Reduce(modsize) => fsize - modsize,
|
||||||
|
TruncateMode::AtMost(modsize) => fsize.min(*modsize),
|
||||||
|
TruncateMode::AtLeast(modsize) => fsize.max(*modsize),
|
||||||
|
TruncateMode::RoundDown(modsize) => fsize - fsize % modsize,
|
||||||
|
TruncateMode::RoundUp(modsize) => fsize + fsize % modsize,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static ABOUT: &str = "Shrink or extend the size of each file to the specified size.";
|
static ABOUT: &str = "Shrink or extend the size of each file to the specified size.";
|
||||||
|
@ -116,114 +140,205 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
let no_create = matches.is_present(options::NO_CREATE);
|
let no_create = matches.is_present(options::NO_CREATE);
|
||||||
let reference = matches.value_of(options::REFERENCE).map(String::from);
|
let reference = matches.value_of(options::REFERENCE).map(String::from);
|
||||||
let size = matches.value_of(options::SIZE).map(String::from);
|
let size = matches.value_of(options::SIZE).map(String::from);
|
||||||
if reference.is_none() && size.is_none() {
|
if let Err(e) = truncate(no_create, io_blocks, reference, size, files) {
|
||||||
crash!(1, "you must specify either --reference or --size");
|
match e.kind() {
|
||||||
} else {
|
ErrorKind::NotFound => {
|
||||||
truncate(no_create, io_blocks, reference, size, files);
|
// TODO Improve error-handling so that the error
|
||||||
|
// returned by `truncate()` provides the necessary
|
||||||
|
// parameter for formatting the error message.
|
||||||
|
let reference = matches.value_of(options::REFERENCE).map(String::from);
|
||||||
|
crash!(
|
||||||
|
1,
|
||||||
|
"cannot stat '{}': No such file or directory",
|
||||||
|
reference.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => crash!(1, "{}", e.to_string()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Truncate the named file to the specified size.
|
||||||
|
///
|
||||||
|
/// If `create` is true, then the file will be created if it does not
|
||||||
|
/// already exist. If `size` is larger than the number of bytes in the
|
||||||
|
/// file, then the file will be padded with zeros. If `size` is smaller
|
||||||
|
/// than the number of bytes in the file, then the file will be
|
||||||
|
/// truncated and any bytes beyond `size` will be lost.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the file could not be opened, or there was a problem setting the
|
||||||
|
/// size of the file.
|
||||||
|
fn file_truncate(filename: &str, create: bool, size: u64) -> std::io::Result<()> {
|
||||||
|
let path = Path::new(filename);
|
||||||
|
let f = OpenOptions::new().write(true).create(create).open(path)?;
|
||||||
|
f.set_len(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate files to a size relative to a given file.
|
||||||
|
///
|
||||||
|
/// `rfilename` is the name of the reference file.
|
||||||
|
///
|
||||||
|
/// `size_string` gives the size relative to the reference file to which
|
||||||
|
/// to set the target files. For example, "+3K" means "set each file to
|
||||||
|
/// be three kilobytes larger than the size of the reference file".
|
||||||
|
///
|
||||||
|
/// If `create` is true, then each file will be created if it does not
|
||||||
|
/// already exist.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the any file could not be opened, or there was a problem setting
|
||||||
|
/// the size of at least one file.
|
||||||
|
fn truncate_reference_and_size(
|
||||||
|
rfilename: &str,
|
||||||
|
size_string: &str,
|
||||||
|
filenames: Vec<String>,
|
||||||
|
create: bool,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let mode = match parse_mode_and_size(size_string) {
|
||||||
|
Ok(m) => match m {
|
||||||
|
TruncateMode::Absolute(_) => {
|
||||||
|
crash!(1, "you must specify a relative ‘--size’ with ‘--reference’")
|
||||||
|
}
|
||||||
|
_ => m,
|
||||||
|
},
|
||||||
|
Err(_) => crash!(1, "Invalid number: ‘{}’", size_string),
|
||||||
|
};
|
||||||
|
let fsize = metadata(rfilename)?.len();
|
||||||
|
let tsize = mode.to_size(fsize);
|
||||||
|
for filename in &filenames {
|
||||||
|
file_truncate(filename, create, tsize)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate files to match the size of a given reference file.
|
||||||
|
///
|
||||||
|
/// `rfilename` is the name of the reference file.
|
||||||
|
///
|
||||||
|
/// If `create` is true, then each file will be created if it does not
|
||||||
|
/// already exist.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the any file could not be opened, or there was a problem setting
|
||||||
|
/// the size of at least one file.
|
||||||
|
fn truncate_reference_file_only(
|
||||||
|
rfilename: &str,
|
||||||
|
filenames: Vec<String>,
|
||||||
|
create: bool,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let tsize = metadata(rfilename)?.len();
|
||||||
|
for filename in &filenames {
|
||||||
|
file_truncate(filename, create, tsize)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Truncate files to a specified size.
|
||||||
|
///
|
||||||
|
/// `size_string` gives either an absolute size or a relative size. A
|
||||||
|
/// relative size adjusts the size of each file relative to its current
|
||||||
|
/// size. For example, "3K" means "set each file to be three kilobytes"
|
||||||
|
/// whereas "+3K" means "set each file to be three kilobytes larger than
|
||||||
|
/// its current size".
|
||||||
|
///
|
||||||
|
/// If `create` is true, then each file will be created if it does not
|
||||||
|
/// already exist.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the any file could not be opened, or there was a problem setting
|
||||||
|
/// the size of at least one file.
|
||||||
|
fn truncate_size_only(
|
||||||
|
size_string: &str,
|
||||||
|
filenames: Vec<String>,
|
||||||
|
create: bool,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let mode = match parse_mode_and_size(size_string) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => crash!(1, "Invalid number: ‘{}’", size_string),
|
||||||
|
};
|
||||||
|
for filename in &filenames {
|
||||||
|
let fsize = metadata(filename).map(|m| m.len()).unwrap_or(0);
|
||||||
|
let tsize = mode.to_size(fsize);
|
||||||
|
file_truncate(filename, create, tsize)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn truncate(
|
fn truncate(
|
||||||
no_create: bool,
|
no_create: bool,
|
||||||
_: bool,
|
_: bool,
|
||||||
reference: Option<String>,
|
reference: Option<String>,
|
||||||
size: Option<String>,
|
size: Option<String>,
|
||||||
filenames: Vec<String>,
|
filenames: Vec<String>,
|
||||||
) {
|
) -> std::io::Result<()> {
|
||||||
let (modsize, mode) = match size {
|
let create = !no_create;
|
||||||
Some(size_string) => {
|
// There are four possibilities
|
||||||
|
// - reference file given and size given,
|
||||||
|
// - reference file given but no size given,
|
||||||
|
// - no reference file given but size given,
|
||||||
|
// - no reference file given and no size given,
|
||||||
|
match (reference, size) {
|
||||||
|
(Some(rfilename), Some(size_string)) => {
|
||||||
|
truncate_reference_and_size(&rfilename, &size_string, filenames, create)
|
||||||
|
}
|
||||||
|
(Some(rfilename), None) => truncate_reference_file_only(&rfilename, filenames, create),
|
||||||
|
(None, Some(size_string)) => truncate_size_only(&size_string, filenames, create),
|
||||||
|
(None, None) => crash!(1, "you must specify either --reference or --size"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decide whether a character is one of the size modifiers, like '+' or '<'.
|
||||||
|
fn is_modifier(c: char) -> bool {
|
||||||
|
c == '+' || c == '-' || c == '<' || c == '>' || c == '/' || c == '%'
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a size string with optional modifier symbol as its first character.
|
||||||
|
///
|
||||||
|
/// A size string is as described in [`parse_size`]. The first character
|
||||||
|
/// of `size_string` might be a modifier symbol, like `'+'` or
|
||||||
|
/// `'<'`. The first element of the pair returned by this function
|
||||||
|
/// indicates which modifier symbol was present, or
|
||||||
|
/// [`TruncateMode::Absolute`] if none.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// If `size_string` is empty, or if no number could be parsed from the
|
||||||
|
/// given string (for example, if the string were `"abc"`).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// assert_eq!(parse_mode_and_size("+123"), (TruncateMode::Extend, 123));
|
||||||
|
/// ```
|
||||||
|
fn parse_mode_and_size(size_string: &str) -> Result<TruncateMode, ()> {
|
||||||
// Trim any whitespace.
|
// Trim any whitespace.
|
||||||
let size_string = size_string.trim();
|
let size_string = size_string.trim();
|
||||||
|
|
||||||
// Get the modifier character from the size string, if any. For
|
// Get the modifier character from the size string, if any. For
|
||||||
// example, if the argument is "+123", then the modifier is '+'.
|
// example, if the argument is "+123", then the modifier is '+'.
|
||||||
let c = size_string.chars().next().unwrap();
|
let c = size_string.chars().next().unwrap();
|
||||||
|
let size_string = if is_modifier(c) {
|
||||||
let mode = match c {
|
&size_string[1..]
|
||||||
|
} else {
|
||||||
|
size_string
|
||||||
|
};
|
||||||
|
parse_size(size_string).map(match c {
|
||||||
'+' => TruncateMode::Extend,
|
'+' => TruncateMode::Extend,
|
||||||
'-' => TruncateMode::Reduce,
|
'-' => TruncateMode::Reduce,
|
||||||
'<' => TruncateMode::AtMost,
|
'<' => TruncateMode::AtMost,
|
||||||
'>' => TruncateMode::AtLeast,
|
'>' => TruncateMode::AtLeast,
|
||||||
'/' => TruncateMode::RoundDown,
|
'/' => TruncateMode::RoundDown,
|
||||||
'%' => TruncateMode::RoundUp,
|
'%' => TruncateMode::RoundUp,
|
||||||
_ => TruncateMode::Absolute, /* assume that the size is just a number */
|
_ => TruncateMode::Absolute,
|
||||||
};
|
})
|
||||||
|
|
||||||
// If there was a modifier character, strip it.
|
|
||||||
let size_string = match mode {
|
|
||||||
TruncateMode::Absolute => size_string,
|
|
||||||
_ => &size_string[1..],
|
|
||||||
};
|
|
||||||
let num_bytes = match parse_size(size_string) {
|
|
||||||
Ok(b) => b,
|
|
||||||
Err(_) => crash!(1, "Invalid number: ‘{}’", size_string),
|
|
||||||
};
|
|
||||||
(num_bytes, mode)
|
|
||||||
}
|
|
||||||
None => (0, TruncateMode::Reference),
|
|
||||||
};
|
|
||||||
|
|
||||||
let refsize = match reference {
|
|
||||||
Some(ref rfilename) => {
|
|
||||||
match mode {
|
|
||||||
// Only Some modes work with a reference
|
|
||||||
TruncateMode::Reference => (), //No --size was given
|
|
||||||
TruncateMode::Extend => (),
|
|
||||||
TruncateMode::Reduce => (),
|
|
||||||
_ => crash!(1, "you must specify a relative ‘--size’ with ‘--reference’"),
|
|
||||||
};
|
|
||||||
match metadata(rfilename) {
|
|
||||||
Ok(meta) => meta.len(),
|
|
||||||
Err(f) => match f.kind() {
|
|
||||||
ErrorKind::NotFound => {
|
|
||||||
crash!(1, "cannot stat '{}': No such file or directory", rfilename)
|
|
||||||
}
|
|
||||||
_ => crash!(1, "{}", f.to_string()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
for filename in &filenames {
|
|
||||||
let path = Path::new(filename);
|
|
||||||
match OpenOptions::new()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.create(!no_create)
|
|
||||||
.open(path)
|
|
||||||
{
|
|
||||||
Ok(file) => {
|
|
||||||
let fsize = match reference {
|
|
||||||
Some(_) => refsize,
|
|
||||||
None => match metadata(filename) {
|
|
||||||
Ok(meta) => meta.len(),
|
|
||||||
Err(f) => {
|
|
||||||
show_warning!("{}", f.to_string());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let tsize: u64 = match mode {
|
|
||||||
TruncateMode::Absolute => modsize,
|
|
||||||
TruncateMode::Reference => fsize,
|
|
||||||
TruncateMode::Extend => fsize + modsize,
|
|
||||||
TruncateMode::Reduce => fsize - modsize,
|
|
||||||
TruncateMode::AtMost => fsize.min(modsize),
|
|
||||||
TruncateMode::AtLeast => fsize.max(modsize),
|
|
||||||
TruncateMode::RoundDown => fsize - fsize % modsize,
|
|
||||||
TruncateMode::RoundUp => fsize + fsize % modsize,
|
|
||||||
};
|
|
||||||
match file.set_len(tsize) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(f) => crash!(1, "{}", f.to_string()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Err(f) => crash!(1, "{}", f.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a size string into a number of bytes.
|
/// Parse a size string into a number of bytes.
|
||||||
|
@ -285,7 +400,9 @@ fn parse_size(size: &str) -> Result<u64, ()> {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::parse_mode_and_size;
|
||||||
use crate::parse_size;
|
use crate::parse_size;
|
||||||
|
use crate::TruncateMode;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_size_zero() {
|
fn test_parse_size_zero() {
|
||||||
|
@ -311,4 +428,15 @@ mod tests {
|
||||||
assert_eq!(parse_size("123M").unwrap(), 123 * 1024 * 1024);
|
assert_eq!(parse_size("123M").unwrap(), 123 * 1024 * 1024);
|
||||||
assert_eq!(parse_size("123MB").unwrap(), 123 * 1000 * 1000);
|
assert_eq!(parse_size("123MB").unwrap(), 123 * 1000 * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_mode_and_size() {
|
||||||
|
assert_eq!(parse_mode_and_size("10"), Ok(TruncateMode::Absolute(10)));
|
||||||
|
assert_eq!(parse_mode_and_size("+10"), Ok(TruncateMode::Extend(10)));
|
||||||
|
assert_eq!(parse_mode_and_size("-10"), Ok(TruncateMode::Reduce(10)));
|
||||||
|
assert_eq!(parse_mode_and_size("<10"), Ok(TruncateMode::AtMost(10)));
|
||||||
|
assert_eq!(parse_mode_and_size(">10"), Ok(TruncateMode::AtLeast(10)));
|
||||||
|
assert_eq!(parse_mode_and_size("/10"), Ok(TruncateMode::RoundDown(10)));
|
||||||
|
assert_eq!(parse_mode_and_size("%10"), Ok(TruncateMode::RoundUp(10)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,3 +262,11 @@ fn test_reference_file_not_found() {
|
||||||
.fails()
|
.fails()
|
||||||
.stderr_contains("cannot stat 'a': No such file or directory");
|
.stderr_contains("cannot stat 'a': No such file or directory");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reference_with_size_file_not_found() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["-r", "a", "-s", "+1", "b"])
|
||||||
|
.fails()
|
||||||
|
.stderr_contains("cannot stat 'a': No such file or directory");
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue