assorted_debian_utils/
version.rs

1// Copyright 2022 Sebastian Ramacher
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! # Version handling
5//!
6//! This module handles versions of Debian packages.
7//!
8//! ```
9//! use assorted_debian_utils::version::PackageVersion;
10//!
11//! let ver1 = PackageVersion::new(None, "1.0", Some("2")).expect("Failed to construct version");
12//! assert_eq!(ver1.to_string(), "1.0-2");
13//! assert!(!ver1.has_epoch());
14//! assert!(!ver1.is_native());
15//!
16//! let ver2 = PackageVersion::new(Some(1), "0.2", Some("1.1")).expect("Failed to construct version");
17//! assert_eq!(ver2.to_string(), "1:0.2-1.1");
18//! assert!(ver2.has_epoch());
19//! assert!(!ver2.is_native());
20//!
21//! assert!(ver1 < ver2);
22//! assert_eq!(ver1, PackageVersion::new(Some(0), "1.0", Some("2")).expect("Failed to construct version"));
23//! ```
24
25use std::{
26    cmp::Ordering,
27    fmt::{Display, Formatter},
28    hash::{Hash, Hasher},
29};
30
31use serde::{Deserialize, Serialize};
32use thiserror::Error;
33
34use crate::utils::TryFromStrVisitor;
35pub use crate::ParseError;
36
37/// Compare non-digits part of a version
38///
39/// Non-letters sort before letters, and ~ always sorts first.
40fn compare_non_digits(mut lhs: &str, mut rhs: &str) -> Ordering {
41    while !lhs.is_empty() || !rhs.is_empty() {
42        let (lhs_tilde, lhs_found) = lhs.find('~').map_or((lhs.len(), false), |i| (i, true));
43        let (rhs_tilde, rhs_found) = rhs.find('~').map_or((rhs.len(), false), |i| (i, true));
44        let c = lhs[..lhs_tilde].cmp(&rhs[..rhs_tilde]);
45        if c != Ordering::Equal {
46            return c;
47        }
48
49        if lhs_found && rhs_found {
50            lhs = &lhs[lhs_tilde + 1..];
51            rhs = &rhs[rhs_tilde + 1..];
52        } else if lhs_found {
53            return Ordering::Less;
54        } else if rhs_found {
55            return Ordering::Greater;
56        } else {
57            return Ordering::Equal;
58        }
59    }
60
61    // both lhs and rhs are empty
62    Ordering::Equal
63}
64
65/// Compare parts of the two versions
66fn compare_parts(mut lhs: &str, mut rhs: &str) -> Ordering {
67    while !lhs.is_empty() || !rhs.is_empty() {
68        // compare initial non-digits
69        let lhs_digit_start = lhs.find(|c| char::is_ascii_digit(&c)).unwrap_or(lhs.len());
70        let rhs_digit_start = rhs.find(|c| char::is_ascii_digit(&c)).unwrap_or(rhs.len());
71        let c = compare_non_digits(&lhs[..lhs_digit_start], &rhs[..rhs_digit_start]);
72        if c != Ordering::Equal {
73            return c;
74        }
75        lhs = &lhs[lhs_digit_start..];
76        rhs = &rhs[rhs_digit_start..];
77
78        // compare initial digits
79        let lhs_digit_end = lhs.find(|c| !char::is_ascii_digit(&c)).unwrap_or(lhs.len());
80        let rhs_digit_end = rhs.find(|c| !char::is_ascii_digit(&c)).unwrap_or(rhs.len());
81        let c = lhs[..lhs_digit_end]
82            .parse::<u64>()
83            .unwrap_or(0)
84            .cmp(&rhs[..rhs_digit_end].parse::<u64>().unwrap_or(0));
85        if c != Ordering::Equal {
86            return c;
87        }
88        lhs = &lhs[lhs_digit_end..];
89        rhs = &rhs[rhs_digit_end..];
90    }
91
92    // both lhs and rhs are empty
93    Ordering::Equal
94}
95
96/// Version errors
97#[derive(Clone, Copy, Debug, Error)]
98pub enum VersionError {
99    #[error("invalid epoch")]
100    /// Epoch is invalid
101    InvalidEpoch,
102    #[error("invalid upstream version")]
103    /// Upstream version is invalid
104    InvalidUpstreamVersion,
105    #[error("invalid Debian revision")]
106    /// Debian revision is invalid
107    InvalidDebianRevision,
108}
109
110/// A version number of a Debian package
111///
112/// Version numbers consists of three components:
113/// * an optional epoch
114/// * the upstream version
115/// * an optional debian revision
116#[derive(Clone, Debug)]
117pub struct PackageVersion {
118    /// The (optional) epoch
119    pub(crate) epoch: Option<u32>,
120    /// The upstream version
121    pub(crate) upstream_version: String,
122    /// The (optional) Debian revision
123    pub(crate) debian_revision: Option<String>,
124}
125
126impl PackageVersion {
127    /// Create a new version struct from the individual components.
128    pub fn new(
129        epoch: Option<u32>,
130        upstream_version: &str,
131        debian_revision: Option<&str>,
132    ) -> Result<Self, VersionError> {
133        // Upstream version may consist of alphanumeric characters and ., +, ~, - (if the revision is non-empty), : (if the epoch is non-empty)
134        if upstream_version.is_empty()
135            || upstream_version.chars().any(|c| {
136                !(c.is_alphanumeric()
137                    || ".+~".contains(c)
138                    || (debian_revision.is_some() && c == '-')
139                    || (epoch.is_some() && c == ':'))
140            })
141        {
142            return Err(VersionError::InvalidUpstreamVersion);
143        }
144
145        // Debian revision may consist of alphanumeric characters and ., +, ~
146        if let Some(rev) = debian_revision {
147            if rev.is_empty()
148                || rev
149                    .chars()
150                    .any(|c| !c.is_alphanumeric() && !".+~".contains(c))
151            {
152                return Err(VersionError::InvalidDebianRevision);
153            }
154        }
155
156        Ok(Self {
157            epoch,
158            upstream_version: String::from(upstream_version),
159            debian_revision: debian_revision.map(String::from),
160        })
161    }
162
163    /// Returns whether version is a native version, i.e., there is no revision.
164    pub fn is_native(&self) -> bool {
165        self.debian_revision.is_none()
166    }
167
168    /// Return whether the version has an epoch.
169    pub fn has_epoch(&self) -> bool {
170        self.epoch.is_some()
171    }
172
173    /// Return epoch of 0 if none set.
174    pub fn epoch_or_0(&self) -> u32 {
175        self.epoch.unwrap_or(0)
176    }
177
178    /// Return whether this version has a binNMU version, i.e., ends in +bX for some integer X.
179    pub fn has_binnmu_version(&self) -> bool {
180        self.binnmu_version().is_some()
181    }
182
183    /// Return binNMU version if available.
184    pub fn binnmu_version(&self) -> Option<u32> {
185        self.debian_revision
186            .as_ref()
187            .map_or(&self.upstream_version, |v| v)
188            .rsplit_once("+b")
189            .and_then(|(_, binnmu_version)| binnmu_version.parse().ok())
190    }
191
192    /// Obtain version without the binNMU version.
193    pub fn without_binnmu_version(mut self) -> Self {
194        if let Some(revision) = self.debian_revision.as_mut() {
195            if let Some(index) = revision.rfind("+b") {
196                revision.truncate(index);
197            }
198        } else if let Some(index) = self.upstream_version.rfind("+b") {
199            self.upstream_version.truncate(index);
200        }
201        self
202    }
203
204    /// Obtain version without the binNMU version.
205    pub fn clone_without_binnmu_version(&self) -> Self {
206        self.clone().without_binnmu_version()
207    }
208}
209
210impl PartialOrd for PackageVersion {
211    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
212        Some(self.cmp(other))
213    }
214}
215
216impl Ord for PackageVersion {
217    fn cmp(&self, other: &Self) -> Ordering {
218        match self.epoch_or_0().cmp(&other.epoch_or_0()) {
219            Ordering::Equal => {}
220            v => return v,
221        };
222
223        match compare_parts(&self.upstream_version, &other.upstream_version) {
224            Ordering::Equal => {}
225            v => return v,
226        };
227
228        match (&self.debian_revision, &other.debian_revision) {
229            (None, None) => Ordering::Equal,
230            (None, Some(_)) => Ordering::Less,
231            (Some(_), None) => Ordering::Greater,
232            (Some(lhs), Some(rhs)) => compare_parts(lhs, rhs),
233        }
234    }
235}
236
237impl PartialEq for PackageVersion {
238    fn eq(&self, other: &Self) -> bool {
239        self.cmp(other) == Ordering::Equal
240    }
241}
242
243impl Eq for PackageVersion {}
244
245impl TryFrom<&str> for PackageVersion {
246    type Error = ParseError;
247
248    fn try_from(mut value: &str) -> Result<Self, Self::Error> {
249        let epoch = if let Some((epoch_str, new_value)) = value.split_once(':') {
250            value = new_value;
251            Some(
252                epoch_str
253                    .parse::<u32>()
254                    .map_err(|_| ParseError::InvalidVersion(VersionError::InvalidEpoch))?,
255            )
256        } else {
257            None
258        };
259
260        let debian_revision = if let Some((new_value, debian_revision_str)) = value.rsplit_once('-')
261        {
262            value = new_value;
263            Some(debian_revision_str)
264        } else {
265            None
266        };
267
268        Self::new(epoch, value, debian_revision).map_err(ParseError::InvalidVersion)
269    }
270}
271
272impl Display for PackageVersion {
273    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
274        if let Some(epoch) = self.epoch {
275            write!(f, "{epoch}:")?;
276        }
277        write!(f, "{}", self.upstream_version)?;
278        if let Some(debian_revision) = &self.debian_revision {
279            write!(f, "-{debian_revision}")?;
280        }
281        Ok(())
282    }
283}
284
285impl Serialize for PackageVersion {
286    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
287    where
288        S: serde::Serializer,
289    {
290        serializer.serialize_str(&self.to_string())
291    }
292}
293
294impl<'de> Deserialize<'de> for PackageVersion {
295    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
296    where
297        D: serde::Deserializer<'de>,
298    {
299        deserializer.deserialize_str(TryFromStrVisitor::<Self>::new("a package version"))
300    }
301}
302
303impl Hash for PackageVersion {
304    fn hash<H: Hasher>(&self, state: &mut H) {
305        self.epoch_or_0().hash(state);
306        self.upstream_version.hash(state);
307        self.debian_revision.hash(state);
308    }
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314
315    #[test]
316    fn conversion() {
317        let version = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
318        assert_eq!(version.epoch, Some(2));
319        assert_eq!(version.upstream_version, "1.0+dfsg");
320        assert_eq!(version.debian_revision, Some("1".into()));
321    }
322
323    #[test]
324    fn invalid_epoch() {
325        assert!(PackageVersion::try_from("-1:1.0-1").is_err());
326        assert!(PackageVersion::try_from(":1.0-1").is_err());
327        assert!(PackageVersion::try_from("a1:1.0-1").is_err());
328    }
329
330    #[test]
331    fn invalid_upstream_version() {
332        assert!(PackageVersion::try_from("-1").is_err());
333        assert!(PackageVersion::try_from("0:-1").is_err());
334        assert!(PackageVersion::new(None, "1:2", None).is_err());
335        assert!(PackageVersion::new(None, "1-2", None).is_err());
336    }
337
338    #[test]
339    fn multi_dash() {
340        let version = PackageVersion::try_from("1.0-2-1").unwrap();
341        assert_eq!(version.epoch, None);
342        assert_eq!(version.upstream_version, "1.0-2");
343        assert_eq!(version.debian_revision.unwrap(), "1");
344    }
345
346    #[test]
347    fn multi_colon() {
348        let version = PackageVersion::try_from("1:1.0:2-1").unwrap();
349        assert_eq!(version.epoch.unwrap(), 1);
350        assert_eq!(version.upstream_version, "1.0:2");
351        assert_eq!(version.debian_revision.unwrap(), "1");
352    }
353
354    #[test]
355    fn binnum() {
356        let version = PackageVersion::try_from("1.0-1").unwrap();
357        assert!(!version.has_binnmu_version());
358        assert_eq!(version.binnmu_version(), None);
359
360        let version = PackageVersion::try_from("1.0-1+b1").unwrap();
361        assert!(version.has_binnmu_version());
362        assert_eq!(version.binnmu_version(), Some(1u32));
363    }
364
365    #[test]
366    fn strip_binnum() {
367        let version = PackageVersion::try_from("1.0-1+b1").unwrap();
368        let version = version.without_binnmu_version();
369        assert_eq!(version, PackageVersion::try_from("1.0-1").unwrap());
370
371        assert!(!version.has_binnmu_version());
372        assert_eq!(version.binnmu_version(), None);
373    }
374
375    #[test]
376    fn compare_non_digits_invariants() {
377        assert_eq!(compare_non_digits("~~", "~~a"), Ordering::Less);
378        assert_eq!(compare_non_digits("~~a", "~"), Ordering::Less);
379        assert_eq!(compare_non_digits("~", ""), Ordering::Less);
380        assert_eq!(compare_non_digits("", "a"), Ordering::Less);
381    }
382
383    #[test]
384    fn epoch_compare() {
385        let version1 = PackageVersion::try_from("2.0-1").unwrap();
386        let version2 = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
387
388        assert!(version2.has_epoch());
389        assert!(!version1.has_epoch());
390        assert!(version1 < version2);
391    }
392
393    #[test]
394    fn zero_epoch_compare() {
395        let version1 = PackageVersion::try_from("2.0-1").unwrap();
396        let version2 = PackageVersion::try_from("0:2.0-1").unwrap();
397        assert_eq!(version1, version2);
398    }
399
400    #[test]
401    fn equal_compare() {
402        let version1 = PackageVersion::try_from("2.0-1").unwrap();
403        assert_eq!(version1, version1);
404
405        let version1 = PackageVersion::try_from("2a.0-1").unwrap();
406        assert_eq!(version1, version1);
407
408        let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
409        assert_eq!(version1, version1);
410    }
411
412    #[test]
413    fn tilde_plus_compare() {
414        let version1 = PackageVersion::try_from("2.0~dfsg-1").unwrap();
415        let version2 = PackageVersion::try_from("2.0-1").unwrap();
416        assert!(version1 < version2);
417
418        let version2 = PackageVersion::try_from("2.0+dfsg-1").unwrap();
419        assert!(version1 < version2);
420
421        let version1 = PackageVersion::try_from("2.0-1").unwrap();
422        assert!(version1 < version2);
423
424        let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
425        let version2 = PackageVersion::try_from("2+dfsg2-1").unwrap();
426        assert!(version1 < version2);
427
428        let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
429        let version2 = PackageVersion::try_from("2.1-1").unwrap();
430        assert!(version1 < version2);
431    }
432
433    #[test]
434    fn letters_compare() {
435        let version1 = PackageVersion::try_from("2dfsg-1").unwrap();
436        let version2 = PackageVersion::try_from("2-1").unwrap();
437        assert!(version1 > version2);
438    }
439
440    #[test]
441    fn less_compare() {
442        let version1 = PackageVersion::try_from("2-1").unwrap();
443        let version2 = PackageVersion::try_from("2.0-1").unwrap();
444        assert!(version1 < version2);
445    }
446
447    #[test]
448    fn native_version_binnmu() {
449        let version1 = PackageVersion::try_from("2+b1").unwrap();
450        let version2 = PackageVersion::try_from("2").unwrap();
451        assert!(version1.has_binnmu_version());
452        assert_eq!(version1.binnmu_version(), Some(1));
453        assert!(!version2.has_binnmu_version());
454        assert_eq!(version1.without_binnmu_version(), version2);
455    }
456}