//! Debian version handling: splitting and validation of //! `[epoch:]upstream[-revision]` version strings. /// A Debian version, split into its `[epoch:]upstream[-revision]` parts. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DebianVersion { /// Optional numeric epoch (part before the first `:`). pub epoch: Option, /// Upstream version (may itself contain `-` when there is no revision). pub upstream: String, /// Optional Debian revision (part after the last `-`). pub debian_revision: Option, } impl DebianVersion { /// Parse and validate a Debian version string. pub fn parse(raw: &str) -> Result { let raw = raw.trim(); if raw.is_empty() { return Err("empty version string".to_string()); } let (epoch, rest) = match raw.split_once(':') { Some((e, r)) => { let epoch: u32 = e .parse() .map_err(|_| format!("invalid epoch '{}' in version '{}'", e, raw))?; (Some(epoch), r) } None => (None, raw), }; // The revision is everything after the last hyphen. let (upstream, debian_revision) = match rest.rsplit_once('-') { Some((u, r)) => (u.to_string(), Some(r.to_string())), None => (rest.to_string(), None), }; if upstream.is_empty() { return Err(format!("missing upstream version in '{}'", raw)); } for c in upstream.chars() { if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '-' | '~' | ':') || !c.is_ascii()) { return Err(format!("invalid character '{}' in version '{}'", c, raw)); } } if let Some(rev) = &debian_revision { for c in rev.chars() { if !(c.is_ascii_alphanumeric() || matches!(c, '.' | '+' | '~') || !c.is_ascii()) { return Err(format!( "invalid character '{}' in revision of version '{}'", c, raw )); } } } Ok(DebianVersion { epoch, upstream, debian_revision, }) } /// Full version string, including the epoch (`[epoch:]upstream[-rev]`). pub fn full(&self) -> String { match (&self.epoch, &self.debian_revision) { (Some(e), Some(r)) => format!("{}:{}-{}", e, self.upstream, r), (Some(e), None) => format!("{}:{}", e, self.upstream), (None, Some(r)) => format!("{}-{}", self.upstream, r), (None, None) => self.upstream.clone(), } } /// Version string without the epoch (`upstream[-rev]`), used in artifact /// file names. pub fn no_epoch(&self) -> String { match &self.debian_revision { Some(r) => format!("{}-{}", self.upstream, r), None => self.upstream.clone(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn version_splitting() { let v = DebianVersion::parse("1.2.3-4ubuntu5").unwrap(); assert_eq!(v.epoch, None); assert_eq!(v.upstream, "1.2.3"); assert_eq!(v.debian_revision.as_deref(), Some("4ubuntu5")); assert_eq!(v.full(), "1.2.3-4ubuntu5"); assert_eq!(v.no_epoch(), "1.2.3-4ubuntu5"); let v = DebianVersion::parse("3:2.10-3").unwrap(); assert_eq!(v.epoch, Some(3)); assert_eq!(v.upstream, "2.10"); assert_eq!(v.no_epoch(), "2.10-3"); assert_eq!(v.full(), "3:2.10-3"); let v = DebianVersion::parse("1.0").unwrap(); assert_eq!(v.debian_revision, None); assert_eq!(v.no_epoch(), "1.0"); } #[test] fn version_validation() { assert!(DebianVersion::parse("").is_err()); assert!(DebianVersion::parse(":1.0").is_err()); assert!(DebianVersion::parse("a:_b").is_err()); assert!(DebianVersion::parse("1.0").is_ok()); assert!(DebianVersion::parse("1.0~rc1-2").is_ok()); } }