debversion-0.4.3/.cargo_vcs_info.json0000644000000001360000000000100132000ustar { "git": { "sha1": "e60d77fdb7357e452790dde38b66f625866a7c9e" }, "path_in_vcs": "" }debversion-0.4.3/.github/CODEOWNERS000064400000000000000000000000121046102023000147140ustar 00000000000000* @jelmer debversion-0.4.3/.github/FUNDING.yml000064400000000000000000000000171046102023000151430ustar 00000000000000github: jelmer debversion-0.4.3/.github/dependabot.yaml000064400000000000000000000010751046102023000163240ustar 00000000000000# Keep GitHub Actions up to date with GitHub's Dependabot... # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" rebase-strategy: "disabled" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly debversion-0.4.3/.github/workflows/publish.yaml000064400000000000000000000010511046102023000177140ustar 00000000000000on: push: tags: - 'v*' # Push events to every tag not containing / workflow_dispatch: name: Publish jobs: publish: name: Publish runs-on: ubuntu-latest steps: - name: Checkout sources uses: actions/checkout@v4 - name: Install stable toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - run: cargo publish --token ${CRATES_TOKEN} env: CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} debversion-0.4.3/.github/workflows/rust.yml000064400000000000000000000011411046102023000171020ustar 00000000000000name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.x' - run: | python -m pip install python_debian - 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.4.3/.gitignore000064400000000000000000000000271046102023000137570ustar 00000000000000/target /Cargo.lock *~ debversion-0.4.3/Cargo.toml0000644000000031670000000000100112050ustar # 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.4.3" authors = ["Jelmer Vernooij "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Debian version parsing, manipulation and comparison" homepage = "https://github.com/jelmer/debversion-rs" documentation = "https://docs.rs/debversion" readme = "README.md" keywords = [ "debian", "version", ] license = "Apache-2.0" repository = "https://github.com/jelmer/debversion-rs" [lib] name = "debversion" path = "src/lib.rs" [dependencies.chrono] version = "0.4.38" features = ["alloc"] default-features = false [dependencies.lazy-regex] version = ">=2" [dependencies.pyo3] version = ">=0.22" optional = true [dependencies.serde] version = "1" optional = true [dependencies.sqlx] version = "0.8" features = ["postgres"] optional = true default-features = false [dev-dependencies.pyo3] version = ">=0.22" features = ["auto-initialize"] [dev-dependencies.sqlx] version = "0.8" features = ["runtime-async-std-native-tls"] default-features = false [features] default = [] python-debian = ["dep:pyo3"] serde = ["dep:serde"] sqlx = ["sqlx/postgres"] debversion-0.4.3/Cargo.toml.orig000064400000000000000000000017711046102023000146650ustar 00000000000000[package] name = "debversion" version = "0.4.3" edition = "2021" authors = [ "Jelmer Vernooij ",] license = "Apache-2.0" description = "Debian version parsing, manipulation and comparison" repository = "https://github.com/jelmer/debversion-rs" documentation = "https://docs.rs/debversion" keywords = ["debian", "version"] homepage = "https://github.com/jelmer/debversion-rs" [features] default = [] sqlx = [ "sqlx/postgres",] python-debian = [ "dep:pyo3",] serde = [ "dep:serde",] [dependencies] chrono = { version = "0.4.38", default-features = false, features = ["alloc"] } lazy-regex = ">=2" [dependencies.pyo3] version = ">=0.22" optional = true [dependencies.sqlx] version = "0.8" optional = true default-features = false features = [ "postgres",] [dependencies.serde] version = "1" optional = true [dev-dependencies.sqlx] version = "0.8" default-features = false features = [ "runtime-async-std-native-tls",] [dev-dependencies.pyo3] version = ">=0.22" features = [ "auto-initialize",] debversion-0.4.3/README.md000064400000000000000000000022131046102023000132450ustar 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``, when using [pyop3](https://github.com/pyo3/pyo3). ### serde The `serde` feature enables serialization to and from simple strings when using serde. debversion-0.4.3/disperse.conf000064400000000000000000000001171046102023000144540ustar 00000000000000# See https://github.com/jelmer/disperse timeout_days: 5 tag_name: "v$VERSION" debversion-0.4.3/src/lib.rs000064400000000000000000000535411046102023000137030ustar 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; pub mod upstream; pub mod vcs; pub mod vendor; /// 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 non_digit_cmp(va: &str, vb: &str) -> Ordering { fn order(x: char) -> i32 { match x { '~' => -1, '0'..='9' => unreachable!(), '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 } #[test] fn test_non_digit_cmp() { assert_eq!(non_digit_cmp("a", "b"), Ordering::Less); assert_eq!(non_digit_cmp("b", "a"), Ordering::Greater); assert_eq!(non_digit_cmp("a", "a"), Ordering::Equal); assert_eq!(non_digit_cmp("a", "-"), Ordering::Less); assert_eq!(non_digit_cmp("a", "+"), Ordering::Less); assert_eq!(non_digit_cmp("a", ""), Ordering::Greater); assert_eq!(non_digit_cmp("", "a"), Ordering::Less); assert_eq!(non_digit_cmp("", ""), Ordering::Equal); assert_eq!(non_digit_cmp("~", ""), Ordering::Less); assert_eq!(non_digit_cmp("~~", "~"), Ordering::Less); assert_eq!(non_digit_cmp("~~", "~~a"), Ordering::Less); assert_eq!(non_digit_cmp("~~a", "~"), Ordering::Less); assert_eq!(non_digit_cmp("~", "a"), Ordering::Less); } fn drop_leading_zeroes(mut s: &str) -> &str { // Drop leading zeroes while the next character is a digit while s.starts_with('0') && s.chars().nth(1).map_or(false, |c| c.is_ascii_digit()) { s = &s[1..]; } s } fn version_cmp_part(mut a: &str, mut b: &str) -> Ordering { while !a.is_empty() || !b.is_empty() { // First, create a for the non-digit leading part of each string let a_non_digit = &a[..a .chars() .position(|c| c.is_ascii_digit()) .unwrap_or(a.len())]; let b_non_digit = &b[..b .chars() .position(|c| c.is_ascii_digit()) .unwrap_or(b.len())]; // Compare the non-digit leading part match non_digit_cmp(a_non_digit, b_non_digit) { Ordering::Equal => (), ordering => return ordering, } // Remove the non-digit leading part from the strings a = &a[a_non_digit.len()..]; b = &b[b_non_digit.len()..]; // Then, create a slice for the digit part of each string let a_digit = &a[..a .chars() .position(|c| !c.is_ascii_digit()) .unwrap_or(a.len())]; let b_digit = &b[..b .chars() .position(|c| !c.is_ascii_digit()) .unwrap_or(b.len())]; let a_num = if a_digit.is_empty() { 0 } else { a_digit.parse::().unwrap() }; let b_num = if b_digit.is_empty() { 0 } else { b_digit.parse::().unwrap() }; // Compare the digit part match a_num.cmp(&b_num) { Ordering::Equal => (), ordering => return ordering, } // Remove the digit part from the strings a = &a[a_digit.len()..]; b = &b[b_digit.len()..]; } Ordering::Equal } 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); } match version_cmp_part(self_norm.1, other_norm.1) { Ordering::Equal => (), ordering => return ordering, } version_cmp_part(self_norm.2, other_norm.2) } } 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 std::fmt::Display for Version { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { if let Some(epoch) = self.epoch.as_ref() { write!(f, "{}:", epoch)?; } f.write_str(&self.upstream_version)?; if let Some(debian_revision) = self.debian_revision.as_ref() { write!(f, "-{}", debian_revision)?; } Ok(()) } } 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, &str, &str) { ( self.epoch.unwrap_or(0), self.upstream_version.as_str(), self.debian_revision.as_deref().unwrap_or("0") ) } /// 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 mut upstream_version = self.upstream_version.as_str(); upstream_version = drop_leading_zeroes(upstream_version); let debian_revision = match self.debian_revision.as_ref() { Some(r) if r.chars().all(|c| c == '0') => None, None => None, Some(revision) => Some(drop_leading_zeroes(revision)), }; Version { epoch, upstream_version: upstream_version.to_string(), debian_revision: debian_revision.map(|r| r.to_string()), } } /// 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(); } } /// Return true if this is a native package pub fn is_native(&self) -> bool { self.debian_revision.is_none() } } #[cfg(test)] mod tests { use super::{version_cmp_part, 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() ); assert_eq!( "0001.0-0".parse::().unwrap().canonicalize(), "1.0".parse::().unwrap() ); assert_eq!( "000.1".parse::().unwrap().canonicalize(), "0.1".parse::().unwrap() ); } #[test] fn test_explicit() { assert_eq!( (0, "1.0", "1"), "1.0-1".parse::().unwrap().explicit() ); assert_eq!( (1, "1.0", "1"), "1:1.0-1".parse::().unwrap().explicit() ); assert_eq!( (0, "1.0", "0"), "1.0".parse::().unwrap().explicit() ); assert_eq!( (0, "1.0", "0"), "1.0-0".parse::().unwrap().explicit() ); assert_eq!( (1, "1.0", "0"), "1:1.0-0".parse::().unwrap().explicit() ); assert_eq!( (0, "000.1", "0"), "000.1".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_part() { assert_eq!(version_cmp_part("1.0", "1.0"), Ordering::Equal); assert_eq!(version_cmp_part("0.1", "0.1"), Ordering::Equal); assert_eq!(version_cmp_part("000.1", "0.1"), Ordering::Equal); assert_eq!(version_cmp_part("1.0", "2.0"), Ordering::Less); assert_eq!(version_cmp_part("1.0", "0.0"), Ordering::Greater); assert_eq!(version_cmp_part("10.0", "2.0"), Ordering::Greater); assert_eq!(version_cmp_part("1.0~rc1", "1.0"), Ordering::Less); } #[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); assert_cmp!("2.50.0", "10.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", Less); assert_cmp!("1.0-1", "1.0~rc1-1", Greater); 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); // Bug 27 assert_cmp!("23.13.9-7", "0.6.45-2", Greater); } #[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() ); assert_eq!( "1:1.0", Version { epoch: Some(1), 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); } #[test] fn is_native() { assert!(!"1.0-1".parse::().unwrap().is_native()); assert!("1.0".parse::().unwrap().is_native()); assert!(!"1.0-0".parse::().unwrap().is_native()); } } #[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, ) -> Result> { 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_bound(ob: &Bound) -> PyResult { let debian_support = Python::import_bound(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_bound("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_bound(py); globals .set_item( "debian_support", py.import_bound("debian.debian_support").unwrap(), ) .unwrap(); let v = py .eval_bound("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.bind(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) } } /// Trait for converting an argument into a Version pub trait AsVersion { fn as_version(self) -> Result; } impl AsVersion for &str { fn as_version(self) -> Result { self.parse() } } impl AsVersion for String { fn as_version(self) -> Result { self.parse() } } impl AsVersion for Version { fn as_version(self) -> Result { Ok(self.clone()) } } debversion-0.4.3/src/upstream.rs000064400000000000000000000263721046102023000147770ustar 00000000000000//! Utilities for working with upstream versions. static DFSG_REGEX: &lazy_regex::Lazy = lazy_regex::regex!(r"^(.*)([\+~])(dfsg|ds)([0-9]*)$"); const DFSG_DEFAULT_STYLE: &str = "+ds"; /// Strip the DFSG suffix from a version. pub fn strip_dfsg_suffix(version: &str) -> Option<&str> { if let Some(m) = DFSG_REGEX.captures(version) { Some(m.get(1).unwrap().as_str()) } else { None } } /// Add a dfsg suffix to an version version string. /// /// Allow old_upstream_version to be passed in so optionally the format can be /// kept consistent. pub fn add_dfsg_suffix(upstream_version: &str, old_upstream_version: Option<&str>) -> String { let style = if let Some(m) = old_upstream_version.and_then(|d| DFSG_REGEX.captures(d)) { let mut style = m.get(2).unwrap().as_str().to_string() + m.get(3).unwrap().as_str(); if !m.get(4).unwrap().as_str().is_empty() { style.push('1'); } style } else { DFSG_DEFAULT_STYLE.to_string() }; upstream_version.to_string() + style.as_str() } #[derive(Debug, Clone, PartialEq)] pub enum VcsSnapshot { Git { date: Option, sha: Option, snapshot: Option, }, Bzr { revno: String, }, Svn { revno: usize, }, } impl VcsSnapshot { fn to_suffix(&self) -> String { match self { VcsSnapshot::Git { date, sha, snapshot, } => { let decoded_gitid = sha.as_ref().map(|sha| &sha[..std::cmp::min(sha.len(), 7)]); let gitdate = date.map(|d| d.format("%Y%m%d").to_string()); if let (Some(decoded_gitid), Some(snapshot), Some(gitdate)) = (decoded_gitid, snapshot, gitdate.as_ref()) { format!("git{}.{}.{}", gitdate, snapshot, decoded_gitid) } else if let (Some(decoded_gitid), Some(gitdate)) = (decoded_gitid, gitdate.as_ref()) { format!("git{}.{}", gitdate, decoded_gitid) } else if let Some(decoded_gitid) = decoded_gitid { format!("git{}", decoded_gitid) } else if let Some(gitdate) = gitdate { format!("git{}", gitdate) } else { "git".to_string() } } VcsSnapshot::Bzr { revno } => { format!("bzr{}", revno) } VcsSnapshot::Svn { revno } => { format!("svn{}", revno) } } } } pub enum Direction { Before, After, } impl From<&str> for Direction { fn from(s: &str) -> Self { match s { "~" => Direction::Before, "+" => Direction::After, _ => panic!("Invalid direction"), } } } impl From for &str { fn from(d: Direction) -> Self { match d { Direction::Before => "~", Direction::After => "+", } } } pub fn get_revision(version_string: &str) -> (&str, Option<(Direction, VcsSnapshot)>) { if let Some((_, b, s, r)) = lazy_regex::regex_captures!(r"^(.*)([\+~])bzr(\d+)$", version_string) { ( b, Some(( s.into(), VcsSnapshot::Bzr { revno: r.to_string(), }, )), ) } else if let Some((_, b, s, d, i)) = lazy_regex::regex_captures!(r"^(.*)([\+~-])git(\d{8})\.([a-f0-9]{7})$", version_string) { ( b, Some(( s.into(), VcsSnapshot::Git { date: chrono::NaiveDate::parse_from_str(d, "%Y%m%d").ok(), sha: Some(i.to_string()), snapshot: None, }, )), ) } else if let Some((_, b, s, d, r, i)) = lazy_regex::regex_captures!( r"^(.*)([\+~-])git(\d{8})\.(\d+)\.([a-f0-9]{7})$", version_string ) { ( b, Some(( s.into(), VcsSnapshot::Git { date: chrono::NaiveDate::parse_from_str(d, "%Y%m%d").ok(), sha: Some(i.to_string()), snapshot: r.parse().ok(), }, )), ) } else if let Some((_, b, s, r)) = lazy_regex::regex_captures!(r"^(.*)([\+~-])svn(\d+)$", version_string) { ( b, Some(( s.into(), VcsSnapshot::Svn { revno: r.parse().unwrap(), }, )), ) } else { (version_string, None) } } /// Update the revision in a upstream version string. /// /// # Arguments /// * `version_string` - Original version string /// * `sep` - Separator to use when adding snapshot /// * `vcs_snapshot` - VCS snapshot information pub fn upstream_version_add_revision( version_string: &str, mut vcs_snapshot: VcsSnapshot, sep: Option, ) -> String { let plain_version = strip_dfsg_suffix(version_string).unwrap_or(version_string); let (base_version, current_sep, current_vcs) = match get_revision(plain_version) { (base_version, Some((sep, current_vcs))) => (base_version, Some(sep), Some(current_vcs)), (base_version, None) => (base_version, None, None), }; let sep = sep.or(current_sep).unwrap_or(Direction::After); if let ( VcsSnapshot::Git { date, sha, snapshot, }, Some(VcsSnapshot::Git { date: c_date, sha: c_sha, snapshot: c_snapshot, }), ) = (&mut vcs_snapshot, current_vcs.as_ref()) { if snapshot.is_none() { *snapshot = if date.as_ref() == c_date.as_ref() && sha.as_ref() != c_sha.as_ref() { c_snapshot.map(|s| s + 1) } else { Some(1) }; } if c_date.is_none() { *date = None; } if c_sha.is_none() { *sha = None; } } let sep: &str = sep.into(); format!("{}{}{}", base_version, sep, vcs_snapshot.to_suffix()) } #[cfg(test)] mod tests { #[test] fn test_strip_dfsg_suffix() { assert_eq!(super::strip_dfsg_suffix("1.2.3+dfsg1"), Some("1.2.3")); assert_eq!(super::strip_dfsg_suffix("1.2.3+ds1"), Some("1.2.3")); assert_eq!(super::strip_dfsg_suffix("1.2.3"), None); } #[test] fn test_add_dfsg_suffix() { assert_eq!(super::add_dfsg_suffix("1.2.3", None), "1.2.3+ds"); assert_eq!( super::add_dfsg_suffix("1.2.3", Some("1.2.3+dfsg1")), "1.2.3+dfsg1" ); assert_eq!( super::add_dfsg_suffix("1.2.3", Some("1.2.3+ds1")), "1.2.3+ds1" ); assert_eq!(super::add_dfsg_suffix("1.2.3", Some("1.2.3")), "1.2.3+ds"); } #[test] fn test_to_suffix() { assert_eq!( "git", super::VcsSnapshot::Git { date: None, sha: None, snapshot: None, } .to_suffix() ); assert_eq!( "git20210101", super::VcsSnapshot::Git { date: Some(chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()), sha: None, snapshot: None, } .to_suffix() ); assert_eq!( "gitabcdefg", super::VcsSnapshot::Git { date: None, sha: Some("abcdefg".to_string()), snapshot: None, } .to_suffix() ); assert_eq!( "git20210101.abcdefg", super::VcsSnapshot::Git { date: Some(chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()), sha: Some("abcdefg".to_string()), snapshot: None, } .to_suffix() ); assert_eq!( "git20210101.1.abcdefg", super::VcsSnapshot::Git { date: Some(chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()), sha: Some("abcdefg".to_string()), snapshot: Some(1), } .to_suffix() ); assert_eq!( "bzr123", super::VcsSnapshot::Bzr { revno: "123".to_string(), } .to_suffix() ); assert_eq!("svn123", super::VcsSnapshot::Svn { revno: 123 }.to_suffix()); } #[test] fn test_upstream_version_add_new_suffix_bzr() { assert_eq!( "1.2.3+bzr123", super::upstream_version_add_revision( "1.2.3", super::VcsSnapshot::Bzr { revno: "123".to_string() }, None ) ); } #[test] fn test_upstream_version_add_existing_suffix_bzr() { assert_eq!( "1.2.3+bzr124", super::upstream_version_add_revision( "1.2.3+bzr123", super::VcsSnapshot::Bzr { revno: "124".to_string() }, None ) ); } #[test] fn test_upstream_version_add_new_suffix_git() { assert_eq!( "1.2.3+git20210101", super::upstream_version_add_revision( "1.2.3", super::VcsSnapshot::Git { date: Some(chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()), sha: None, snapshot: None, }, None ) ); } #[test] fn test_upstream_version_add_existing_suffix_git() { assert_eq!( super::VcsSnapshot::Git { date: Some(chrono::NaiveDate::from_ymd_opt(2021, 1, 1).unwrap()), sha: Some("abcdefa".to_string()), snapshot: None, }, super::get_revision("1.2.3+git20210101.abcdefa") .1 .unwrap() .1 ); assert_eq!( "1.2.3+gitabcdefa", super::upstream_version_add_revision( "1.2.3+git20210101.1.abcdefa", super::VcsSnapshot::Git { date: None, sha: Some("abcdefa".to_string()), snapshot: None, }, None ) ); } #[test] fn test_upstream_version_add_new_suffix_svn() { assert_eq!( "1.2.3+svn123", super::upstream_version_add_revision( "1.2.3", super::VcsSnapshot::Svn { revno: 123 }, None ) ); } #[test] fn test_upstream_version_add_existing_suffix_svn() { assert_eq!( super::VcsSnapshot::Svn { revno: 123 }, super::get_revision("1.2.3+svn123").1.unwrap().1 ); assert_eq!( "1.2.3+svn124", super::upstream_version_add_revision( "1.2.3+svn123", super::VcsSnapshot::Svn { revno: 124 }, None ) ); } } debversion-0.4.3/src/vcs.rs000064400000000000000000000022401046102023000137160ustar 00000000000000use crate::Version; pub fn mangle_version_for_git(version: &Version) -> String { let version = version.to_string(); // See https://dep-team.pages.debian.net/deps/dep14/ let mut manipulated = version .replace("~", "_") .replace(":", "%") .replace("..", ".#."); if manipulated.ends_with(".") { manipulated.push('#'); } if let Some(prefix) = manipulated.strip_suffix(".lock") { manipulated = prefix.to_string() + "#lock" } manipulated } #[cfg(test)] mod tests { use super::*; #[test] fn test_mangle_version_for_git() { assert_eq!(mangle_version_for_git(&"1.0.0".parse().unwrap()), "1.0.0"); assert_eq!( mangle_version_for_git(&"1.0.0~rc1".parse().unwrap()), "1.0.0_rc1" ); assert_eq!( mangle_version_for_git(&"2:1.0.0-1".parse().unwrap()), "2%1.0.0-1" ); assert_eq!( mangle_version_for_git(&"1.0.0..rc1".parse().unwrap()), "1.0.0.#.rc1" ); assert_eq!( mangle_version_for_git(&"1.0.0.lock".parse().unwrap()), "1.0.0#lock" ); } } debversion-0.4.3/src/vendor.rs000064400000000000000000000035341046102023000144270ustar 00000000000000/// Ideally we wouldn't have a list like this, but unfortunately we do. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Vendor { Debian, Ubuntu, Kali, } impl From<&str> for Vendor { fn from(s: &str) -> Self { match s { "debian" | "Debian" => Vendor::Debian, "ubuntu" | "Ubuntu" => Vendor::Ubuntu, "kali" | "Kali" => Vendor::Kali, _ => panic!("Unknown vendor"), } } } impl std::fmt::Display for Vendor { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { Vendor::Debian => write!(f, "Debian"), Vendor::Ubuntu => write!(f, "Ubuntu"), Vendor::Kali => write!(f, "Kali"), } } } pub fn initial_debian_revision(vendor: Vendor) -> &'static str { match vendor { Vendor::Debian => "1", Vendor::Ubuntu => "0ubuntu1", Vendor::Kali => "0kali1", } } #[cfg(test)] mod tests { use super::*; #[test] fn test_vendor_from_str() { assert_eq!(Vendor::from("debian"), Vendor::Debian); assert_eq!(Vendor::from("Debian"), Vendor::Debian); assert_eq!(Vendor::from("ubuntu"), Vendor::Ubuntu); assert_eq!(Vendor::from("Ubuntu"), Vendor::Ubuntu); assert_eq!(Vendor::from("kali"), Vendor::Kali); assert_eq!(Vendor::from("Kali"), Vendor::Kali); } #[test] fn test_vendor_display() { assert_eq!(Vendor::Debian.to_string(), "Debian"); assert_eq!(Vendor::Ubuntu.to_string(), "Ubuntu"); assert_eq!(Vendor::Kali.to_string(), "Kali"); } #[test] fn test_initial_debian_revision() { assert_eq!(initial_debian_revision(Vendor::Debian), "1"); assert_eq!(initial_debian_revision(Vendor::Ubuntu), "0ubuntu1"); assert_eq!(initial_debian_revision(Vendor::Kali), "0kali1"); } }