1use std::{
12 cmp::Ordering,
13 fmt::{Display, Formatter},
14 str::FromStr,
15};
16
17use alpm_parsers::prelude::*;
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20#[cfg(feature = "serde")]
21use serde_with::DeserializeFromStr;
22use winnow::{
23 ascii::{dec_uint, digit1},
24 combinator::opt,
25 error::ErrMode,
26 token::take_while,
27};
28
29#[cfg(doc)]
30use crate::Version;
31use crate::{Error, VersionSegments};
32
33#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
54#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
55pub struct Epoch(pub usize);
56
57impl Epoch {
58 pub fn new(epoch: usize) -> Self {
60 Epoch(epoch)
61 }
62}
63
64impl AlpmParser for Epoch {
65 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
71 dec_uint
72 .expected_text("non-negative decimal integer")
73 .map(Self)
74 .layer("alpm-epoch")
75 .parse_next(input)
76 }
77
78 fn delimiter_error_context<'a, O, P>(
79 parser: P,
80 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
81 where
82 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
83 {
84 parser
85 .expected_text("positive non-zero decimal integer")
86 .layer("alpm-epoch")
87 }
88}
89
90impl FromStr for Epoch {
91 type Err = Error;
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 Ok(Self::parser_until_eof.parse(Input::new(s))?)
95 }
96}
97
98impl Display for Epoch {
99 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
100 write!(fmt, "{}", self.0)
101 }
102}
103
104#[derive(Clone, Debug, Eq, PartialEq)]
128#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
129pub struct PackageRelease {
130 pub major: usize,
132 pub minor: Option<usize>,
134}
135
136impl PackageRelease {
137 pub fn new(major: usize, minor: Option<usize>) -> Self {
149 PackageRelease { major, minor }
150 }
151}
152
153impl AlpmParser for PackageRelease {
154 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
160 let parser = move |input: &mut Input<'a>| -> PResult<'a, Self> {
161 let major = digit1
162 .try_map(FromStr::from_str)
163 .label("package release")
164 .expected_text("positive decimal integer")
165 .parse_next(input)?;
166
167 let minor = if opt('.').parse_next(input)?.is_some() {
169 let minor = digit1
170 .try_map(FromStr::from_str)
171 .label("package release")
172 .expected_text("single '.' followed by positive decimal integer")
173 .parse_next(input)?;
174
175 Some(minor)
176 } else {
177 None
178 };
179
180 Ok(Self { major, minor })
181 };
182
183 parser.layer("alpm-pkgrel").parse_next(input)
184 }
185
186 fn delimiter_error_context<'a, O, P>(
187 parser: P,
188 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
189 where
190 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
191 {
192 parser
193 .label("package release")
194 .expected_text("single '.' followed by positive decimal integer")
195 .layer("alpm-pkgrel")
196 }
197}
198
199impl FromStr for PackageRelease {
200 type Err = Error;
201 fn from_str(s: &str) -> Result<Self, Self::Err> {
209 Ok(Self::parser_until_eof.parse(Input::new(s))?)
210 }
211}
212
213impl Display for PackageRelease {
214 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
215 write!(fmt, "{}", self.major)?;
216 if let Some(minor) = self.minor {
217 write!(fmt, ".{minor}")?;
218 }
219 Ok(())
220 }
221}
222
223impl PartialOrd for PackageRelease {
224 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225 Some(self.cmp(other))
226 }
227}
228
229impl Ord for PackageRelease {
230 fn cmp(&self, other: &Self) -> Ordering {
231 let major_order = self.major.cmp(&other.major);
232 if major_order != Ordering::Equal {
233 return major_order;
234 }
235
236 match (self.minor, other.minor) {
237 (None, None) => Ordering::Equal,
238 (None, Some(_)) => Ordering::Less,
239 (Some(_), None) => Ordering::Greater,
240 (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
241 }
242 }
243}
244
245#[derive(Clone, Debug, Eq)]
271#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
272pub struct PackageVersion(pub(crate) String);
273
274impl PackageVersion {
275 pub fn new(pkgver: String) -> Result<Self, Error> {
277 PackageVersion::from_str(pkgver.as_str())
278 }
279
280 pub fn segments(&self) -> VersionSegments<'_> {
282 VersionSegments::new(&self.0)
283 }
284}
285
286impl AlpmParser for PackageVersion {
287 fn parser<'a>(input: &mut Input<'a>) -> PResult<'a, Self> {
295 let allowed = |c: char| {
298 c.is_ascii() && ![':', '/', '-', '<', '>', '='].contains(&c) && !c.is_whitespace()
299 };
300
301 take_while(1.., allowed)
302 .label("character")
303 .expected_text("an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters")
304 .layer("alpm-pkgver")
305 .map(|s: &str| Self(s.to_string()))
306 .parse_next(input)
307 }
308
309 fn delimiter_error_context<'a, O, P>(
310 parser: P,
311 ) -> impl Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>
312 where
313 P: Parser<Input<'a>, O, ErrMode<ParseStack<'a>>>,
314 {
315 parser.label("character")
316 .expected_text("an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace character")
317 .layer("alpm-pkgver")
318 }
319}
320
321impl AsRef<str> for PackageVersion {
322 fn as_ref(&self) -> &str {
324 &self.0
325 }
326}
327
328impl FromStr for PackageVersion {
329 type Err = Error;
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
332 Ok(Self::parser_until_eof.parse(Input::new(s))?)
333 }
334}
335
336impl Display for PackageVersion {
337 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
338 write!(fmt, "{}", self.as_ref())
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use insta::assert_snapshot;
345 use rstest::rstest;
346 use testresult::TestResult;
347
348 use super::*;
349 use crate::configure_insta;
350
351 #[rstest]
352 #[case("0", Epoch(0))]
353 #[case("1", Epoch(1))]
354 fn epoch(#[case] version: &str, #[case] expected: Epoch) -> TestResult {
355 assert_eq!(expected, Epoch::from_str(version)?);
356
357 Ok(())
358 }
359
360 #[rstest]
361 #[case("-0", "expected non-negative decimal integer")]
362 #[case("z", "expected non-negative decimal integer")]
363 fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
364 let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
365 panic!("'{input}' erroneously parsed as Epoch")
366 };
367 assert!(
368 err_msg.contains(err_snippet),
369 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
370 );
371 }
372
373 #[rstest]
375 #[case("foo")]
376 #[case("1.0.0")]
377 #[case(".xd")]
379 fn valid_pkgver(#[case] pkgver: &str) -> TestResult {
380 let parsed = PackageVersion::new(pkgver.to_string());
381 assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
382 assert_eq!(
383 parsed.as_ref()?.to_string(),
384 pkgver,
385 "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
386 parsed?,
387 pkgver
388 );
389
390 Ok(())
391 }
392
393 #[rstest]
395 #[case("1:foo")]
396 #[case("foo-1")]
397 #[case("foo/1")]
398 #[case("ß")]
400 #[case("1.ß")]
401 #[case("")]
402 fn invalid_pkgver(#[case] pkgver: &str) {
403 let (test_name, _guard) = configure_insta();
404
405 let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
406 panic!("Expected pkgver {pkgver} to be invalid.")
407 };
408
409 assert_snapshot!(test_name, err_msg.to_string());
410 }
411
412 #[cfg(feature = "serde")]
414 #[rstest]
415 #[case("1:foo")]
416 #[case("foo-1")]
417 fn package_version_deserialize_error(#[case] input: &str) {
418 let Err(serde_json::Error { .. }) =
419 serde_json::from_str::<PackageVersion>(&format!("\"{input}\""))
420 else {
421 panic!("'{input}' erroneously deserialized as a PackageVersion")
422 };
423 }
424
425 #[rstest]
427 #[case("0")]
428 #[case("1")]
429 #[case("10")]
430 #[case("1.0")]
431 #[case("10.5")]
432 #[case("0.1")]
433 fn valid_pkgrel(#[case] pkgrel: &str) -> TestResult {
434 let parsed = PackageRelease::from_str(pkgrel);
435 assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
436 assert_eq!(
437 parsed.as_ref()?.to_string(),
438 pkgrel,
439 "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
440 parsed?,
441 pkgrel
442 );
443
444 Ok(())
445 }
446
447 #[rstest]
449 #[case(".1")]
450 #[case("1.")]
451 #[case("1..1")]
452 #[case("-1")]
453 #[case("a")]
454 #[case("1.a")]
455 #[case("1.0.0")]
456 #[case("")]
457 fn invalid_pkgrel(#[case] pkgrel: &str) {
458 let (test_name, _guard) = configure_insta();
459
460 let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
461 panic!("'{pkgrel}' erroneously parsed as PackageRelease")
462 };
463
464 assert_snapshot!(test_name, err_msg.to_string());
465 }
466
467 #[rstest]
469 #[case("1", "1.0", Ordering::Less)]
470 #[case("1.0", "2", Ordering::Less)]
471 #[case("1", "1.1", Ordering::Less)]
472 #[case("1.0", "1.1", Ordering::Less)]
473 #[case("0", "1.1", Ordering::Less)]
474 #[case("1", "11", Ordering::Less)]
475 #[case("1", "1", Ordering::Equal)]
476 #[case("1.2", "1.2", Ordering::Equal)]
477 #[case("2.0", "2.0", Ordering::Equal)]
478 #[case("2", "1.0", Ordering::Greater)]
479 #[case("1.1", "1", Ordering::Greater)]
480 #[case("1.1", "1.0", Ordering::Greater)]
481 #[case("1.1", "0", Ordering::Greater)]
482 #[case("11", "1", Ordering::Greater)]
483 fn pkgrel_cmp(
484 #[case] first: &str,
485 #[case] second: &str,
486 #[case] order: Ordering,
487 ) -> TestResult {
488 let first = PackageRelease::from_str(first)?;
489 let second = PackageRelease::from_str(second)?;
490 assert_eq!(
491 first.cmp(&second),
492 order,
493 "{first} should be {order:?} to {second}"
494 );
495
496 Ok(())
497 }
498}