1use std::{
6 cmp::Ordering,
7 fmt::{Display, Formatter},
8 str::FromStr,
9};
10
11use alpm_parsers::prelude::*;
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14use winnow::{combinator::opt, error::ErrMode};
15
16use crate::{Epoch, Error, PackageRelease, PackageVersion, Version};
17
18#[derive(Clone, Debug, Eq, PartialEq)]
46#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
47pub struct FullVersion {
48 pub pkgver: PackageVersion,
50 pub pkgrel: PackageRelease,
52 pub epoch: Option<Epoch>,
54}
55
56impl FullVersion {
57 pub fn new(pkgver: PackageVersion, pkgrel: PackageRelease, epoch: Option<Epoch>) -> Self {
82 Self {
83 pkgver,
84 pkgrel,
85 epoch,
86 }
87 }
88
89 pub fn vercmp(&self, other: &FullVersion) -> i8 {
129 match self.cmp(other) {
130 Ordering::Less => -1,
131 Ordering::Equal => 0,
132 Ordering::Greater => 1,
133 }
134 }
135}
136
137impl AlpmParser for FullVersion {
138 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
147 let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
148 let epoch = opt(Epoch::parser_until_inclusive(":")).parse_next(input)?;
153
154 let pkgver: PackageVersion = PackageVersion::parser.parse_next(input)?;
157
158 "-".expected_text("the '-' delimiter that divides the alpm-pkgver and alpm-pkgrel")
159 .parse_next(input)?;
160
161 let pkgrel: PackageRelease = PackageRelease::parser.parse_next(input)?;
166
167 Ok(Self {
168 epoch,
169 pkgver,
170 pkgrel,
171 })
172 };
173
174 parser.layer("full alpm-package-version").parse_next(input)
175 }
176
177 fn delimiter_error_context<'a, O, P>(
178 parser: P,
179 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
180 where
181 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
182 {
183 parser
184 .description(concat!(
185 "The package version must end with a valid package release.\n",
186 "I.e. a positive integer followed by an optional `.` and another positive integer",
187 ))
188 .layer("full alpm-package-version")
189 }
190}
191
192impl Display for FullVersion {
193 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
194 if let Some(epoch) = self.epoch {
195 write!(fmt, "{epoch}:")?;
196 }
197 write!(fmt, "{}-{}", self.pkgver, self.pkgrel)?;
198
199 Ok(())
200 }
201}
202
203impl FromStr for FullVersion {
204 type Err = Error;
205 fn from_str(s: &str) -> Result<Self, Self::Err> {
213 Ok(Self::parser_until_eof.parse(Input::new(s))?)
214 }
215}
216
217impl Ord for FullVersion {
218 fn cmp(&self, other: &Self) -> Ordering {
257 match (self.epoch, other.epoch) {
258 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
259 return self_epoch.cmp(&other_epoch);
260 }
261 (Some(_), None) => return Ordering::Greater,
262 (None, Some(_)) => return Ordering::Less,
263 (_, _) => {}
264 }
265
266 let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
267 if pkgver_cmp.is_ne() {
268 return pkgver_cmp;
269 }
270
271 self.pkgrel.cmp(&other.pkgrel)
272 }
273}
274
275impl PartialOrd for FullVersion {
276 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
277 Some(self.cmp(other))
278 }
279}
280
281impl TryFrom<Version> for FullVersion {
282 type Error = crate::Error;
283
284 fn try_from(value: Version) -> Result<Self, Self::Error> {
290 Ok(Self {
291 pkgver: value.pkgver,
292 pkgrel: value.pkgrel.ok_or(Error::MissingComponent {
293 component: "pkgrel",
294 })?,
295 epoch: value.epoch,
296 })
297 }
298}
299
300impl TryFrom<&Version> for FullVersion {
301 type Error = crate::Error;
302
303 fn try_from(value: &Version) -> Result<Self, Self::Error> {
309 Self::try_from(value.clone())
310 }
311}
312
313impl From<FullVersion> for Version {
314 fn from(value: FullVersion) -> Self {
316 Self {
317 pkgver: value.pkgver,
318 pkgrel: Some(value.pkgrel),
319 epoch: value.epoch,
320 }
321 }
322}
323
324impl From<&FullVersion> for Version {
325 fn from(value: &FullVersion) -> Self {
327 Self::from(value.clone())
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use insta::assert_snapshot;
334 use log::{LevelFilter, debug};
335 use rstest::rstest;
336 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
337 use testresult::TestResult;
338
339 use super::*;
340 use crate::configure_insta;
341
342 fn init_logger() {
344 if TermLogger::init(
345 LevelFilter::Trace,
346 Config::default(),
347 TerminalMode::Stderr,
348 ColorChoice::Auto,
349 )
350 .is_err()
351 {
352 debug!("Not initializing another logger, as one is initialized already.");
353 }
354 }
355
356 #[rstest]
358 #[case::full_with_epoch(
359 "1:foo-1",
360 FullVersion {
361 pkgver: PackageVersion::from_str("foo")?,
362 epoch: Some(Epoch::from_str("1")?),
363 pkgrel: PackageRelease::from_str("1")?,
364 },
365 )]
366 #[case::full(
367 "foo-1",
368 FullVersion {
369 pkgver: PackageVersion::from_str("foo")?,
370 epoch: None,
371 pkgrel: PackageRelease::from_str("1")?
372 }
373 )]
374 fn valid_full_version_from_string(
375 #[case] version: &str,
376 #[case] expected: FullVersion,
377 ) -> TestResult {
378 init_logger();
379
380 assert_eq!(
381 FullVersion::from_str(version)?,
382 expected,
383 "Expected valid parsing for FullVersion {version}"
384 );
385
386 Ok(())
387 }
388
389 #[rstest]
391 #[case::two_pkgrel("1:foo-1-1")]
392 #[case::two_epoch("1:1:foo-1")]
393 #[case::empty_string("")]
394 #[case::colon(":")]
395 #[case::dot(".")]
396 #[case::no_pkgrel_with_epoch("1:1.0.0")]
397 #[case::no_pkgrel("1.0.0")]
398 #[case::no_pkgrel_dash_end("1.0.0-")]
399 #[case::starts_with_dash("-1foo:1")]
400 #[case::ends_with_colon("1-foo:")]
401 #[case::ends_with_colon_number("1-foo:1")]
402 fn parse_error_in_full_version_from_string(#[case] input: &str) {
403 init_logger();
404
405 let (test_name, _guard) = configure_insta();
406
407 let Err(Error::ParseError(err_msg)) = FullVersion::from_str(input) else {
408 panic!("'{input}' erroneously parsed as a FullVersion")
409 };
410
411 assert_snapshot!(test_name, err_msg.to_string());
412 }
413
414 #[rstest]
416 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, FullVersion::from_str("1:1.0.0-1")?)]
417 #[case::full(Version::from_str("1.0.0-1")?, FullVersion::from_str("1.0.0-1")?)]
418 fn full_version_try_from_version_succeeds(
419 #[case] version: Version,
420 #[case] expected: FullVersion,
421 ) -> TestResult {
422 assert_eq!(FullVersion::try_from(&version)?, expected);
423 Ok(())
424 }
425
426 #[rstest]
428 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?)]
429 #[case::minimal(Version::from_str("1.0.0")?)]
430 fn full_version_try_from_version_fails_on_incompatible_version(
431 #[case] version: Version,
432 ) -> TestResult {
433 let Err(Error::MissingComponent {
434 component: "pkgrel",
435 }) = FullVersion::try_from(&version)
436 else {
437 panic!("Expected to fail with Error::MissingComponent");
438 };
439
440 Ok(())
441 }
442
443 #[rstest]
446 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, FullVersion::from_str("1:1.0.0-1")?)]
447 #[case::full(Version::from_str("1.0.0-1")?, FullVersion::from_str("1.0.0-1")?)]
448 fn version_from_full_version(
449 #[case] version: Version,
450 #[case] full_version: FullVersion,
451 ) -> TestResult {
452 assert_eq!(Version::from(&full_version), version);
453 Ok(())
454 }
455
456 #[rstest]
458 #[case::with_epoch("1:1-1")]
459 #[case::plain("1-1")]
460 fn full_version_to_string(#[case] input: &str) -> TestResult {
461 assert_eq!(format!("{}", FullVersion::from_str(input)?), input);
462 Ok(())
463 }
464
465 #[rstest]
470 #[case::full_equal("1.0.0-1", "1.0.0-1", Ordering::Equal)]
471 #[case::full_less("1.0.0-1", "1.0.0-2", Ordering::Less)]
472 #[case::full_greater("1.0.0-2", "1.0.0-1", Ordering::Greater)]
473 #[case::full_with_epoch_equal("1:1.0.0-1", "1:1.0.0-1", Ordering::Equal)]
474 #[case::full_with_epoch_less("1.0.0-1", "1:1.0.0-1", Ordering::Less)]
475 #[case::full_with_epoch_less("1:1.0.0-1", "2:1.0.0-1", Ordering::Less)]
476 #[case::full_with_epoch_greater("1:1.0.0-1", "1.0.0-1", Ordering::Greater)]
477 #[case::full_with_epoch_greater("2:1.0.0-1", "1:1.0.0-1", Ordering::Greater)]
478 fn full_version_comparison(
479 #[case] version_a: &str,
480 #[case] version_b: &str,
481 #[case] expected: Ordering,
482 ) -> TestResult {
483 let version_a = FullVersion::from_str(version_a)?;
484 let version_b = FullVersion::from_str(version_b)?;
485
486 let vercmp_result = match &expected {
488 Ordering::Equal => 0,
489 Ordering::Greater => 1,
490 Ordering::Less => -1,
491 };
492
493 let ordering = version_a.cmp(&version_b);
494 assert_eq!(
495 ordering, expected,
496 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
497 );
498
499 assert_eq!(version_a.vercmp(&version_b), vercmp_result);
500
501 #[cfg(feature = "_compatibility_tests")]
503 {
504 let output = std::process::Command::new("vercmp")
505 .arg(version_a.to_string())
506 .arg(version_b.to_string())
507 .output()?;
508 let result = String::from_utf8_lossy(&output.stdout);
509 assert_eq!(result.trim(), vercmp_result.to_string());
510 }
511
512 let reverse_vercmp_result = match &expected {
514 Ordering::Equal => 0,
515 Ordering::Greater => -1,
516 Ordering::Less => 1,
517 };
518 let reverse_expected = match &expected {
519 Ordering::Equal => Ordering::Equal,
520 Ordering::Greater => Ordering::Less,
521 Ordering::Less => Ordering::Greater,
522 };
523
524 let reverse_ordering = version_b.cmp(&version_a);
525 assert_eq!(
526 reverse_ordering, reverse_expected,
527 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
528 );
529
530 assert_eq!(version_b.vercmp(&version_a), reverse_vercmp_result);
531
532 Ok(())
533 }
534}