Skip to main content

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