use snafu::Snafu;
#[derive(Debug, Clone, PartialEq, Eq, Snafu)]
pub enum MaxSizeParseError {
#[snafu(display("max_size cannot be the empty string"))]
Empty,
#[snafu(display("max_size expects a digit at byte position 0"))]
NoDigits,
#[snafu(display("max_size suffix `{suffix}` is not one of B/KB/MB/GB/TB/KiB/MiB/GiB/TiB"))]
UnknownSuffix {
suffix: String,
},
#[snafu(display("max_size `{input}` exceeds the representable byte range"))]
Overflow {
input: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MaxSize(u64);
impl MaxSize {
pub fn parse(input: &str) -> Result<Self, MaxSizeParseError> {
if input.is_empty() {
return Err(MaxSizeParseError::Empty);
}
let bytes = input.as_bytes();
let mut idx = 0usize;
while idx < bytes.len() && bytes[idx].is_ascii_digit() {
idx += 1;
}
if idx == 0 {
return Err(MaxSizeParseError::NoDigits);
}
let number_str = &input[..idx];
let suffix = &input[idx..];
let multiplier: u64 = match suffix {
"" | "B" => 1,
"KB" => 1_000,
"MB" => 1_000_000,
"GB" => 1_000_000_000,
"TB" => 1_000_000_000_000,
"KiB" => 1024,
"MiB" => 1024 * 1024,
"GiB" => 1024 * 1024 * 1024,
"TiB" => 1024_u64.pow(4),
other => {
return Err(MaxSizeParseError::UnknownSuffix {
suffix: other.to_owned(),
});
}
};
let n: u64 = number_str
.parse()
.map_err(|_| MaxSizeParseError::Overflow {
input: input.to_owned(),
})?;
let total = n
.checked_mul(multiplier)
.ok_or_else(|| MaxSizeParseError::Overflow {
input: input.to_owned(),
})?;
Ok(Self(total))
}
#[must_use]
pub const fn as_bytes(&self) -> u64 {
self.0
}
}
#[cfg(test)]
mod tests {
use super::{MaxSize, MaxSizeParseError};
#[test]
fn parse_accepts_bare_integer_as_bytes() {
assert_eq!(MaxSize::parse("1024").unwrap().as_bytes(), 1024);
}
#[test]
fn parse_accepts_b_suffix() {
assert_eq!(MaxSize::parse("1B").unwrap().as_bytes(), 1);
}
#[test]
fn parse_accepts_si_suffixes() {
assert_eq!(MaxSize::parse("5KB").unwrap().as_bytes(), 5_000);
assert_eq!(MaxSize::parse("5MB").unwrap().as_bytes(), 5_000_000);
assert_eq!(MaxSize::parse("5GB").unwrap().as_bytes(), 5_000_000_000);
assert_eq!(MaxSize::parse("5TB").unwrap().as_bytes(), 5_000_000_000_000);
}
#[test]
fn parse_accepts_iec_suffixes() {
assert_eq!(MaxSize::parse("5KiB").unwrap().as_bytes(), 5 * 1024);
assert_eq!(MaxSize::parse("5MiB").unwrap().as_bytes(), 5 * 1024 * 1024);
assert_eq!(
MaxSize::parse("5GiB").unwrap().as_bytes(),
5 * 1024 * 1024 * 1024
);
assert_eq!(
MaxSize::parse("5TiB").unwrap().as_bytes(),
5_u64 * 1024_u64.pow(4),
);
}
#[test]
fn parse_accepts_zero() {
assert_eq!(MaxSize::parse("0").unwrap().as_bytes(), 0);
assert_eq!(MaxSize::parse("0B").unwrap().as_bytes(), 0);
assert_eq!(MaxSize::parse("0GB").unwrap().as_bytes(), 0);
}
#[test]
fn parse_rejects_empty_string() {
assert_eq!(MaxSize::parse(""), Err(MaxSizeParseError::Empty));
}
#[test]
fn parse_rejects_lowercase_suffix() {
match MaxSize::parse("5gb") {
Err(MaxSizeParseError::UnknownSuffix { suffix }) => assert_eq!(suffix, "gb"),
other => panic!("expected UnknownSuffix, got {other:?}"),
}
}
#[test]
fn parse_rejects_whitespace() {
match MaxSize::parse("5 GB") {
Err(MaxSizeParseError::UnknownSuffix { suffix }) => assert_eq!(suffix, " GB"),
other => panic!("expected UnknownSuffix, got {other:?}"),
}
}
#[test]
fn parse_rejects_fractional() {
match MaxSize::parse("1.5GB") {
Err(MaxSizeParseError::UnknownSuffix { suffix }) => assert_eq!(suffix, ".5GB"),
other => panic!("expected UnknownSuffix, got {other:?}"),
}
}
#[test]
fn parse_rejects_leading_non_digit() {
assert_eq!(MaxSize::parse("GB"), Err(MaxSizeParseError::NoDigits));
}
#[test]
fn parse_rejects_overflow() {
let huge = format!("{}TiB", u64::MAX);
match MaxSize::parse(&huge) {
Err(MaxSizeParseError::Overflow { input }) => assert_eq!(input, huge),
other => panic!("expected Overflow, got {other:?}"),
}
}
}