Skip to content

Commit 36b3af0

Browse files
authored
mcookie: add support for human-readable sizes with -m option (#268)
* Create size.rs * parse max size from human-readable strings * tests for human-readable strings * fmt * fix failed tests and edge cases * fix fmt * cpr & license * use usimpleerror * fmt
1 parent c1a4462 commit 36b3af0

3 files changed

Lines changed: 141 additions & 7 deletions

File tree

src/uu/mcookie/src/mcookie.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ use std::{fs::File, io::Read};
88
use clap::{crate_version, Arg, ArgAction, Command};
99
use md5::{Digest, Md5};
1010
use rand::RngCore;
11-
use uucore::{error::UResult, format_usage, help_about, help_usage};
11+
use uucore::{
12+
error::{UResult, USimpleError},
13+
format_usage, help_about, help_usage,
14+
};
15+
mod size;
16+
use size::Size;
1217

1318
mod options {
1419
pub const FILE: &str = "file";
@@ -31,10 +36,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
3136
.map(|v| v.as_str())
3237
.collect();
3338

34-
// TODO: Parse max size from human-readable strings (KiB, MiB, GiB etc.)
35-
let max_size = matches
36-
.get_one::<String>(options::MAX_SIZE)
37-
.map(|v| v.parse::<u64>().expect("Failed to parse max-size value"));
39+
let max_size = if let Some(size_str) = matches.get_one::<String>(options::MAX_SIZE) {
40+
match Size::parse(size_str) {
41+
Ok(size) => Some(size.size_bytes()),
42+
Err(_) => {
43+
return Err(USimpleError::new(1, "Failed to parse max-size value"));
44+
}
45+
}
46+
} else {
47+
None
48+
};
3849

3950
let mut hasher = Md5::new();
4051

@@ -99,7 +110,7 @@ pub fn uu_app() -> Command {
99110
.long("max-size")
100111
.value_name("num")
101112
.action(ArgAction::Set)
102-
.help("limit how much is read from seed files"),
113+
.help("limit how much is read from seed files (supports B suffix or binary units: KiB, MiB, GiB, TiB)"),
103114
)
104115
.arg(
105116
Arg::new(options::VERBOSE)

src/uu/mcookie/src/size.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// This file is part of the uutils util-linux package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use std::error::Error;
7+
use std::fmt;
8+
9+
#[derive(Debug)]
10+
pub struct ParseSizeError(String);
11+
12+
impl fmt::Display for ParseSizeError {
13+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14+
write!(f, "Invalid size format: {}", self.0)
15+
}
16+
}
17+
18+
impl Error for ParseSizeError {}
19+
20+
pub struct Size(u64);
21+
22+
impl Size {
23+
pub fn parse(s: &str) -> Result<Self, ParseSizeError> {
24+
let s = s.trim();
25+
26+
// Handle bytes with "B" suffix
27+
if s.ends_with('B') && !s.ends_with("iB") {
28+
if let Some(nums) = s.strip_suffix('B') {
29+
return nums
30+
.trim()
31+
.parse::<u64>()
32+
.map(Self)
33+
.map_err(|_| ParseSizeError(s.to_string()));
34+
}
35+
}
36+
37+
// Handle binary units (KiB, MiB, GiB, TiB)
38+
for (suffix, exponent) in [("KiB", 1), ("MiB", 2), ("GiB", 3), ("TiB", 4)] {
39+
if let Some(nums) = s.strip_suffix(suffix) {
40+
return nums
41+
.trim()
42+
.parse::<u64>()
43+
.map(|n| Self(n * 1024_u64.pow(exponent)))
44+
.map_err(|_| ParseSizeError(s.to_string()));
45+
}
46+
}
47+
48+
// If no suffix, treat as bytes
49+
s.parse::<u64>()
50+
.map(Self)
51+
.map_err(|_| ParseSizeError(s.to_string()))
52+
}
53+
54+
pub fn size_bytes(&self) -> u64 {
55+
self.0
56+
}
57+
}
58+
59+
#[cfg(test)]
60+
mod tests {
61+
use super::*;
62+
63+
#[test]
64+
fn test_parse_numeric() {
65+
assert_eq!(Size::parse("1234").unwrap().size_bytes(), 1234);
66+
}
67+
68+
#[test]
69+
fn test_parse_with_suffix() {
70+
assert_eq!(Size::parse("1024B").unwrap().size_bytes(), 1024);
71+
assert_eq!(Size::parse("1KiB").unwrap().size_bytes(), 1024);
72+
assert_eq!(Size::parse("1MiB").unwrap().size_bytes(), 1024 * 1024);
73+
assert_eq!(
74+
Size::parse("1GiB").unwrap().size_bytes(),
75+
1024 * 1024 * 1024
76+
);
77+
assert_eq!(
78+
Size::parse("1TiB").unwrap().size_bytes(),
79+
1024 * 1024 * 1024 * 1024
80+
);
81+
}
82+
83+
#[test]
84+
fn test_invalid_input() {
85+
// Invalid format
86+
assert!(Size::parse("invalid").is_err());
87+
}
88+
}

tests/by-util/test_mcookie.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ fn test_verbose() {
3232
}
3333

3434
#[test]
35-
fn test_seed_files_and_max_size() {
35+
fn test_seed_files_and_max_size_raw() {
3636
let mut file1 = NamedTempFile::new().unwrap();
3737
const CONTENT1: &str = "Some seed data";
3838
file1.write_all(CONTENT1.as_bytes()).unwrap();
@@ -63,3 +63,38 @@ fn test_seed_files_and_max_size() {
6363
file2.path().to_str().unwrap()
6464
));
6565
}
66+
67+
#[test]
68+
fn test_seed_files_and_max_size_human_readable() {
69+
let mut file = NamedTempFile::new().unwrap();
70+
const CONTENT: [u8; 4096] = [1; 4096];
71+
file.write_all(&CONTENT).unwrap();
72+
73+
let res = new_ucmd!()
74+
.arg("--verbose")
75+
.arg("-f")
76+
.arg(file.path())
77+
.arg("-m")
78+
.arg("2KiB")
79+
.succeeds();
80+
81+
// Ensure we only read up to 2KiB (2048 bytes)
82+
res.stderr_contains(format!(
83+
"Got 2048 bytes from {}",
84+
file.path().to_str().unwrap()
85+
));
86+
}
87+
88+
#[test]
89+
fn test_invalid_size_format() {
90+
let file = NamedTempFile::new().unwrap();
91+
92+
let res = new_ucmd!()
93+
.arg("-f")
94+
.arg(file.path())
95+
.arg("-m")
96+
.arg("invalid")
97+
.fails();
98+
99+
res.stderr_contains("Failed to parse max-size value");
100+
}

0 commit comments

Comments
 (0)