Skip to main content

mvm_core/
util.rs

1//! Utility functions for parsing and formatting.
2
3use anyhow::{Result, anyhow};
4
5/// Parse a human-readable size string into megabytes.
6///
7/// Supports suffixes: K/KB, M/MB, G/GB (case-insensitive).
8/// If no suffix is provided, assumes megabytes.
9///
10/// # Examples
11///
12/// ```
13/// use mvm_core::util::parse_human_size;
14///
15/// assert_eq!(parse_human_size("512").unwrap(), 512);
16/// assert_eq!(parse_human_size("512M").unwrap(), 512);
17/// assert_eq!(parse_human_size("512MB").unwrap(), 512);
18/// assert_eq!(parse_human_size("4G").unwrap(), 4096);
19/// assert_eq!(parse_human_size("4GB").unwrap(), 4096);
20/// assert_eq!(parse_human_size("1024K").unwrap(), 1);
21/// assert_eq!(parse_human_size("1024KB").unwrap(), 1);
22/// ```
23pub fn parse_human_size(input: &str) -> Result<u32> {
24    let input = input.trim();
25    if input.is_empty() {
26        return Err(anyhow!("Empty size string"));
27    }
28
29    // Find where the numeric part ends
30    let (num_part, suffix) = {
31        let mut num_end = 0;
32        for (i, ch) in input.chars().enumerate() {
33            if ch.is_ascii_digit() || ch == '.' {
34                num_end = i + 1;
35            } else {
36                break;
37            }
38        }
39        (&input[..num_end], &input[num_end..])
40    };
41
42    if num_part.is_empty() {
43        return Err(anyhow!("No numeric value found in '{}'", input));
44    }
45
46    let num: f64 = num_part
47        .parse()
48        .map_err(|_| anyhow!("Invalid number '{}'", num_part))?;
49
50    if num < 0.0 {
51        return Err(anyhow!("Size cannot be negative"));
52    }
53
54    // Determine multiplier based on suffix (case-insensitive)
55    let multiplier: f64 = match suffix.trim().to_uppercase().as_str() {
56        "" | "M" | "MB" | "MIB" => 1.0,
57        "G" | "GB" | "GIB" => 1024.0,
58        "K" | "KB" | "KIB" => 1.0 / 1024.0,
59        other => return Err(anyhow!("Unknown size suffix '{}'", other)),
60    };
61
62    let result = num * multiplier;
63
64    // Check for overflow and round to nearest integer
65    if result > u32::MAX as f64 {
66        return Err(anyhow!("Size too large (max: {} MiB)", u32::MAX));
67    }
68
69    Ok(result.round() as u32)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_parse_plain_number() {
78        assert_eq!(parse_human_size("512").unwrap(), 512);
79        assert_eq!(parse_human_size("1024").unwrap(), 1024);
80        assert_eq!(parse_human_size("1").unwrap(), 1);
81    }
82
83    #[test]
84    fn test_parse_megabytes() {
85        assert_eq!(parse_human_size("512M").unwrap(), 512);
86        assert_eq!(parse_human_size("512MB").unwrap(), 512);
87        assert_eq!(parse_human_size("512MiB").unwrap(), 512);
88        assert_eq!(parse_human_size("512m").unwrap(), 512);
89        assert_eq!(parse_human_size("512mb").unwrap(), 512);
90    }
91
92    #[test]
93    fn test_parse_gigabytes() {
94        assert_eq!(parse_human_size("1G").unwrap(), 1024);
95        assert_eq!(parse_human_size("1GB").unwrap(), 1024);
96        assert_eq!(parse_human_size("1GiB").unwrap(), 1024);
97        assert_eq!(parse_human_size("4G").unwrap(), 4096);
98        assert_eq!(parse_human_size("4g").unwrap(), 4096);
99        assert_eq!(parse_human_size("0.5G").unwrap(), 512);
100    }
101
102    #[test]
103    fn test_parse_kilobytes() {
104        assert_eq!(parse_human_size("1024K").unwrap(), 1);
105        assert_eq!(parse_human_size("1024KB").unwrap(), 1);
106        assert_eq!(parse_human_size("1024KiB").unwrap(), 1);
107        assert_eq!(parse_human_size("512K").unwrap(), 1); // rounds 0.5 to 1
108        assert_eq!(parse_human_size("2048k").unwrap(), 2);
109    }
110
111    #[test]
112    fn test_parse_with_whitespace() {
113        assert_eq!(parse_human_size("  512  ").unwrap(), 512);
114        assert_eq!(parse_human_size("  4G  ").unwrap(), 4096);
115        assert_eq!(parse_human_size("512 M").unwrap(), 512);
116    }
117
118    #[test]
119    fn test_parse_decimal() {
120        assert_eq!(parse_human_size("1.5G").unwrap(), 1536);
121        assert_eq!(parse_human_size("0.5G").unwrap(), 512);
122        assert_eq!(parse_human_size("2.5G").unwrap(), 2560);
123    }
124
125    #[test]
126    fn test_parse_errors() {
127        assert!(parse_human_size("").is_err());
128        assert!(parse_human_size("abc").is_err());
129        assert!(parse_human_size("512T").is_err()); // Unsupported suffix
130        assert!(parse_human_size("-512M").is_err()); // Negative
131        assert!(parse_human_size("512XB").is_err()); // Invalid suffix
132    }
133
134    #[test]
135    fn test_parse_overflow() {
136        // u32::MAX is 4294967295, way larger than practical memory values
137        // but let's test the boundary
138        assert!(parse_human_size("9999999G").is_err());
139    }
140
141    #[test]
142    fn test_parse_zero() {
143        assert_eq!(parse_human_size("0").unwrap(), 0);
144        assert_eq!(parse_human_size("0M").unwrap(), 0);
145        assert_eq!(parse_human_size("0G").unwrap(), 0);
146    }
147}