1use std::{
6 cmp::Ordering,
7 fmt::{Display, Formatter},
8 str::FromStr,
9};
10
11use alpm_parsers::traits::{AlpmParser, ParserUntil, ParserUntilInclusive};
12use fluent_i18n::t;
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15use winnow::{
16 ModalResult,
17 Parser,
18 combinator::opt,
19 error::{ContextError, ErrMode, StrContext, StrContextValue},
20};
21
22use crate::{Epoch, Error, PackageVersion, Version};
23#[cfg(doc)]
24use crate::{FullVersion, PackageRelease};
25
26#[derive(Clone, Debug, Eq, PartialEq)]
55#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
56pub struct MinimalVersion {
57 pub pkgver: PackageVersion,
59 pub epoch: Option<Epoch>,
61}
62
63impl MinimalVersion {
64 pub fn new(pkgver: PackageVersion, epoch: Option<Epoch>) -> Self {
84 Self { pkgver, epoch }
85 }
86
87 pub fn vercmp(&self, other: &MinimalVersion) -> i8 {
127 match self.cmp(other) {
128 Ordering::Less => -1,
129 Ordering::Equal => 0,
130 Ordering::Greater => 1,
131 }
132 }
133}
134
135impl AlpmParser for MinimalVersion {
136 fn parser(input: &mut &str) -> ModalResult<Self> {
145 let epoch = opt(Epoch::parser_until_inclusive(":")).parse_next(input)?;
150
151 let pkgver: PackageVersion = PackageVersion::parser.parse_next(input)?;
154
155 Ok(Self { epoch, pkgver })
156 }
157
158 fn delimiter_error_context<'a, O, P>(
159 parser: P,
160 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
161 where
162 P: Parser<&'a str, O, ErrMode<ContextError>>,
163 {
164 parser
165 .context(StrContext::Label("minimal alpm-package-version"))
166 .context(StrContext::Expected(StrContextValue::Description(
167 "the package version to end with an alpm-pkgver",
168 )))
169 }
170}
171
172impl Display for MinimalVersion {
173 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
174 if let Some(epoch) = self.epoch {
175 write!(fmt, "{epoch}:")?;
176 }
177 write!(fmt, "{}", self.pkgver)?;
178
179 Ok(())
180 }
181}
182
183impl FromStr for MinimalVersion {
184 type Err = Error;
185 fn from_str(s: &str) -> Result<Self, Self::Err> {
193 Ok(Self::parser_until_eof.parse(s)?)
194 }
195}
196
197impl Ord for MinimalVersion {
198 fn cmp(&self, other: &Self) -> Ordering {
237 match (self.epoch, other.epoch) {
238 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
239 return self_epoch.cmp(&other_epoch);
240 }
241 (Some(_), None) => return Ordering::Greater,
242 (None, Some(_)) => return Ordering::Less,
243 (_, _) => {}
244 }
245
246 self.pkgver.cmp(&other.pkgver)
247 }
248}
249
250impl PartialOrd for MinimalVersion {
251 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
252 Some(self.cmp(other))
253 }
254}
255
256impl TryFrom<Version> for MinimalVersion {
257 type Error = crate::Error;
258
259 fn try_from(value: Version) -> Result<Self, Self::Error> {
265 if value.pkgrel.is_some() {
266 Err(Error::InvalidComponent {
267 component: "pkgrel",
268 context: t!("error-context-convert-full-to-minimal"),
269 })
270 } else {
271 Ok(Self {
272 pkgver: value.pkgver,
273 epoch: value.epoch,
274 })
275 }
276 }
277}
278
279impl TryFrom<&Version> for MinimalVersion {
280 type Error = crate::Error;
281
282 fn try_from(value: &Version) -> Result<Self, Self::Error> {
288 Self::try_from(value.clone())
289 }
290}
291
292impl From<MinimalVersion> for Version {
293 fn from(value: MinimalVersion) -> Self {
295 Self {
296 pkgver: value.pkgver,
297 pkgrel: None,
298 epoch: value.epoch,
299 }
300 }
301}
302
303impl From<&MinimalVersion> for Version {
304 fn from(value: &MinimalVersion) -> Self {
306 Self::from(value.clone())
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use insta::assert_snapshot;
313 use log::{LevelFilter, debug};
314 use rstest::rstest;
315 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
316 use testresult::TestResult;
317
318 use super::*;
319 use crate::configure_insta;
320 fn init_logger() {
322 if TermLogger::init(
323 LevelFilter::Trace,
324 Config::default(),
325 TerminalMode::Stderr,
326 ColorChoice::Auto,
327 )
328 .is_err()
329 {
330 debug!("Not initializing another logger, as one is initialized already.");
331 }
332 }
333
334 #[rstest]
336 #[case::minimal_with_epoch(
337 "1:foo",
338 MinimalVersion {
339 pkgver: PackageVersion::from_str("foo")?,
340 epoch: Some(Epoch::from_str("1")?),
341 },
342 )]
343 #[case::minimal(
344 "foo",
345 MinimalVersion {
346 pkgver: PackageVersion::from_str("foo")?,
347 epoch: None,
348 }
349 )]
350 #[case::minimal_dot(
352 ".",
353 MinimalVersion {
354 pkgver: PackageVersion::from_str(".")?,
355 epoch: None,
356 }
357 )]
358 fn minimal_version_from_str_succeeds(
359 #[case] version: &str,
360 #[case] expected: MinimalVersion,
361 ) -> TestResult {
362 init_logger();
363
364 assert_eq!(
365 MinimalVersion::from_str(version),
366 Ok(expected),
367 "Expected valid parsing for MinimalVersion {version}"
368 );
369
370 Ok(())
371 }
372
373 #[rstest]
375 #[case::two_pkgrel("1:foo-1-1")]
376 #[case::two_epoch("1:1:foo-1")]
377 #[case::empty_string("")]
378 #[case::colon(":")]
379 #[case::minimal_with_epoch("1:1.0.0-1")]
380 #[case::minimal("1.0.0-1")]
381 #[case::no_pkgrel_dash_end("1.0.0-")]
382 #[case::starts_with_dash("-1foo:1")]
383 #[case::ends_with_colon("1-foo:")]
384 #[case::ends_with_colon_number("1-foo:1")]
385 fn minimal_version_from_str_parse_error(#[case] version: &str) {
386 let Err(Error::ParseError(err)) = MinimalVersion::from_str(version) else {
387 panic!("parsing '{version}' as MinimalVersion did not fail as expected")
388 };
389
390 let (test_name, _guard) = configure_insta();
391 assert_snapshot!(test_name, err.to_string());
392 }
393
394 #[rstest]
397 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, Ok(MinimalVersion::from_str("1:1.0.0")?))]
398 #[case::minimal(Version::from_str("1.0.0")?, Ok(MinimalVersion::from_str("1.0.0")?))]
399 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, Err(Error::InvalidComponent{component: "pkgrel", context: t!("error-context-convert-full-to-minimal")}))]
400 #[case::full(Version::from_str("1.0.0-1")?, Err(Error::InvalidComponent{component: "pkgrel", context: t!("error-context-convert-full-to-minimal")}))]
401 fn minimal_version_try_from_version(
402 #[case] version: Version,
403 #[case] expected: Result<MinimalVersion, Error>,
404 ) -> TestResult {
405 assert_eq!(MinimalVersion::try_from(&version), expected);
406 Ok(())
407 }
408
409 #[rstest]
412 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, MinimalVersion::from_str("1:1.0.0")?)]
413 #[case::minimal(Version::from_str("1.0.0")?, MinimalVersion::from_str("1.0.0")?)]
414 fn version_from_minimal_version(
415 #[case] version: Version,
416 #[case] full_version: MinimalVersion,
417 ) -> TestResult {
418 assert_eq!(Version::from(&full_version), version);
419 Ok(())
420 }
421
422 #[rstest]
424 #[case::with_epoch("1:1.0.0")]
425 #[case::plain("1.0.0")]
426 fn minimal_version_to_string(#[case] input: &str) -> TestResult {
427 assert_eq!(format!("{}", MinimalVersion::from_str(input)?), input);
428 Ok(())
429 }
430
431 #[rstest]
436 #[case::minimal_equal("1.0.0", "1.0.0", Ordering::Equal)]
437 #[case::minimal_less("1.0.0", "2.0.0", Ordering::Less)]
438 #[case::minimal_greater("2.0.0", "1.0.0", Ordering::Greater)]
439 #[case::minimal_with_epoch_equal("1:1.0.0", "1:1.0.0", Ordering::Equal)]
440 #[case::minimal_with_epoch_less("1.0.0", "1:1.0.0", Ordering::Less)]
441 #[case::minimal_with_epoch_less("1:1.0.0", "2:1.0.0", Ordering::Less)]
442 #[case::minimal_with_epoch_greater("1:1.0.0", "1.0.0", Ordering::Greater)]
443 #[case::minimal_with_epoch_greater("2:1.0.0", "1:1.0.0", Ordering::Greater)]
444 fn minimal_version_comparison(
445 #[case] version_a: &str,
446 #[case] version_b: &str,
447 #[case] expected: Ordering,
448 ) -> TestResult {
449 let version_a = MinimalVersion::from_str(version_a)?;
450 let version_b = MinimalVersion::from_str(version_b)?;
451
452 let vercmp_result = match &expected {
454 Ordering::Equal => 0,
455 Ordering::Greater => 1,
456 Ordering::Less => -1,
457 };
458
459 let ordering = version_a.cmp(&version_b);
460 assert_eq!(
461 ordering, expected,
462 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
463 );
464
465 assert_eq!(version_a.vercmp(&version_b), vercmp_result);
466
467 #[cfg(feature = "_compatibility_tests")]
469 {
470 let output = std::process::Command::new("vercmp")
471 .arg(version_a.to_string())
472 .arg(version_b.to_string())
473 .output()?;
474 let result = String::from_utf8_lossy(&output.stdout);
475 assert_eq!(result.trim(), vercmp_result.to_string());
476 }
477
478 let reverse_vercmp_result = match &expected {
480 Ordering::Equal => 0,
481 Ordering::Greater => -1,
482 Ordering::Less => 1,
483 };
484 let reverse_expected = match &expected {
485 Ordering::Equal => Ordering::Equal,
486 Ordering::Greater => Ordering::Less,
487 Ordering::Less => Ordering::Greater,
488 };
489
490 let reverse_ordering = version_b.cmp(&version_a);
491 assert_eq!(
492 reverse_ordering, reverse_expected,
493 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
494 );
495
496 assert_eq!(version_b.vercmp(&version_a), reverse_vercmp_result);
497
498 Ok(())
499 }
500}