From 047a6be73c35e83ab440a2f5291b16bba4c94639 Mon Sep 17 00:00:00 2001 From: aweinstock Date: Fri, 27 Feb 2026 13:20:12 -0500 Subject: [PATCH] uucore: Permit 'D' as a decimal suffix modifier in size parsing for GNU compatibility. truncate: Reject 'b' as a unit. --- src/uu/truncate/src/truncate.rs | 30 ++++++++++++------- .../src/lib/features/parser/parse_size.rs | 30 ++++++++++++------- tests/by-util/test_truncate.rs | 4 +++ 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 4461a4db280..6f460cfd708 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -16,7 +16,7 @@ use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::format_usage; use uucore::translate; -use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; +use uucore::parser::parse_size::{ParseSizeError, Parser, allow_list_with_all_suffixes}; #[derive(Debug, Eq, PartialEq)] enum TruncateMode { @@ -298,7 +298,7 @@ fn is_modifier(c: char) -> bool { /// Parse a size string with optional modifier symbol as its first character. /// -/// A size string is as described in [`parse_size_u64`]. The first character +/// A size string is as described in [`Parser::parse_u64`]. 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 @@ -323,15 +323,20 @@ fn parse_mode_and_size(size_string: &str) -> Result TruncateMode::Extend, - '-' => TruncateMode::Reduce, - '<' => TruncateMode::AtMost, - '>' => TruncateMode::AtLeast, - '/' => TruncateMode::RoundDown, - '%' => TruncateMode::RoundUp, - _ => TruncateMode::Absolute, - }) + let allow_list = allow_list_with_all_suffixes("EgGkKmMPQRtTYZ"); + let allow_list_ref = allow_list.iter().map(AsRef::as_ref).collect::>(); + Parser::default() + .with_allow_list(&allow_list_ref) + .parse_u64(size_string) + .map(match c { + '+' => TruncateMode::Extend, + '-' => TruncateMode::Reduce, + '<' => TruncateMode::AtMost, + '>' => TruncateMode::AtLeast, + '/' => TruncateMode::RoundDown, + '%' => TruncateMode::RoundUp, + _ => TruncateMode::Absolute, + }) } else { Err(ParseSizeError::ParseFailure(size_string.to_string())) } @@ -351,6 +356,9 @@ mod tests { 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))); + assert_eq!(parse_mode_and_size("1kB"), Ok(TruncateMode::Absolute(1000))); + assert_eq!(parse_mode_and_size("1kD"), Ok(TruncateMode::Absolute(1000))); + assert!(parse_mode_and_size("1b").is_err()); } #[test] diff --git a/src/uucore/src/lib/features/parser/parse_size.rs b/src/uucore/src/lib/features/parser/parse_size.rs index 05c270e4cf8..cc7e5e9e2e4 100644 --- a/src/uucore/src/lib/features/parser/parse_size.rs +++ b/src/uucore/src/lib/features/parser/parse_size.rs @@ -244,16 +244,16 @@ impl<'parser> Parser<'parser> { "YiB" | "yiB" | "Y" | "y" => (1024, 8), "RiB" | "riB" | "R" | "r" => (1024, 9), "QiB" | "qiB" | "Q" | "q" => (1024, 10), - "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), - "RB" | "rB" => (1000, 9), - "QB" | "qB" => (1000, 10), + "KB" | "kB" | "KD" | "kD" => (1000, 1), + "MB" | "mB" | "MD" | "mD" => (1000, 2), + "GB" | "gB" | "GD" | "gD" => (1000, 3), + "TB" | "tB" | "TD" | "tD" => (1000, 4), + "PB" | "pB" | "PD" | "pD" => (1000, 5), + "EB" | "eB" | "ED" | "eD" => (1000, 6), + "ZB" | "zB" | "ZD" | "zD" => (1000, 7), + "YB" | "yB" | "YD" | "yD" => (1000, 8), + "RB" | "rB" | "RD" | "rD" => (1000, 9), + "QB" | "qB" | "QD" | "qD" => (1000, 10), _ if numeric_string.is_empty() => return Err(ParseSizeError::parse_failure(size)), _ => return Err(ParseSizeError::invalid_suffix(size)), }; @@ -373,6 +373,16 @@ impl<'parser> Parser<'parser> { } } +pub fn allow_list_with_all_suffixes(units: &str) -> Vec { + let mut allow_list = Vec::with_capacity(4 * units.len()); + for unit in units.chars() { + for suffix in &["", "iB", "B", "D"] { + allow_list.push(format!("{unit}{suffix}")); + } + } + allow_list +} + /// Parse a size string into a number of bytes /// using Default Parser (no custom settings) /// diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 3057dcfbe68..a875d158bd1 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -254,6 +254,10 @@ fn test_invalid_numbers() { .args(&["-s", "0B", "file"]) .fails() .stderr_contains("Invalid number: '0B'"); + new_ucmd!() + .args(&["-s", "1b", "file"]) + .fails() + .stderr_contains("Invalid number: '1b'"); } #[test]