1use std::{
6 cmp::Ordering,
7 fmt::{Display, Formatter},
8 str::FromStr,
9};
10
11use alpm_parsers::traits::{AlpmParser, ParserUntil, ParserUntilInclusive};
12use serde::{Deserialize, Serialize};
13use winnow::{
14 ModalResult,
15 Parser,
16 combinator::opt,
17 error::{ContextError, ErrMode, StrContext, StrContextValue},
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
140impl AlpmParser for FullVersion {
141 fn parser(input: &mut &str) -> ModalResult<Self> {
150 let epoch = opt(Epoch::parser_until_inclusive(":")).parse_next(input)?;
155
156 let pkgver: PackageVersion = PackageVersion::parser.parse_next(input)?;
159
160 "-".context(StrContext::Label("full alpm-package-version"))
161 .context(StrContext::Expected(StrContextValue::Description(
162 "the '-' delimiter that divides the alpm-pkgver and alpm-pkgrel in a full alpm-package-version",
163 )))
164 .parse_next(input)?;
165
166 let pkgrel: PackageRelease = PackageRelease::parser.parse_next(input)?;
171
172 Ok(Self {
173 epoch,
174 pkgver,
175 pkgrel,
176 })
177 }
178
179 fn delimiter_error_context<'a, O, P>(
180 parser: P,
181 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
182 where
183 P: Parser<&'a str, O, ErrMode<ContextError>>,
184 {
185 parser
186 .context(StrContext::Label("full alpm-package-version"))
187 .context(StrContext::Expected(StrContextValue::Description(
188 "the package version to end with a valid package release",
189 )))
190 .context(StrContext::Expected(StrContextValue::Description(
191 "i.e. a positive integer followed by an optional `.` and another positive integer",
192 )))
193 }
194}
195
196impl Display for FullVersion {
197 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
198 if let Some(epoch) = self.epoch {
199 write!(fmt, "{epoch}:")?;
200 }
201 write!(fmt, "{}-{}", self.pkgver, self.pkgrel)?;
202
203 Ok(())
204 }
205}
206
207impl FromStr for FullVersion {
208 type Err = Error;
209 fn from_str(s: &str) -> Result<Self, Self::Err> {
217 Ok(Self::parser_until_eof.parse(s)?)
218 }
219}
220
221impl Ord for FullVersion {
222 fn cmp(&self, other: &Self) -> Ordering {
261 match (self.epoch, other.epoch) {
262 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
263 return self_epoch.cmp(&other_epoch);
264 }
265 (Some(_), None) => return Ordering::Greater,
266 (None, Some(_)) => return Ordering::Less,
267 (_, _) => {}
268 }
269
270 let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
271 if pkgver_cmp.is_ne() {
272 return pkgver_cmp;
273 }
274
275 self.pkgrel.cmp(&other.pkgrel)
276 }
277}
278
279impl PartialOrd for FullVersion {
280 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
281 Some(self.cmp(other))
282 }
283}
284
285impl TryFrom<Version> for FullVersion {
286 type Error = crate::Error;
287
288 fn try_from(value: Version) -> Result<Self, Self::Error> {
294 Ok(Self {
295 pkgver: value.pkgver,
296 pkgrel: value.pkgrel.ok_or(Error::MissingComponent {
297 component: "pkgrel",
298 })?,
299 epoch: value.epoch,
300 })
301 }
302}
303
304impl TryFrom<&Version> for FullVersion {
305 type Error = crate::Error;
306
307 fn try_from(value: &Version) -> Result<Self, Self::Error> {
313 Self::try_from(value.clone())
314 }
315}
316
317impl From<FullVersion> for Version {
318 fn from(value: FullVersion) -> Self {
320 Self {
321 pkgver: value.pkgver,
322 pkgrel: Some(value.pkgrel),
323 epoch: value.epoch,
324 }
325 }
326}
327
328impl From<&FullVersion> for Version {
329 fn from(value: &FullVersion) -> Self {
331 Self::from(value.clone())
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use insta::assert_snapshot;
338 use log::{LevelFilter, debug};
339 use rstest::rstest;
340 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
341 use testresult::TestResult;
342
343 use super::*;
344 use crate::configure_insta;
345
346 fn init_logger() {
348 if TermLogger::init(
349 LevelFilter::Trace,
350 Config::default(),
351 TerminalMode::Stderr,
352 ColorChoice::Auto,
353 )
354 .is_err()
355 {
356 debug!("Not initializing another logger, as one is initialized already.");
357 }
358 }
359
360 #[rstest]
362 #[case::full_with_epoch(
363 "1:foo-1",
364 FullVersion {
365 pkgver: PackageVersion::from_str("foo")?,
366 epoch: Some(Epoch::from_str("1")?),
367 pkgrel: PackageRelease::from_str("1")?,
368 },
369 )]
370 #[case::full(
371 "foo-1",
372 FullVersion {
373 pkgver: PackageVersion::from_str("foo")?,
374 epoch: None,
375 pkgrel: PackageRelease::from_str("1")?
376 }
377 )]
378 fn valid_full_version_from_string(
379 #[case] version: &str,
380 #[case] expected: FullVersion,
381 ) -> TestResult {
382 init_logger();
383
384 assert_eq!(
385 FullVersion::from_str(version),
386 Ok(expected),
387 "Expected valid parsing for FullVersion {version}"
388 );
389
390 Ok(())
391 }
392
393 #[rstest]
395 #[case::two_pkgrel("1:foo-1-1")]
396 #[case::two_epoch("1:1:foo-1")]
397 #[case::empty_string("")]
398 #[case::colon(":")]
399 #[case::dot(".")]
400 #[case::no_pkgrel_with_epoch("1:1.0.0")]
401 #[case::no_pkgrel("1.0.0")]
402 #[case::no_pkgrel_dash_end("1.0.0-")]
403 #[case::starts_with_dash("-1foo:1")]
404 #[case::ends_with_colon("1-foo:")]
405 #[case::ends_with_colon_number("1-foo:1")]
406 fn parse_error_in_full_version_from_string(#[case] input: &str) {
407 init_logger();
408
409 let Err(Error::ParseError(err_msg)) = FullVersion::from_str(input) else {
410 panic!("'{input}' erroneously parsed as a FullVersion")
411 };
412
413 let (test_name, _guard) = configure_insta();
414 assert_snapshot!(test_name, err_msg.to_string());
415 }
416
417 #[rstest]
420 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, Ok(FullVersion::from_str("1:1.0.0-1")?))]
421 #[case::full(Version::from_str("1.0.0-1")?, Ok(FullVersion::from_str("1.0.0-1")?))]
422 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, Err(Error::MissingComponent{component: "pkgrel"}))]
423 #[case::minimal(Version::from_str("1.0.0")?, Err(Error::MissingComponent{component: "pkgrel"}))]
424 fn full_version_try_from_version(
425 #[case] version: Version,
426 #[case] expected: Result<FullVersion, Error>,
427 ) -> TestResult {
428 assert_eq!(FullVersion::try_from(&version), expected);
429 assert_eq!(FullVersion::try_from(version), expected);
430 Ok(())
431 }
432
433 #[rstest]
436 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, FullVersion::from_str("1:1.0.0-1")?)]
437 #[case::full(Version::from_str("1.0.0-1")?, FullVersion::from_str("1.0.0-1")?)]
438 fn version_from_full_version(
439 #[case] version: Version,
440 #[case] full_version: FullVersion,
441 ) -> TestResult {
442 assert_eq!(Version::from(&full_version), version);
443 Ok(())
444 }
445
446 #[rstest]
448 #[case::with_epoch("1:1-1")]
449 #[case::plain("1-1")]
450 fn full_version_to_string(#[case] input: &str) -> TestResult {
451 assert_eq!(format!("{}", FullVersion::from_str(input)?), input);
452 Ok(())
453 }
454
455 #[rstest]
460 #[case::full_equal("1.0.0-1", "1.0.0-1", Ordering::Equal)]
461 #[case::full_less("1.0.0-1", "1.0.0-2", Ordering::Less)]
462 #[case::full_greater("1.0.0-2", "1.0.0-1", Ordering::Greater)]
463 #[case::full_with_epoch_equal("1:1.0.0-1", "1:1.0.0-1", Ordering::Equal)]
464 #[case::full_with_epoch_less("1.0.0-1", "1:1.0.0-1", Ordering::Less)]
465 #[case::full_with_epoch_less("1:1.0.0-1", "2:1.0.0-1", Ordering::Less)]
466 #[case::full_with_epoch_greater("1:1.0.0-1", "1.0.0-1", Ordering::Greater)]
467 #[case::full_with_epoch_greater("2:1.0.0-1", "1:1.0.0-1", Ordering::Greater)]
468 fn full_version_comparison(
469 #[case] version_a: &str,
470 #[case] version_b: &str,
471 #[case] expected: Ordering,
472 ) -> TestResult {
473 let version_a = FullVersion::from_str(version_a)?;
474 let version_b = FullVersion::from_str(version_b)?;
475
476 let vercmp_result = match &expected {
478 Ordering::Equal => 0,
479 Ordering::Greater => 1,
480 Ordering::Less => -1,
481 };
482
483 let ordering = version_a.cmp(&version_b);
484 assert_eq!(
485 ordering, expected,
486 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
487 );
488
489 assert_eq!(version_a.vercmp(&version_b), vercmp_result);
490
491 #[cfg(feature = "compatibility_tests")]
493 {
494 let output = std::process::Command::new("vercmp")
495 .arg(version_a.to_string())
496 .arg(version_b.to_string())
497 .output()?;
498 let result = String::from_utf8_lossy(&output.stdout);
499 assert_eq!(result.trim(), vercmp_result.to_string());
500 }
501
502 let reverse_vercmp_result = match &expected {
504 Ordering::Equal => 0,
505 Ordering::Greater => -1,
506 Ordering::Less => 1,
507 };
508 let reverse_expected = match &expected {
509 Ordering::Equal => Ordering::Equal,
510 Ordering::Greater => Ordering::Less,
511 Ordering::Less => Ordering::Greater,
512 };
513
514 let reverse_ordering = version_b.cmp(&version_a);
515 assert_eq!(
516 reverse_ordering, reverse_expected,
517 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
518 );
519
520 assert_eq!(version_b.vercmp(&version_a), reverse_vercmp_result);
521
522 Ok(())
523 }
524}