parser: add binary support to determine_number_system and parse_size (#9659)

* parser: add binary support to determine_number_system and parse_size

* docs

* tests

* tests: threshold
This commit is contained in:
Kornél Csernai
2025-12-22 09:24:44 +01:00
committed by GitHub
parent a738fbaa43
commit eed7a0aca7
7 changed files with 249 additions and 11 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ df-after-help = Display values are in units of the first available SIZE from --b
SIZE is an integer and optional unit (example: 10M is 10*1024*1024).
Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers
of 1000).
of 1000). Units can be decimal, hexadecimal, octal, binary.
# Help messages
df-help-print-help = Print help information.
+1 -1
View File
@@ -7,7 +7,7 @@ df-after-help = Les valeurs affichées sont en unités de la première TAILLE di
TAILLE est un entier et une unité optionnelle (exemple : 10M est 10*1024*1024).
Les unités sont K, M, G, T, P, E, Z, Y (puissances de 1024) ou KB, MB,... (puissances
de 1000).
de 1000). Les unités peuvent être décimales, hexadécimales, octales, binaires.
# Messages d'aide
df-help-print-help = afficher les informations d'aide.
+1 -1
View File
@@ -7,7 +7,7 @@ du-after-help = Display values are in units of the first available SIZE from --b
SIZE is an integer and optional unit (example: 10M is 10*1024*1024).
Units are K, M, G, T, P, E, Z, Y (powers of 1024) or KB, MB,... (powers
of 1000).
of 1000). Units can be decimal, hexadecimal, octal, binary.
PATTERN allows some advanced exclusions. For example, the following syntaxes
are supported:
+1 -1
View File
@@ -7,7 +7,7 @@ du-after-help = Les valeurs affichées sont en unités de la première TAILLE di
TAILLE est un entier et une unité optionnelle (exemple : 10M est 10*1024*1024).
Les unités sont K, M, G, T, P, E, Z, Y (puissances de 1024) ou KB, MB,... (puissances
de 1000).
de 1000). Les unités peuvent être décimales, hexadécimales, octales, binaires.
MOTIF permet des exclusions avancées. Par exemple, les syntaxes suivantes
sont supportées :
@@ -106,6 +106,7 @@ enum NumberSystem {
Decimal,
Octal,
Hexadecimal,
Binary,
}
impl<'parser> Parser<'parser> {
@@ -134,10 +135,11 @@ impl<'parser> Parser<'parser> {
}
/// 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, Y, R or Q (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.
/// A size string comprises an integer and an optional unit. The integer
/// may be in decimal, octal (0 prefix), hexadecimal (0x prefix), or
/// binary (0b prefix) notation. The unit may be K, M, G, T, P, E, Z, Y,
/// R or Q (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
///
@@ -159,6 +161,7 @@ impl<'parser> Parser<'parser> {
/// assert_eq!(Ok(9 * 1000), parser.parse("9kB")); // kB is 1000
/// assert_eq!(Ok(2 * 1024), parser.parse("2K")); // K is 1024
/// assert_eq!(Ok(44251 * 1024), parser.parse("0xACDBK")); // 0xACDB is 44251 in decimal
/// assert_eq!(Ok(44251 * 1024 * 1024), parser.parse("0b1010110011011011")); // 0b1010110011011011 is 44251 in decimal, default M
/// ```
pub fn parse(&self, size: &str) -> Result<u128, ParseSizeError> {
if size.is_empty() {
@@ -176,6 +179,11 @@ impl<'parser> Parser<'parser> {
.take(2)
.chain(size.chars().skip(2).take_while(char::is_ascii_hexdigit))
.collect(),
NumberSystem::Binary => size
.chars()
.take(2)
.chain(size.chars().skip(2).take_while(|c| c.is_digit(2)))
.collect(),
_ => size.chars().take_while(char::is_ascii_digit).collect(),
};
let mut unit: &str = &size[numeric_string.len()..];
@@ -268,6 +276,10 @@ impl<'parser> Parser<'parser> {
let trimmed_string = numeric_string.trim_start_matches("0x");
Self::parse_number(trimmed_string, 16, size)?
}
NumberSystem::Binary => {
let trimmed_string = numeric_string.trim_start_matches("0b");
Self::parse_number(trimmed_string, 2, size)?
}
};
number
@@ -328,6 +340,14 @@ impl<'parser> Parser<'parser> {
return NumberSystem::Hexadecimal;
}
// Binary prefix: "0b" followed by at least one binary digit (0 or 1)
// Note: "0b" alone is treated as decimal 0 with suffix "b"
if let Some(prefix) = size.strip_prefix("0b") {
if !prefix.is_empty() {
return NumberSystem::Binary;
}
}
let num_digits: usize = size
.chars()
.take_while(char::is_ascii_digit)
@@ -363,7 +383,9 @@ impl<'parser> Parser<'parser> {
/// assert_eq!(Ok(123), parse_size_u128("123"));
/// assert_eq!(Ok(9 * 1000), parse_size_u128("9kB")); // kB is 1000
/// assert_eq!(Ok(2 * 1024), parse_size_u128("2K")); // K is 1024
/// assert_eq!(Ok(44251 * 1024), parse_size_u128("0xACDBK"));
/// assert_eq!(Ok(44251 * 1024), parse_size_u128("0xACDBK")); // hexadecimal
/// assert_eq!(Ok(10), parse_size_u128("0b1010")); // binary
/// assert_eq!(Ok(10 * 1024), parse_size_u128("0b1010K")); // binary with suffix
/// ```
pub fn parse_size_u128(size: &str) -> Result<u128, ParseSizeError> {
Parser::default().parse(size)
@@ -564,6 +586,7 @@ mod tests {
assert!(parse_size_u64("1Y").is_err());
assert!(parse_size_u64("1R").is_err());
assert!(parse_size_u64("1Q").is_err());
assert!(parse_size_u64("0b1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111").is_err());
assert!(variant_eq(
&parse_size_u64("1Z").unwrap_err(),
@@ -634,6 +657,7 @@ mod tests {
#[test]
fn b_suffix() {
assert_eq!(Ok(3 * 512), parse_size_u64("3b")); // b is 512
assert_eq!(Ok(0), parse_size_u64("0b")); // b should be used as a suffix in this case instead of signifying binary
}
#[test]
@@ -774,6 +798,12 @@ mod tests {
assert_eq!(Ok(44251 * 1024), parse_size_u128("0xACDBK"));
}
#[test]
fn parse_binary_size() {
assert_eq!(Ok(44251), parse_size_u64("0b1010110011011011"));
assert_eq!(Ok(44251 * 1024), parse_size_u64("0b1010110011011011K"));
}
#[test]
#[cfg(target_os = "linux")]
fn parse_percent() {
+73
View File
@@ -648,6 +648,53 @@ fn test_block_size_with_suffix() {
assert_eq!(get_header("1GB"), "1GB-blocks");
}
#[test]
fn test_df_binary_block_size() {
fn get_header(block_size: &str) -> String {
let output = new_ucmd!()
.args(&["-B", block_size, "--output=size"])
.succeeds()
.stdout_str_lossy();
output.lines().next().unwrap().trim().to_string()
}
let test_cases = [
("0b1", "1"),
("0b10100", "20"),
("0b1000000000", "512"),
("0b10K", "2K"),
];
for (binary, decimal) in test_cases {
let binary_result = get_header(binary);
let decimal_result = get_header(decimal);
assert_eq!(
binary_result, decimal_result,
"Binary {binary} should equal decimal {decimal}"
);
}
}
#[test]
fn test_df_binary_env_block_size() {
fn get_header(env_var: &str, env_value: &str) -> String {
let output = new_ucmd!()
.env(env_var, env_value)
.args(&["--output=size"])
.succeeds()
.stdout_str_lossy();
output.lines().next().unwrap().trim().to_string()
}
let binary_header = get_header("DF_BLOCK_SIZE", "0b10000000000");
let decimal_header = get_header("DF_BLOCK_SIZE", "1024");
assert_eq!(binary_header, decimal_header);
let binary_header = get_header("BLOCK_SIZE", "0b10000000000");
let decimal_header = get_header("BLOCK_SIZE", "1024");
assert_eq!(binary_header, decimal_header);
}
#[test]
fn test_block_size_in_posix_portability_mode() {
fn get_header(block_size: &str) -> String {
@@ -849,6 +896,32 @@ fn test_invalid_block_size_suffix() {
.stderr_contains("invalid suffix in --block-size argument '1.2'");
}
#[test]
fn test_df_invalid_binary_size() {
new_ucmd!()
.arg("--block-size=0b123")
.fails()
.stderr_contains("invalid suffix in --block-size argument '0b123'");
}
#[test]
fn test_df_binary_edge_cases() {
new_ucmd!()
.arg("-B0b")
.fails()
.stderr_contains("invalid --block-size argument '0b'");
new_ucmd!()
.arg("-B0B")
.fails()
.stderr_contains("invalid suffix in --block-size argument '0B'");
new_ucmd!()
.arg("--block-size=0b1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
.fails()
.stderr_contains("too large");
}
#[test]
fn test_output_selects_columns() {
let output = new_ucmd!()
+137 -2
View File
@@ -282,6 +282,120 @@ fn test_du_env_block_size_hierarchy() {
assert_eq!(expected, result2);
}
#[test]
fn test_du_binary_block_size() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
let dir = "a";
at.mkdir(dir);
let fpath = at.plus(format!("{dir}/file"));
std::fs::File::create(&fpath)
.expect("cannot create test file")
.set_len(100_000)
.expect("cannot set file size");
let test_cases = [
("0b1", "1"),
("0b10100", "20"),
("0b1000000000", "512"),
("0b10K", "2K"),
];
for (binary, decimal) in test_cases {
let decimal = ts
.ucmd()
.arg(dir)
.arg(format!("--block-size={decimal}"))
.succeeds()
.stdout_move_str();
let binary = ts
.ucmd()
.arg(dir)
.arg(format!("--block-size={binary}"))
.succeeds()
.stdout_move_str();
assert_eq!(
decimal, binary,
"Binary {binary} should equal decimal {decimal}"
);
}
}
#[test]
fn test_du_binary_env_block_size() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
let dir = "a";
at.mkdir(dir);
let fpath = at.plus(format!("{dir}/file"));
std::fs::File::create(&fpath)
.expect("cannot create test file")
.set_len(100_000)
.expect("cannot set file size");
let expected = ts
.ucmd()
.arg(dir)
.arg("--block-size=1024")
.succeeds()
.stdout_move_str();
let result = ts
.ucmd()
.arg(dir)
.env("DU_BLOCK_SIZE", "0b10000000000")
.succeeds()
.stdout_move_str();
assert_eq!(expected, result);
}
#[test]
fn test_du_invalid_binary_size() {
let ts = TestScenario::new(util_name!());
ts.ucmd()
.arg("--block-size=0b123")
.arg("/tmp")
.fails_with_code(1)
.stderr_only("du: invalid suffix in --block-size argument '0b123'\n");
ts.ucmd()
.arg("--threshold=0b123")
.arg("/tmp")
.fails_with_code(1)
.stderr_only("du: invalid suffix in --threshold argument '0b123'\n");
}
#[test]
fn test_du_binary_edge_cases() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.write("foo", "test");
ts.ucmd()
.arg("-B0b")
.arg("foo")
.fails()
.stderr_only("du: invalid --block-size argument '0b'\n");
ts.ucmd()
.arg("-B0B")
.arg("foo")
.fails()
.stderr_only("du: invalid suffix in --block-size argument '0B'\n");
ts.ucmd()
.arg("--block-size=0b1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
.arg("foo")
.fails_with_code(1)
.stderr_contains("too large");
}
#[test]
fn test_du_non_existing_files() {
new_ucmd!()
@@ -978,7 +1092,7 @@ fn test_du_threshold() {
at.write("subdir/links/bigfile.txt", &"x".repeat(10000)); // ~10K file
at.write("subdir/deeper/deeper_dir/smallfile.txt", "small"); // small file
let threshold = if cfg!(windows) { "7K" } else { "10K" };
let threshold = "10K";
ts.ucmd()
.arg("--apparent-size")
@@ -995,6 +1109,27 @@ fn test_du_threshold() {
.stdout_contains("deeper_dir");
}
#[test]
#[cfg(not(target_os = "openbsd"))]
fn test_du_binary_threshold() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.mkdir_all("subdir/links");
at.mkdir_all("subdir/deeper/deeper_dir");
at.write("subdir/links/bigfile.txt", &"x".repeat(10000));
at.write("subdir/deeper/deeper_dir/smallfile.txt", "small");
let threshold_bin = "0b10011100010000";
ts.ucmd()
.arg("--apparent-size")
.arg(format!("--threshold={threshold_bin}"))
.succeeds()
.stdout_contains("links")
.stdout_does_not_contain("deeper_dir");
}
#[test]
fn test_du_invalid_threshold() {
let ts = TestScenario::new(util_name!());
@@ -1528,7 +1663,7 @@ fn test_du_blocksize_zero_do_not_panic() {
let ts = TestScenario::new(util_name!());
let at = &ts.fixtures;
at.write("foo", "some content");
for block_size in ["0", "00", "000", "0x0"] {
for block_size in ["0", "00", "000", "0x0", "0b0"] {
ts.ucmd()
.arg(format!("-B{block_size}"))
.arg("foo")