use anyhow::{Result, bail};
pub fn parse_size_mb(input: &str) -> Result<u64> {
let s = input.trim().to_ascii_lowercase();
let split = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
let (num, unit) = s.split_at(split);
if num.is_empty() {
bail!("invalid size {input:?}: expected a number like 2GB or 2048MB");
}
let n: u64 = num.parse()?;
match unit.trim() {
"g" | "gb" | "gib" => Ok(n * 1024),
"m" | "mb" | "mib" | "" => Ok(n),
other => bail!("invalid size unit {other:?} in {input:?}: use MB or GB"),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImageRef {
pub api: u32,
pub minor: Option<u32>,
pub flavour: Option<String>,
}
impl std::fmt::Display for ImageRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "android:{}", self.api)?;
if let Some(m) = self.minor {
write!(f, ".{m}")?;
}
if let Some(fl) = &self.flavour {
write!(f, "-{fl}")?;
}
Ok(())
}
}
impl ImageRef {
pub fn parse(input: &str) -> Result<Self> {
let Some(tag) = input.strip_prefix("android:") else {
bail!(
"unsupported image {input:?}: only base images like `android:35` are supported \
in this release (built images arrive with `dorker build`)"
);
};
let (api, flavour) = match tag.split_once('-') {
Some((api, flavour)) if !flavour.is_empty() => (api, Some(flavour.to_string())),
_ => (tag, None),
};
let bad = || anyhow::anyhow!("invalid Android API level in {input:?}");
let (major, minor) = match api.split_once('.') {
Some((a, b)) => (a, Some(b.parse::<u32>().map_err(|_| bad())?)),
None => (api, None),
};
let api: u32 = major.parse().map_err(|_| bad())?;
Ok(Self {
api,
minor,
flavour,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sizes() {
assert_eq!(parse_size_mb("2GB").unwrap(), 2048);
assert_eq!(parse_size_mb("2g").unwrap(), 2048);
assert_eq!(parse_size_mb("512MB").unwrap(), 512);
assert_eq!(parse_size_mb("1024").unwrap(), 1024);
assert!(parse_size_mb("abc").is_err());
assert!(parse_size_mb("2TB").is_err());
}
#[test]
fn image_refs_round_trip_through_display() {
for s in [
"android:35",
"android:36.1",
"android:35-google_apis",
"android:36.1-google_apis_playstore",
] {
assert_eq!(ImageRef::parse(s).unwrap().to_string(), s);
}
}
#[test]
fn image_refs() {
let r = ImageRef::parse("android:35").unwrap();
assert_eq!((r.api, r.flavour), (35, None));
let r = ImageRef::parse("android:35-google_apis").unwrap();
assert_eq!(r.flavour.as_deref(), Some("google_apis"));
let r = ImageRef::parse("android:36.1-google_apis_playstore").unwrap();
assert_eq!((r.api, r.minor), (36, Some(1)));
assert!(ImageRef::parse("android:36.x").is_err());
assert!(ImageRef::parse("myapp:latest").is_err());
assert!(ImageRef::parse("android:abc").is_err());
}
}