archmeld 1.3.0

Secure, memory-safe, type-safe CLI for multi-format archive extraction, inspection and decompression
Documentation
//! Human-readable size suffix parsing.
//!
//! Parses size strings with optional K/M/G/T suffixes
//! (e.g. `"100M"`, `"1G"`, `"512K"`) into byte counts.

use crate::error::Error;

/// Parse a size string with optional K/M/G/T suffix.
///
/// Supports: plain bytes (`"100"`), kilobytes (`"100K"`),
/// megabytes (`"100M"`), gigabytes (`"1G"`), terabytes
/// (`"1T"`). An optional trailing `B` is stripped
/// (`"100MB"` == `"100M"`). Case-insensitive.
///
/// # Errors
///
/// Returns [`Error::InvalidSize`] on empty input, invalid
/// number, unknown suffix, or overflow.
pub fn parse_size(input: &str) -> Result<u64, Error> {
    let input = input.trim();
    if input.is_empty() {
        return Err(Error::InvalidSize(input.to_owned()));
    }

    // Strip optional trailing 'B' or 'b'
    let s = input
        .strip_suffix('B')
        .or_else(|| input.strip_suffix('b'))
        .unwrap_or(input);

    // Split into numeric prefix and suffix
    let split_pos = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());

    let num_str = &s[..split_pos];
    let suffix = &s[split_pos..];

    if num_str.is_empty() {
        return Err(Error::InvalidSize(input.to_owned()));
    }

    let base: u64 = num_str
        .parse()
        .map_err(|_| Error::InvalidSize(input.to_owned()))?;

    let multiplier: u64 = match suffix.to_ascii_uppercase().as_str() {
        "" => 1,
        "K" | "KI" => 1024,
        "M" | "MI" => 1024 * 1024,
        "G" | "GI" => 1024 * 1024 * 1024,
        "T" | "TI" => 1024 * 1024 * 1024 * 1024,
        _ => {
            return Err(Error::InvalidSize(input.to_owned()));
        },
    };

    base.checked_mul(multiplier)
        .ok_or_else(|| Error::InvalidSize(input.to_owned()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_plain_bytes() {
        assert_eq!(parse_size("100").unwrap(), 100);
        assert_eq!(parse_size("0").unwrap(), 0);
        assert_eq!(parse_size("1").unwrap(), 1);
    }

    #[test]
    fn test_kilobytes() {
        assert_eq!(parse_size("1K").unwrap(), 1024);
        assert_eq!(parse_size("100K").unwrap(), 102_400);
        assert_eq!(parse_size("1KB").unwrap(), 1024);
        assert_eq!(parse_size("1k").unwrap(), 1024);
        assert_eq!(parse_size("1kb").unwrap(), 1024);
    }

    #[test]
    fn test_megabytes() {
        assert_eq!(parse_size("1M").unwrap(), 1024 * 1024);
        assert_eq!(parse_size("100M").unwrap(), 100 * 1024 * 1024);
        assert_eq!(parse_size("1MB").unwrap(), 1024 * 1024);
        assert_eq!(parse_size("1m").unwrap(), 1024 * 1024);
    }

    #[test]
    fn test_gigabytes() {
        assert_eq!(parse_size("1G").unwrap(), 1024 * 1024 * 1024);
        assert_eq!(parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
        assert_eq!(parse_size("2g").unwrap(), 2 * 1024 * 1024 * 1024);
    }

    #[test]
    fn test_terabytes() {
        assert_eq!(parse_size("1T").unwrap(), 1024 * 1024 * 1024 * 1024);
        assert_eq!(parse_size("1TB").unwrap(), 1024 * 1024 * 1024 * 1024);
    }

    #[test]
    fn test_whitespace_trimmed() {
        assert_eq!(parse_size("  100M  ").unwrap(), 100 * 1024 * 1024);
    }

    #[test]
    fn test_empty_string_error() {
        assert!(parse_size("").is_err());
        assert!(parse_size("  ").is_err());
    }

    #[test]
    fn test_invalid_suffix_error() {
        assert!(parse_size("100X").is_err());
        assert!(parse_size("100Z").is_err());
        assert!(parse_size("abc").is_err());
    }

    #[test]
    fn test_overflow_error() {
        // u64::MAX is ~18.4 exabytes; 18446744073709551615T
        // would overflow
        assert!(parse_size("99999999999999T").is_err());
    }

    #[test]
    fn test_suffix_only_error() {
        assert!(parse_size("M").is_err());
        assert!(parse_size("KB").is_err());
    }
}