debversion-0.2.2/.cargo_vcs_info.json0000644000000001360000000000100131750ustar { "git": { "sha1": "d1d770455e57957cdf4e9dacc6aa751a7ed28ba0" }, "path_in_vcs": "" }debversion-0.2.2/.github/workflows/rust.yml000064400000000000000000000005521046102023000171040ustar 00000000000000name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build run: cargo build --verbose - name: Install cargo-all-features run: cargo install cargo-all-features - name: Run tests run: cargo test-all-features --verbose debversion-0.2.2/.gitignore000064400000000000000000000000271046102023000137540ustar 00000000000000/target /Cargo.lock *~ debversion-0.2.2/Cargo.toml0000644000000023170000000000100111760ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "debversion" version = "0.2.2" authors = ["Jelmer Vernooij "] description = "Debian version parsing, manipulation and comparison" readme = "README.md" license = "Apache-2.0" [dependencies.lazy-regex] version = "3" [dependencies.pyo3] version = ">=0.17" optional = true [dependencies.serde] version = "1" optional = true [dependencies.sqlx] version = "0.7" features = ["postgres"] optional = true default-features = false [dev-dependencies.pyo3] version = ">=0.17" features = ["auto-initialize"] [dev-dependencies.sqlx] version = "0.7" features = ["runtime-async-std-native-tls"] default-features = false [features] default = [] python-debian = ["dep:pyo3"] serde = ["dep:serde"] sqlx = ["sqlx/postgres"] debversion-0.2.2/Cargo.toml.orig000064400000000000000000000013551046102023000146600ustar 00000000000000[package] name = "debversion" version = "0.2.2" edition = "2021" authors = [ "Jelmer Vernooij ",] license = "Apache-2.0" description = "Debian version parsing, manipulation and comparison" [features] default = [] sqlx = [ "sqlx/postgres",] python-debian = [ "dep:pyo3",] serde = [ "dep:serde",] [dependencies] lazy-regex = "3" [dependencies.pyo3] version = ">=0.17" optional = true [dependencies.sqlx] version = "0.7" optional = true default-features = false features = [ "postgres",] [dependencies.serde] version = "1" optional = true [dev-dependencies.sqlx] version = "0.7" default-features = false features = [ "runtime-async-std-native-tls",] [dev-dependencies.pyo3] version = ">=0.17" features = [ "auto-initialize",] debversion-0.2.2/README.md000064400000000000000000000021311046102023000132410ustar 00000000000000# debian version handling in rust This simple crate provides a struct for parsing, validating, manipulating and comparing Debian version strings. It aims to follow the version specification as described in Debian policy 5.6.12. Example: ```rust use debversion::Version; let version: Version = "1.0-1".parse()?; assert_eq!(version.epoch, Some(0)); assert_eq!(version.upstream_version, "1.0"); assert_eq!(version.debian_revision, Some("1")); let version1: Version = "1.0-0".parse()?; let version2: Version = "1.0".parse()?; assert_eq!(version1, version2); let version1: Version = "1.0-1".parse()?; let version2: Version = "1.0~alpha1-1".parse()?; assert!(version2 < version1); ``` ## Features ### sqlx The `sqlx` feature adds serialization support for the postgres [debversion extension](https://pgxn.org/dist/debversion/) when using sqlx. ### python-debian The `python-debian` feature provides conversion support between the debversion Rust type and the ``Version`` class provided by ``python-debian``. ### serde The `serde` feature enables serialization to and from simple strings when using serde. debversion-0.2.2/disperse.conf000064400000000000000000000001171046102023000144510ustar 00000000000000# See https://github.com/jelmer/disperse timeout_days: 5 tag_name: "v$VERSION" debversion-0.2.2/src/lib.rs000064400000000000000000000455471046102023000137070ustar 00000000000000//! Debian version type, consistent with Section 5.6.12 in the Debian Policy Manual //! //! This structure can be used for validating, dissecting and comparing Debian version strings. //! //! # Examples //! //! ``` //! use debversion::Version; //! //! let version1: Version = "1.2.3".parse().unwrap(); //! assert_eq!(version1.upstream_version.as_str(), "1.2.3"); //! assert_eq!(version1.debian_revision, None); //! assert_eq!(version1.epoch, None); //! //! let version2: Version = "1:1.2.3".parse().unwrap(); //! assert_eq!(version2.upstream_version.as_str(), "1.2.3"); //! assert_eq!(version2.debian_revision, None); //! assert_eq!(version2.epoch, Some(1)); //! //! assert_eq!(version1, version1); //! assert!(version1 < version2); use lazy_regex::{regex_captures, regex_replace}; use std::cmp::Ordering; use std::str::FromStr; /// A Debian version string /// /// #[derive(Debug, Clone)] pub struct Version { /// The epoch of the version, if any pub epoch: Option, /// The upstream version pub upstream_version: String, /// The Debian revision, if any pub debian_revision: Option, } fn version_cmp_string(va: &str, vb: &str) -> Ordering { fn order(x: char) -> i32 { match x { '~' => -1, '0'..='9' => x.to_digit(10).unwrap_or(0) as i32 + 1, 'A'..='Z' | 'a'..='z' => x as i32, _ => x as i32 + 256, } } let la: Vec = va.chars().map(order).collect(); let lb: Vec = vb.chars().map(order).collect(); let mut la_iter = la.iter(); let mut lb_iter = lb.iter(); while la_iter.len() > 0 || lb_iter.len() > 0 { let a = if let Some(a) = la_iter.next() { *a } else { 0 }; let b = if let Some(b) = lb_iter.next() { *b } else { 0 }; if a < b { return Ordering::Less; } if a > b { return Ordering::Greater; } } Ordering::Equal } /// Compare two version string parts fn version_cmp_part(va: &str, vb: &str) -> Ordering { let mut la_iter = va.chars(); let mut lb_iter = vb.chars(); let mut la = String::new(); let mut lb = String::new(); let mut res = Ordering::Equal; while let (Some(a), Some(b)) = (la_iter.next(), lb_iter.next()) { if a.is_ascii_digit() && b.is_ascii_digit() { la.clear(); lb.clear(); la.push(a); lb.push(b); for digit_a in la_iter.by_ref() { if digit_a.is_ascii_digit() { la.push(digit_a); } else { break; } } for digit_b in lb_iter.by_ref() { if digit_b.is_ascii_digit() { lb.push(digit_b); } else { break; } } let aval = la.parse::().unwrap(); let bval = lb.parse::().unwrap(); if aval < bval { res = Ordering::Less; break; } if aval > bval { res = Ordering::Greater; break; } } else { res = version_cmp_string(&a.to_string(), &b.to_string()); if res != Ordering::Equal { break; } } } res } impl Ord for Version { fn cmp(&self, other: &Self) -> std::cmp::Ordering { let self_norm = self.explicit(); let other_norm = other.explicit(); if self_norm.0 != other_norm.0 { return std::cmp::Ord::cmp(&self_norm.0, &other_norm.0); } if self.upstream_version != other.upstream_version { return self.upstream_version.cmp(&other.upstream_version); } match version_cmp_part(self_norm.1.as_str(), other_norm.1.as_str()) { Ordering::Equal => (), ordering => return ordering, } version_cmp_part(self_norm.2.as_str(), other_norm.2.as_str()) } } impl PartialOrd for Version { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for Version { fn eq(&self, other: &Self) -> bool { self.partial_cmp(other) == Some(std::cmp::Ordering::Equal) } } impl Eq for Version {} #[derive(Debug, PartialEq, Eq)] pub struct ParseError(String); impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str(&self.0) } } impl std::error::Error for ParseError {} impl FromStr for Version { type Err = ParseError; fn from_str(text: &str) -> Result { let (_, epoch, upstream_version, debian_revision) = match regex_captures!( r"^(?:(\d+):)?([A-Za-z0-9.+:~-]+?)(?:-([A-Za-z0-9+.~]+))?$", text ) { Some(c) => c, None => return Err(ParseError(format!("Invalid version string: {}", text))), }; let epoch = Some(epoch) .filter(|e| !e.is_empty()) .map(|e| { e.parse() .map_err(|e| ParseError(format!("Error parsing epoch: {}", e))) }) .transpose()?; let debian_revision = Some(debian_revision).filter(|e| !e.is_empty()); Ok(Version { epoch, upstream_version: upstream_version.to_string(), debian_revision: debian_revision.map(|e| e.to_string()), }) } } impl ToString for Version { fn to_string(&self) -> String { let mut ret = vec![self.upstream_version.clone()]; if let Some(epoch) = self.epoch.as_ref() { ret.insert(0, format!("{}:", epoch)); } if let Some(debian_revision) = self.debian_revision.as_ref() { ret.push(format!("-{}", debian_revision)); } ret.concat() } } impl std::hash::Hash for Version { fn hash(&self, state: &mut H) { ( self.epoch, self.upstream_version.as_str(), self.debian_revision.as_deref(), ) .hash(state); } } impl Version { /// Return explicit tuple of this version /// /// This will return an explicit 0 for epochs and debian revisions /// that are not set. fn explicit(&self) -> (u32, String, String) { ( self.epoch.unwrap_or(0), self.upstream_version.trim_start_matches('0').to_string(), self.debian_revision.as_deref().unwrap_or("0").to_string(), ) } /// Return canonicalized version of this version /// /// # Examples /// /// ``` /// use debversion::Version; /// assert_eq!("1.0-0".parse::().unwrap().canonicalize(), "1.0".parse::().unwrap()); /// assert_eq!("1.0-1".parse::().unwrap().canonicalize(), "1.0-1".parse::().unwrap()); /// ``` pub fn canonicalize(&self) -> Version { let epoch = match self.epoch { Some(0) | None => None, Some(epoch) => Some(epoch), }; let upstream_version = self.upstream_version.trim_start_matches('0'); let debian_revision = match self.debian_revision.as_ref() { Some(r) if r.chars().all(|c| c == '0') => None, None => None, Some(revision) => Some(revision.clone()), }; Version { epoch, upstream_version: upstream_version.to_string(), debian_revision, } } /// Increment the Debian revision. /// /// For native packages, increment the upstream version number. /// For other packages, increment the debian revision. pub fn increment_debian(&mut self) { if self.debian_revision.is_some() { self.debian_revision = self.debian_revision.as_ref().map(|v| { { regex_replace!(r"\d+$", v, |x: &str| (x.parse::().unwrap() + 1) .to_string()) } .to_string() }); } else { self.upstream_version = regex_replace!(r"\d+$", self.upstream_version.as_ref(), |x: &str| (x .parse::() .unwrap() + 1) .to_string()) .to_string(); } } } #[cfg(test)] mod tests { use super::{version_cmp_string, ParseError, Version}; use std::cmp::Ordering; #[test] fn test_canonicalize() { assert_eq!( "1.0-1".parse::().unwrap().canonicalize(), "1.0-1".parse::().unwrap() ); assert_eq!( "1.0-0".parse::().unwrap().canonicalize(), "1.0".parse::().unwrap() ); assert_eq!( "0:1.0-2".parse::().unwrap().canonicalize(), "1.0-2".parse::().unwrap() ); } #[test] fn test_explicit() { assert_eq!( (0, "1.0".to_string(), "1".to_string()), "1.0-1".parse::().unwrap().explicit() ); assert_eq!( (1, "1.0".to_string(), "1".to_string()), "1:1.0-1".parse::().unwrap().explicit() ); assert_eq!( (0, "1.0".to_string(), "0".to_string()), "1.0".parse::().unwrap().explicit() ); assert_eq!( (0, "1.0".to_string(), "0".to_string()), "1.0-0".parse::().unwrap().explicit() ); assert_eq!( (1, "1.0".to_string(), "0".to_string()), "1:1.0-0".parse::().unwrap().explicit() ); } macro_rules! assert_cmp( ($a:expr, $b:expr, $cmp:tt) => { assert_eq!($a.parse::().unwrap().cmp(&$b.parse::().unwrap()), std::cmp::Ordering::$cmp); } ); #[test] fn test_version_cmp_string() { assert_eq!(version_cmp_string("1.0", "1.0"), Ordering::Equal); assert_eq!(version_cmp_string("1.0", "2.0"), Ordering::Less); assert_eq!(version_cmp_string("1.0", "0.0"), Ordering::Greater); } #[test] fn test_cmp() { assert_cmp!("1.0-1", "1.0-1", Equal); assert_cmp!("1.0-1", "1.0-2", Less); assert_cmp!("1.0-2", "1.0-1", Greater); assert_cmp!("1.0-1", "1.0", Greater); assert_cmp!("1.0", "1.0-1", Less); // Epoch assert_cmp!("1:1.0-1", "1.0-1", Greater); assert_cmp!("1.0-1", "1:1.0-1", Less); assert_cmp!("1:1.0-1", "1:1.0-1", Equal); assert_cmp!("1:1.0-1", "2:1.0-1", Less); assert_cmp!("2:1.0-1", "1:1.0-1", Greater); // ~ symbol assert_cmp!("1.0~rc1-1", "1.0-1", Greater); assert_cmp!("1.0-1", "1.0~rc1-1", Less); assert_cmp!("1.0~rc1-1", "1.0~rc1-1", Equal); assert_cmp!("1.0~rc1-1", "1.0~rc2-1", Less); assert_cmp!("1.0~rc2-1", "1.0~rc1-1", Greater); // letters assert_cmp!("1.0a-1", "1.0-1", Greater); assert_cmp!("1.0-1", "1.0a-1", Less); assert_cmp!("1.0a-1", "1.0a-1", Equal); } #[test] fn test_parse() { assert_eq!( Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: Some("1".to_string()) }, "1.0-1".parse().unwrap() ); assert_eq!( Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: None }, "1.0".parse().unwrap() ); assert_eq!( Version { epoch: Some(1), upstream_version: "1.0".to_string(), debian_revision: Some("1".to_string()) }, "1:1.0-1".parse().unwrap() ); assert_eq!( "1:;a".parse::().unwrap_err(), ParseError("Invalid version string: 1:;a".to_string()) ); } #[test] fn test_to_string() { assert_eq!( "1.0-1", Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: Some("1".to_string()) } .to_string() ); assert_eq!( "1.0", Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: None, } .to_string() ); } #[test] fn test_eq() { assert_eq!( "1.0-1".parse::().unwrap(), "1.0-1".parse::().unwrap() ); } #[test] fn test_hash() { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut hasher1 = DefaultHasher::new(); let mut hasher2 = DefaultHasher::new(); let mut hasher3 = DefaultHasher::new(); "1.0-1".parse::().unwrap().hash(&mut hasher1); "1.0-1".parse::().unwrap().hash(&mut hasher2); "0:1.0-1".parse::().unwrap().hash(&mut hasher3); let hash1 = hasher1.finish(); let hash2 = hasher2.finish(); let hash3 = hasher3.finish(); assert_eq!(hash1, hash2); assert_ne!(hash1, hash3); } #[test] fn to_string() { assert_eq!( "1.0-1", Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: Some("1".to_string()) } .to_string() ); assert_eq!( "1.0", Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: None, } .to_string() ); } #[test] fn partial_eq() { assert!("1.0-1" .parse::() .unwrap() .eq(&"1.0-1".parse::().unwrap())); } #[test] fn increment() { let mut v = "1.0-1".parse::().unwrap(); v.increment_debian(); assert_eq!("1.0-2".parse::().unwrap(), v); let mut v = "1.0".parse::().unwrap(); v.increment_debian(); assert_eq!("1.1".parse::().unwrap(), v); let mut v = "1.0ubuntu1".parse::().unwrap(); v.increment_debian(); assert_eq!("1.0ubuntu2".parse::().unwrap(), v); let mut v = "1.0-0ubuntu1".parse::().unwrap(); v.increment_debian(); assert_eq!("1.0-0ubuntu2".parse::().unwrap(), v); } } #[cfg(feature = "sqlx")] use sqlx::{postgres::PgTypeInfo, Postgres}; #[cfg(feature = "sqlx")] impl sqlx::Type for Version { fn type_info() -> PgTypeInfo { PgTypeInfo::with_name("debversion") } } #[cfg(feature = "sqlx")] impl sqlx::Encode<'_, Postgres> for Version { fn encode_by_ref(&self, buf: &mut sqlx::postgres::PgArgumentBuffer) -> sqlx::encode::IsNull { sqlx::Encode::::encode_by_ref(&self.to_string().as_str(), buf) } } #[cfg(feature = "sqlx")] impl sqlx::Decode<'_, Postgres> for Version { fn decode( value: sqlx::postgres::PgValueRef<'_>, ) -> Result> { let s: &str = sqlx::Decode::::decode(value)?; Ok(s.parse::()?) } } #[cfg(all(feature = "sqlx", test))] mod sqlx_tests { #[test] fn type_info() { use super::Version; use sqlx::postgres::PgTypeInfo; use sqlx::Type; assert_eq!(PgTypeInfo::with_name("debversion"), Version::type_info()); } } #[cfg(feature = "python-debian")] use pyo3::prelude::*; #[cfg(feature = "python-debian")] impl FromPyObject<'_> for Version { fn extract(ob: &PyAny) -> PyResult { let debian_support = Python::import(ob.py(), "debian.debian_support")?; let version_cls = debian_support.getattr("Version")?; if !ob.is_instance(version_cls)? { return Err(pyo3::exceptions::PyTypeError::new_err("Expected a Version")); } Ok(Version { epoch: ob .getattr("epoch")? .extract::>()? .map(|s| s.parse().unwrap()), upstream_version: ob.getattr("upstream_version")?.extract::()?, debian_revision: ob.getattr("debian_revision")?.extract::>()?, }) } } #[cfg(feature = "python-debian")] impl ToPyObject for Version { fn to_object(&self, py: Python) -> PyObject { let debian_support = py.import("debian.debian_support").unwrap(); let version_cls = debian_support.getattr("Version").unwrap(); version_cls .call1((self.to_string(),)) .unwrap() .to_object(py) } } #[cfg(feature = "python-debian")] impl IntoPy for Version { fn into_py(self, py: Python) -> PyObject { self.to_object(py) } } #[cfg(feature = "python-debian")] mod python_tests { #[test] fn test_from_pyobject() { use super::Version; use pyo3::prelude::*; Python::with_gil(|py| { let globals = pyo3::types::PyDict::new(py); globals .set_item( "debian_support", py.import("debian.debian_support").unwrap(), ) .unwrap(); let v = py .eval("debian_support.Version('1.0-1')", Some(globals), None) .unwrap() .extract::() .unwrap(); assert_eq!( v, Version { epoch: None, upstream_version: "1.0".to_string(), debian_revision: Some("1".to_string()) } ); }); } #[test] fn test_to_pyobject() { use super::Version; use pyo3::prelude::*; Python::with_gil(|py| { let v = Version { epoch: Some(1), upstream_version: "1.0".to_string(), debian_revision: Some("1".to_string()), }; let v = v.to_object(py); let expected: Version = "1:1.0-1".parse().unwrap(); assert_eq!(v.extract::(py).unwrap(), expected); assert_eq!(v.as_ref(py).get_type().name().unwrap(), "Version"); }); } } #[cfg(feature = "serde")] impl serde::Serialize for Version { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { serializer.serialize_str(&self.to_string()) } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Version { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let concatenated: String = String::deserialize(deserializer)?; concatenated.parse().map_err(serde::de::Error::custom) } }