diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a1c2182c2..b7c53eb72 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1,9 +1,9 @@ -// This file is part of the uutils coreutils package. -// -// (c) Derek Chiang -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. +// * This file is part of the uutils coreutils package. +// * +// * (c) Derek Chiang +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. #[macro_use] extern crate uucore; @@ -12,6 +12,7 @@ use chrono::prelude::DateTime; use chrono::Local; use clap::{crate_version, App, Arg}; use std::collections::HashSet; +use std::convert::TryFrom; use std::env; use std::fs; #[cfg(not(windows))] @@ -28,6 +29,7 @@ use std::os::windows::io::AsRawHandle; use std::path::Path; use std::path::PathBuf; use std::time::{Duration, UNIX_EPOCH}; +use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::InvalidEncodingHandling; #[cfg(windows)] use winapi::shared::minwindef::{DWORD, LPVOID}; @@ -44,7 +46,7 @@ mod options { pub const NULL: &str = "0"; pub const ALL: &str = "all"; pub const APPARENT_SIZE: &str = "apparent-size"; - pub const BLOCK_SIZE: &str = "B"; + pub const BLOCK_SIZE: &str = "block-size"; pub const BYTES: &str = "b"; pub const TOTAL: &str = "c"; pub const MAX_DEPTH: &str = "d"; @@ -227,63 +229,22 @@ fn get_file_info(path: &Path) -> Option { result } -fn unit_string_to_number(s: &str) -> Option { - let mut offset = 0; - let mut s_chars = s.chars().rev(); - - let (mut ch, multiple) = match s_chars.next()? { - 'B' | 'b' => ('B', 1000u64), - ch => (ch, 1024u64), - }; - if ch == 'B' { - ch = s_chars.next()?; - offset += 1; - } - ch = ch.to_ascii_uppercase(); - - let unit = UNITS - .iter() - .rev() - .find(|&&(unit_ch, _)| unit_ch == ch) - .map(|&(_, val)| { - // we found a match, so increment offset - offset += 1; - val - }) - .or_else(|| if multiple == 1024 { Some(0) } else { None })?; - - let number = s[..s.len() - offset].parse::().ok()?; - - Some(number * multiple.pow(unit)) -} - -fn translate_to_pure_number(s: &Option<&str>) -> Option { - match *s { - Some(s) => unit_string_to_number(s), - None => None, - } -} - -fn read_block_size(s: Option<&str>) -> u64 { - match translate_to_pure_number(&s) { - Some(v) => v, - None => { - if let Some(value) = s { - show_error!("invalid --block-size argument '{}'", value); - }; - - for env_var in &["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { - let env_size = env::var(env_var).ok(); - if let Some(quantity) = translate_to_pure_number(&env_size.as_deref()) { - return quantity; +fn read_block_size(s: Option<&str>) -> usize { + if let Some(s) = s { + parse_size(s) + .unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::BLOCK_SIZE))) + } else { + for env_var in &["DU_BLOCK_SIZE", "BLOCK_SIZE", "BLOCKSIZE"] { + if let Ok(env_size) = env::var(env_var) { + if let Ok(v) = parse_size(&env_size) { + return v; } } - - if env::var("POSIXLY_CORRECT").is_ok() { - 512 - } else { - 1024 - } + } + if env::var("POSIXLY_CORRECT").is_ok() { + 512 + } else { + 1024 } } } @@ -449,7 +410,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .arg( Arg::with_name(options::BLOCK_SIZE) .short("B") - .long("block-size") + .long(options::BLOCK_SIZE) .value_name("SIZE") .help( "scale sizes by SIZE before printing them. \ @@ -623,7 +584,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } }; - let block_size = read_block_size(matches.value_of(options::BLOCK_SIZE)); + let block_size = u64::try_from(read_block_size(matches.value_of(options::BLOCK_SIZE))).unwrap(); let multiplier: u64 = if matches.is_present(options::SI) { 1000 @@ -759,31 +720,27 @@ Try '{} --help' for more information.", 0 } +fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { + // NOTE: + // GNU's du echos affected flag, -B or --block-size (-t or --threshold), depending user's selection + // GNU's du does distinguish between "invalid (suffix in) argument" + match error { + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), + ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + } +} + #[cfg(test)] mod test_du { #[allow(unused_imports)] use super::*; - #[test] - fn test_translate_to_pure_number() { - let test_data = [ - (Some("10".to_string()), Some(10)), - (Some("10K".to_string()), Some(10 * 1024)), - (Some("5M".to_string()), Some(5 * 1024 * 1024)), - (Some("900KB".to_string()), Some(900 * 1000)), - (Some("BAD_STRING".to_string()), None), - ]; - for it in test_data.iter() { - assert_eq!(translate_to_pure_number(&it.0.as_deref()), it.1); - } - } - #[test] fn test_read_block_size() { let test_data = [ - (Some("10".to_string()), 10), + (Some("1024".to_string()), 1024), + (Some("K".to_string()), 1024), (None, 1024), - (Some("BAD_STRING".to_string()), 1024), ]; for it in test_data.iter() { assert_eq!(read_block_size(it.0.as_deref()), it.1); diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index c6d2f98bd..aceecd941 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -1,3 +1,8 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + // spell-checker:ignore (vars) zlines use clap::{crate_version, App, Arg}; @@ -75,7 +80,7 @@ fn app<'a>() -> App<'a, 'a> { .arg( Arg::with_name(options::QUIET_NAME) .short("q") - .long("--quiet") + .long("quiet") .visible_alias("silent") .help("never print headers giving file names") .overrides_with_all(&[options::VERBOSE_NAME, options::QUIET_NAME]), @@ -108,12 +113,7 @@ where { match parse::parse_num(src) { Ok((n, last)) => Ok((closure(n), last)), - Err(reason) => match reason { - parse::ParseError::Syntax => Err(format!("'{}'", src)), - parse::ParseError::Overflow => { - Err(format!("'{}': Value too large for defined datatype", src)) - } - }, + Err(e) => Err(e.to_string()), } } @@ -466,7 +466,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let args = match HeadOptions::get_from(args) { Ok(o) => o, Err(s) => { - crash!(EXIT_FAILURE, "head: {}", s); + crash!(EXIT_FAILURE, "{}", s); } }; match uu_head(&args) { diff --git a/src/uu/head/src/parse.rs b/src/uu/head/src/parse.rs index c04962f1b..f6f291814 100644 --- a/src/uu/head/src/parse.rs +++ b/src/uu/head/src/parse.rs @@ -1,5 +1,10 @@ -use std::convert::TryFrom; +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + use std::ffi::OsString; +use uucore::parse_size::{parse_size, ParseSizeError}; #[derive(PartialEq, Debug)] pub enum ParseError { @@ -92,88 +97,25 @@ pub fn parse_obsolete(src: &str) -> Option } /// Parses an -c or -n argument, /// the bool specifies whether to read from the end -pub fn parse_num(src: &str) -> Result<(usize, bool), ParseError> { - let mut num_start = 0; - let (mut chars, all_but_last) = { - let mut chars = src.char_indices(); - let (_, c) = chars.next().ok_or(ParseError::Syntax)?; - if c == '-' { - num_start += 1; - (chars, true) - } else { - (src.char_indices(), false) - } - }; - let mut num_end = 0usize; - let mut last_char = 0 as char; - let mut num_count = 0usize; - for (n, c) in &mut chars { - if c.is_numeric() { - num_end = n; - num_count += 1; - } else { - last_char = c; - break; - } - } +pub fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> { + let mut size_string = src.trim(); + let mut all_but_last = false; - let num = if num_count > 0 { - Some( - src[num_start..=num_end] - .parse::() - .map_err(|_| ParseError::Overflow)?, - ) - } else { - None - }; - - if last_char == 0 as char { - if let Some(n) = num { - Ok((n, all_but_last)) - } else { - Err(ParseError::Syntax) - } - } else { - let base: u128 = match chars.next() { - Some((_, c)) => { - let b = match c { - 'B' if last_char != 'b' => 1000, - 'i' if last_char != 'b' => { - if let Some((_, 'B')) = chars.next() { - 1024 - } else { - return Err(ParseError::Syntax); - } - } - _ => return Err(ParseError::Syntax), - }; - if chars.next().is_some() { - return Err(ParseError::Syntax); - } else { - b - } + if let Some(c) = size_string.chars().next() { + if c == '+' || c == '-' { + // head: '+' is not documented (8.32 man pages) + size_string = &size_string[1..]; + if c == '-' { + all_but_last = true; } - None => 1024, - }; - let mul = match last_char.to_lowercase().next().unwrap() { - 'b' => 512, - 'k' => base.pow(1), - 'm' => base.pow(2), - 'g' => base.pow(3), - 't' => base.pow(4), - 'p' => base.pow(5), - 'e' => base.pow(6), - 'z' => base.pow(7), - 'y' => base.pow(8), - _ => return Err(ParseError::Syntax), - }; - let mul = usize::try_from(mul).map_err(|_| ParseError::Overflow)?; - match num.unwrap_or(1).checked_mul(mul) { - Some(n) => Ok((n, all_but_last)), - None => Err(ParseError::Overflow), } + } else { + return Err(ParseSizeError::ParseFailure(src.to_string())); } + + parse_size(size_string).map(|n| (n, all_but_last)) } + #[cfg(test)] mod tests { use super::*; @@ -191,44 +133,6 @@ mod tests { Some(Ok(src.iter().map(|s| s.to_string()).collect())) } #[test] - #[cfg(not(target_pointer_width = "128"))] - fn test_parse_overflow_x64() { - assert_eq!(parse_num("1Y"), Err(ParseError::Overflow)); - assert_eq!(parse_num("1Z"), Err(ParseError::Overflow)); - assert_eq!(parse_num("100E"), Err(ParseError::Overflow)); - assert_eq!(parse_num("100000P"), Err(ParseError::Overflow)); - assert_eq!(parse_num("1000000000T"), Err(ParseError::Overflow)); - assert_eq!( - parse_num("10000000000000000000000"), - Err(ParseError::Overflow) - ); - } - #[test] - #[cfg(target_pointer_width = "32")] - fn test_parse_overflow_x32() { - assert_eq!(parse_num("1T"), Err(ParseError::Overflow)); - assert_eq!(parse_num("1000G"), Err(ParseError::Overflow)); - } - #[test] - fn test_parse_bad_syntax() { - assert_eq!(parse_num("5MiB nonsense"), Err(ParseError::Syntax)); - assert_eq!(parse_num("Nonsense string"), Err(ParseError::Syntax)); - assert_eq!(parse_num("5mib"), Err(ParseError::Syntax)); - assert_eq!(parse_num("biB"), Err(ParseError::Syntax)); - assert_eq!(parse_num("-"), Err(ParseError::Syntax)); - assert_eq!(parse_num(""), Err(ParseError::Syntax)); - } - #[test] - fn test_parse_numbers() { - assert_eq!(parse_num("k"), Ok((1024, false))); - assert_eq!(parse_num("MiB"), Ok((1024 * 1024, false))); - assert_eq!(parse_num("-5"), Ok((5, true))); - assert_eq!(parse_num("b"), Ok((512, false))); - assert_eq!(parse_num("-2GiB"), Ok((2 * 1024 * 1024 * 1024, true))); - assert_eq!(parse_num("5M"), Ok((5 * 1024 * 1024, false))); - assert_eq!(parse_num("5MB"), Ok((5 * 1000 * 1000, false))); - } - #[test] fn test_parse_numbers_obsolete() { assert_eq!(obsolete("-5"), obsolete_result(&["-n", "5"])); assert_eq!(obsolete("-100"), obsolete_result(&["-n", "100"])); diff --git a/src/uu/od/src/od.rs b/src/uu/od/src/od.rs index 642618cac..bf6c39011 100644 --- a/src/uu/od/src/od.rs +++ b/src/uu/od/src/od.rs @@ -43,6 +43,7 @@ use crate::partialreader::*; use crate::peekreader::*; use crate::prn_char::format_ascii_dump; use clap::{self, crate_version, AppSettings, Arg, ArgMatches}; +use uucore::parse_size::ParseSizeError; use uucore::InvalidEncodingHandling; const PEEK_BUFFER_SIZE: usize = 4; // utf-8 can be 4 bytes @@ -128,13 +129,11 @@ impl OdOptions { } }; - let mut skip_bytes = matches - .value_of(options::SKIP_BYTES) - .map(|s| { - parse_number_of_bytes(s).map_err(|_| format!("Invalid argument --skip-bytes={}", s)) + let mut skip_bytes = matches.value_of(options::SKIP_BYTES).map_or(0, |s| { + parse_number_of_bytes(s).unwrap_or_else(|e| { + crash!(1, "{}", format_error_message(e, s, options::SKIP_BYTES)) }) - .transpose()? - .unwrap_or(0); + }); let mut label: Option = None; @@ -150,11 +149,14 @@ impl OdOptions { let formats = parse_format_flags(&args)?; - let mut line_bytes = match matches.value_of(options::WIDTH) { - None => 16, - Some(_) if matches.occurrences_of(options::WIDTH) == 0 => 16, - Some(s) => s.parse::().unwrap_or(0), - }; + let mut line_bytes = matches.value_of(options::WIDTH).map_or(16, |s| { + if matches.occurrences_of(options::WIDTH) == 0 { + return 16; + }; + parse_number_of_bytes(s) + .unwrap_or_else(|e| crash!(1, "{}", format_error_message(e, s, options::WIDTH))) + }); + let min_bytes = formats.iter().fold(1, |max, next| { cmp::max(max, next.formatter_item_info.byte_size) }); @@ -165,12 +167,11 @@ impl OdOptions { let output_duplicates = matches.is_present(options::OUTPUT_DUPLICATES); - let read_bytes = matches - .value_of(options::READ_BYTES) - .map(|s| { - parse_number_of_bytes(s).map_err(|_| format!("Invalid argument --read-bytes={}", s)) + let read_bytes = matches.value_of(options::READ_BYTES).map(|s| { + parse_number_of_bytes(s).unwrap_or_else(|e| { + crash!(1, "{}", format_error_message(e, s, options::READ_BYTES)) }) - .transpose()?; + }); let radix = match matches.value_of(options::ADDRESS_RADIX) { None => Radix::Octal, @@ -251,7 +252,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .short("S") .long(options::STRINGS) .help( - "output strings of at least BYTES graphic chars. 3 is assumed when \ + "NotImplemented: output strings of at least BYTES graphic chars. 3 is assumed when \ BYTES is not specified.", ) .default_value("3") @@ -441,8 +442,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let od_options = match OdOptions::new(clap_matches, args) { Err(s) => { - show_usage_error!("{}", s); - return 1; + crash!(1, "{}", s); } Ok(o) => o, }; @@ -624,3 +624,13 @@ fn open_input_peek_reader( let pr = PartialReader::new(mf, skip_bytes, read_bytes); PeekReader::new(pr) } + +fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { + // NOTE: + // GNU's od echos affected flag, -N or --read-bytes (-j or --skip-bytes, etc.), depending user's selection + // GNU's od does distinguish between "invalid (suffix in) argument" + match error { + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), + ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + } +} diff --git a/src/uu/od/src/parse_nrofbytes.rs b/src/uu/od/src/parse_nrofbytes.rs index d2ba1527b..d6329c60a 100644 --- a/src/uu/od/src/parse_nrofbytes.rs +++ b/src/uu/od/src/parse_nrofbytes.rs @@ -1,14 +1,17 @@ -pub fn parse_number_of_bytes(s: &str) -> Result { +use uucore::parse_size::{parse_size, ParseSizeError}; + +pub fn parse_number_of_bytes(s: &str) -> Result { let mut start = 0; let mut len = s.len(); - let mut radix = 10; + let mut radix = 16; let mut multiply = 1; if s.starts_with("0x") || s.starts_with("0X") { start = 2; - radix = 16; } else if s.starts_with('0') { radix = 8; + } else { + return parse_size(&s[start..]); } let mut ends_with = s.chars().rev(); @@ -56,78 +59,33 @@ pub fn parse_number_of_bytes(s: &str) -> Result { Some('P') => 1000 * 1000 * 1000 * 1000 * 1000, #[cfg(target_pointer_width = "64")] Some('E') => 1000 * 1000 * 1000 * 1000 * 1000 * 1000, - _ => return Err("parse failed"), + _ => return Err(ParseSizeError::ParseFailure(s.to_string())), } } _ => {} } - match usize::from_str_radix(&s[start..len], radix) { - Ok(i) => Ok(i * multiply), - Err(_) => Err("parse failed"), - } -} - -#[allow(dead_code)] -fn parse_number_of_bytes_str(s: &str) -> Result { - parse_number_of_bytes(&String::from(s)) + let factor = match usize::from_str_radix(&s[start..len], radix) { + Ok(f) => f, + Err(e) => return Err(ParseSizeError::ParseFailure(e.to_string())), + }; + factor + .checked_mul(multiply) + .ok_or_else(|| ParseSizeError::SizeTooBig(s.to_string())) } #[test] fn test_parse_number_of_bytes() { - // normal decimal numbers - assert_eq!(0, parse_number_of_bytes_str("0").unwrap()); - assert_eq!(5, parse_number_of_bytes_str("5").unwrap()); - assert_eq!(999, parse_number_of_bytes_str("999").unwrap()); - assert_eq!(2 * 512, parse_number_of_bytes_str("2b").unwrap()); - assert_eq!(2 * 1024, parse_number_of_bytes_str("2k").unwrap()); - assert_eq!(4 * 1024, parse_number_of_bytes_str("4K").unwrap()); - assert_eq!(2 * 1048576, parse_number_of_bytes_str("2m").unwrap()); - assert_eq!(4 * 1048576, parse_number_of_bytes_str("4M").unwrap()); - assert_eq!(1073741824, parse_number_of_bytes_str("1G").unwrap()); - assert_eq!(2000, parse_number_of_bytes_str("2kB").unwrap()); - assert_eq!(4000, parse_number_of_bytes_str("4KB").unwrap()); - assert_eq!(2000000, parse_number_of_bytes_str("2mB").unwrap()); - assert_eq!(4000000, parse_number_of_bytes_str("4MB").unwrap()); - assert_eq!(2000000000, parse_number_of_bytes_str("2GB").unwrap()); - // octal input - assert_eq!(8, parse_number_of_bytes_str("010").unwrap()); - assert_eq!(8 * 512, parse_number_of_bytes_str("010b").unwrap()); - assert_eq!(8 * 1024, parse_number_of_bytes_str("010k").unwrap()); - assert_eq!(8 * 1048576, parse_number_of_bytes_str("010m").unwrap()); + assert_eq!(8, parse_number_of_bytes("010").unwrap()); + assert_eq!(8 * 512, parse_number_of_bytes("010b").unwrap()); + assert_eq!(8 * 1024, parse_number_of_bytes("010k").unwrap()); + assert_eq!(8 * 1_048_576, parse_number_of_bytes("010m").unwrap()); // hex input - assert_eq!(15, parse_number_of_bytes_str("0xf").unwrap()); - assert_eq!(15, parse_number_of_bytes_str("0XF").unwrap()); - assert_eq!(27, parse_number_of_bytes_str("0x1b").unwrap()); - assert_eq!(16 * 1024, parse_number_of_bytes_str("0x10k").unwrap()); - assert_eq!(16 * 1048576, parse_number_of_bytes_str("0x10m").unwrap()); - - // invalid input - parse_number_of_bytes_str("").unwrap_err(); - parse_number_of_bytes_str("-1").unwrap_err(); - parse_number_of_bytes_str("1e2").unwrap_err(); - parse_number_of_bytes_str("xyz").unwrap_err(); - parse_number_of_bytes_str("b").unwrap_err(); - parse_number_of_bytes_str("1Y").unwrap_err(); - parse_number_of_bytes_str("∞").unwrap_err(); -} - -#[test] -#[cfg(target_pointer_width = "64")] -fn test_parse_number_of_bytes_64bits() { - assert_eq!(1099511627776, parse_number_of_bytes_str("1T").unwrap()); - assert_eq!(1125899906842624, parse_number_of_bytes_str("1P").unwrap()); - assert_eq!( - 1152921504606846976, - parse_number_of_bytes_str("1E").unwrap() - ); - - assert_eq!(2000000000000, parse_number_of_bytes_str("2TB").unwrap()); - assert_eq!(2000000000000000, parse_number_of_bytes_str("2PB").unwrap()); - assert_eq!( - 2000000000000000000, - parse_number_of_bytes_str("2EB").unwrap() - ); + assert_eq!(15, parse_number_of_bytes("0xf").unwrap()); + assert_eq!(15, parse_number_of_bytes("0XF").unwrap()); + assert_eq!(27, parse_number_of_bytes("0x1b").unwrap()); + assert_eq!(16 * 1024, parse_number_of_bytes("0x10k").unwrap()); + assert_eq!(16 * 1_048_576, parse_number_of_bytes("0x10m").unwrap()); } diff --git a/src/uu/sleep/Cargo.toml b/src/uu/sleep/Cargo.toml index fe7ee2941..618ea7e28 100644 --- a/src/uu/sleep/Cargo.toml +++ b/src/uu/sleep/Cargo.toml @@ -16,7 +16,7 @@ path = "src/sleep.rs" [dependencies] clap = "2.33" -uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["parse_time"] } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore" } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } [[bin]] diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 53619d0d6..c2b82ee68 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -43,6 +43,7 @@ use std::ops::Range; use std::path::Path; use std::path::PathBuf; use unicode_width::UnicodeWidthStr; +use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::InvalidEncodingHandling; static NAME: &str = "sort"; @@ -162,32 +163,29 @@ pub struct GlobalSettings { } impl GlobalSettings { - /// Interpret this `&str` as a number with an optional trailing si unit. - /// - /// If there is no trailing si unit, the implicit unit is K. - /// The suffix B causes the number to be interpreted as a byte count. - fn parse_byte_count(input: &str) -> usize { - const SI_UNITS: &[char] = &['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; + /// Parse a SIZE string into a number of bytes. + /// A size string comprises an integer and an optional unit. + /// The unit may be k, K, m, M, g, G, t, T, P, E, Z, Y (powers of 1024), or b which is 1. + /// Default is K. + fn parse_byte_count(input: &str) -> Result { + // GNU sort (8.32) valid: 1b, k, K, m, M, g, G, t, T, P, E, Z, Y + // GNU sort (8.32) invalid: b, B, 1B, p, e, z, y + const ALLOW_LIST: &[char] = &[ + 'b', 'k', 'K', 'm', 'M', 'g', 'G', 't', 'T', 'P', 'E', 'Z', 'Y', + ]; + let mut size_string = input.trim().to_string(); - let input = input.trim(); - - let (num_str, si_unit) = - if input.ends_with(|c: char| SI_UNITS.contains(&c.to_ascii_uppercase())) { - let mut chars = input.chars(); - let si_suffix = chars.next_back().unwrap().to_ascii_uppercase(); - let si_unit = SI_UNITS.iter().position(|&c| c == si_suffix).unwrap(); - let num_str = chars.as_str(); - (num_str, si_unit) - } else { - (input, 1) - }; - - let num_usize: usize = num_str - .trim() - .parse() - .unwrap_or_else(|e| crash!(1, "failed to parse buffer size `{}`: {}", num_str, e)); - - num_usize.saturating_mul(1000usize.saturating_pow(si_unit as u32)) + if size_string.ends_with(|c: char| ALLOW_LIST.contains(&c) || c.is_digit(10)) { + // b 1, K 1024 (default) + if size_string.ends_with(|c: char| c.is_digit(10)) { + size_string.push('K'); + } else if size_string.ends_with('b') { + size_string.pop(); + } + parse_size(&size_string) + } else { + Err(ParseSizeError::ParseFailure("invalid suffix".to_string())) + } } fn out_writer(&self) -> BufWriter> { @@ -1176,8 +1174,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { settings.buffer_size = matches .value_of(OPT_BUF_SIZE) - .map(GlobalSettings::parse_byte_count) - .unwrap_or(DEFAULT_BUF_SIZE); + .map_or(DEFAULT_BUF_SIZE, |s| { + GlobalSettings::parse_byte_count(s) + .unwrap_or_else(|e| crash!(2, "{}", format_error_message(e, s, OPT_BUF_SIZE))) + }); settings.tmp_dir = matches .value_of(OPT_TMP_DIR) @@ -1587,6 +1587,16 @@ fn open(path: impl AsRef) -> Box { } } +fn format_error_message(error: ParseSizeError, s: &str, option: &str) -> String { + // NOTE: + // GNU's sort echos affected flag, -S or --buffer-size, depending user's selection + // GNU's sort does distinguish between "invalid (suffix in) argument" + match error { + ParseSizeError::ParseFailure(_) => format!("invalid --{} argument '{}'", option, s), + ParseSizeError::SizeTooBig(_) => format!("--{} argument '{}' too large", option, s), + } +} + #[cfg(test)] mod tests { @@ -1676,4 +1686,48 @@ mod tests { // How big is a selection? Constant cost all lines pay when we need selections. assert_eq!(std::mem::size_of::(), 24); } + + #[test] + fn test_parse_byte_count() { + let valid_input = [ + ("0", 0), + ("50K", 50 * 1024), + ("50k", 50 * 1024), + ("1M", 1024 * 1024), + ("100M", 100 * 1024 * 1024), + #[cfg(not(target_pointer_width = "32"))] + ("1000G", 1000 * 1024 * 1024 * 1024), + #[cfg(not(target_pointer_width = "32"))] + ("10T", 10 * 1024 * 1024 * 1024 * 1024), + ("1b", 1), + ("1024b", 1024), + ("1024Mb", 1024 * 1024 * 1024), // NOTE: This might not be how GNU `sort` behaves for 'Mb' + ("1", 1024), // K is default + ("50", 50 * 1024), + ("K", 1024), + ("k", 1024), + ("m", 1024 * 1024), + #[cfg(not(target_pointer_width = "32"))] + ("E", 1024 * 1024 * 1024 * 1024 * 1024 * 1024), + ]; + for (input, expected_output) in &valid_input { + assert_eq!( + GlobalSettings::parse_byte_count(input), + Ok(*expected_output) + ); + } + + // SizeTooBig + let invalid_input = ["500E", "1Y"]; + for input in &invalid_input { + #[cfg(not(target_pointer_width = "128"))] + assert!(GlobalSettings::parse_byte_count(input).is_err()); + } + + // ParseFailure + let invalid_input = ["nonsense", "1B", "B", "b", "p", "e", "z", "y"]; + for input in &invalid_input { + assert!(GlobalSettings::parse_byte_count(input).is_err()); + } + } } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 6550c35ac..0d5543d8b 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -13,11 +13,13 @@ extern crate uucore; mod platform; use clap::{crate_version, App, Arg}; +use std::convert::TryFrom; use std::env; use std::fs::File; use std::io::{stdin, BufRead, BufReader, BufWriter, Read, Write}; use std::path::Path; use std::{char, fs::remove_file}; +use uucore::parse_size::parse_size; static NAME: &str = "split"; @@ -231,10 +233,9 @@ struct LineSplitter { impl LineSplitter { fn new(settings: &Settings) -> LineSplitter { LineSplitter { - lines_per_split: settings - .strategy_param - .parse() - .unwrap_or_else(|e| crash!(1, "invalid number of lines: {}", e)), + lines_per_split: settings.strategy_param.parse().unwrap_or_else(|_| { + crash!(1, "invalid number of lines: ‘{}’", settings.strategy_param) + }), } } } @@ -276,40 +277,14 @@ struct ByteSplitter { impl ByteSplitter { fn new(settings: &Settings) -> ByteSplitter { - // These multipliers are the same as supported by GNU coreutils. - let modifiers: Vec<(&str, u128)> = vec![ - ("K", 1024u128), - ("M", 1024 * 1024), - ("G", 1024 * 1024 * 1024), - ("T", 1024 * 1024 * 1024 * 1024), - ("P", 1024 * 1024 * 1024 * 1024 * 1024), - ("E", 1024 * 1024 * 1024 * 1024 * 1024 * 1024), - ("Z", 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024), - ("Y", 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024), - ("KB", 1000), - ("MB", 1000 * 1000), - ("GB", 1000 * 1000 * 1000), - ("TB", 1000 * 1000 * 1000 * 1000), - ("PB", 1000 * 1000 * 1000 * 1000 * 1000), - ("EB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000), - ("ZB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000), - ("YB", 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000), - ]; - - // This sequential find is acceptable since none of the modifiers are - // suffixes of any other modifiers, a la Huffman codes. - let (suffix, multiplier) = modifiers - .iter() - .find(|(suffix, _)| settings.strategy_param.ends_with(suffix)) - .unwrap_or(&("", 1)); - - // Try to parse the actual numeral. - let n = &settings.strategy_param[0..(settings.strategy_param.len() - suffix.len())] - .parse::() - .unwrap_or_else(|e| crash!(1, "invalid number of bytes: {}", e)); + let size_string = &settings.strategy_param; + let size_num = match parse_size(size_string) { + Ok(n) => n, + Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()), + }; ByteSplitter { - bytes_per_split: n * multiplier, + bytes_per_split: u128::try_from(size_num).unwrap(), } } } diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index a69e0c2c5..fc0c83ec8 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -19,6 +19,7 @@ use std::path::PathBuf; use std::process::Command; use tempfile::tempdir; use tempfile::TempDir; +use uucore::parse_size::parse_size; use uucore::InvalidEncodingHandling; static ABOUT: &str = @@ -55,7 +56,7 @@ const STDBUF_INJECT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libstdbuf enum BufferType { Default, Line, - Size(u64), + Size(usize), } struct ProgramOptions { @@ -104,41 +105,6 @@ fn preload_strings() -> (&'static str, &'static str) { crash!(1, "Command not supported for this operating system!") } -fn parse_size(size: &str) -> Option { - let ext = size.trim_start_matches(|c: char| c.is_digit(10)); - let num = size.trim_end_matches(char::is_alphabetic); - let mut recovered = num.to_owned(); - recovered.push_str(ext); - if recovered != size { - return None; - } - let buf_size: u64 = match num.parse().ok() { - Some(m) => m, - None => return None, - }; - let (power, base): (u32, u64) = match ext { - "" => (0, 0), - "KB" => (1, 1024), - "K" => (1, 1000), - "MB" => (2, 1024), - "M" => (2, 1000), - "GB" => (3, 1024), - "G" => (3, 1000), - "TB" => (4, 1024), - "T" => (4, 1000), - "PB" => (5, 1024), - "P" => (5, 1000), - "EB" => (6, 1024), - "E" => (6, 1000), - "ZB" => (7, 1024), - "Z" => (7, 1000), - "YB" => (8, 1024), - "Y" => (8, 1000), - _ => return None, - }; - Some(buf_size * base.pow(power)) -} - fn check_option(matches: &ArgMatches, name: &str) -> Result { match matches.value_of(name) { Some(value) => match value { @@ -151,11 +117,10 @@ fn check_option(matches: &ArgMatches, name: &str) -> Result { - let size = parse_size(x) - .ok_or_else(|| ProgramOptionsError(format!("invalid mode {}", x)))?; - Ok(BufferType::Size(size)) - } + x => parse_size(x).map_or_else( + |e| crash!(125, "invalid mode {}", e), + |m| Ok(BufferType::Size(m)), + ), }, None => Ok(BufferType::Default), } diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 7670313a4..8950886a2 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -5,7 +5,6 @@ // * // * For the full copyright and license information, please view the LICENSE // * file that was distributed with this source code. -// * // spell-checker:ignore (ToDO) seekable seek'd tail'ing ringbuffer ringbuf @@ -21,19 +20,18 @@ use chunks::ReverseChunks; use clap::{App, Arg}; use std::collections::VecDeque; -use std::error::Error; use std::fmt; use std::fs::File; use std::io::{stdin, stdout, BufRead, BufReader, Read, Seek, SeekFrom, Write}; use std::path::Path; use std::thread::sleep; use std::time::Duration; +use uucore::parse_size::{parse_size, ParseSizeError}; use uucore::ringbuffer::RingBuffer; pub mod options { pub mod verbosity { pub static QUIET: &str = "quiet"; - pub static SILENT: &str = "silent"; pub static VERBOSE: &str = "verbose"; } pub static BYTES: &str = "bytes"; @@ -42,13 +40,12 @@ pub mod options { pub static PID: &str = "pid"; pub static SLEEP_INT: &str = "sleep-interval"; pub static ZERO_TERM: &str = "zero-terminated"; + pub static ARG_FILES: &str = "files"; } -static ARG_FILES: &str = "files"; - enum FilterMode { - Bytes(u64), - Lines(u64, u8), // (number of lines, delimiter) + Bytes(usize), + Lines(usize, u8), // (number of lines, delimiter) } struct Settings { @@ -78,12 +75,14 @@ pub fn uumain(args: impl uucore::Args) -> i32 { let app = App::new(executable!()) .version(crate_version!()) .about("output the last part of files") + // TODO: add usage .arg( Arg::with_name(options::BYTES) .short("c") .long(options::BYTES) .takes_value(true) .allow_hyphen_values(true) + .overrides_with_all(&[options::BYTES, options::LINES]) .help("Number of bytes to print"), ) .arg( @@ -98,6 +97,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .long(options::LINES) .takes_value(true) .allow_hyphen_values(true) + .overrides_with_all(&[options::BYTES, options::LINES]) .help("Number of lines to print"), ) .arg( @@ -110,13 +110,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::verbosity::QUIET) .short("q") .long(options::verbosity::QUIET) + .visible_alias("silent") + .overrides_with_all(&[options::verbosity::QUIET, options::verbosity::VERBOSE]) .help("never output headers giving file names"), ) - .arg( - Arg::with_name(options::verbosity::SILENT) - .long(options::verbosity::SILENT) - .help("synonym of --quiet"), - ) .arg( Arg::with_name(options::SLEEP_INT) .short("s") @@ -128,6 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::verbosity::VERBOSE) .short("v") .long(options::verbosity::VERBOSE) + .overrides_with_all(&[options::verbosity::QUIET, options::verbosity::VERBOSE]) .help("always output headers giving file names"), ) .arg( @@ -137,7 +135,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 { .help("Line delimiter is NUL, not newline"), ) .arg( - Arg::with_name(ARG_FILES) + Arg::with_name(options::ARG_FILES) .multiple(true) .takes_value(true) .min_values(1), @@ -171,38 +169,21 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } } - match matches.value_of(options::LINES) { - Some(n) => { - let mut slice: &str = n; - if slice.as_bytes().first() == Some(&b'+') { - settings.beginning = true; - slice = &slice[1..]; - } - match parse_size(slice) { - Ok(m) => settings.mode = FilterMode::Lines(m, b'\n'), - Err(e) => { - show_error!("{}", e.to_string()); - return 1; - } - } + let mode_and_beginning = if let Some(arg) = matches.value_of(options::BYTES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Bytes(n), beginning), + Err(e) => crash!(1, "invalid number of bytes: {}", e.to_string()), } - None => { - if let Some(n) = matches.value_of(options::BYTES) { - let mut slice: &str = n; - if slice.as_bytes().first() == Some(&b'+') { - settings.beginning = true; - slice = &slice[1..]; - } - match parse_size(slice) { - Ok(m) => settings.mode = FilterMode::Bytes(m), - Err(e) => { - show_error!("{}", e.to_string()); - return 1; - } - } - } + } else if let Some(arg) = matches.value_of(options::LINES) { + match parse_num(arg) { + Ok((n, beginning)) => (FilterMode::Lines(n, b'\n'), beginning), + Err(e) => crash!(1, "invalid number of lines: {}", e.to_string()), } + } else { + (FilterMode::Lines(10, b'\n'), false) }; + settings.mode = mode_and_beginning.0; + settings.beginning = mode_and_beginning.1; if matches.is_present(options::ZERO_TERM) { if let FilterMode::Lines(count, _) = settings.mode { @@ -211,11 +192,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { } let verbose = matches.is_present(options::verbosity::VERBOSE); - let quiet = matches.is_present(options::verbosity::QUIET) - || matches.is_present(options::verbosity::SILENT); + let quiet = matches.is_present(options::verbosity::QUIET); let files: Vec = matches - .values_of(ARG_FILES) + .values_of(options::ARG_FILES) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -264,92 +244,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 { 0 } -#[derive(Debug, PartialEq, Eq)] -pub enum ParseSizeErr { - ParseFailure(String), - SizeTooBig(String), -} - -impl Error for ParseSizeErr { - fn description(&self) -> &str { - match *self { - ParseSizeErr::ParseFailure(ref s) => &*s, - ParseSizeErr::SizeTooBig(ref s) => &*s, - } - } -} - -impl fmt::Display for ParseSizeErr { - fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { - let s = match self { - ParseSizeErr::ParseFailure(s) => s, - ParseSizeErr::SizeTooBig(s) => s, - }; - write!(f, "{}", s) - } -} - -impl ParseSizeErr { - fn parse_failure(s: &str) -> ParseSizeErr { - ParseSizeErr::ParseFailure(format!("invalid size: '{}'", s)) - } - - fn size_too_big(s: &str) -> ParseSizeErr { - ParseSizeErr::SizeTooBig(format!( - "invalid size: '{}': Value too large to be stored in data type", - s - )) - } -} - -pub type ParseSizeResult = Result; - -pub fn parse_size(mut size_slice: &str) -> Result { - let mut base = if size_slice.as_bytes().last() == Some(&b'B') { - size_slice = &size_slice[..size_slice.len() - 1]; - 1000u64 - } else { - 1024u64 - }; - - let exponent = match size_slice.as_bytes().last() { - Some(unit) => match unit { - b'K' | b'k' => 1u64, - b'M' => 2u64, - b'G' => 3u64, - b'T' => 4u64, - b'P' => 5u64, - b'E' => 6u64, - b'Z' | b'Y' => { - return Err(ParseSizeErr::size_too_big(size_slice)); - } - b'b' => { - base = 512u64; - 1u64 - } - _ => 0u64, - }, - None => 0u64, - }; - if exponent != 0 { - size_slice = &size_slice[..size_slice.len() - 1]; - } - - let mut multiplier = 1u64; - for _ in 0u64..exponent { - multiplier *= base; - } - if base == 1000u64 && exponent == 0u64 { - // sole B is not a valid suffix - Err(ParseSizeErr::parse_failure(size_slice)) - } else { - let value: Option = size_slice.parse().ok(); - value - .map(|v| Ok((multiplier as i64 * v.abs()) as u64)) - .unwrap_or_else(|| Err(ParseSizeErr::parse_failure(size_slice))) - } -} - fn follow(readers: &mut [BufReader], filenames: &[String], settings: &Settings) { assert!(settings.follow); let mut last = readers.len() - 1; @@ -463,7 +357,7 @@ fn bounded_tail(file: &mut File, settings: &Settings) { /// If any element of `iter` is an [`Err`], then this function panics. fn unbounded_tail_collect( iter: impl Iterator>, - count: u64, + count: usize, beginning: bool, ) -> VecDeque where @@ -508,3 +402,22 @@ fn print_byte(stdout: &mut T, ch: u8) { crash!(1, "{}", err); } } + +fn parse_num(src: &str) -> Result<(usize, bool), ParseSizeError> { + let mut size_string = src.trim(); + let mut starting_with = false; + + if let Some(c) = size_string.chars().next() { + if c == '+' || c == '-' { + // tail: '-' is not documented (8.32 man pages) + size_string = &size_string[1..]; + if c == '+' { + starting_with = true; + } + } + } else { + return Err(ParseSizeError::ParseFailure(src.to_string())); + } + + parse_size(size_string).map(|n| (n, starting_with)) +} diff --git a/src/uu/timeout/Cargo.toml b/src/uu/timeout/Cargo.toml index d16559858..a46b029bd 100644 --- a/src/uu/timeout/Cargo.toml +++ b/src/uu/timeout/Cargo.toml @@ -17,7 +17,7 @@ path = "src/timeout.rs" [dependencies] clap = "2.33" libc = "0.2.42" -uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["parse_time", "process", "signals"] } +uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["process", "signals"] } uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 22c0252f7..f81a95ab2 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -11,19 +11,21 @@ extern crate uucore; use clap::{crate_version, App, Arg}; +use std::convert::TryFrom; use std::fs::{metadata, OpenOptions}; use std::io::ErrorKind; use std::path::Path; +use uucore::parse_size::{parse_size, ParseSizeError}; #[derive(Debug, Eq, PartialEq)] enum TruncateMode { - Absolute(u64), - Extend(u64), - Reduce(u64), - AtMost(u64), - AtLeast(u64), - RoundDown(u64), - RoundUp(u64), + Absolute(usize), + Extend(usize), + Reduce(usize), + AtMost(usize), + AtLeast(usize), + RoundDown(usize), + RoundUp(usize), } impl TruncateMode { @@ -38,7 +40,7 @@ impl TruncateMode { /// let fsize = 10; /// assert_eq!(mode.to_size(fsize), 15); /// ``` - fn to_size(&self, fsize: u64) -> u64 { + fn to_size(&self, fsize: usize) -> usize { match self { TruncateMode::Absolute(size) => *size, TruncateMode::Extend(size) => fsize + size, @@ -58,10 +60,9 @@ pub mod options { pub static NO_CREATE: &str = "no-create"; pub static REFERENCE: &str = "reference"; pub static SIZE: &str = "size"; + pub static ARG_FILES: &str = "files"; } -static ARG_FILES: &str = "files"; - fn get_usage() -> String { format!("{0} [OPTION]... [FILE]...", executable!()) } @@ -113,21 +114,28 @@ pub fn uumain(args: impl uucore::Args) -> i32 { Arg::with_name(options::REFERENCE) .short("r") .long(options::REFERENCE) + .required_unless(options::SIZE) .help("base the size of each file on the size of RFILE") .value_name("RFILE") ) .arg( Arg::with_name(options::SIZE) .short("s") - .long("size") + .long(options::SIZE) + .required_unless(options::REFERENCE) .help("set or adjust the size of each file according to SIZE, which is in bytes unless --io-blocks is specified") .value_name("SIZE") ) - .arg(Arg::with_name(ARG_FILES).multiple(true).takes_value(true).min_values(1)) + .arg(Arg::with_name(options::ARG_FILES) + .value_name("FILE") + .multiple(true) + .takes_value(true) + .required(true) + .min_values(1)) .get_matches_from(args); let files: Vec = matches - .values_of(ARG_FILES) + .values_of(options::ARG_FILES) .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); @@ -149,8 +157,8 @@ pub fn uumain(args: impl uucore::Args) -> i32 { crash!( 1, "cannot stat '{}': No such file or directory", - reference.unwrap() - ); + reference.unwrap_or_else(|| "".to_string()) + ); // TODO: fix '--no-create' see test_reference and test_truncate_bytes_size } _ => crash!(1, "{}", e.to_string()), } @@ -172,10 +180,10 @@ pub fn uumain(args: impl uucore::Args) -> i32 { /// /// 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<()> { +fn file_truncate(filename: &str, create: bool, size: usize) -> std::io::Result<()> { let path = Path::new(filename); let f = OpenOptions::new().write(true).create(create).open(path)?; - f.set_len(size) + f.set_len(u64::try_from(size).unwrap()) } /// Truncate files to a size relative to a given file. @@ -206,9 +214,9 @@ fn truncate_reference_and_size( } _ => m, }, - Err(_) => crash!(1, "Invalid number: ‘{}’", size_string), + Err(e) => crash!(1, "Invalid number: {}", e.to_string()), }; - let fsize = metadata(rfilename)?.len(); + let fsize = usize::try_from(metadata(rfilename)?.len()).unwrap(); let tsize = mode.to_size(fsize); for filename in &filenames { file_truncate(filename, create, tsize)?; @@ -232,7 +240,7 @@ fn truncate_reference_file_only( filenames: Vec, create: bool, ) -> std::io::Result<()> { - let tsize = metadata(rfilename)?.len(); + let tsize = usize::try_from(metadata(rfilename)?.len()).unwrap(); for filename in &filenames { file_truncate(filename, create, tsize)?; } @@ -261,10 +269,10 @@ fn truncate_size_only( ) -> std::io::Result<()> { let mode = match parse_mode_and_size(size_string) { Ok(m) => m, - Err(_) => crash!(1, "Invalid number: ‘{}’", size_string), + Err(e) => crash!(1, "Invalid number: {}", e.to_string()), }; for filename in &filenames { - let fsize = metadata(filename).map(|m| m.len()).unwrap_or(0); + let fsize = usize::try_from(metadata(filename)?.len()).unwrap(); let tsize = mode.to_size(fsize); file_truncate(filename, create, tsize)?; } @@ -290,7 +298,7 @@ fn truncate( } (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"), + (None, None) => crash!(1, "you must specify either --reference or --size"), // this case cannot happen anymore because it's handled by clap } } @@ -317,114 +325,35 @@ fn is_modifier(c: char) -> bool { /// ```rust,ignore /// assert_eq!(parse_mode_and_size("+123"), (TruncateMode::Extend, 123)); /// ``` -fn parse_mode_and_size(size_string: &str) -> Result { +fn parse_mode_and_size(size_string: &str) -> Result { // Trim any whitespace. - let size_string = size_string.trim(); + let mut size_string = size_string.trim(); // Get the modifier character from the size string, if any. For // example, if the argument is "+123", then the modifier is '+'. - let c = size_string.chars().next().unwrap(); - let size_string = if is_modifier(c) { - &size_string[1..] + if let Some(c) = size_string.chars().next() { + if is_modifier(c) { + size_string = &size_string[1..]; + } + parse_size(size_string).map(match c { + '+' => TruncateMode::Extend, + '-' => TruncateMode::Reduce, + '<' => TruncateMode::AtMost, + '>' => TruncateMode::AtLeast, + '/' => TruncateMode::RoundDown, + '%' => TruncateMode::RoundUp, + _ => TruncateMode::Absolute, + }) } else { - size_string - }; - parse_size(size_string).map(match c { - '+' => TruncateMode::Extend, - '-' => TruncateMode::Reduce, - '<' => TruncateMode::AtMost, - '>' => TruncateMode::AtLeast, - '/' => TruncateMode::RoundDown, - '%' => TruncateMode::RoundUp, - _ => TruncateMode::Absolute, - }) -} - -/// Parse a size string into a number of bytes. -/// -/// A size string comprises an integer and an optional unit. The unit -/// may be K, M, G, T, P, E, Z, or Y (powers of 1024) or KB, MB, -/// etc. (powers of 1000). -/// -/// # Errors -/// -/// This function returns an error if the string does not begin with a -/// numeral, or if the unit is not one of the supported units described -/// in the preceding section. -/// -/// # Examples -/// -/// ```rust,ignore -/// assert_eq!(parse_size("123").unwrap(), 123); -/// assert_eq!(parse_size("123K").unwrap(), 123 * 1024); -/// assert_eq!(parse_size("123KB").unwrap(), 123 * 1000); -/// ``` -fn parse_size(size: &str) -> Result { - // Get the numeric part of the size argument. For example, if the - // argument is "123K", then the numeric part is "123". - let numeric_string: String = size.chars().take_while(|c| c.is_digit(10)).collect(); - let number: u64 = numeric_string.parse().map_err(|_| ())?; - - // Get the alphabetic units part of the size argument and compute - // the factor it represents. For example, if the argument is "123K", - // then the unit part is "K" and the factor is 1024. This may be the - // empty string, in which case, the factor is 1. - let n = numeric_string.len(); - let (base, exponent): (u64, u32) = match &size[n..] { - "" => (1, 0), - "K" | "k" => (1024, 1), - "M" | "m" => (1024, 2), - "G" | "g" => (1024, 3), - "T" | "t" => (1024, 4), - "P" | "p" => (1024, 5), - "E" | "e" => (1024, 6), - "Z" | "z" => (1024, 7), - "Y" | "y" => (1024, 8), - "KB" | "kB" => (1000, 1), - "MB" | "mB" => (1000, 2), - "GB" | "gB" => (1000, 3), - "TB" | "tB" => (1000, 4), - "PB" | "pB" => (1000, 5), - "EB" | "eB" => (1000, 6), - "ZB" | "zB" => (1000, 7), - "YB" | "yB" => (1000, 8), - _ => return Err(()), - }; - let factor = base.pow(exponent); - Ok(number * factor) + Err(ParseSizeError::ParseFailure(size_string.to_string())) + } } #[cfg(test)] mod tests { use crate::parse_mode_and_size; - use crate::parse_size; use crate::TruncateMode; - #[test] - fn test_parse_size_zero() { - assert_eq!(parse_size("0").unwrap(), 0); - assert_eq!(parse_size("0K").unwrap(), 0); - assert_eq!(parse_size("0KB").unwrap(), 0); - } - - #[test] - fn test_parse_size_without_factor() { - assert_eq!(parse_size("123").unwrap(), 123); - } - - #[test] - fn test_parse_size_kilobytes() { - assert_eq!(parse_size("123K").unwrap(), 123 * 1024); - assert_eq!(parse_size("123KB").unwrap(), 123 * 1000); - } - - #[test] - fn test_parse_size_megabytes() { - assert_eq!(parse_size("123").unwrap(), 123); - assert_eq!(parse_size("123M").unwrap(), 123 * 1024 * 1024); - 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))); diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 482252680..0c11d2c15 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -44,7 +44,6 @@ entries = ["libc"] fs = ["libc"] fsext = ["libc", "time"] mode = ["libc"] -parse_time = [] perms = ["libc"] process = ["libc"] ringbuffer = [] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 310a41fe1..c1e1ec31e 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -6,8 +6,6 @@ pub mod encoding; pub mod fs; #[cfg(feature = "fsext")] pub mod fsext; -#[cfg(feature = "parse_time")] -pub mod parse_time; #[cfg(feature = "ringbuffer")] pub mod ringbuffer; #[cfg(feature = "zero-copy")] diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 0b0d0fddf..f765b7b3e 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -19,10 +19,10 @@ pub extern crate winapi; //## internal modules -mod macros; // crate macros (macro_rules-type; exported to `crate::...`) - mod features; // feature-gated code modules +mod macros; // crate macros (macro_rules-type; exported to `crate::...`) mod mods; // core cross-platform modules +mod parser; // string parsing modules // * cross-platform modules pub use crate::mods::backup_control; @@ -31,6 +31,10 @@ pub use crate::mods::os; pub use crate::mods::panic; pub use crate::mods::ranges; +// * string parsing modules +pub use crate::parser::parse_size; +pub use crate::parser::parse_time; + // * feature-gated modules #[cfg(feature = "encoding")] pub use crate::features::encoding; @@ -38,8 +42,6 @@ pub use crate::features::encoding; pub use crate::features::fs; #[cfg(feature = "fsext")] pub use crate::features::fsext; -#[cfg(feature = "parse_time")] -pub use crate::features::parse_time; #[cfg(feature = "ringbuffer")] pub use crate::features::ringbuffer; #[cfg(feature = "zero-copy")] diff --git a/src/uucore/src/lib/parser.rs b/src/uucore/src/lib/parser.rs new file mode 100644 index 000000000..d09777e10 --- /dev/null +++ b/src/uucore/src/lib/parser.rs @@ -0,0 +1,2 @@ +pub mod parse_size; +pub mod parse_time; diff --git a/src/uucore/src/lib/parser/parse_size.rs b/src/uucore/src/lib/parser/parse_size.rs new file mode 100644 index 000000000..58213adef --- /dev/null +++ b/src/uucore/src/lib/parser/parse_size.rs @@ -0,0 +1,324 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + +// spell-checker:ignore (ToDO) hdsf ghead gtail + +use std::convert::TryFrom; +use std::error::Error; +use std::fmt; + +/// Parse a size string into a number of bytes. +/// +/// A size string comprises an integer and an optional unit. The unit +/// may be K, M, G, T, P, E, Z or Y (powers of 1024), or KB, MB, +/// etc. (powers of 1000), or b which is 512. +/// Binary prefixes can be used, too: KiB=K, MiB=M, and so on. +/// +/// # Errors +/// +/// Will return `ParseSizeError` if it’s not possible to parse this +/// string into a number, e.g. if the string does not begin with a +/// numeral, or if the unit is not one of the supported units described +/// in the preceding section. +/// +/// # Examples +/// +/// ```rust +/// use uucore::parse_size::parse_size; +/// assert_eq!(Ok(123), parse_size("123")); +/// assert_eq!(Ok(9 * 1000), parse_size("9kB")); // kB is 1000 +/// assert_eq!(Ok(2 * 1024), parse_size("2K")); // K is 1024 +/// ``` +pub fn parse_size(size: &str) -> Result { + if size.is_empty() { + return Err(ParseSizeError::parse_failure(size)); + } + // Get the numeric part of the size argument. For example, if the + // argument is "123K", then the numeric part is "123". + let numeric_string: String = size.chars().take_while(|c| c.is_digit(10)).collect(); + let number: usize = if !numeric_string.is_empty() { + match numeric_string.parse() { + Ok(n) => n, + Err(_) => return Err(ParseSizeError::parse_failure(size)), + } + } else { + 1 + }; + + // Get the alphabetic units part of the size argument and compute + // the factor it represents. For example, if the argument is "123K", + // then the unit part is "K" and the factor is 1024. This may be the + // empty string, in which case, the factor is 1. + let unit = &size[numeric_string.len()..]; + let (base, exponent): (u128, u32) = match unit { + "" => (1, 0), + "b" => (512, 1), // (`od`, `head` and `tail` use "b") + "KiB" | "kiB" | "K" | "k" => (1024, 1), + "MiB" | "miB" | "M" | "m" => (1024, 2), + "GiB" | "giB" | "G" | "g" => (1024, 3), + "TiB" | "tiB" | "T" | "t" => (1024, 4), + "PiB" | "piB" | "P" | "p" => (1024, 5), + "EiB" | "eiB" | "E" | "e" => (1024, 6), + "ZiB" | "ziB" | "Z" | "z" => (1024, 7), + "YiB" | "yiB" | "Y" | "y" => (1024, 8), + "KB" | "kB" => (1000, 1), + "MB" | "mB" => (1000, 2), + "GB" | "gB" => (1000, 3), + "TB" | "tB" => (1000, 4), + "PB" | "pB" => (1000, 5), + "EB" | "eB" => (1000, 6), + "ZB" | "zB" => (1000, 7), + "YB" | "yB" => (1000, 8), + _ => return Err(ParseSizeError::parse_failure(size)), + }; + let factor = match usize::try_from(base.pow(exponent)) { + Ok(n) => n, + Err(_) => return Err(ParseSizeError::size_too_big(size)), + }; + number + .checked_mul(factor) + .ok_or_else(|| ParseSizeError::size_too_big(size)) +} + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseSizeError { + ParseFailure(String), // Syntax + SizeTooBig(String), // Overflow +} + +impl Error for ParseSizeError { + fn description(&self) -> &str { + match *self { + ParseSizeError::ParseFailure(ref s) => &*s, + ParseSizeError::SizeTooBig(ref s) => &*s, + } + } +} + +impl fmt::Display for ParseSizeError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let s = match self { + ParseSizeError::ParseFailure(s) => s, + ParseSizeError::SizeTooBig(s) => s, + }; + write!(f, "{}", s) + } +} + +impl ParseSizeError { + fn parse_failure(s: &str) -> ParseSizeError { + // stderr on linux (GNU coreutils 8.32) + // has to be handled in the respective uutils because strings differ, e.g.: + // + // `NUM` + // head: invalid number of bytes: ‘1fb’ + // tail: invalid number of bytes: ‘1fb’ + // + // `SIZE` + // split: invalid number of bytes: ‘1fb’ + // truncate: Invalid number: ‘1fb’ + // + // `MODE` + // stdbuf: invalid mode ‘1fb’ + // + // `SIZE` + // sort: invalid suffix in --buffer-size argument '1fb' + // sort: invalid --buffer-size argument 'fb' + // + // `SIZE` + // du: invalid suffix in --buffer-size argument '1fb' + // du: invalid suffix in --threshold argument '1fb' + // du: invalid --buffer-size argument 'fb' + // du: invalid --threshold argument 'fb' + // + // `BYTES` + // od: invalid suffix in --read-bytes argument '1fb' + // od: invalid --read-bytes argument argument 'fb' + // --skip-bytes + // --width + // --strings + // etc. + ParseSizeError::ParseFailure(format!("‘{}’", s)) + } + + fn size_too_big(s: &str) -> ParseSizeError { + // stderr on linux (GNU coreutils 8.32) + // has to be handled in the respective uutils because strings differ, e.g.: + // + // head: invalid number of bytes: ‘1Y’: Value too large for defined data type + // tail: invalid number of bytes: ‘1Y’: Value too large for defined data type + // split: invalid number of bytes: ‘1Y’: Value too large for defined data type + // truncate: Invalid number: ‘1Y’: Value too large for defined data type + // stdbuf: invalid mode ‘1Y’: Value too large for defined data type + // sort: -S argument '1Y' too large + // du: -B argument '1Y' too large + // od: -N argument '1Y' too large + // etc. + // + // stderr on macos (brew - GNU coreutils 8.32) also differs for the same version, e.g.: + // ghead: invalid number of bytes: ‘1Y’: Value too large to be stored in data type + // gtail: invalid number of bytes: ‘1Y’: Value too large to be stored in data type + ParseSizeError::SizeTooBig(format!("‘{}’: Value too large for defined data type", s)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn variant_eq(a: &ParseSizeError, b: &ParseSizeError) -> bool { + std::mem::discriminant(a) == std::mem::discriminant(b) + } + + #[test] + fn all_suffixes() { + // Units are K,M,G,T,P,E,Z,Y (powers of 1024) or KB,MB,... (powers of 1000). + // Binary prefixes can be used, too: KiB=K, MiB=M, and so on. + let suffixes = [ + ('K', 1u32), + ('M', 2u32), + ('G', 3u32), + ('T', 4u32), + ('P', 5u32), + ('E', 6u32), + #[cfg(target_pointer_width = "128")] + ('Z', 7u32), // ParseSizeError::SizeTooBig on x64 + #[cfg(target_pointer_width = "128")] + ('Y', 8u32), // ParseSizeError::SizeTooBig on x64 + ]; + + for &(c, exp) in &suffixes { + let s = format!("2{}B", c); // KB + assert_eq!(Ok((2 * (1000_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("2{}", c); // K + assert_eq!(Ok((2 * (1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("2{}iB", c); // KiB + assert_eq!(Ok((2 * (1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("2{}iB", c.to_lowercase()); // kiB + assert_eq!(Ok((2 * (1024_u128).pow(exp)) as usize), parse_size(&s)); + + // suffix only + let s = format!("{}B", c); // KB + assert_eq!(Ok(((1000_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("{}", c); // K + assert_eq!(Ok(((1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("{}iB", c); // KiB + assert_eq!(Ok(((1024_u128).pow(exp)) as usize), parse_size(&s)); + let s = format!("{}iB", c.to_lowercase()); // kiB + assert_eq!(Ok(((1024_u128).pow(exp)) as usize), parse_size(&s)); + } + } + + #[test] + #[cfg(not(target_pointer_width = "128"))] + fn overflow_x64() { + assert!(parse_size("10000000000000000000000").is_err()); + assert!(parse_size("1000000000T").is_err()); + assert!(parse_size("100000P").is_err()); + assert!(parse_size("100E").is_err()); + assert!(parse_size("1Z").is_err()); + assert!(parse_size("1Y").is_err()); + + assert!(variant_eq( + &parse_size("1Z").unwrap_err(), + &ParseSizeError::SizeTooBig(String::new()) + )); + + assert_eq!( + ParseSizeError::SizeTooBig("‘1Y’: Value too large for defined data type".to_string()), + parse_size("1Y").unwrap_err() + ); + } + + #[test] + #[cfg(target_pointer_width = "32")] + fn overflow_x32() { + assert!(variant_eq( + &parse_size("1T").unwrap_err(), + &ParseSizeError::SizeTooBig(String::new()) + )); + assert!(variant_eq( + &parse_size("1000G").unwrap_err(), + &ParseSizeError::SizeTooBig(String::new()) + )); + } + + #[test] + fn invalid_syntax() { + let test_strings = [ + "328hdsf3290", + "5MiB nonsense", + "5mib", + "biB", + "-", + "+", + "", + "-1", + "1e2", + "∞", + ]; + for &test_string in &test_strings { + assert_eq!( + parse_size(test_string).unwrap_err(), + ParseSizeError::ParseFailure(format!("‘{}’", test_string)) + ); + } + } + + #[test] + fn b_suffix() { + assert_eq!(Ok(3 * 512), parse_size("3b")); // b is 512 + } + + #[test] + fn no_suffix() { + assert_eq!(Ok(1234), parse_size("1234")); + assert_eq!(Ok(0), parse_size("0")); + assert_eq!(Ok(5), parse_size("5")); + assert_eq!(Ok(999), parse_size("999")); + } + + #[test] + fn kilobytes_suffix() { + assert_eq!(Ok(123 * 1000), parse_size("123KB")); // KB is 1000 + assert_eq!(Ok(9 * 1000), parse_size("9kB")); // kB is 1000 + assert_eq!(Ok(2 * 1024), parse_size("2K")); // K is 1024 + assert_eq!(Ok(0), parse_size("0K")); + assert_eq!(Ok(0), parse_size("0KB")); + assert_eq!(Ok(1000), parse_size("KB")); + assert_eq!(Ok(1024), parse_size("K")); + assert_eq!(Ok(2000), parse_size("2kB")); + assert_eq!(Ok(4000), parse_size("4KB")); + } + + #[test] + fn megabytes_suffix() { + assert_eq!(Ok(123 * 1024 * 1024), parse_size("123M")); + assert_eq!(Ok(123 * 1000 * 1000), parse_size("123MB")); + assert_eq!(Ok(1024 * 1024), parse_size("M")); + assert_eq!(Ok(1000 * 1000), parse_size("MB")); + assert_eq!(Ok(2 * 1_048_576), parse_size("2m")); + assert_eq!(Ok(4 * 1_048_576), parse_size("4M")); + assert_eq!(Ok(2_000_000), parse_size("2mB")); + assert_eq!(Ok(4_000_000), parse_size("4MB")); + } + + #[test] + fn gigabytes_suffix() { + assert_eq!(Ok(1_073_741_824), parse_size("1G")); + assert_eq!(Ok(2_000_000_000), parse_size("2GB")); + } + + #[test] + #[cfg(target_pointer_width = "64")] + fn x64() { + assert_eq!(Ok(1_099_511_627_776), parse_size("1T")); + assert_eq!(Ok(1_125_899_906_842_624), parse_size("1P")); + assert_eq!(Ok(1_152_921_504_606_846_976), parse_size("1E")); + assert_eq!(Ok(2_000_000_000_000), parse_size("2TB")); + assert_eq!(Ok(2_000_000_000_000_000), parse_size("2PB")); + assert_eq!(Ok(2_000_000_000_000_000_000), parse_size("2EB")); + } +} diff --git a/src/uucore/src/lib/features/parse_time.rs b/src/uucore/src/lib/parser/parse_time.rs similarity index 100% rename from src/uucore/src/lib/features/parse_time.rs rename to src/uucore/src/lib/parser/parse_time.rs diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 231451b95..8d1267423 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -1,3 +1,8 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + // spell-checker:ignore (paths) sublink subwords use crate::common::util::*; @@ -73,6 +78,23 @@ fn _du_basics_subdir(s: &str) { } } +#[test] +fn test_du_invalid_size() { + new_ucmd!() + .arg("--block-size=1fb4t") + .arg("/tmp") + .fails() + .code_is(1) + .stderr_only("du: invalid --block-size argument '1fb4t'"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .arg("--block-size=1Y") + .arg("/tmp") + .fails() + .code_is(1) + .stderr_only("du: --block-size argument '1Y' too large"); +} + #[test] fn test_du_basics_bad_name() { new_ucmd!() diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 7daa80e3a..2c4b66696 100755 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -1,4 +1,9 @@ -// spell-checker:ignore (words) bogusfile emptyfile +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + +// spell-checker:ignore (words) bogusfile emptyfile abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu use crate::common::util::*; @@ -244,3 +249,61 @@ hello ", ); } + +#[test] +fn test_head_invalid_num() { + new_ucmd!() + .args(&["-c", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of bytes: ‘1024R’"); + new_ucmd!() + .args(&["-n", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of lines: ‘1024R’"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-c", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of bytes: ‘1Y’: Value too large for defined data type"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-n", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("head: invalid number of lines: ‘1Y’: Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["-c", size]) + .fails() + .code_is(1) + .stderr_only(format!( + "head: invalid number of bytes: ‘{}’: Value too large for defined data type", + size + )); + } + } +} + +#[test] +fn test_head_num_with_undocumented_sign_bytes() { + // tail: '-' is not documented (8.32 man pages) + // head: '+' is not documented (8.32 man pages) + const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz"; + new_ucmd!() + .args(&["-c", "5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("abcde"); + new_ucmd!() + .args(&["-c", "-5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("abcdefghijklmnopqrstu"); + new_ucmd!() + .args(&["-c", "+5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("abcde"); +} diff --git a/tests/by-util/test_od.rs b/tests/by-util/test_od.rs index c21c683dc..33d7d4dc4 100644 --- a/tests/by-util/test_od.rs +++ b/tests/by-util/test_od.rs @@ -1,3 +1,8 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + extern crate unindent; use self::unindent::*; @@ -804,3 +809,40 @@ fn test_traditional_only_label() { ", )); } + +#[test] +fn test_od_invalid_bytes() { + const INVALID_SIZE: &str = "1fb4t"; + const BIG_SIZE: &str = "1Y"; + + // NOTE: + // GNU's od (8.32) with option '--width' does not accept 'Y' as valid suffix. + // According to the man page it should be valid in the same way it is valid for + // '--read-bytes' and '--skip-bytes'. + + let options = [ + "--read-bytes", + "--skip-bytes", + "--width", + // "--strings", // TODO: consider testing here once '--strings' is implemented + ]; + for option in &options { + new_ucmd!() + .arg(format!("{}={}", option, INVALID_SIZE)) + .arg("file") + .fails() + .code_is(1) + .stderr_only(format!( + "od: invalid {} argument '{}'", + option, INVALID_SIZE + )); + + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .arg(format!("{}={}", option, BIG_SIZE)) + .arg("file") + .fails() + .code_is(1) + .stderr_only(format!("od: {} argument '{}' too large", option, BIG_SIZE)); + } +} diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 7a0143b43..2894d3007 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1,3 +1,8 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + // spell-checker:ignore (words) ints use crate::common::util::*; @@ -21,9 +26,7 @@ fn test_helper(file_name: &str, possible_args: &[&str]) { #[test] fn test_buffer_sizes() { - let buffer_sizes = [ - "0", "50K", "50k", "1M", "100M", "1000G", "10T", "500E", "1Y", - ]; + let buffer_sizes = ["0", "50K", "50k", "1M", "100M"]; for buffer_size in &buffer_sizes { new_ucmd!() .arg("-n") @@ -32,6 +35,20 @@ fn test_buffer_sizes() { .arg("ext_sort.txt") .succeeds() .stdout_is_fixture("ext_sort.expected"); + + #[cfg(not(target_pointer_width = "32"))] + { + let buffer_sizes = ["1000G", "10T"]; + for buffer_size in &buffer_sizes { + new_ucmd!() + .arg("-n") + .arg("-S") + .arg(buffer_size) + .arg("ext_sort.txt") + .succeeds() + .stdout_is_fixture("ext_sort.expected"); + } + } } } @@ -43,11 +60,39 @@ fn test_invalid_buffer_size() { .arg("-S") .arg(invalid_buffer_size) .fails() + .code_is(2) .stderr_only(format!( - "sort: failed to parse buffer size `{}`: invalid digit found in string", + "sort: invalid --buffer-size argument '{}'", invalid_buffer_size )); } + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .arg("-n") + .arg("-S") + .arg("1Y") + .arg("ext_sort.txt") + .fails() + .code_is(2) + .stderr_only("sort: --buffer-size argument '1Y' too large"); + + #[cfg(target_pointer_width = "32")] + { + let buffer_sizes = ["1000G", "10T"]; + for buffer_size in &buffer_sizes { + new_ucmd!() + .arg("-n") + .arg("-S") + .arg(buffer_size) + .arg("ext_sort.txt") + .fails() + .code_is(2) + .stderr_only(format!( + "sort: --buffer-size argument '{}' too large", + buffer_size + )); + } + } } #[test] @@ -804,7 +849,7 @@ fn sort_multiple() { #[test] fn sort_empty_chunk() { new_ucmd!() - .args(&["-S", "40B"]) + .args(&["-S", "40b"]) .pipe_in("a\na\n") .succeeds() .stdout_is("a\na\n"); @@ -844,7 +889,7 @@ fn test_compress_fail() { #[test] fn test_merge_batches() { new_ucmd!() - .args(&["ext_sort.txt", "-n", "-S", "150B"]) + .args(&["ext_sort.txt", "-n", "-S", "150b"]) .succeeds() .stdout_only_fixture("ext_sort.expected"); } diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 85b28326b..a1350534f 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -1,3 +1,8 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + extern crate rand; extern crate regex; @@ -285,3 +290,53 @@ fn test_filter_command_fails() { ucmd.args(&["--filter=/a/path/that/totally/does/not/exist", name]) .fails(); } + +#[test] +fn test_split_lines_number() { + // Test if stdout/stderr for '--lines' option is correct + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.touch("file"); + + scene + .ucmd() + .args(&["--lines", "2", "file"]) + .succeeds() + .no_stderr() + .no_stdout(); + scene + .ucmd() + .args(&["--lines", "2fb", "file"]) + .fails() + .code_is(1) + .stderr_only("split: invalid number of lines: ‘2fb’"); +} + +#[test] +fn test_split_invalid_bytes_size() { + new_ucmd!() + .args(&["-b", "1024R"]) + .fails() + .code_is(1) + .stderr_only("split: invalid number of bytes: ‘1024R’"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-b", "1Y"]) + .fails() + .code_is(1) + .stderr_only("split: invalid number of bytes: ‘1Y’: Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["-b", size]) + .fails() + .code_is(1) + .stderr_only(format!( + "split: invalid number of bytes: ‘{}’: Value too large for defined data type", + size + )); + } + } +} diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 2e09601ce..fc1b9324a 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -57,8 +57,18 @@ fn test_stdbuf_line_buffering_stdin_fails() { #[cfg(not(target_os = "windows"))] #[test] fn test_stdbuf_invalid_mode_fails() { - new_ucmd!() - .args(&["-i", "1024R", "head"]) - .fails() - .stderr_is("stdbuf: invalid mode 1024R\nTry 'stdbuf --help' for more information."); + let options = ["--input", "--output", "--error"]; + for option in &options { + new_ucmd!() + .args(&[*option, "1024R", "head"]) + .fails() + .code_is(125) + .stderr_only("stdbuf: invalid mode ‘1024R’"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&[*option, "1Y", "head"]) + .fails() + .code_is(125) + .stderr_contains("stdbuf: invalid mode ‘1Y’: Value too large for defined data type"); + } } diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index b31344c34..8478944e2 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -1,6 +1,12 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + +// spell-checker:ignore (ToDO) abcdefghijklmnopqrstuvwxyz efghijklmnopqrstuvwxyz vwxyz emptyfile + extern crate tail; -use self::tail::parse_size; use crate::common::util::*; use std::char::from_digit; use std::io::Write; @@ -236,41 +242,6 @@ fn test_bytes_big() { } } -#[test] -fn test_parse_size() { - // No suffix. - assert_eq!(Ok(1234), parse_size("1234")); - - // kB is 1000 - assert_eq!(Ok(9 * 1000), parse_size("9kB")); - - // K is 1024 - assert_eq!(Ok(2 * 1024), parse_size("2K")); - - let suffixes = [ - ('M', 2u32), - ('G', 3u32), - ('T', 4u32), - ('P', 5u32), - ('E', 6u32), - ]; - - for &(c, exp) in &suffixes { - let s = format!("2{}B", c); - assert_eq!(Ok(2 * (1000_u64).pow(exp)), parse_size(&s)); - - let s = format!("2{}", c); - assert_eq!(Ok(2 * (1024_u64).pow(exp)), parse_size(&s)); - } - - // Sizes that are too big. - assert!(parse_size("1Z").is_err()); - assert!(parse_size("1Y").is_err()); - - // Bad number - assert!(parse_size("328hdsf3290").is_err()); // spell-checker:disable-line -} - #[test] fn test_lines_with_size_suffix() { const FILE: &str = "test_lines_with_size_suffix.txt"; @@ -320,12 +291,11 @@ fn test_multiple_input_files_with_suppressed_headers() { #[test] fn test_multiple_input_quiet_flag_overrides_verbose_flag_for_suppressing_headers() { - // TODO: actually the later one should win, i.e. -qv should lead to headers being printed, -vq to them being suppressed new_ucmd!() .arg(FOOBAR_TXT) .arg(FOOBAR_2_TXT) - .arg("-q") .arg("-v") + .arg("-q") .run() .stdout_is_fixture("foobar_multiple_quiet.expected"); } @@ -388,3 +358,61 @@ fn test_positive_zero_lines() { .succeeds() .stdout_is("a\nb\nc\nd\ne\n"); } + +#[test] +fn test_tail_invalid_num() { + new_ucmd!() + .args(&["-c", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of bytes: ‘1024R’"); + new_ucmd!() + .args(&["-n", "1024R", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of lines: ‘1024R’"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-c", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of bytes: ‘1Y’: Value too large for defined data type"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["-n", "1Y", "emptyfile.txt"]) + .fails() + .stderr_is("tail: invalid number of lines: ‘1Y’: Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["-c", size]) + .fails() + .code_is(1) + .stderr_only(format!( + "tail: invalid number of bytes: ‘{}’: Value too large for defined data type", + size + )); + } + } +} + +#[test] +fn test_tail_num_with_undocumented_sign_bytes() { + // tail: '-' is not documented (8.32 man pages) + // head: '+' is not documented (8.32 man pages) + const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz"; + new_ucmd!() + .args(&["-c", "5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("vwxyz"); + new_ucmd!() + .args(&["-c", "-5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("vwxyz"); + new_ucmd!() + .args(&["-c", "+5"]) + .pipe_in(ALPHABET) + .succeeds() + .stdout_is("efghijklmnopqrstuvwxyz"); +} diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index bb0f4a596..2da59035e 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -1,3 +1,10 @@ +// * This file is part of the uutils coreutils package. +// * +// * For the full copyright and license information, please view the LICENSE +// * file that was distributed with this source code. + +// spell-checker:ignore (words) RFILE + use crate::common::util::*; use std::io::{Seek, SeekFrom, Write}; @@ -45,9 +52,18 @@ fn test_reference() { let at = &scene.fixtures; let mut file = at.make_file(FILE2); - scene.ucmd().arg("-s").arg("+5KB").arg(FILE1).run(); + // manpage: "A FILE argument that does not exist is created." + // TODO: 'truncate' does not create the file in this case, + // but should because '--no-create' wasn't specified. + at.touch(FILE1); // TODO: remove this when 'no-create' is fixed + scene.ucmd().arg("-s").arg("+5KB").arg(FILE1).succeeds(); - scene.ucmd().arg("--reference").arg(FILE1).arg(FILE2).run(); + scene + .ucmd() + .arg("--reference") + .arg(FILE1) + .arg(FILE2) + .succeeds(); file.seek(SeekFrom::End(0)).unwrap(); let actual = file.seek(SeekFrom::Current(0)).unwrap(); @@ -231,11 +247,18 @@ fn test_size_and_reference() { ); } +#[test] +fn test_error_filename_only() { + // truncate: you must specify either ‘--size’ or ‘--reference’ + new_ucmd!().args(&["file"]).fails().stderr_contains( + "error: The following required arguments were not provided: + --reference + --size ", + ); +} + #[test] fn test_invalid_numbers() { - // TODO For compatibility with GNU, `truncate -s 0X` should cause - // the same error as `truncate -s 0X file`, but currently it returns - // a different error. new_ucmd!() .args(&["-s", "0X", "file"]) .fails() @@ -265,3 +288,36 @@ fn test_reference_with_size_file_not_found() { .fails() .stderr_contains("cannot stat 'a': No such file or directory"); } + +#[test] +fn test_truncate_bytes_size() { + // TODO: this should succeed without error, uncomment when '--no-create' is fixed + // new_ucmd!() + // .args(&["--no-create", "--size", "K", "file"]) + // .succeeds(); + new_ucmd!() + .args(&["--size", "1024R", "file"]) + .fails() + .code_is(1) + .stderr_only("truncate: Invalid number: ‘1024R’"); + #[cfg(not(target_pointer_width = "128"))] + new_ucmd!() + .args(&["--size", "1Y", "file"]) + .fails() + .code_is(1) + .stderr_only("truncate: Invalid number: ‘1Y’: Value too large for defined data type"); + #[cfg(target_pointer_width = "32")] + { + let sizes = ["1000G", "10T"]; + for size in &sizes { + new_ucmd!() + .args(&["--size", size, "file"]) + .fails() + .code_is(1) + .stderr_only(format!( + "truncate: Invalid number: ‘{}’: Value too large for defined data type", + size + )); + } + } +}