Files
pkh/src/debian/version.rs
T
vhaudiquet c2e1288bc5 build,debian: extract reusable Debian format primitives from build/
Move the generic components out of src/build/ into a new src/debian/
module so they can be reused independently of the build pipeline:
deb822 control parsing (plus the debian/control model), file checksum
registry, debian/files registry, Debian version handling and changelog
entry parsing.

Merge OpenPGP clearsigning into utils/gpg.rs next to the existing key
discovery helper, making signing available outside of builds.

Delegate changelog.rs header/footer parsing to the new
debian::changelog parser, removing the duplicate regex implementation.

src/build/ keeps only build-specific logic: the pipeline driver,
build types, environment setup and the .buildinfo/.changes writers.
2026-08-23 01:43:41 +02:00

121 lines
4.1 KiB
Rust

//! 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<u32>,
/// Upstream version (may itself contain `-` when there is no revision).
pub upstream: String,
/// Optional Debian revision (part after the last `-`).
pub debian_revision: Option<String>,
}
impl DebianVersion {
/// Parse and validate a Debian version string.
pub fn parse(raw: &str) -> Result<DebianVersion, String> {
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());
}
}