1use std::{
4    cmp::Ordering,
5    fmt::{Display, Formatter},
6    str::FromStr,
7};
8
9use serde::{Deserialize, Serialize};
10use winnow::{
11    ModalResult,
12    Parser,
13    combinator::{cut_err, eof, opt, preceded, seq, terminated},
14    error::{StrContext, StrContextValue},
15    token::take_till,
16};
17
18use crate::{Epoch, Error, PackageRelease, PackageVersion};
19#[cfg(doc)]
20use crate::{FullVersion, MinimalVersion};
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct Version {
52    pub pkgver: PackageVersion,
54    pub epoch: Option<Epoch>,
56    pub pkgrel: Option<PackageRelease>,
58}
59
60impl Version {
61    pub fn new(
63        pkgver: PackageVersion,
64        epoch: Option<Epoch>,
65        pkgrel: Option<PackageRelease>,
66    ) -> Self {
67        Version {
68            pkgver,
69            epoch,
70            pkgrel,
71        }
72    }
73
74    pub fn vercmp(a: &Version, b: &Version) -> i8 {
106        match a.cmp(b) {
107            Ordering::Less => -1,
108            Ordering::Equal => 0,
109            Ordering::Greater => 1,
110        }
111    }
112
113    pub fn parser(input: &mut &str) -> ModalResult<Self> {
121        let mut epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
122            cut_err(Epoch::parser),
124        ))
125        .context(StrContext::Expected(StrContextValue::Description(
126            "followed by a ':'",
127        )));
128
129        seq!(Self {
130            epoch: epoch,
131            pkgver: take_till(1.., '-')
132                .context(StrContext::Expected(StrContextValue::Description("pkgver string")))
134                .and_then(PackageVersion::parser),
135            pkgrel: opt(preceded('-', cut_err(PackageRelease::parser))),
136            _: eof.context(StrContext::Expected(StrContextValue::Description("end of version string"))),
137        })
138        .parse_next(input)
139    }
140}
141
142impl FromStr for Version {
143    type Err = Error;
144    fn from_str(s: &str) -> Result<Version, Self::Err> {
152        Ok(Self::parser.parse(s)?)
153    }
154}
155
156impl Display for Version {
157    fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
158        if let Some(epoch) = self.epoch {
159            write!(fmt, "{epoch}:")?;
160        }
161
162        write!(fmt, "{}", self.pkgver)?;
163
164        if let Some(pkgrel) = &self.pkgrel {
165            write!(fmt, "-{pkgrel}")?;
166        }
167
168        Ok(())
169    }
170}
171
172impl Ord for Version {
173    fn cmp(&self, other: &Self) -> Ordering {
174        match (self.epoch, other.epoch) {
175            (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
176                return self_epoch.cmp(&other_epoch);
177            }
178            (Some(_), None) => return Ordering::Greater,
179            (None, Some(_)) => return Ordering::Less,
180            (_, _) => {}
181        }
182
183        let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
184        if pkgver_cmp.is_ne() {
185            return pkgver_cmp;
186        }
187
188        self.pkgrel.cmp(&other.pkgrel)
189    }
190}
191
192impl PartialOrd for Version {
193    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
194        Some(self.cmp(other))
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use std::num::NonZeroUsize;
201
202    use rstest::rstest;
203
204    use super::*;
205
206    #[rstest]
208    #[case(
209        "foo",
210        Version {
211            epoch: None,
212            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
213            pkgrel: None
214        },
215    )]
216    #[case(
217        "1:foo-1",
218        Version {
219            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
220            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
221            pkgrel: Some(PackageRelease::new(1, None))
222        },
223    )]
224    #[case(
225        "1:foo",
226        Version {
227            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
228            epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
229            pkgrel: None,
230        },
231    )]
232    #[case(
233        "foo-1",
234        Version {
235            pkgver: PackageVersion::new("foo".to_string()).unwrap(),
236            epoch: None,
237            pkgrel: Some(PackageRelease::new(1, None))
238        }
239    )]
240    fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
241        assert_eq!(
242            Version::from_str(version),
243            Ok(expected),
244            "Expected valid parsing for version {version}"
245        )
246    }
247
248    #[rstest]
250    #[case::two_pkgrel("1:foo-1-1", "expected end of package release value")]
251    #[case::two_epoch("1:1:foo-1", "invalid pkgver character")]
252    #[case::no_version("", "expected pkgver string")]
253    #[case::no_version(":", "invalid first pkgver character")]
254    #[case::no_version(".", "invalid first pkgver character")]
255    #[case::invalid_integer(
256        "-1foo:1",
257        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
258    )]
259    #[case::invalid_integer(
260        "1-foo:1",
261        "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
262    )]
263    fn parse_error_in_version_from_string(#[case] version: &str, #[case] err_snippet: &str) {
264        let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
265            panic!("parsing '{version}' did not fail as expected")
266        };
267        assert!(
268            err_msg.contains(err_snippet),
269            "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
270        );
271    }
272
273    #[rstest]
275    #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
276    #[case(Version::from_str("1-1").unwrap(), "1-1")]
277    #[case(Version::from_str("1").unwrap(), "1")]
278    #[case(Version::from_str("1:1").unwrap(), "1:1")]
279    fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
280        assert_eq!(format!("{version}"), to_str);
281    }
282
283    #[rstest]
284    #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
286    #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
287    #[case(
288        Version::from_str("20220102"),
289        Version::from_str("20220202"),
290        Ordering::Less
291    )]
292    #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
294    #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
295    #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
296    #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
297    #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
298    #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
300    #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
301    #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
302    #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
303    #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
304    #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
305    #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
306    #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
308    #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
309    #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
310    #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
311    #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
312    #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
313    #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
314    #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
315    #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
316    #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
317    #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
318    #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
319    #[case(
320        Version::from_str("1.alpha0.0"),
321        Version::from_str("1.alpha.0"),
322        Ordering::Less
323    )]
324    #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
326    #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
328    #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
329    #[case(
330        Version::from_str("1.0.alpha.0"),
331        Version::from_str("1.0."),
332        Ordering::Less
333    )]
334    #[case(
335        Version::from_str("1.a001a.1"),
336        Version::from_str("1.a1a.1"),
337        Ordering::Equal
338    )]
339    fn version_cmp(
340        #[case] version_a: Result<Version, Error>,
341        #[case] version_b: Result<Version, Error>,
342        #[case] expected: Ordering,
343    ) {
344        let version_a = version_a.unwrap();
346        let version_b = version_b.unwrap();
347
348        let vercmp_result = match &expected {
350            Ordering::Equal => 0,
351            Ordering::Greater => 1,
352            Ordering::Less => -1,
353        };
354
355        let ordering = version_a.cmp(&version_b);
356        assert_eq!(
357            ordering, expected,
358            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
359        );
360
361        assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
362
363        #[cfg(feature = "compatibility_tests")]
365        {
366            let output = std::process::Command::new("vercmp")
367                .arg(version_a.to_string())
368                .arg(version_b.to_string())
369                .output()
370                .unwrap();
371            let result = String::from_utf8_lossy(&output.stdout);
372            assert_eq!(result.trim(), vercmp_result.to_string());
373        }
374
375        let reverse_vercmp_result = match &expected {
377            Ordering::Equal => 0,
378            Ordering::Greater => -1,
379            Ordering::Less => 1,
380        };
381        let reverse_expected = match &expected {
382            Ordering::Equal => Ordering::Equal,
383            Ordering::Greater => Ordering::Less,
384            Ordering::Less => Ordering::Greater,
385        };
386
387        let reverse_ordering = version_b.cmp(&version_a);
388        assert_eq!(
389            reverse_ordering, reverse_expected,
390            "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
391        );
392
393        assert_eq!(
394            Version::vercmp(&version_b, &version_a),
395            reverse_vercmp_result
396        );
397    }
398}