diff --git a/src/uucore/src/lib/features/parser/parse_signed_num.rs b/src/uucore/src/lib/features/parser/parse_signed_num.rs index 836c4f0a05..f094b321fc 100644 --- a/src/uucore/src/lib/features/parser/parse_signed_num.rs +++ b/src/uucore/src/lib/features/parser/parse_signed_num.rs @@ -8,7 +8,31 @@ //! These utilities accept arguments like `-5`, `+10`, `-100K` where the leading //! sign indicates different behavior (e.g., "first N" vs "last N" vs "starting from N"). -use super::parse_size::{ParseSizeError, parse_size_u64, parse_size_u64_max, size_offset}; +use super::parse_size::{ + ParseSizeError, Parser, allow_list_with_all_suffixes, parse_size_u64, size_offset, +}; + +/// The multiplier suffixes accepted on a count argument. +/// +/// Each of these is also valid followed by `B`, `iB` or `D`. A lowercase +/// letter is only accepted for `k` and `m`; the remaining multipliers must be +/// uppercase. `b` (512-byte blocks) is handled separately because it is the +/// one suffix that has no `B`/`iB`/`D` form. +const MULTIPLIER_SUFFIXES: &str = "kmKMGTPEZYRQ"; + +/// Parse the numeric part of a count argument, rejecting any suffix that is +/// not one of [`MULTIPLIER_SUFFIXES`] or a bare `b`. +/// +/// The generic size parser accepts a lowercase form of every multiplier, which +/// is more than these utilities allow. +fn parse_count(size: &str) -> Result { + let mut allow_list = allow_list_with_all_suffixes(MULTIPLIER_SUFFIXES); + allow_list.push("b".to_string()); + let allow_list: Vec<&str> = allow_list.iter().map(AsRef::as_ref).collect(); + Parser::default() + .with_allow_list(&allow_list) + .parse_u64_max(size) +} /// The sign prefix found on a numeric argument. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -95,10 +119,10 @@ pub fn parse_signed_num_max(src: &str) -> Result { // Otherwise "0K" would parse as 1KiB (bare suffix means 1). // A genuinely bare suffix with no digits at all (e.g. "kiB") // still parses as 1 of that unit. - parse_size_u64_max(trimmed)?; + parse_count(trimmed)?; 0 } else { - parse_size_u64_max(trimmed)? + parse_count(trimmed)? }; Ok(SignedNum { value, sign }) diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 7ce7dac6b1..d266e458fc 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -1181,3 +1181,33 @@ head: invalid number of bytes: '1fb' .stderr_is("head: invalid number of bytes: '1fb'\n"); } } + +#[test] +fn test_lowercase_multiplier_suffixes_rejected() { + // GNU accepts a lowercase suffix only for "k" and "m"; every other + // multiplier must be uppercase. "b" is bare-only (no B/iB/D form). + for suffix in ["g", "t", "p", "e", "z", "y", "r", "q"] { + new_ucmd!() + .args(&["-c", &format!("2{suffix}")]) + .fails_with_code(1) + .stderr_is(format!("head: invalid number of bytes: '2{suffix}'\n")); + new_ucmd!() + .args(&["-n", &format!("2{suffix}")]) + .fails_with_code(1) + .stderr_is(format!("head: invalid number of lines: '2{suffix}'\n")); + } +} + +#[test] +fn test_accepted_multiplier_suffixes() { + for suffix in [ + "b", "k", "m", "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q", "kB", "KiB", "kD", "MiB", + "GB", + ] { + new_ucmd!() + .args(&["-c", &format!("1{suffix}")]) + .pipe_in("x") + .ignore_stdin_write_error() + .succeeds(); + } +} diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index a935b69db6..e57e6decc7 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -1245,6 +1245,38 @@ fn test_invalid_num() { .starts_with("tail: invalid number of bytes: '³'"); } +#[test] +fn test_lowercase_multiplier_suffixes_rejected() { + // GNU accepts a lowercase suffix only for "k" and "m"; every other + // multiplier must be uppercase. "b" is bare-only (no B/iB/D form). + for suffix in ["g", "t", "p", "e", "z", "y", "r", "q"] { + new_ucmd!() + .args(&["-c", &format!("2{suffix}")]) + .fails() + .stderr_str() + .starts_with(&format!("tail: invalid number of bytes: '2{suffix}'")); + new_ucmd!() + .args(&["-n", &format!("2{suffix}")]) + .fails() + .stderr_str() + .starts_with(&format!("tail: invalid number of lines: '2{suffix}'")); + } +} + +#[test] +fn test_accepted_multiplier_suffixes() { + for suffix in [ + "b", "k", "m", "K", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q", "kB", "KiB", "kD", "MiB", + "GB", + ] { + new_ucmd!() + .args(&["-c", &format!("1{suffix}")]) + .pipe_in("x") + .ignore_stdin_write_error() + .succeeds(); + } +} + #[test] fn test_oversized_num() { const BIG: &str = "99999999999999999999999999999";