debversion/
lib.rs

1#![deny(missing_docs)]
2#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
3// Until we drop support for PyO3 0.22
4#![allow(deprecated)]
5
6use lazy_regex::{regex_captures, regex_replace};
7use num_bigint::BigInt;
8use std::cmp::Ordering;
9use std::str::FromStr;
10
11pub mod upstream;
12pub mod vcs;
13pub mod vendor;
14
15/// A Debian version string
16///
17///
18#[derive(Debug, Clone)]
19pub struct Version {
20    /// The epoch of the version, if any
21    pub epoch: Option<u32>,
22
23    /// The upstream version
24    pub upstream_version: String,
25
26    /// The Debian revision, if any
27    pub debian_revision: Option<String>,
28}
29
30fn non_digit_cmp(va: &str, vb: &str) -> Ordering {
31    fn order(x: char) -> i32 {
32        match x {
33            '~' => -1,
34            '0'..='9' => unreachable!(),
35            'A'..='Z' | 'a'..='z' => x as i32,
36            _ => x as i32 + 256,
37        }
38    }
39
40    va.chars()
41        .map(order)
42        .chain(std::iter::repeat(0))
43        .zip(vb.chars().map(order).chain(std::iter::repeat(0)))
44        .take(va.len().max(vb.len()))
45        .find_map(|(a, b)| match a.cmp(&b) {
46            Ordering::Equal => None,
47            other => Some(other),
48        })
49        .unwrap_or(Ordering::Equal)
50}
51
52#[test]
53fn test_non_digit_cmp() {
54    assert_eq!(non_digit_cmp("a", "b"), Ordering::Less);
55    assert_eq!(non_digit_cmp("b", "a"), Ordering::Greater);
56    assert_eq!(non_digit_cmp("a", "a"), Ordering::Equal);
57    assert_eq!(non_digit_cmp("a", "-"), Ordering::Less);
58    assert_eq!(non_digit_cmp("a", "+"), Ordering::Less);
59    assert_eq!(non_digit_cmp("a", ""), Ordering::Greater);
60    assert_eq!(non_digit_cmp("", "a"), Ordering::Less);
61    assert_eq!(non_digit_cmp("", ""), Ordering::Equal);
62    assert_eq!(non_digit_cmp("~", ""), Ordering::Less);
63    assert_eq!(non_digit_cmp("~~", "~"), Ordering::Less);
64    assert_eq!(non_digit_cmp("~~", "~~a"), Ordering::Less);
65    assert_eq!(non_digit_cmp("~~a", "~"), Ordering::Less);
66    assert_eq!(non_digit_cmp("~", "a"), Ordering::Less);
67    // Test special characters that exercise the arithmetic on line 36
68    assert_eq!(non_digit_cmp("!", "@"), Ordering::Less); // ! = 33, @ = 64 + 256 = 320
69    assert_eq!(non_digit_cmp("@", "!"), Ordering::Greater);
70    assert_eq!(non_digit_cmp("#", "$"), Ordering::Less); // # = 35, $ = 36
71    assert_eq!(non_digit_cmp("|", "}"), Ordering::Less); // | = 124, } = 125
72}
73
74fn drop_leading_zeroes(s: &str) -> &str {
75    // Drop leading zeroes while the next character is a digit
76    let bytes = s.as_bytes();
77    let mut start = 0;
78    while start + 1 < bytes.len() && bytes[start] == b'0' && bytes[start + 1].is_ascii_digit() {
79        start += 1;
80    }
81    &s[start..]
82}
83
84fn version_cmp_part(mut a: &str, mut b: &str) -> Ordering {
85    while !a.is_empty() || !b.is_empty() {
86        // First, create a for the non-digit leading part of each string
87        let a_non_digit = &a[..a
88            .chars()
89            .position(|c| c.is_ascii_digit())
90            .unwrap_or(a.len())];
91        let b_non_digit = &b[..b
92            .chars()
93            .position(|c| c.is_ascii_digit())
94            .unwrap_or(b.len())];
95
96        // Compare the non-digit leading part
97        match non_digit_cmp(a_non_digit, b_non_digit) {
98            Ordering::Equal => (),
99            ordering => return ordering,
100        }
101
102        // Remove the non-digit leading part from the strings
103        a = &a[a_non_digit.len()..];
104        b = &b[b_non_digit.len()..];
105
106        // Then, create a slice for the digit part of each string
107        let a_digit = &a[..a
108            .chars()
109            .position(|c| !c.is_ascii_digit())
110            .unwrap_or(a.len())];
111        let b_digit = &b[..b
112            .chars()
113            .position(|c| !c.is_ascii_digit())
114            .unwrap_or(b.len())];
115
116        // Compare the digit part
117        let ordering = match (a_digit.len(), b_digit.len()) {
118            (0, 0) => Ordering::Equal,
119            (0, _) => Ordering::Less,
120            (_, 0) => Ordering::Greater,
121            // For small numbers that fit in u64, avoid BigInt allocation
122            (a_len, b_len) if a_len <= 19 && b_len <= 19 => {
123                match (a_digit.parse::<u64>(), b_digit.parse::<u64>()) {
124                    (Ok(a_num), Ok(b_num)) => a_num.cmp(&b_num),
125                    // Fallback to BigInt if parsing fails (shouldn't happen with valid version strings)
126                    _ => a_digit
127                        .parse::<BigInt>()
128                        .unwrap()
129                        .cmp(&b_digit.parse::<BigInt>().unwrap()),
130                }
131            }
132            // For very long digit sequences, use BigInt
133            _ => a_digit
134                .parse::<BigInt>()
135                .unwrap()
136                .cmp(&b_digit.parse::<BigInt>().unwrap()),
137        };
138
139        match ordering {
140            Ordering::Equal => (),
141            ordering => return ordering,
142        }
143
144        // Remove the digit part from the strings
145        a = &a[a_digit.len()..];
146        b = &b[b_digit.len()..];
147    }
148    Ordering::Equal
149}
150
151impl Ord for Version {
152    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
153        let self_norm = self.explicit();
154        let other_norm = other.explicit();
155        if self_norm.0 != other_norm.0 {
156            return std::cmp::Ord::cmp(&self_norm.0, &other_norm.0);
157        }
158
159        match version_cmp_part(self_norm.1, other_norm.1) {
160            Ordering::Equal => (),
161            ordering => return ordering,
162        }
163
164        version_cmp_part(self_norm.2, other_norm.2)
165    }
166}
167
168impl PartialOrd for Version {
169    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
170        Some(self.cmp(other))
171    }
172}
173
174impl PartialEq for Version {
175    fn eq(&self, other: &Self) -> bool {
176        self.partial_cmp(other) == Some(std::cmp::Ordering::Equal)
177    }
178}
179
180impl Eq for Version {}
181
182/// Error parsing a version string
183#[derive(Debug, PartialEq, Eq)]
184pub struct ParseError(String);
185
186impl std::fmt::Display for ParseError {
187    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
188        f.write_str(&self.0)
189    }
190}
191
192impl std::error::Error for ParseError {}
193
194impl FromStr for Version {
195    type Err = ParseError;
196
197    fn from_str(text: &str) -> Result<Self, Self::Err> {
198        let (_, epoch, upstream_version, debian_revision) = if let Some(c) = regex_captures!(
199            r"^(?:(\d+):)?([A-Za-z0-9.+:~-]+?)(?:-([A-Za-z0-9+.~]+))?$",
200            text
201        ) {
202            c
203        } else {
204            return Err(ParseError(format!("Invalid version string: {}", text)));
205        };
206
207        let epoch = Some(epoch)
208            .filter(|e| !e.is_empty())
209            .map(|e| {
210                e.parse()
211                    .map_err(|e| ParseError(format!("Error parsing epoch: {}", e)))
212            })
213            .transpose()?;
214
215        let debian_revision = if debian_revision.is_empty() {
216            None
217        } else {
218            Some(debian_revision.to_string())
219        };
220
221        Ok(Version {
222            epoch,
223            upstream_version: upstream_version.to_string(),
224            debian_revision,
225        })
226    }
227}
228
229impl std::fmt::Display for Version {
230    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
231        if let Some(epoch) = self.epoch.as_ref() {
232            write!(f, "{}:", epoch)?;
233        }
234        f.write_str(&self.upstream_version)?;
235        if let Some(debian_revision) = self.debian_revision.as_ref() {
236            write!(f, "-{}", debian_revision)?;
237        }
238        Ok(())
239    }
240}
241
242impl std::hash::Hash for Version {
243    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
244        (
245            self.epoch,
246            self.upstream_version.as_str(),
247            self.debian_revision.as_deref(),
248        )
249            .hash(state);
250    }
251}
252
253impl Version {
254    /// Return explicit tuple of this version
255    ///
256    /// This will return an explicit 0 for epochs and debian revisions
257    /// that are not set.
258    fn explicit(&self) -> (u32, &str, &str) {
259        (
260            self.epoch.unwrap_or(0),
261            self.upstream_version.as_str(),
262            self.debian_revision.as_deref().unwrap_or("0"),
263        )
264    }
265
266    /// Is this a binNMU?
267    ///
268    /// A binNMU is a binary-only NMU (Non-Maintainer Upload) where the source package is not
269    /// changed.
270    ///
271    /// Note that this checks for the presence of the `+b[:digit:]` suffix, which is not part of the Debian
272    /// Policy Manual, but it is commonly used to indicate a binNMU.
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use debversion::Version;
278    /// assert!("1.0+b1".parse::<Version>().unwrap().is_bin_nmu());
279    /// assert!("1.0-1+b1".parse::<Version>().unwrap().is_bin_nmu());
280    /// assert!(!"1.0-1".parse::<Version>().unwrap().is_bin_nmu());
281    /// assert!(!"1.0".parse::<Version>().unwrap().is_bin_nmu());
282    /// ```
283    pub fn is_bin_nmu(&self) -> bool {
284        self.bin_nmu_count().is_some()
285    }
286
287    /// Return the binNMU count of this version
288    ///
289    /// This will return the binNMU count of this version, or None if this is not a binNMU.
290    pub fn bin_nmu_count(&self) -> Option<i32> {
291        fn bin_nmu_suffix(s: &str) -> Option<i32> {
292            s.split_once("+b").and_then(|(_, rest)| rest.parse().ok())
293        }
294        if let Some(debian_revision) = self.debian_revision.as_ref() {
295            bin_nmu_suffix(debian_revision)
296        } else {
297            bin_nmu_suffix(self.upstream_version.as_str())
298        }
299    }
300
301    /// Create a binNMU version from this version
302    ///
303    /// This will increment the binNMU suffix by one, or add a `+b1` suffix if there is no binNMU
304    /// suffix.
305    pub fn increment_bin_nmu(self) -> Version {
306        fn increment_bin_nmu_suffix(s: &str) -> String {
307            match s.split_once("+b") {
308                Some((prefix, rest)) => match rest.parse::<i32>() {
309                    Ok(num) => format!("{}+b{}", prefix, num + 1),
310                    Err(_) => format!("{}+b1", s),
311                },
312                None => format!("{}+b1", s),
313            }
314        }
315
316        if let Some(debian_revision) = self.debian_revision.as_ref() {
317            Version {
318                epoch: self.epoch,
319                upstream_version: self.upstream_version,
320                debian_revision: Some(increment_bin_nmu_suffix(debian_revision)),
321            }
322        } else {
323            Version {
324                epoch: self.epoch,
325                upstream_version: increment_bin_nmu_suffix(&self.upstream_version),
326                debian_revision: self.debian_revision,
327            }
328        }
329    }
330
331    /// Check if this version is a sourceful NMU
332    ///
333    /// A sourceful NMU is a Non-Maintainer Upload where the source package is changed.
334    /// This is indicated by the presence of a `+nmu[:digit:]` suffix.
335    /// This is not part of the Debian Policy Manual, but it is commonly used to indicate a
336    /// sourceful NMU.
337    pub fn is_nmu(&self) -> bool {
338        self.nmu_count().is_some()
339    }
340
341    /// Return the sourceful NMU count of this version
342    ///
343    /// This will return the sourceful NMU count of this version, or None if this is not a
344    /// sourceful NMU.
345    pub fn nmu_count(&self) -> Option<i32> {
346        fn nmu_suffix(s: &str) -> Option<i32> {
347            s.split_once("+nmu").and_then(|(_, rest)| rest.parse().ok())
348        }
349        if let Some(debian_revision) = self.debian_revision.as_ref() {
350            nmu_suffix(debian_revision)
351        } else {
352            nmu_suffix(self.upstream_version.as_str())
353        }
354    }
355
356    /// Return canonicalized version of this version
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use debversion::Version;
362    /// assert_eq!("1.0-0".parse::<Version>().unwrap().canonicalize(), "1.0".parse::<Version>().unwrap());
363    /// assert_eq!("1.0-1".parse::<Version>().unwrap().canonicalize(), "1.0-1".parse::<Version>().unwrap());
364    /// ```
365    pub fn canonicalize(&self) -> Version {
366        let epoch = match self.epoch {
367            Some(0) => None,
368            epoch => epoch,
369        };
370
371        let upstream_version_stripped = drop_leading_zeroes(&self.upstream_version);
372        let upstream_version = if upstream_version_stripped == self.upstream_version {
373            self.upstream_version.clone()
374        } else {
375            upstream_version_stripped.to_string()
376        };
377
378        let debian_revision = match self.debian_revision.as_ref() {
379            Some(r) if r.chars().all(|c| c == '0') => None,
380            None => None,
381            Some(revision) => {
382                let stripped = drop_leading_zeroes(revision);
383                if stripped == revision {
384                    Some(revision.clone())
385                } else {
386                    Some(stripped.to_string())
387                }
388            }
389        };
390
391        Version {
392            epoch,
393            upstream_version,
394            debian_revision,
395        }
396    }
397
398    /// Increment the Debian revision.
399    ///
400    /// For native packages, increment the upstream version number.
401    /// For other packages, increment the debian revision.
402    pub fn increment_debian(&mut self) {
403        if let Some(ref mut debian_revision) = self.debian_revision {
404            *debian_revision = regex_replace!(r"\d+$", debian_revision, |x: &str| {
405                (x.parse::<i32>().unwrap() + 1).to_string()
406            })
407            .into_owned();
408        } else {
409            self.upstream_version = regex_replace!(r"\d+$", &self.upstream_version, |x: &str| {
410                (x.parse::<i32>().unwrap() + 1).to_string()
411            })
412            .into_owned();
413        }
414    }
415
416    /// Return true if this is a native package
417    pub fn is_native(&self) -> bool {
418        self.debian_revision.is_none()
419    }
420}
421
422#[cfg(feature = "sqlx")]
423use sqlx::{postgres::PgTypeInfo, Postgres};
424
425#[cfg(feature = "sqlx")]
426impl sqlx::Type<Postgres> for Version {
427    fn type_info() -> PgTypeInfo {
428        PgTypeInfo::with_name("debversion")
429    }
430}
431
432#[cfg(feature = "sqlx")]
433impl sqlx::Encode<'_, Postgres> for Version {
434    fn encode_by_ref(
435        &self,
436        buf: &mut sqlx::postgres::PgArgumentBuffer,
437    ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync>> {
438        let version_str = self.to_string();
439        sqlx::Encode::<Postgres>::encode_by_ref(&version_str.as_str(), buf)
440    }
441}
442
443#[cfg(feature = "sqlx")]
444impl sqlx::Decode<'_, Postgres> for Version {
445    fn decode(
446        value: sqlx::postgres::PgValueRef<'_>,
447    ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
448        let s: &str = sqlx::Decode::<Postgres>::decode(value)?;
449        Ok(s.parse::<Version>()?)
450    }
451}
452
453#[cfg(all(feature = "sqlx", test))]
454mod sqlx_tests {
455    #[test]
456    fn type_info() {
457        use super::Version;
458        use sqlx::postgres::PgTypeInfo;
459        use sqlx::Type;
460
461        assert_eq!(PgTypeInfo::with_name("debversion"), Version::type_info());
462    }
463}
464
465#[cfg(feature = "python-debian")]
466use pyo3::prelude::*;
467
468#[cfg(feature = "python-debian")]
469impl FromPyObject<'_, '_> for Version {
470    type Error = PyErr;
471
472    fn extract(ob: pyo3::Borrowed<'_, '_, PyAny>) -> PyResult<Self> {
473        let debian_support = Python::import(ob.py(), "debian.debian_support")?;
474        let version_cls = debian_support.getattr("Version")?;
475        if !ob.is_instance(&version_cls)? {
476            return Err(pyo3::exceptions::PyTypeError::new_err("Expected a Version"));
477        }
478        Ok(Version {
479            epoch: ob
480                .getattr("epoch")?
481                .extract::<Option<String>>()?
482                .map(|s| s.parse().unwrap()),
483            upstream_version: ob.getattr("upstream_version")?.extract::<String>()?,
484            debian_revision: ob.getattr("debian_revision")?.extract::<Option<String>>()?,
485        })
486    }
487}
488
489#[cfg(feature = "python-debian")]
490impl<'py> IntoPyObject<'py> for Version {
491    type Target = PyAny;
492
493    type Output = Bound<'py, Self::Target>;
494
495    type Error = PyErr;
496
497    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
498        let debian_support = py.import("debian.debian_support").unwrap();
499        let version_cls = debian_support.getattr("Version").unwrap();
500        version_cls.call1((self.to_string(),))
501    }
502}
503
504#[cfg(feature = "python-debian")]
505mod python_tests {
506    #[test]
507    fn test_from_pyobject() {
508        use super::Version;
509        use pyo3::prelude::*;
510        use std::ffi::CString;
511
512        Python::with_gil(|py| {
513            let globals = pyo3::types::PyDict::new(py);
514            globals
515                .set_item(
516                    "debian_support",
517                    py.import("debian.debian_support").unwrap(),
518                )
519                .unwrap();
520            let v = py
521                .eval(
522                    &CString::new("debian_support.Version('1.0-1')").unwrap(),
523                    Some(&globals),
524                    None,
525                )
526                .unwrap()
527                .extract::<Version>()
528                .unwrap();
529            assert_eq!(
530                v,
531                Version {
532                    epoch: None,
533                    upstream_version: "1.0".to_string(),
534                    debian_revision: Some("1".to_string())
535                }
536            );
537        });
538    }
539
540    #[test]
541    fn test_to_pyobject() {
542        use super::Version;
543        use pyo3::prelude::*;
544
545        Python::with_gil(|py| {
546            let v = Version {
547                epoch: Some(1),
548                upstream_version: "1.0".to_string(),
549                debian_revision: Some("1".to_string()),
550            };
551            let v = v.into_pyobject(py).unwrap();
552            let expected: Version = "1:1.0-1".parse().unwrap();
553            assert_eq!(v.get_type().name().unwrap(), "Version");
554            assert_eq!(v.unbind().extract::<Version>(py).unwrap(), expected);
555        });
556    }
557
558    #[test]
559    fn test_from_pyobject_error() {
560        use super::Version;
561        use pyo3::prelude::*;
562        use std::ffi::CString;
563
564        Python::with_gil(|py| {
565            // Test that extracting from a non-Version object fails
566            let string_obj = py
567                .eval(&CString::new("'not a version'").unwrap(), None, None)
568                .unwrap();
569            let result = string_obj.extract::<Version>();
570            assert!(result.is_err());
571        });
572    }
573}
574
575#[cfg(feature = "serde")]
576impl serde::Serialize for Version {
577    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
578    where
579        S: serde::Serializer,
580    {
581        serializer.serialize_str(&self.to_string())
582    }
583}
584
585#[cfg(feature = "serde")]
586impl<'de> serde::Deserialize<'de> for Version {
587    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
588    where
589        D: serde::Deserializer<'de>,
590    {
591        let concatenated: String = String::deserialize(deserializer)?;
592        concatenated.parse().map_err(serde::de::Error::custom)
593    }
594}
595
596/// Trait for converting an argument into a Version
597pub trait AsVersion {
598    /// Convert the argument into a Version
599    fn into_version(self) -> Result<Version, ParseError>;
600}
601
602impl AsVersion for &str {
603    fn into_version(self) -> Result<Version, ParseError> {
604        self.parse()
605    }
606}
607
608impl AsVersion for String {
609    fn into_version(self) -> Result<Version, ParseError> {
610        self.parse()
611    }
612}
613
614impl AsVersion for Version {
615    fn into_version(self) -> Result<Version, ParseError> {
616        Ok(self)
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::{version_cmp_part, ParseError, Version};
623    use std::cmp::Ordering;
624
625    #[test]
626    fn test_canonicalize() {
627        assert_eq!(
628            "1.0-1".parse::<Version>().unwrap().canonicalize(),
629            "1.0-1".parse::<Version>().unwrap()
630        );
631        assert_eq!(
632            "1.0-0".parse::<Version>().unwrap().canonicalize(),
633            "1.0".parse::<Version>().unwrap()
634        );
635        assert_eq!(
636            "0:1.0-2".parse::<Version>().unwrap().canonicalize(),
637            "1.0-2".parse::<Version>().unwrap()
638        );
639        assert_eq!(
640            "0001.0-0".parse::<Version>().unwrap().canonicalize(),
641            "1.0".parse::<Version>().unwrap()
642        );
643        assert_eq!(
644            "000.1".parse::<Version>().unwrap().canonicalize(),
645            "0.1".parse::<Version>().unwrap()
646        );
647    }
648
649    #[test]
650    fn test_explicit() {
651        assert_eq!(
652            (0, "1.0", "1"),
653            "1.0-1".parse::<Version>().unwrap().explicit()
654        );
655        assert_eq!(
656            (1, "1.0", "1"),
657            "1:1.0-1".parse::<Version>().unwrap().explicit()
658        );
659        assert_eq!(
660            (0, "1.0", "0"),
661            "1.0".parse::<Version>().unwrap().explicit()
662        );
663        assert_eq!(
664            (0, "1.0", "0"),
665            "1.0-0".parse::<Version>().unwrap().explicit()
666        );
667        assert_eq!(
668            (1, "1.0", "0"),
669            "1:1.0-0".parse::<Version>().unwrap().explicit()
670        );
671        assert_eq!(
672            (0, "000.1", "0"),
673            "000.1".parse::<Version>().unwrap().explicit()
674        );
675    }
676
677    macro_rules! assert_cmp(
678        ($a:expr, $b:expr, $cmp:tt) => {
679            assert_eq!($a.parse::<Version>().unwrap().cmp(&$b.parse::<Version>().unwrap()), std::cmp::Ordering::$cmp);
680        }
681    );
682
683    #[test]
684    fn test_version_cmp_part() {
685        assert_eq!(version_cmp_part("1.0", "1.0"), Ordering::Equal);
686        assert_eq!(version_cmp_part("0.1", "0.1"), Ordering::Equal);
687        assert_eq!(version_cmp_part("000.1", "0.1"), Ordering::Equal);
688        assert_eq!(version_cmp_part("1.0", "2.0"), Ordering::Less);
689        assert_eq!(version_cmp_part("1.0", "0.0"), Ordering::Greater);
690        assert_eq!(version_cmp_part("10.0", "2.0"), Ordering::Greater);
691        assert_eq!(version_cmp_part("1.0~rc1", "1.0"), Ordering::Less);
692    }
693
694    #[test]
695    fn test_cmp() {
696        assert_cmp!("1.0-1", "1.0-1", Equal);
697        assert_cmp!("1.0-1", "1.0-2", Less);
698        assert_cmp!("1.0-2", "1.0-1", Greater);
699        assert_cmp!("1.0-1", "1.0", Greater);
700        assert_cmp!("1.0", "1.0-1", Less);
701        assert_cmp!("2.50.0", "10.0.1", Less);
702
703        // Epoch
704        assert_cmp!("1:1.0-1", "1.0-1", Greater);
705        assert_cmp!("1.0-1", "1:1.0-1", Less);
706        assert_cmp!("1:1.0-1", "1:1.0-1", Equal);
707        assert_cmp!("1:1.0-1", "2:1.0-1", Less);
708        assert_cmp!("2:1.0-1", "1:1.0-1", Greater);
709
710        // ~ symbol
711        assert_cmp!("1.0~rc1-1", "1.0-1", Less);
712        assert_cmp!("1.0-1", "1.0~rc1-1", Greater);
713        assert_cmp!("1.0~rc1-1", "1.0~rc1-1", Equal);
714        assert_cmp!("1.0~rc1-1", "1.0~rc2-1", Less);
715        assert_cmp!("1.0~rc2-1", "1.0~rc1-1", Greater);
716
717        // letters
718        assert_cmp!("1.0a-1", "1.0-1", Greater);
719        assert_cmp!("1.0-1", "1.0a-1", Less);
720        assert_cmp!("1.0a-1", "1.0a-1", Equal);
721
722        // Bug 27
723        assert_cmp!("23.13.9-7", "0.6.45-2", Greater);
724    }
725
726    #[test]
727    fn test_parse() {
728        assert_eq!(
729            Version {
730                epoch: None,
731                upstream_version: "1.0".to_string(),
732                debian_revision: Some("1".to_string())
733            },
734            "1.0-1".parse().unwrap()
735        );
736
737        assert_eq!(
738            Version {
739                epoch: None,
740                upstream_version: "1.0".to_string(),
741                debian_revision: None
742            },
743            "1.0".parse().unwrap()
744        );
745
746        assert_eq!(
747            Version {
748                epoch: Some(1),
749                upstream_version: "1.0".to_string(),
750                debian_revision: Some("1".to_string())
751            },
752            "1:1.0-1".parse().unwrap()
753        );
754        assert_eq!(
755            "1:;a".parse::<Version>().unwrap_err(),
756            ParseError("Invalid version string: 1:;a".to_string())
757        );
758    }
759
760    #[test]
761    fn test_parse_error_display() {
762        let error = ParseError("test error message".to_string());
763        assert_eq!(format!("{}", error), "test error message");
764        assert_eq!(error.to_string(), "test error message");
765    }
766
767    #[test]
768    fn test_to_string() {
769        assert_eq!(
770            "1.0-1",
771            Version {
772                epoch: None,
773                upstream_version: "1.0".to_string(),
774                debian_revision: Some("1".to_string())
775            }
776            .to_string()
777        );
778        assert_eq!(
779            "1.0",
780            Version {
781                epoch: None,
782                upstream_version: "1.0".to_string(),
783                debian_revision: None,
784            }
785            .to_string()
786        );
787    }
788
789    #[test]
790    fn test_eq() {
791        assert_eq!(
792            "1.0-1".parse::<Version>().unwrap(),
793            "1.0-1".parse::<Version>().unwrap()
794        );
795    }
796
797    #[test]
798    fn test_hash() {
799        use std::collections::hash_map::DefaultHasher;
800        use std::hash::{Hash, Hasher};
801
802        let mut hasher1 = DefaultHasher::new();
803        let mut hasher2 = DefaultHasher::new();
804        let mut hasher3 = DefaultHasher::new();
805
806        "1.0-1".parse::<Version>().unwrap().hash(&mut hasher1);
807        "1.0-1".parse::<Version>().unwrap().hash(&mut hasher2);
808        "0:1.0-1".parse::<Version>().unwrap().hash(&mut hasher3);
809
810        let hash1 = hasher1.finish();
811        let hash2 = hasher2.finish();
812        let hash3 = hasher3.finish();
813
814        assert_eq!(hash1, hash2);
815        assert_ne!(hash1, hash3);
816    }
817
818    #[test]
819    fn to_string() {
820        assert_eq!(
821            "1.0-1",
822            Version {
823                epoch: None,
824                upstream_version: "1.0".to_string(),
825                debian_revision: Some("1".to_string())
826            }
827            .to_string()
828        );
829        assert_eq!(
830            "1.0",
831            Version {
832                epoch: None,
833                upstream_version: "1.0".to_string(),
834                debian_revision: None,
835            }
836            .to_string()
837        );
838        assert_eq!(
839            "1:1.0",
840            Version {
841                epoch: Some(1),
842                upstream_version: "1.0".to_string(),
843                debian_revision: None,
844            }
845            .to_string()
846        );
847    }
848
849    #[test]
850    fn partial_eq() {
851        assert!("1.0-1"
852            .parse::<Version>()
853            .unwrap()
854            .eq(&"1.0-1".parse::<Version>().unwrap()));
855    }
856
857    #[test]
858    fn increment() {
859        let mut v = "1.0-1".parse::<Version>().unwrap();
860        v.increment_debian();
861
862        assert_eq!("1.0-2".parse::<Version>().unwrap(), v);
863
864        let mut v = "1.0".parse::<Version>().unwrap();
865        v.increment_debian();
866        assert_eq!("1.1".parse::<Version>().unwrap(), v);
867
868        let mut v = "1.0ubuntu1".parse::<Version>().unwrap();
869        v.increment_debian();
870        assert_eq!("1.0ubuntu2".parse::<Version>().unwrap(), v);
871
872        let mut v = "1.0-0ubuntu1".parse::<Version>().unwrap();
873        v.increment_debian();
874        assert_eq!("1.0-0ubuntu2".parse::<Version>().unwrap(), v);
875    }
876
877    #[test]
878    fn is_native() {
879        assert!(!"1.0-1".parse::<Version>().unwrap().is_native());
880        assert!("1.0".parse::<Version>().unwrap().is_native());
881        assert!(!"1.0-0".parse::<Version>().unwrap().is_native());
882    }
883
884    #[test]
885    fn test_is_binnmu() {
886        assert!("1.0+b1".parse::<Version>().unwrap().is_bin_nmu());
887        assert!("1.0-1+b1".parse::<Version>().unwrap().is_bin_nmu());
888        assert!(!"1.0-1".parse::<Version>().unwrap().is_bin_nmu());
889        assert!(!"1.0".parse::<Version>().unwrap().is_bin_nmu());
890    }
891
892    #[test]
893    fn test_bin_nmu_count() {
894        assert_eq!(
895            Some(1),
896            "1.0+b1".parse::<Version>().unwrap().bin_nmu_count()
897        );
898        assert_eq!(
899            Some(1),
900            "1.0-1+b1".parse::<Version>().unwrap().bin_nmu_count()
901        );
902        assert_eq!(None, "1.0-1".parse::<Version>().unwrap().bin_nmu_count());
903        assert_eq!(None, "1.0".parse::<Version>().unwrap().bin_nmu_count());
904    }
905
906    #[test]
907    fn test_increment_bin_nmu() {
908        assert_eq!(
909            "1.0+b2".parse::<Version>().unwrap(),
910            "1.0+b1".parse::<Version>().unwrap().increment_bin_nmu()
911        );
912        assert_eq!(
913            "1.0-1+b2".parse::<Version>().unwrap(),
914            "1.0-1+b1".parse::<Version>().unwrap().increment_bin_nmu()
915        );
916        assert_eq!(
917            "1.0+b1".parse::<Version>().unwrap(),
918            "1.0".parse::<Version>().unwrap().increment_bin_nmu()
919        );
920        assert_eq!(
921            "1.0-1+b1".parse::<Version>().unwrap(),
922            "1.0-1".parse::<Version>().unwrap().increment_bin_nmu()
923        );
924    }
925
926    #[test]
927    fn test_nmu_count() {
928        assert_eq!(Some(1), "1.0+nmu1".parse::<Version>().unwrap().nmu_count());
929        assert_eq!(
930            Some(1),
931            "1.0-1+nmu1".parse::<Version>().unwrap().nmu_count()
932        );
933        assert_eq!(None, "1.0-1".parse::<Version>().unwrap().nmu_count());
934        assert_eq!(None, "1.0".parse::<Version>().unwrap().nmu_count());
935    }
936
937    #[test]
938    fn test_is_nmu() {
939        assert!("1.0+nmu1".parse::<Version>().unwrap().is_nmu());
940        assert!("1.0-1+nmu1".parse::<Version>().unwrap().is_nmu());
941        assert!(!"1.0-1".parse::<Version>().unwrap().is_nmu());
942        assert!(!"1.0".parse::<Version>().unwrap().is_nmu());
943    }
944
945    #[test]
946    fn test_comparing_very_long_versions() {
947        // These are actual version numbers seen in actual apt repositories
948        let a = "1:11.1.0~++20210314110124+1fdec59bffc1-1~exp1~20210314220751.162";
949        let b = "1:11.1.0~++20211011013104+1fdec59bffc1-1~exp1~20211011133507.6";
950        assert_cmp!(a, b, Less);
951    }
952
953    #[test]
954    fn test_drop_leading_zeroes() {
955        use super::drop_leading_zeroes;
956
957        // Test basic cases
958        assert_eq!(drop_leading_zeroes("1.0"), "1.0");
959        assert_eq!(drop_leading_zeroes("001.0"), "1.0");
960        assert_eq!(drop_leading_zeroes("000.1"), "0.1");
961
962        // Test edge cases for missed mutants
963        assert_eq!(drop_leading_zeroes("0"), "0");
964        assert_eq!(drop_leading_zeroes("00"), "0");
965        assert_eq!(drop_leading_zeroes("0a"), "0a");
966        assert_eq!(drop_leading_zeroes("01a"), "1a");
967
968        // Test single character
969        assert_eq!(drop_leading_zeroes("a"), "a");
970
971        // Test empty string
972        assert_eq!(drop_leading_zeroes(""), "");
973    }
974
975    #[test]
976    fn test_version_cmp_part_edge_cases() {
977        // Test cases that should hit the missed mutants in version_cmp_part
978
979        // Test empty strings
980        assert_eq!(version_cmp_part("", ""), Ordering::Equal);
981        assert_eq!(version_cmp_part("", "1"), Ordering::Less);
982        assert_eq!(version_cmp_part("1", ""), Ordering::Greater);
983
984        // Test digit comparison edge cases
985        assert_eq!(version_cmp_part("1", "1"), Ordering::Equal);
986        assert_eq!(version_cmp_part("01", "1"), Ordering::Equal);
987
988        // Test very long digit sequences to hit BigInt path
989        let long_a = "123456789012345678901234567890";
990        let long_b = "123456789012345678901234567891";
991        assert_eq!(version_cmp_part(long_a, long_b), Ordering::Less);
992
993        // Test mixed digit/non-digit sequences
994        assert_eq!(version_cmp_part("1a2", "1a3"), Ordering::Less);
995        assert_eq!(version_cmp_part("1a2", "1b1"), Ordering::Less);
996
997        // Test tilde handling
998        assert_eq!(version_cmp_part("1~", "1"), Ordering::Less);
999        assert_eq!(version_cmp_part("~1", "1"), Ordering::Less);
1000    }
1001
1002    #[test]
1003    fn test_canonicalize_edge_cases() {
1004        // Test cases that should hit missed mutants in canonicalize
1005
1006        // Test epoch handling
1007        let v1 = Version {
1008            epoch: Some(0),
1009            upstream_version: "1.0".to_string(),
1010            debian_revision: None,
1011        };
1012        let canonical = v1.canonicalize();
1013        assert_eq!(canonical.epoch, None);
1014
1015        // Test debian revision all zeros
1016        let v2 = Version {
1017            epoch: None,
1018            upstream_version: "1.0".to_string(),
1019            debian_revision: Some("000".to_string()),
1020        };
1021        let canonical2 = v2.canonicalize();
1022        assert_eq!(canonical2.debian_revision, None);
1023
1024        // Test debian revision with leading zeros
1025        let v3 = Version {
1026            epoch: None,
1027            upstream_version: "1.0".to_string(),
1028            debian_revision: Some("001".to_string()),
1029        };
1030        let canonical3 = v3.canonicalize();
1031        assert_eq!(canonical3.debian_revision, Some("1".to_string()));
1032
1033        // Test upstream version with leading zeros
1034        let v4 = Version {
1035            epoch: None,
1036            upstream_version: "001.0".to_string(),
1037            debian_revision: None,
1038        };
1039        let canonical4 = v4.canonicalize();
1040        assert_eq!(canonical4.upstream_version, "1.0");
1041
1042        // Test unchanged cases
1043        let v5 = Version {
1044            epoch: Some(1),
1045            upstream_version: "1.0".to_string(),
1046            debian_revision: Some("1".to_string()),
1047        };
1048        let canonical5 = v5.canonicalize();
1049        assert_eq!(canonical5.upstream_version, "1.0");
1050        assert_eq!(canonical5.debian_revision, Some("1".to_string()));
1051    }
1052
1053    #[test]
1054    fn test_partial_eq_false() {
1055        // Test PartialEq returning false to catch the missed mutant
1056        assert!("1.0-1"
1057            .parse::<Version>()
1058            .unwrap()
1059            .ne(&"1.0-2".parse::<Version>().unwrap()));
1060
1061        assert!("1.0-1"
1062            .parse::<Version>()
1063            .unwrap()
1064            .ne(&"2.0-1".parse::<Version>().unwrap()));
1065
1066        assert!("1:1.0-1"
1067            .parse::<Version>()
1068            .unwrap()
1069            .ne(&"2:1.0-1".parse::<Version>().unwrap()));
1070    }
1071
1072    #[test]
1073    fn test_non_digit_cmp_edge_cases() {
1074        use super::non_digit_cmp;
1075
1076        // Test tilde vs regular chars
1077        assert_eq!(non_digit_cmp("~", "a"), Ordering::Less);
1078        assert_eq!(non_digit_cmp("~", "A"), Ordering::Less);
1079        assert_eq!(non_digit_cmp("~", "!"), Ordering::Less);
1080
1081        // Test special character ordering
1082        assert_eq!(non_digit_cmp("!", "@"), Ordering::Less);
1083        assert_eq!(non_digit_cmp("@", "A"), Ordering::Greater);
1084        assert_eq!(non_digit_cmp("Z", "["), Ordering::Less);
1085
1086        // Test empty strings
1087        assert_eq!(non_digit_cmp("", ""), Ordering::Equal);
1088        assert_eq!(non_digit_cmp("", "a"), Ordering::Less);
1089        assert_eq!(non_digit_cmp("a", ""), Ordering::Greater);
1090    }
1091}