1use std::{
6 cmp::Ordering,
7 fmt::{Display, Formatter},
8 str::FromStr,
9};
10
11use fluent_i18n::t;
12use serde::{Deserialize, Serialize};
13use winnow::{
14 ModalResult,
15 Parser,
16 combinator::{cut_err, eof, opt, terminated},
17 error::{StrContext, StrContextValue},
18 token::take_till,
19};
20
21use crate::{Epoch, Error, PackageVersion, Version};
22#[cfg(doc)]
23use crate::{FullVersion, PackageRelease};
24
25#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
54pub struct MinimalVersion {
55 pub pkgver: PackageVersion,
57 pub epoch: Option<Epoch>,
59}
60
61impl MinimalVersion {
62 pub fn new(pkgver: PackageVersion, epoch: Option<Epoch>) -> Self {
82 Self { pkgver, epoch }
83 }
84
85 pub fn vercmp(&self, other: &MinimalVersion) -> i8 {
125 match self.cmp(other) {
126 Ordering::Less => -1,
127 Ordering::Equal => 0,
128 Ordering::Greater => 1,
129 }
130 }
131
132 pub fn parser(input: &mut &str) -> ModalResult<Self> {
143 let epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
146 cut_err(Epoch::parser),
148 ))
149 .context(StrContext::Expected(StrContextValue::Description(
150 "followed by a ':'",
151 )))
152 .parse_next(input)?;
153
154 let pkgver: PackageVersion = cut_err(PackageVersion::parser)
157 .context(StrContext::Expected(StrContextValue::Description(
158 "alpm-pkgver string",
159 )))
160 .parse_next(input)?;
161
162 eof.context(StrContext::Expected(StrContextValue::Description(
164 "end of full alpm-package-version string",
165 )))
166 .parse_next(input)?;
167
168 Ok(Self { epoch, pkgver })
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.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 log::{LevelFilter, debug};
313 use rstest::rstest;
314 use simplelog::{ColorChoice, Config, TermLogger, TerminalMode};
315 use testresult::TestResult;
316
317 use super::*;
318 fn init_logger() {
320 if TermLogger::init(
321 LevelFilter::Trace,
322 Config::default(),
323 TerminalMode::Stderr,
324 ColorChoice::Auto,
325 )
326 .is_err()
327 {
328 debug!("Not initializing another logger, as one is initialized already.");
329 }
330 }
331
332 #[rstest]
334 #[case::minimal_with_epoch(
335 "1:foo",
336 MinimalVersion {
337 pkgver: PackageVersion::from_str("foo")?,
338 epoch: Some(Epoch::from_str("1")?),
339 },
340 )]
341 #[case::minimal(
342 "foo",
343 MinimalVersion {
344 pkgver: PackageVersion::from_str("foo")?,
345 epoch: None,
346 }
347 )]
348 #[case::minimal_dot(
350 ".",
351 MinimalVersion {
352 pkgver: PackageVersion::from_str(".")?,
353 epoch: None,
354 }
355 )]
356 fn minimal_version_from_str_succeeds(
357 #[case] version: &str,
358 #[case] expected: MinimalVersion,
359 ) -> TestResult {
360 init_logger();
361
362 assert_eq!(
363 MinimalVersion::from_str(version),
364 Ok(expected),
365 "Expected valid parsing for MinimalVersion {version}"
366 );
367
368 Ok(())
369 }
370
371 #[rstest]
373 #[case::two_pkgrel(
374 "1:foo-1-1",
375 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
376 )]
377 #[case::two_epoch(
378 "1:1:foo-1",
379 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
380 )]
381 #[case::empty_string(
382 "",
383 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
384 )]
385 #[case::colon(
386 ":",
387 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
388 )]
389 #[case::full_with_epoch(
390 "1:1.0.0-1",
391 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
392 )]
393 #[case::full(
394 "1.0.0-1",
395 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
396 )]
397 #[case::no_pkgrel_dash_end(
398 "1.0.0-",
399 "invalid pkgver character\nexpected an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters, alpm-pkgver string"
400 )]
401 #[case::starts_with_dash(
402 "-1foo:1",
403 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
404 )]
405 #[case::ends_with_colon(
406 "1-foo:",
407 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
408 )]
409 #[case::ends_with_colon_number(
410 "1-foo:1",
411 "invalid package epoch\nexpected positive non-zero decimal integer, followed by a ':'"
412 )]
413 fn minimal_version_from_str_parse_error(#[case] version: &str, #[case] err_snippet: &str) {
414 init_logger();
415
416 let Err(Error::ParseError(err_msg)) = MinimalVersion::from_str(version) else {
417 panic!("parsing '{version}' as MinimalVersion did not fail as expected")
418 };
419 assert!(
420 err_msg.contains(err_snippet),
421 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
422 );
423 }
424
425 #[rstest]
428 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, Ok(MinimalVersion::from_str("1:1.0.0")?))]
429 #[case::minimal(Version::from_str("1.0.0")?, Ok(MinimalVersion::from_str("1.0.0")?))]
430 #[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")}))]
431 #[case::full(Version::from_str("1.0.0-1")?, Err(Error::InvalidComponent{component: "pkgrel", context: t!("error-context-convert-full-to-minimal")}))]
432 fn minimal_version_try_from_version(
433 #[case] version: Version,
434 #[case] expected: Result<MinimalVersion, Error>,
435 ) -> TestResult {
436 assert_eq!(MinimalVersion::try_from(&version), expected);
437 Ok(())
438 }
439
440 #[rstest]
443 #[case::minimal_with_epoch(Version::from_str("1:1.0.0")?, MinimalVersion::from_str("1:1.0.0")?)]
444 #[case::minimal(Version::from_str("1.0.0")?, MinimalVersion::from_str("1.0.0")?)]
445 fn version_from_minimal_version(
446 #[case] version: Version,
447 #[case] full_version: MinimalVersion,
448 ) -> TestResult {
449 assert_eq!(Version::from(&full_version), version);
450 Ok(())
451 }
452
453 #[rstest]
455 #[case::with_epoch("1:1.0.0")]
456 #[case::plain("1.0.0")]
457 fn minimal_version_to_string(#[case] input: &str) -> TestResult {
458 assert_eq!(format!("{}", MinimalVersion::from_str(input)?), input);
459 Ok(())
460 }
461
462 #[rstest]
467 #[case::minimal_equal("1.0.0", "1.0.0", Ordering::Equal)]
468 #[case::minimal_less("1.0.0", "2.0.0", Ordering::Less)]
469 #[case::minimal_greater("2.0.0", "1.0.0", Ordering::Greater)]
470 #[case::minimal_with_epoch_equal("1:1.0.0", "1:1.0.0", Ordering::Equal)]
471 #[case::minimal_with_epoch_less("1.0.0", "1:1.0.0", Ordering::Less)]
472 #[case::minimal_with_epoch_less("1:1.0.0", "2:1.0.0", Ordering::Less)]
473 #[case::minimal_with_epoch_greater("1:1.0.0", "1.0.0", Ordering::Greater)]
474 #[case::minimal_with_epoch_greater("2:1.0.0", "1:1.0.0", Ordering::Greater)]
475 fn minimal_version_comparison(
476 #[case] version_a: &str,
477 #[case] version_b: &str,
478 #[case] expected: Ordering,
479 ) -> TestResult {
480 let version_a = MinimalVersion::from_str(version_a)?;
481 let version_b = MinimalVersion::from_str(version_b)?;
482
483 let vercmp_result = match &expected {
485 Ordering::Equal => 0,
486 Ordering::Greater => 1,
487 Ordering::Less => -1,
488 };
489
490 let ordering = version_a.cmp(&version_b);
491 assert_eq!(
492 ordering, expected,
493 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
494 );
495
496 assert_eq!(version_a.vercmp(&version_b), vercmp_result);
497
498 #[cfg(feature = "compatibility_tests")]
500 {
501 let output = std::process::Command::new("vercmp")
502 .arg(version_a.to_string())
503 .arg(version_b.to_string())
504 .output()?;
505 let result = String::from_utf8_lossy(&output.stdout);
506 assert_eq!(result.trim(), vercmp_result.to_string());
507 }
508
509 let reverse_vercmp_result = match &expected {
511 Ordering::Equal => 0,
512 Ordering::Greater => -1,
513 Ordering::Less => 1,
514 };
515 let reverse_expected = match &expected {
516 Ordering::Equal => Ordering::Equal,
517 Ordering::Greater => Ordering::Less,
518 Ordering::Less => Ordering::Greater,
519 };
520
521 let reverse_ordering = version_b.cmp(&version_a);
522 assert_eq!(
523 reverse_ordering, reverse_expected,
524 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
525 );
526
527 assert_eq!(version_b.vercmp(&version_a), reverse_vercmp_result);
528
529 Ok(())
530 }
531}