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}