1use std::{
6 cmp::Ordering,
7 fmt::{Display, Formatter},
8 str::FromStr,
9};
10
11use serde::{Deserialize, Serialize};
12use winnow::{
13 ModalResult,
14 Parser,
15 combinator::{cut_err, eof, opt, preceded, terminated},
16 error::{StrContext, StrContextValue},
17 token::{take_till, take_until},
18};
19
20use crate::{Epoch, Error, PackageRelease, PackageVersion, Version};
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
50pub struct FullVersion {
51 pub pkgver: PackageVersion,
53 pub pkgrel: PackageRelease,
55 pub epoch: Option<Epoch>,
57}
58
59impl FullVersion {
60 pub fn new(pkgver: PackageVersion, pkgrel: PackageRelease, epoch: Option<Epoch>) -> Self {
85 Self {
86 pkgver,
87 pkgrel,
88 epoch,
89 }
90 }
91
92 pub fn vercmp(&self, other: &FullVersion) -> i8 {
132 match self.cmp(other) {
133 Ordering::Less => -1,
134 Ordering::Equal => 0,
135 Ordering::Greater => 1,
136 }
137 }
138
139 pub fn parser(input: &mut &str) -> ModalResult<Self> {
150 let epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
153 cut_err(Epoch::parser),
155 ))
156 .context(StrContext::Expected(StrContextValue::Description(
157 "followed by a ':'",
158 )))
159 .parse_next(input)?;
160
161 let pkgver: PackageVersion = cut_err(take_until(0.., "-"))
164 .context(StrContext::Expected(StrContextValue::Description(
165 "alpm-pkgver string, followed by a '-' and an alpm-pkgrel string",
166 )))
167 .take()
168 .and_then(cut_err(PackageVersion::parser))
169 .parse_next(input)?;
170
171 let pkgrel: PackageRelease = preceded("-", cut_err(PackageRelease::parser))
176 .context(StrContext::Expected(StrContextValue::Description(
177 "alpm-pkgrel string",
178 )))
179 .parse_next(input)?;
180
181 eof.context(StrContext::Expected(StrContextValue::Description(
183 "end of full alpm-package-version string",
184 )))
185 .parse_next(input)?;
186
187 Ok(Self {
188 epoch,
189 pkgver,
190 pkgrel,
191 })
192 }
193}
194
195impl Display for FullVersion {
196 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
197 if let Some(epoch) = self.epoch {
198 write!(fmt, "{epoch}:")?;
199 }
200 write!(fmt, "{}-{}", self.pkgver, self.pkgrel)?;
201
202 Ok(())
203 }
204}
205
206impl FromStr for FullVersion {
207 type Err = Error;
208 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 Ok(Self::parser.parse(s)?)
217 }
218}
219
220impl Ord for FullVersion {
221 fn cmp(&self, other: &Self) -> Ordering {
260 match (self.epoch, other.epoch) {
261 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
262 return self_epoch.cmp(&other_epoch);
263 }
264 (Some(_), None) => return Ordering::Greater,
265 (None, Some(_)) => return Ordering::Less,
266 (_, _) => {}
267 }
268
269 let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
270 if pkgver_cmp.is_ne() {
271 return pkgver_cmp;
272 }
273
274 self.pkgrel.cmp(&other.pkgrel)
275 }
276}
277
278impl PartialOrd for FullVersion {
279 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
280 Some(self.cmp(other))
281 }
282}
283
284impl TryFrom<Version> for FullVersion {
285 type Error = crate::Error;
286
287 fn try_from(value: Version) -> Result<Self, Self::Error> {
293 Ok(Self {
294 pkgver: value.pkgver,
295 pkgrel: value.pkgrel.ok_or(Error::MissingComponent {
296 component: "pkgrel",
297 })?,
298 epoch: value.epoch,
299 })
300 }
301}
302
303impl TryFrom<&Version> for FullVersion {
304 type Error = crate::Error;
305
306 fn try_from(value: &Version) -> Result<Self, Self::Error> {
312 Self::try_from(value.clone())
313 }
314}
315
316impl From<FullVersion> for Version {
317 fn from(value: FullVersion) -> Self {
319 Self {
320 pkgver: value.pkgver,
321 pkgrel: Some(value.pkgrel),
322 epoch: value.epoch,
323 }
324 }
325}
326
327impl From<&FullVersion> for Version {
328 fn from(value: &FullVersion) -> Self {
330 Self::from(value.clone())
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use log::{LevelFilter, debug};
337 use rstest::rstest;
338 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
339 use testresult::TestResult;
340
341 use super::*;
342
343 fn init_logger() {
345 if TermLogger::init(
346 LevelFilter::Trace,
347 Config::default(),
348 TerminalMode::Stderr,
349 ColorChoice::Auto,
350 )
351 .is_err()
352 {
353 debug!("Not initializing another logger, as one is initialized already.");
354 }
355 }
356
357 #[rstest]
359 #[case::full_with_epoch(
360 "1:foo-1",
361 FullVersion {
362 pkgver: PackageVersion::from_str("foo")?,
363 epoch: Some(Epoch::from_str("1")?),
364 pkgrel: PackageRelease::from_str("1")?,
365 },
366 )]
367 #[case::full(
368 "foo-1",
369 FullVersion {
370 pkgver: PackageVersion::from_str("foo")?,
371 epoch: None,
372 pkgrel: PackageRelease::from_str("1")?
373 }
374 )]
375 fn valid_full_version_from_string(
376 #[case] version: &str,
377 #[case] expected: FullVersion,
378 ) -> TestResult {
379 init_logger();
380
381 assert_eq!(
382 FullVersion::from_str(version),
383 Ok(expected),
384 "Expected valid parsing for FullVersion {version}"
385 );
386
387 Ok(())
388 }
389
390 #[rstest]
392 #[case::two_pkgrel("1:foo-1-1", "expected end of package release value")]
393 #[case::two_epoch("1:1:foo-1", "invalid pkgver character")]
394 #[case::empty_string(
395 "",
396 "expected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string"
397 )]
398 #[case::colon(
399 ":",
400 "expected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string"
401 )]
402 #[case::dot(
403 ".",
404 "expected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string"
405 )]
406 #[case::no_pkgrel_with_epoch(
407 "1:1.0.0",
408 "expected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string"
409 )]
410 #[case::no_pkgrel(
411 "1.0.0",
412 "expected alpm-pkgver string, followed by a '-' and an alpm-pkgrel string"
413 )]
414 #[case::no_pkgrel_dash_end(
415 "1.0.0-",
416 "invalid package release\nexpected positive decimal integer, alpm-pkgrel string"
417 )]
418 #[case::starts_with_dash(
419 "-1foo:1",
420 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
421 )]
422 #[case::ends_with_colon(
423 "1-foo:",
424 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
425 )]
426 #[case::ends_with_colon_number(
427 "1-foo:1",
428 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
429 )]
430 fn parse_error_in_full_version_from_string(#[case] version: &str, #[case] err_snippet: &str) {
431 init_logger();
432
433 let Err(Error::ParseError(err_msg)) = FullVersion::from_str(version) else {
434 panic!("parsing '{version}' as FullVersion did not fail as expected")
435 };
436 assert!(
437 err_msg.contains(err_snippet),
438 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
439 );
440 }
441
442 #[rstest]
445 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, Ok(FullVersion::from_str("1:1.0.0-1")?))]
446 #[case::full(Version::from_str("1.0.0-1")?, Ok(FullVersion::from_str("1.0.0-1")?))]
447 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, Err(Error::MissingComponent{component: "pkgrel"}))]
448 #[case::minimal(Version::from_str("1.0.0")?, Err(Error::MissingComponent{component: "pkgrel"}))]
449 fn full_version_try_from_version(
450 #[case] version: Version,
451 #[case] expected: Result<FullVersion, Error>,
452 ) -> TestResult {
453 assert_eq!(FullVersion::try_from(&version), expected);
454 assert_eq!(FullVersion::try_from(version), expected);
455 Ok(())
456 }
457
458 #[rstest]
461 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, FullVersion::from_str("1:1.0.0-1")?)]
462 #[case::full(Version::from_str("1.0.0-1")?, FullVersion::from_str("1.0.0-1")?)]
463 fn version_from_full_version(
464 #[case] version: Version,
465 #[case] full_version: FullVersion,
466 ) -> TestResult {
467 assert_eq!(Version::from(&full_version), version);
468 Ok(())
469 }
470
471 #[rstest]
473 #[case::with_epoch("1:1-1")]
474 #[case::plain("1-1")]
475 fn full_version_to_string(#[case] input: &str) -> TestResult {
476 assert_eq!(format!("{}", FullVersion::from_str(input)?), input);
477 Ok(())
478 }
479
480 #[rstest]
485 #[case::full_equal("1.0.0-1", "1.0.0-1", Ordering::Equal)]
486 #[case::full_less("1.0.0-1", "1.0.0-2", Ordering::Less)]
487 #[case::full_greater("1.0.0-2", "1.0.0-1", Ordering::Greater)]
488 #[case::full_with_epoch_equal("1:1.0.0-1", "1:1.0.0-1", Ordering::Equal)]
489 #[case::full_with_epoch_less("1.0.0-1", "1:1.0.0-1", Ordering::Less)]
490 #[case::full_with_epoch_less("1:1.0.0-1", "2:1.0.0-1", Ordering::Less)]
491 #[case::full_with_epoch_greater("1:1.0.0-1", "1.0.0-1", Ordering::Greater)]
492 #[case::full_with_epoch_greater("2:1.0.0-1", "1:1.0.0-1", Ordering::Greater)]
493 fn full_version_comparison(
494 #[case] version_a: &str,
495 #[case] version_b: &str,
496 #[case] expected: Ordering,
497 ) -> TestResult {
498 let version_a = FullVersion::from_str(version_a)?;
499 let version_b = FullVersion::from_str(version_b)?;
500
501 let vercmp_result = match &expected {
503 Ordering::Equal => 0,
504 Ordering::Greater => 1,
505 Ordering::Less => -1,
506 };
507
508 let ordering = version_a.cmp(&version_b);
509 assert_eq!(
510 ordering, expected,
511 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
512 );
513
514 assert_eq!(version_a.vercmp(&version_b), vercmp_result);
515
516 #[cfg(feature = "compatibility_tests")]
518 {
519 let output = std::process::Command::new("vercmp")
520 .arg(version_a.to_string())
521 .arg(version_b.to_string())
522 .output()?;
523 let result = String::from_utf8_lossy(&output.stdout);
524 assert_eq!(result.trim(), vercmp_result.to_string());
525 }
526
527 let reverse_vercmp_result = match &expected {
529 Ordering::Equal => 0,
530 Ordering::Greater => -1,
531 Ordering::Less => 1,
532 };
533 let reverse_expected = match &expected {
534 Ordering::Equal => Ordering::Equal,
535 Ordering::Greater => Ordering::Less,
536 Ordering::Less => Ordering::Greater,
537 };
538
539 let reverse_ordering = version_b.cmp(&version_a);
540 assert_eq!(
541 reverse_ordering, reverse_expected,
542 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
543 );
544
545 assert_eq!(version_b.vercmp(&version_a), reverse_vercmp_result);
546
547 Ok(())
548 }
549}