1use std::path::Path;
8
9use anyhow::Result;
10use walkdir::WalkDir;
11
12#[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
34pub 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
89fn 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
109fn 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
126fn 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
139fn 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
145fn 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
151fn 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); 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); assert_eq!(parse_size("2.75GiB")?, 2_952_790_016); 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 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 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 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 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 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 assert!(parse_integer_size("not_a_number", 1000).is_err());
320 Ok(())
321 }
322
323 #[test]
324 fn test_edge_cases() -> anyhow::Result<()> {
325 assert_eq!(parse_size("0.001KB")?, 1);
327
328 let large_but_valid = (u64::MAX / 1_000_000_000).to_string() + "GB";
330 assert!(parse_size(&large_but_valid).is_ok());
331
332 assert_eq!(parse_size("0KB")?, 0);
334 assert_eq!(parse_size("0.0MB")?, 0);
335 Ok(())
336 }
337}