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, terminated},
16 error::{StrContext, StrContextValue},
17 token::take_till,
18};
19
20use crate::{Epoch, Error, PackageVersion, Version};
21#[cfg(doc)]
22use crate::{FullVersion, PackageRelease};
23
24#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
53pub struct MinimalVersion {
54 pub pkgver: PackageVersion,
56 pub epoch: Option<Epoch>,
58}
59
60impl MinimalVersion {
61 pub fn new(pkgver: PackageVersion, epoch: Option<Epoch>) -> Self {
81 Self { pkgver, epoch }
82 }
83
84 pub fn vercmp(&self, other: &MinimalVersion) -> i8 {
124 match self.cmp(other) {
125 Ordering::Less => -1,
126 Ordering::Equal => 0,
127 Ordering::Greater => 1,
128 }
129 }
130
131 pub fn parser(input: &mut &str) -> ModalResult<Self> {
142 let epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
145 cut_err(Epoch::parser),
147 ))
148 .context(StrContext::Expected(StrContextValue::Description(
149 "followed by a ':'",
150 )))
151 .parse_next(input)?;
152
153 let pkgver: PackageVersion = cut_err(PackageVersion::parser)
156 .context(StrContext::Expected(StrContextValue::Description(
157 "alpm-pkgver string",
158 )))
159 .parse_next(input)?;
160
161 eof.context(StrContext::Expected(StrContextValue::Description(
163 "end of full alpm-package-version string",
164 )))
165 .parse_next(input)?;
166
167 Ok(Self { epoch, pkgver })
168 }
169}
170
171impl Display for MinimalVersion {
172 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
173 if let Some(epoch) = self.epoch {
174 write!(fmt, "{epoch}:")?;
175 }
176 write!(fmt, "{}", self.pkgver)?;
177
178 Ok(())
179 }
180}
181
182impl FromStr for MinimalVersion {
183 type Err = Error;
184 fn from_str(s: &str) -> Result<Self, Self::Err> {
192 Ok(Self::parser.parse(s)?)
193 }
194}
195
196impl Ord for MinimalVersion {
197 fn cmp(&self, other: &Self) -> Ordering {
236 match (self.epoch, other.epoch) {
237 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
238 return self_epoch.cmp(&other_epoch);
239 }
240 (Some(_), None) => return Ordering::Greater,
241 (None, Some(_)) => return Ordering::Less,
242 (_, _) => {}
243 }
244
245 self.pkgver.cmp(&other.pkgver)
246 }
247}
248
249impl PartialOrd for MinimalVersion {
250 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
251 Some(self.cmp(other))
252 }
253}
254
255impl TryFrom<Version> for MinimalVersion {
256 type Error = crate::Error;
257
258 fn try_from(value: Version) -> Result<Self, Self::Error> {
264 if value.pkgrel.is_some() {
265 Err(Error::InvalidComponent {
266 component: "pkgrel",
267 context: "converting a full alpm-package-version to a minimal alpm-package-version",
268 })
269 } else {
270 Ok(Self {
271 pkgver: value.pkgver,
272 epoch: value.epoch,
273 })
274 }
275 }
276}
277
278impl TryFrom<&Version> for MinimalVersion {
279 type Error = crate::Error;
280
281 fn try_from(value: &Version) -> Result<Self, Self::Error> {
287 Self::try_from(value.clone())
288 }
289}
290
291impl From<MinimalVersion> for Version {
292 fn from(value: MinimalVersion) -> Self {
294 Self {
295 pkgver: value.pkgver,
296 pkgrel: None,
297 epoch: value.epoch,
298 }
299 }
300}
301
302impl From<&MinimalVersion> for Version {
303 fn from(value: &MinimalVersion) -> Self {
305 Self::from(value.clone())
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use log::{LevelFilter, debug};
312 use rstest::rstest;
313 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
314 use testresult::TestResult;
315
316 use super::*;
317 fn init_logger() {
319 if TermLogger::init(
320 LevelFilter::Trace,
321 Config::default(),
322 TerminalMode::Stderr,
323 ColorChoice::Auto,
324 )
325 .is_err()
326 {
327 debug!("Not initializing another logger, as one is initialized already.");
328 }
329 }
330
331 #[rstest]
333 #[case::minimal_with_epoch(
334 "1:foo",
335 MinimalVersion {
336 pkgver: PackageVersion::from_str("foo")?,
337 epoch: Some(Epoch::from_str("1")?),
338 },
339 )]
340 #[case::minimal(
341 "foo",
342 MinimalVersion {
343 pkgver: PackageVersion::from_str("foo")?,
344 epoch: None,
345 }
346 )]
347 fn minimal_version_from_str_succeeds(
348 #[case] version: &str,
349 #[case] expected: MinimalVersion,
350 ) -> TestResult {
351 init_logger();
352
353 assert_eq!(
354 MinimalVersion::from_str(version),
355 Ok(expected),
356 "Expected valid parsing for MinimalVersion {version}"
357 );
358
359 Ok(())
360 }
361
362 #[rstest]
364 #[case::two_pkgrel(
365 "1:foo-1-1",
366 "invalid pkgver character\nexpected ASCII alphanumeric character, `_`, `+`, `.`, alpm-pkgver string"
367 )]
368 #[case::two_epoch(
369 "1:1:foo-1",
370 "invalid pkgver character\nexpected ASCII alphanumeric character, `_`, `+`, `.`, alpm-pkgver string"
371 )]
372 #[case::empty_string(
373 "",
374 "invalid first pkgver character\nexpected ASCII alphanumeric character, alpm-pkgver string"
375 )]
376 #[case::colon(
377 ":",
378 "invalid first pkgver character\nexpected ASCII alphanumeric character, alpm-pkgver string"
379 )]
380 #[case::dot(
381 ".",
382 "invalid first pkgver character\nexpected ASCII alphanumeric character, alpm-pkgver string"
383 )]
384 #[case::full_with_epoch(
385 "1:1.0.0-1",
386 "invalid pkgver character\nexpected ASCII alphanumeric character, `_`, `+`, `.`, alpm-pkgver string"
387 )]
388 #[case::full(
389 "1.0.0-1",
390 "invalid pkgver character\nexpected ASCII alphanumeric character, `_`, `+`, `.`, alpm-pkgver string"
391 )]
392 #[case::no_pkgrel_dash_end(
393 "1.0.0-",
394 "invalid pkgver character\nexpected ASCII alphanumeric character, `_`, `+`, `.`, alpm-pkgver string"
395 )]
396 #[case::starts_with_dash(
397 "-1foo:1",
398 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
399 )]
400 #[case::ends_with_colon(
401 "1-foo:",
402 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
403 )]
404 #[case::ends_with_colon_number(
405 "1-foo:1",
406 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
407 )]
408 fn minimal_version_from_str_parse_error(#[case] version: &str, #[case] err_snippet: &str) {
409 init_logger();
410
411 let Err(Error::ParseError(err_msg)) = MinimalVersion::from_str(version) else {
412 panic!("parsing '{version}' as MinimalVersion did not fail as expected")
413 };
414 assert!(
415 err_msg.contains(err_snippet),
416 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
417 );
418 }
419
420 #[rstest]
423 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, Ok(MinimalVersion::from_str("1:1.0.0")?))]
424 #[case::minimal(Version::from_str("1.0.0")?, Ok(MinimalVersion::from_str("1.0.0")?))]
425 #[case::full_with_epoch(Version::from_str("1:1.0.0-1")?, Err(Error::InvalidComponent{component: "pkgrel", context: "converting a full alpm-package-version to a minimal alpm-package-version"}))]
426 #[case::full(Version::from_str("1.0.0-1")?, Err(Error::InvalidComponent{component: "pkgrel", context: "converting a full alpm-package-version to a minimal alpm-package-version"}))]
427 fn minimal_version_try_from_version(
428 #[case] version: Version,
429 #[case] expected: Result<MinimalVersion, Error>,
430 ) -> TestResult {
431 assert_eq!(MinimalVersion::try_from(&version), expected);
432 Ok(())
433 }
434
435 #[rstest]
438 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, MinimalVersion::from_str("1:1.0.0")?)]
439 #[case::minimal(Version::from_str("1.0.0")?, MinimalVersion::from_str("1.0.0")?)]
440 fn version_from_minimal_version(
441 #[case] version: Version,
442 #[case] full_version: MinimalVersion,
443 ) -> TestResult {
444 assert_eq!(Version::from(&full_version), version);
445 Ok(())
446 }
447
448 #[rstest]
450 #[case::with_epoch("1:1.0.0")]
451 #[case::plain("1.0.0")]
452 fn minimal_version_to_string(#[case] input: &str) -> TestResult {
453 assert_eq!(format!("{}", MinimalVersion::from_str(input)?), input);
454 Ok(())
455 }
456
457 #[rstest]
462 #[case::minimal_equal("1.0.0", "1.0.0", Ordering::Equal)]
463 #[case::minimal_less("1.0.0", "2.0.0", Ordering::Less)]
464 #[case::minimal_greater("2.0.0", "1.0.0", Ordering::Greater)]
465 #[case::minimal_with_epoch_equal("1:1.0.0", "1:1.0.0", Ordering::Equal)]
466 #[case::minimal_with_epoch_less("1.0.0", "1:1.0.0", Ordering::Less)]
467 #[case::minimal_with_epoch_less("1:1.0.0", "2:1.0.0", Ordering::Less)]
468 #[case::minimal_with_epoch_greater("1:1.0.0", "1.0.0", Ordering::Greater)]
469 #[case::minimal_with_epoch_greater("2:1.0.0", "1:1.0.0", Ordering::Greater)]
470 fn minimal_version_comparison(
471 #[case] version_a: &str,
472 #[case] version_b: &str,
473 #[case] expected: Ordering,
474 ) -> TestResult {
475 let version_a = MinimalVersion::from_str(version_a)?;
476 let version_b = MinimalVersion::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}