clean_dev_dirs/utils/
size.rs

1//! Size parsing and manipulation utilities.
2//!
3//! This module provides functions for parsing human-readable size strings
4//! (like "100MB" or "1.5GiB") into byte values.
5
6use anyhow::Result;
7
8/// Parse a human-readable size string into bytes.
9///
10/// Supports both decimal (KB, MB, GB) and binary (KiB, MiB, GiB) units,
11/// as well as decimal numbers (e.g., "1.5GB").
12///
13/// # Arguments
14///
15/// * `size_str` - A string representing the size (e.g., "100MB", "1.5GiB", "1,000,000")
16///
17/// # Returns
18///
19/// - `Ok(u64)` - The size in bytes
20/// - `Err(anyhow::Error)` - If the string format is invalid or causes overflow
21///
22/// # Errors
23///
24/// This function will return an error if:
25/// - The size string format is invalid (e.g., "1.2.3MB", "invalid")
26/// - The number cannot be parsed as a valid integer or decimal
27/// - The resulting value would overflow `u64`
28/// - The decimal has too many fractional digits (more than 9)
29///
30/// # Examples
31///
32/// ```
33/// # use clean_dev_dirs::utils::parse_size;
34/// # use anyhow::Result;
35/// # fn main() -> Result<()> {
36/// assert_eq!(parse_size("100KB")?, 100_000);
37/// assert_eq!(parse_size("1.5MB")?, 1_500_000);
38/// assert_eq!(parse_size("1GiB")?, 1_073_741_824);
39/// # Ok(())
40/// # }
41/// ```
42///
43/// # Supported Units
44///
45/// - **Decimal**: KB (1000), MB (1000²), GB (1000³)
46/// - **Binary**: KiB (1024), MiB (1024²), GiB (1024³)
47/// - **Bytes**: Plain numbers without units
48pub fn parse_size(size_str: &str) -> Result<u64> {
49    if size_str == "0" {
50        return Ok(0);
51    }
52
53    let size_str = size_str.to_uppercase();
54    let (number_str, multiplier) = parse_size_unit(&size_str);
55
56    if number_str.contains('.') {
57        parse_decimal_size(number_str, multiplier)
58    } else {
59        parse_integer_size(number_str, multiplier)
60    }
61}
62
63/// Parse the unit suffix and return the numeric part with its multiplier.
64fn parse_size_unit(size_str: &str) -> (&str, u64) {
65    const UNITS: &[(&str, u64)] = &[
66        ("GIB", 1_073_741_824),
67        ("MIB", 1_048_576),
68        ("KIB", 1_024),
69        ("GB", 1_000_000_000),
70        ("MB", 1_000_000),
71        ("KB", 1_000),
72    ];
73
74    for (suffix, multiplier) in UNITS {
75        if size_str.ends_with(suffix) {
76            return (size_str.trim_end_matches(suffix), *multiplier);
77        }
78    }
79
80    (size_str, 1)
81}
82
83/// Parse a decimal size value (e.g., "1.5").
84fn parse_decimal_size(number_str: &str, multiplier: u64) -> Result<u64> {
85    let parts: Vec<&str> = number_str.split('.').collect();
86    if parts.len() != 2 {
87        return Err(anyhow::anyhow!("Invalid decimal format: {number_str}"));
88    }
89
90    let integer_part: u64 = parts[0].parse().unwrap_or(0);
91    let fractional_result = parse_fractional_part(parts[1])?;
92
93    let integer_bytes = multiply_with_overflow_check(integer_part, multiplier)?;
94    let fractional_bytes =
95        multiply_with_overflow_check(fractional_result, multiplier)? / 1_000_000_000;
96
97    add_with_overflow_check(integer_bytes, fractional_bytes)
98}
99
100/// Parse the fractional part of a decimal number.
101fn parse_fractional_part(fractional_str: &str) -> Result<u64> {
102    let fractional_digits = fractional_str.len();
103    if fractional_digits > 9 {
104        return Err(anyhow::anyhow!("Too many decimal places: {fractional_str}"));
105    }
106
107    let fractional_part: u64 = fractional_str.parse()?;
108    let fractional_multiplier = 10u64.pow(9 - u32::try_from(fractional_digits)?);
109
110    Ok(fractional_part * fractional_multiplier)
111}
112
113/// Parse an integer size value.
114fn parse_integer_size(number_str: &str, multiplier: u64) -> Result<u64> {
115    let number: u64 = number_str.parse()?;
116    multiply_with_overflow_check(number, multiplier)
117}
118
119/// Multiply two values with overflow checking.
120fn multiply_with_overflow_check(a: u64, b: u64) -> Result<u64> {
121    a.checked_mul(b)
122        .ok_or_else(|| anyhow::anyhow!("Size value overflow: {a} * {b}"))
123}
124
125/// Add two values with overflow checking.
126fn add_with_overflow_check(a: u64, b: u64) -> Result<u64> {
127    a.checked_add(b)
128        .ok_or_else(|| anyhow::anyhow!("Final overflow: {a} + {b}"))
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_parse_size_zero() {
137        assert_eq!(parse_size("0").unwrap(), 0);
138    }
139
140    #[test]
141    fn test_parse_size_plain_bytes() {
142        assert_eq!(parse_size("1000").unwrap(), 1000);
143        assert_eq!(parse_size("12345").unwrap(), 12345);
144        assert_eq!(parse_size("1").unwrap(), 1);
145    }
146
147    #[test]
148    fn test_parse_size_decimal_units() {
149        assert_eq!(parse_size("1KB").unwrap(), 1_000);
150        assert_eq!(parse_size("100KB").unwrap(), 100_000);
151        assert_eq!(parse_size("1MB").unwrap(), 1_000_000);
152        assert_eq!(parse_size("5MB").unwrap(), 5_000_000);
153        assert_eq!(parse_size("1GB").unwrap(), 1_000_000_000);
154        assert_eq!(parse_size("2GB").unwrap(), 2_000_000_000);
155    }
156
157    #[test]
158    fn test_parse_size_binary_units() {
159        assert_eq!(parse_size("1KiB").unwrap(), 1_024);
160        assert_eq!(parse_size("1MiB").unwrap(), 1_048_576);
161        assert_eq!(parse_size("1GiB").unwrap(), 1_073_741_824);
162        assert_eq!(parse_size("2KiB").unwrap(), 2_048);
163        assert_eq!(parse_size("10MiB").unwrap(), 10_485_760);
164    }
165
166    #[test]
167    fn test_parse_size_case_insensitive() {
168        assert_eq!(parse_size("1kb").unwrap(), 1_000);
169        assert_eq!(parse_size("1Kb").unwrap(), 1_000);
170        assert_eq!(parse_size("1kB").unwrap(), 1_000);
171        assert_eq!(parse_size("1mb").unwrap(), 1_000_000);
172        assert_eq!(parse_size("1mib").unwrap(), 1_048_576);
173        assert_eq!(parse_size("1gib").unwrap(), 1_073_741_824);
174    }
175
176    #[test]
177    fn test_parse_size_decimal_values() {
178        assert_eq!(parse_size("1.5KB").unwrap(), 1_500);
179        assert_eq!(parse_size("2.5MB").unwrap(), 2_500_000);
180        assert_eq!(parse_size("1.5MiB").unwrap(), 1_572_864); // 1.5 * 1048576
181        assert_eq!(parse_size("0.5GB").unwrap(), 500_000_000);
182        assert_eq!(parse_size("0.1KB").unwrap(), 100);
183    }
184
185    #[test]
186    fn test_parse_size_complex_decimals() {
187        assert_eq!(parse_size("1.25MB").unwrap(), 1_250_000);
188        assert_eq!(parse_size("3.14159KB").unwrap(), 3_141); // Truncated due to precision
189        assert_eq!(parse_size("2.75GiB").unwrap(), 2_952_790_016); // 2.75 * 1073741824
190    }
191
192    #[test]
193    fn test_parse_size_invalid_formats() {
194        assert!(parse_size("").is_err());
195        assert!(parse_size("invalid").is_err());
196        assert!(parse_size("1.2.3MB").is_err());
197        assert!(parse_size("MB1").is_err());
198        assert!(parse_size("1XB").is_err());
199        assert!(parse_size("-1MB").is_err());
200    }
201
202    #[test]
203    fn test_parse_size_unit_order() {
204        // Test that longer units are matched first (GiB before GB, MiB before MB, etc.)
205        assert_eq!(parse_size("1GiB").unwrap(), 1_073_741_824);
206        assert_eq!(parse_size("1GB").unwrap(), 1_000_000_000);
207        assert_eq!(parse_size("1MiB").unwrap(), 1_048_576);
208        assert_eq!(parse_size("1MB").unwrap(), 1_000_000);
209    }
210
211    #[test]
212    fn test_parse_size_overflow() {
213        // Test with values that would cause overflow
214        let max_u64_str = format!("{}", u64::MAX);
215        let too_large = format!("{}GB", u64::MAX / 1000 + 1);
216
217        assert!(parse_size(&max_u64_str).is_ok());
218        assert!(parse_size(&too_large).is_err());
219        assert!(parse_size("999999999999999999999999GB").is_err());
220    }
221
222    #[test]
223    fn test_parse_fractional_part() {
224        assert_eq!(parse_fractional_part("5").unwrap(), 500_000_000);
225        assert_eq!(parse_fractional_part("25").unwrap(), 250_000_000);
226        assert_eq!(parse_fractional_part("125").unwrap(), 125_000_000);
227        assert_eq!(parse_fractional_part("999999999").unwrap(), 999_999_999);
228
229        // Too many decimal places
230        assert!(parse_fractional_part("1234567890").is_err());
231    }
232
233    #[test]
234    fn test_multiply_with_overflow_check() {
235        assert_eq!(multiply_with_overflow_check(100, 200).unwrap(), 20_000);
236        assert_eq!(multiply_with_overflow_check(0, 999).unwrap(), 0);
237        assert_eq!(multiply_with_overflow_check(1, 1).unwrap(), 1);
238
239        // Test overflow
240        assert!(multiply_with_overflow_check(u64::MAX, 2).is_err());
241        assert!(multiply_with_overflow_check(u64::MAX / 2 + 1, 2).is_err());
242    }
243
244    #[test]
245    fn test_add_with_overflow_check() {
246        assert_eq!(add_with_overflow_check(100, 200).unwrap(), 300);
247        assert_eq!(add_with_overflow_check(0, 999).unwrap(), 999);
248        assert_eq!(add_with_overflow_check(u64::MAX - 1, 1).unwrap(), u64::MAX);
249
250        // Test overflow
251        assert!(add_with_overflow_check(u64::MAX, 1).is_err());
252        assert!(add_with_overflow_check(u64::MAX - 1, 2).is_err());
253    }
254
255    #[test]
256    fn test_parse_size_unit() {
257        assert_eq!(parse_size_unit("100GB"), ("100", 1_000_000_000));
258        assert_eq!(parse_size_unit("50MIB"), ("50", 1_048_576));
259        assert_eq!(parse_size_unit("1024"), ("1024", 1));
260        assert_eq!(parse_size_unit("2.5KB"), ("2.5", 1_000));
261        assert_eq!(parse_size_unit("1.5GIB"), ("1.5", 1_073_741_824));
262    }
263
264    #[test]
265    fn test_parse_decimal_size() {
266        assert_eq!(parse_decimal_size("1.5", 1_000_000).unwrap(), 1_500_000);
267        assert_eq!(parse_decimal_size("2.25", 1_000).unwrap(), 2_250);
268        assert_eq!(
269            parse_decimal_size("0.5", 2_000_000_000).unwrap(),
270            1_000_000_000
271        );
272
273        // Invalid formats
274        assert!(parse_decimal_size("1.2.3", 1000).is_err());
275        assert!(parse_decimal_size("invalid", 1000).is_err());
276    }
277
278    #[test]
279    fn test_parse_integer_size() {
280        assert_eq!(parse_integer_size("100", 1_000).unwrap(), 100_000);
281        assert_eq!(parse_integer_size("0", 999).unwrap(), 0);
282        assert_eq!(
283            parse_integer_size("1", 1_000_000_000).unwrap(),
284            1_000_000_000
285        );
286
287        // Invalid format
288        assert!(parse_integer_size("not_a_number", 1000).is_err());
289    }
290
291    #[test]
292    fn test_edge_cases() {
293        // Very small decimal
294        assert_eq!(parse_size("0.001KB").unwrap(), 1);
295
296        // Very large valid number
297        let large_but_valid = (u64::MAX / 1_000_000_000).to_string() + "GB";
298        assert!(parse_size(&large_but_valid).is_ok());
299
300        // Zero with units
301        assert_eq!(parse_size("0KB").unwrap(), 0);
302        assert_eq!(parse_size("0.0MB").unwrap(), 0);
303    }
304}