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