debian_packaging/
package_version.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Debian package version string handling. */
6
7use {
8    crate::error::{DebianError, Result},
9    std::{
10        cmp::Ordering,
11        fmt::{Display, Formatter},
12        str::FromStr,
13    },
14};
15
16/// A Debian package version.
17///
18/// Debian package versions consist of multiple sub-components and have rules about
19/// sorting. The semantics are defined at
20/// <https://www.debian.org/doc/debian-policy/ch-controlfields.html#version>. This type
21/// attempts to implement all the details.
22///
23/// The concise version is the format is `[epoch:]upstream_version[-debian_revision]`
24/// and each component has rules about what characters are allowed. Our
25/// [Self::parse()] should be compliant with the specification and reject invalid
26/// version strings and parse components to the appropriate field.
27///
28/// This type implements a custom ordering function that implements the complex rules
29/// around Debian package version ordering.
30///
31/// ```rust
32/// use debian_packaging::package_version::PackageVersion;
33///
34/// let v = PackageVersion::parse("1:4.7.0+dfsg1-2").unwrap();
35/// assert_eq!(v.epoch(), Some(1));
36/// assert_eq!(v.upstream_version(), "4.7.0+dfsg1");
37/// assert_eq!(v.debian_revision(), Some("2"));
38/// assert_eq!(format!("{}", v), "1:4.7.0+dfsg1-2");
39///
40/// assert!(v < PackageVersion::parse("1:4.7.0+dfsg1-3").unwrap());
41/// ```
42#[derive(Clone, Debug, Eq, PartialEq, Hash)]
43pub struct PackageVersion {
44    epoch: Option<u32>,
45    upstream_version: String,
46    debian_revision: Option<String>,
47}
48
49impl PackageVersion {
50    /// Construct an instance by parsing a version string.
51    pub fn parse(s: &str) -> Result<Self> {
52        // Epoch is the part before a colon, if present.
53        // upstream_version and debian_revision are discovered by splitting on last hyphen.
54
55        let (epoch, remainder) = if let Some(pos) = s.find(':') {
56            (Some(&s[0..pos]), &s[pos + 1..])
57        } else {
58            (None, s)
59        };
60
61        let (upstream, debian) = if let Some(pos) = remainder.rfind('-') {
62            (&remainder[0..pos], Some(&remainder[pos + 1..]))
63        } else {
64            (remainder, None)
65        };
66
67        // Now do our validation.
68
69        // The epoch is numeric.
70        let epoch = if let Some(epoch) = epoch {
71            if !epoch.chars().all(|c| c.is_ascii_digit()) {
72                return Err(DebianError::EpochNonNumeric(s.to_string()));
73            }
74
75            Some(u32::from_str(epoch)?)
76        } else {
77            None
78        };
79
80        // The upstream_version must contain only alphanumerics and the characters . + - ~ (full stop,
81        // plus, hyphen, tilde) and should start with a digit. If there is no debian_revision then
82        // hyphens are not allowed.
83        if !upstream.chars().all(|c| match c {
84            c if c.is_ascii_alphanumeric() => true,
85            '.' | '+' | '~' => true,
86            '-' => debian.is_some(),
87            _ => false,
88        }) {
89            return Err(DebianError::UpstreamVersionIllegalChar(s.to_string()));
90        }
91
92        let upstream_version = upstream.to_string();
93
94        let debian_revision = if let Some(debian) = debian {
95            // It must contain only alphanumerics and the characters + . ~ (plus, full stop, tilde)
96            if !debian.chars().all(|c| match c {
97                c if c.is_ascii_alphanumeric() => true,
98                '+' | '.' | '~' => true,
99                _ => false,
100            }) {
101                return Err(DebianError::DebianRevisionIllegalChar(s.to_string()));
102            }
103
104            Some(debian.to_string())
105        } else {
106            None
107        };
108
109        Ok(Self {
110            epoch,
111            upstream_version,
112            debian_revision,
113        })
114    }
115
116    /// The `epoch` component of the version string.
117    ///
118    /// Only `Some` if present or defined explicitly.
119    pub fn epoch(&self) -> Option<u32> {
120        self.epoch
121    }
122
123    /// Assumed value of `epoch` component.
124    ///
125    /// If the component isn't explicitly defined, a default of `0` will be assumed.
126    pub fn epoch_assumed(&self) -> u32 {
127        if let Some(epoch) = &self.epoch {
128            *epoch
129        } else {
130            0
131        }
132    }
133
134    /// `upstream` component of the version string.
135    ///
136    /// This is the main part of the version number.
137    ///
138    /// It is typically the original version of the software from which this package came. Although
139    /// it may be massaged to be compatible with packaging requirements.
140    pub fn upstream_version(&self) -> &str {
141        &self.upstream_version
142    }
143
144    /// `debian_revision` component of the version string.
145    ///
146    /// The part of the version string that specifies the version of the Debian package based on
147    /// the upstream version.
148    pub fn debian_revision(&self) -> Option<&str> {
149        self.debian_revision.as_deref()
150    }
151}
152
153impl Display for PackageVersion {
154    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155        // [epoch:]upstream_version[-debian_revision]
156        write!(
157            f,
158            "{}{}{}{}{}",
159            if let Some(epoch) = self.epoch {
160                format!("{}", epoch)
161            } else {
162                "".to_string()
163            },
164            if self.epoch.is_some() { ":" } else { "" },
165            self.upstream_version,
166            if self.debian_revision.is_some() {
167                "-"
168            } else {
169                ""
170            },
171            if let Some(v) = &self.debian_revision {
172                v
173            } else {
174                ""
175            }
176        )
177    }
178}
179
180/// Split a string on the first non-digit character.
181///
182/// Returns the leading component with non-digits and everything else afterwards.
183/// Either value can be an empty string.
184fn split_first_digit(s: &str) -> (&str, &str) {
185    let first_nondigit_index = s.chars().position(|c| c.is_ascii_digit());
186
187    match first_nondigit_index {
188        Some(0) => ("", s),
189        Some(pos) => (&s[0..pos], &s[pos..]),
190        None => (s, ""),
191    }
192}
193
194fn split_first_nondigit(s: &str) -> (&str, &str) {
195    let pos = s.chars().position(|c| !c.is_ascii_digit());
196
197    match pos {
198        Some(0) => ("", s),
199        Some(pos) => (&s[0..pos], &s[pos..]),
200        None => (s, ""),
201    }
202}
203
204/// Split a string on the first non-digit character and convert the leading digits to an integer.
205fn split_first_digit_number(s: &str) -> (u64, &str) {
206    let (digits, remaining) = split_first_nondigit(s);
207
208    let numeric = if digits.is_empty() {
209        0
210    } else {
211        u64::from_str(digits).expect("digits should deserialize to string")
212    };
213
214    (numeric, remaining)
215}
216
217fn lexical_compare(a: &str, b: &str) -> Ordering {
218    // The lexical comparison is a comparison of ASCII values modified so that all the letters sort
219    // earlier than all the non-letters and so that a tilde sorts before anything, even the end of a
220    // part.
221
222    let mut a_chars = a.chars();
223    let mut b_chars = b.chars();
224
225    // We compare character by character, taking our modified lexical sort into
226    // consideration. This gets funky when string lengths are different. Normally the
227    // shorter string would sort lower. But our custom lexical compare applies when comparing
228    // against a missing character!
229
230    loop {
231        let ord = match (a_chars.next(), b_chars.next()) {
232            (Some('~'), Some('~')) => Ordering::Equal,
233            (Some('~'), _) => Ordering::Less,
234            (Some(_), None) => Ordering::Greater,
235            (None, Some('~')) => Ordering::Greater,
236            (None, Some(_)) => Ordering::Less,
237            (Some(a), Some(b)) if a.is_ascii_alphabetic() && !b.is_ascii_alphabetic() => {
238                Ordering::Less
239            }
240            (Some(a), Some(b)) if !a.is_ascii_alphabetic() && b.is_ascii_alphabetic() => {
241                Ordering::Greater
242            }
243            (Some(a), Some(b)) => a.cmp(&b),
244            (None, None) => break,
245        };
246
247        if ord != Ordering::Equal {
248            return ord;
249        }
250    }
251
252    Ordering::Equal
253}
254
255/// Compare a version component string using Debian rules.
256fn compare_component(a: &str, b: &str) -> Ordering {
257    // The comparison consists of iterations of a 2 step process until both inputs are exhausted.
258    //
259    // Step 1: Initial part of each string consisting of non-digit characters is compared using
260    // a custom lexical sort.
261    //
262    // Step 2: Initial part of remaining string consisting of digit characters is compared using
263    // numerical sort.
264    let mut a_remaining = a;
265    let mut b_remaining = b;
266
267    loop {
268        let a_res = split_first_digit(a_remaining);
269        let a_leading_nondigit = a_res.0;
270        a_remaining = a_res.1;
271
272        let b_res = split_first_digit(b_remaining);
273        let b_leading_nondigit = b_res.0;
274        b_remaining = b_res.1;
275
276        // These two parts (one of which may be empty) are compared lexically. If a difference is
277        // found it is returned.
278        match lexical_compare(a_leading_nondigit, b_leading_nondigit) {
279            Ordering::Equal => {}
280            res => {
281                return res;
282            }
283        }
284
285        // Then the initial part of the remainder of each string which consists entirely of digit
286        // characters is determined.
287
288        // The numerical values of these two parts are compared, and any difference found is
289        // returned as the result of the comparison. For these purposes an empty string (which can
290        // only occur at the end of one or both version strings being compared) counts as zero.
291
292        let a_res = split_first_digit_number(a_remaining);
293        let a_numeric = a_res.0;
294        a_remaining = a_res.1;
295
296        let b_res = split_first_digit_number(b_remaining);
297        let b_numeric = b_res.0;
298        b_remaining = b_res.1;
299
300        match a_numeric.cmp(&b_numeric) {
301            Ordering::Equal => {}
302            res => {
303                return res;
304            }
305        }
306
307        if a_remaining.is_empty() && b_remaining.is_empty() {
308            return Ordering::Equal;
309        }
310    }
311}
312
313impl PartialOrd<Self> for PackageVersion {
314    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
315        Some(self.cmp(other))
316    }
317}
318
319impl Ord for PackageVersion {
320    fn cmp(&self, other: &Self) -> Ordering {
321        // Epoch is compared numerically. Then upstream and debian components are compared
322        // using a custom algorithm. The absence of a debian revision is equivalent to `0`.
323
324        match self.epoch_assumed().cmp(&other.epoch_assumed()) {
325            Ordering::Less => Ordering::Less,
326            Ordering::Greater => Ordering::Greater,
327            Ordering::Equal => {
328                match compare_component(&self.upstream_version, &other.upstream_version) {
329                    Ordering::Less => Ordering::Less,
330                    Ordering::Greater => Ordering::Greater,
331                    Ordering::Equal => {
332                        let a = self.debian_revision.as_deref().unwrap_or("0");
333                        let b = other.debian_revision.as_deref().unwrap_or("0");
334
335                        compare_component(a, b)
336                    }
337                }
338            }
339        }
340    }
341}
342
343#[cfg(test)]
344mod test {
345    use super::*;
346
347    #[test]
348    fn parse() -> Result<()> {
349        assert_eq!(
350            PackageVersion::parse("1:4.7.0+dfsg1-2")?,
351            PackageVersion {
352                epoch: Some(1),
353                upstream_version: "4.7.0+dfsg1".into(),
354                debian_revision: Some("2".into()),
355            }
356        );
357        assert_eq!(
358            PackageVersion::parse("3.3.2.final~github")?,
359            PackageVersion {
360                epoch: None,
361                upstream_version: "3.3.2.final~github".into(),
362                debian_revision: None,
363            }
364        );
365        assert_eq!(
366            PackageVersion::parse("3.3.2.final~github-2")?,
367            PackageVersion {
368                epoch: None,
369                upstream_version: "3.3.2.final~github".into(),
370                debian_revision: Some("2".into()),
371            }
372        );
373        assert_eq!(
374            PackageVersion::parse("0.18.0+dfsg-2+b1")?,
375            PackageVersion {
376                epoch: None,
377                upstream_version: "0.18.0+dfsg".into(),
378                debian_revision: Some("2+b1".into())
379            }
380        );
381
382        Ok(())
383    }
384
385    #[test]
386    fn format() -> Result<()> {
387        for s in ["1:4.7.0+dfsg1-2", "3.3.2.final~github", "0.18.0+dfsg-2+b1"] {
388            let v = PackageVersion::parse(s)?;
389            assert_eq!(format!("{}", v), s);
390        }
391
392        Ok(())
393    }
394
395    #[test]
396    fn test_lexical_compare() {
397        assert_eq!(lexical_compare("~~", "~~a"), Ordering::Less);
398        assert_eq!(lexical_compare("~~a", "~~"), Ordering::Greater);
399        assert_eq!(lexical_compare("~~a", "~"), Ordering::Less);
400        assert_eq!(lexical_compare("~", "~~a"), Ordering::Greater);
401        assert_eq!(lexical_compare("~", ""), Ordering::Less);
402        assert_eq!(lexical_compare("", "~"), Ordering::Greater);
403        assert_eq!(lexical_compare("", "a"), Ordering::Less);
404        assert_eq!(lexical_compare("a", ""), Ordering::Greater);
405        assert_eq!(lexical_compare("a", "b"), Ordering::Less);
406        assert_eq!(lexical_compare("b", "a"), Ordering::Greater);
407        assert_eq!(lexical_compare("c", "db"), Ordering::Less);
408        assert_eq!(lexical_compare("b", "+a"), Ordering::Less);
409
410        // 1.0~beta1~svn1245 sorts earlier than 1.0~beta1, which sorts earlier than 1.0.
411    }
412
413    #[test]
414    fn test_compare_component() {
415        assert_eq!(
416            compare_component("1.0~beta1~svn1245", "1.0~beta1"),
417            Ordering::Less
418        );
419        assert_eq!(compare_component("1.0~beta1", "1.0"), Ordering::Less);
420    }
421
422    #[test]
423    fn compare_version() {
424        assert_eq!(
425            PackageVersion {
426                epoch: Some(1),
427                upstream_version: "ignored".into(),
428                debian_revision: None,
429            }
430            .cmp(&PackageVersion {
431                epoch: Some(0),
432                upstream_version: "ignored".into(),
433                debian_revision: None
434            }),
435            Ordering::Greater
436        );
437    }
438}