1use std::{
12 cmp::Ordering,
13 fmt::{Display, Formatter},
14 str::FromStr,
15};
16
17use alpm_parsers::traits::{AlpmParser, ParserUntil};
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20#[cfg(feature = "serde")]
21use serde_with::DeserializeFromStr;
22use winnow::{
23 ModalResult,
24 Parser,
25 ascii::{dec_uint, digit1},
26 combinator::opt,
27 error::{ContextError, ErrMode, StrContext, StrContextValue},
28 token::take_while,
29};
30
31#[cfg(doc)]
32use crate::Version;
33use crate::{Error, VersionSegments};
34
35#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
56#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
57pub struct Epoch(pub usize);
58
59impl Epoch {
60 pub fn new(epoch: usize) -> Self {
62 Epoch(epoch)
63 }
64}
65
66impl AlpmParser for Epoch {
67 fn parser(input: &mut &str) -> ModalResult<Self> {
73 dec_uint
74 .context(StrContext::Label("package epoch"))
75 .context(StrContext::Expected(StrContextValue::Description(
76 "non-negative decimal integer",
77 )))
78 .map(Self)
79 .parse_next(input)
80 }
81
82 fn delimiter_error_context<'a, O, P>(
83 parser: P,
84 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
85 where
86 P: Parser<&'a str, O, ErrMode<ContextError>>,
87 {
88 parser
89 .context(StrContext::Label("package epoch"))
90 .context(StrContext::Expected(StrContextValue::Description(
91 "positive non-zero decimal integer",
92 )))
93 }
94}
95
96impl FromStr for Epoch {
97 type Err = Error;
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
100 Ok(Self::parser_until_eof.parse(s)?)
101 }
102}
103
104impl Display for Epoch {
105 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
106 write!(fmt, "{}", self.0)
107 }
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
134#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
135pub struct PackageRelease {
136 pub major: usize,
138 pub minor: Option<usize>,
140}
141
142impl PackageRelease {
143 pub fn new(major: usize, minor: Option<usize>) -> Self {
155 PackageRelease { major, minor }
156 }
157}
158
159impl AlpmParser for PackageRelease {
160 fn parser(input: &mut &str) -> ModalResult<Self> {
166 let major = digit1
167 .try_map(FromStr::from_str)
168 .context(StrContext::Label("package release"))
169 .context(StrContext::Expected(StrContextValue::Description(
170 "positive decimal integer",
171 )))
172 .parse_next(input)?;
173
174 let minor = if opt('.').parse_next(input)?.is_some() {
176 let minor = digit1
177 .try_map(FromStr::from_str)
178 .context(StrContext::Label("package release"))
179 .context(StrContext::Expected(StrContextValue::Description(
180 "single '.' followed by positive decimal integer",
181 )))
182 .parse_next(input)?;
183
184 Some(minor)
185 } else {
186 None
187 };
188
189 Ok(Self { major, minor })
190 }
191
192 fn delimiter_error_context<'a, O, P>(
193 parser: P,
194 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
195 where
196 P: Parser<&'a str, O, ErrMode<ContextError>>,
197 {
198 parser
199 .context(StrContext::Label("package release"))
200 .context(StrContext::Expected(StrContextValue::Description(
201 "single '.' followed by positive decimal integer",
202 )))
203 }
204}
205
206impl FromStr for PackageRelease {
207 type Err = Error;
208 fn from_str(s: &str) -> Result<Self, Self::Err> {
216 Ok(Self::parser_until_eof.parse(s)?)
217 }
218}
219
220impl Display for PackageRelease {
221 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
222 write!(fmt, "{}", self.major)?;
223 if let Some(minor) = self.minor {
224 write!(fmt, ".{minor}")?;
225 }
226 Ok(())
227 }
228}
229
230impl PartialOrd for PackageRelease {
231 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
232 Some(self.cmp(other))
233 }
234}
235
236impl Ord for PackageRelease {
237 fn cmp(&self, other: &Self) -> Ordering {
238 let major_order = self.major.cmp(&other.major);
239 if major_order != Ordering::Equal {
240 return major_order;
241 }
242
243 match (self.minor, other.minor) {
244 (None, None) => Ordering::Equal,
245 (None, Some(_)) => Ordering::Less,
246 (Some(_), None) => Ordering::Greater,
247 (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
248 }
249 }
250}
251
252#[derive(Clone, Debug, Eq)]
278#[cfg_attr(feature = "serde", derive(DeserializeFromStr, Serialize))]
279pub struct PackageVersion(pub(crate) String);
280
281impl PackageVersion {
282 pub fn new(pkgver: String) -> Result<Self, Error> {
284 PackageVersion::from_str(pkgver.as_str())
285 }
286
287 pub fn inner(&self) -> &str {
289 &self.0
290 }
291
292 pub fn segments(&self) -> VersionSegments<'_> {
294 VersionSegments::new(&self.0)
295 }
296}
297
298impl AlpmParser for PackageVersion {
299 fn parser(input: &mut &str) -> ModalResult<Self> {
307 let allowed = |c: char| {
310 c.is_ascii() && ![':', '/', '-', '<', '>', '='].contains(&c) && !c.is_whitespace()
311 };
312
313 take_while(1.., allowed)
314 .context(StrContext::Label("alpm-pkgver character"))
315 .context(StrContext::Expected(StrContextValue::Description(
316 "an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters",
317 )))
318 .map(|s: &str| Self(s.to_string()))
319 .parse_next(input)
320 }
321
322 fn delimiter_error_context<'a, O, P>(
323 parser: P,
324 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
325 where
326 P: Parser<&'a str, O, ErrMode<ContextError>>,
327 {
328 parser.context(StrContext::Label("pkgver character"))
329 .context(StrContext::Expected(StrContextValue::Description(
330 "an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace character",
331 )))
332 }
333}
334
335impl FromStr for PackageVersion {
336 type Err = Error;
337 fn from_str(s: &str) -> Result<Self, Self::Err> {
339 Ok(Self::parser_until_eof.parse(s)?)
340 }
341}
342
343impl Display for PackageVersion {
344 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
345 write!(fmt, "{}", self.inner())
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use insta::assert_snapshot;
352 use rstest::rstest;
353
354 use super::*;
355 use crate::configure_insta;
356
357 #[rstest]
358 #[case("0", Ok(Epoch(0)))]
359 #[case("1", Ok(Epoch(1)))]
360 fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
361 assert_eq!(result, Epoch::from_str(version));
362 }
363
364 #[rstest]
365 #[case("-0", "expected non-negative decimal integer")]
366 #[case("z", "expected non-negative decimal integer")]
367 fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
368 let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
369 panic!("'{input}' erroneously parsed as Epoch")
370 };
371 assert!(
372 err_msg.contains(err_snippet),
373 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
374 );
375 }
376
377 #[rstest]
379 #[case("foo")]
380 #[case("1.0.0")]
381 #[case(".xd")]
383 fn valid_pkgver(#[case] pkgver: &str) {
384 let parsed = PackageVersion::new(pkgver.to_string());
385 assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
386 assert_eq!(
387 parsed.as_ref().unwrap().to_string(),
388 pkgver,
389 "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
390 parsed.unwrap(),
391 pkgver
392 );
393 }
394
395 #[rstest]
397 #[case("1:foo")]
398 #[case("foo-1")]
399 #[case("foo/1")]
400 #[case("ß")]
402 #[case("1.ß")]
403 #[case("")]
404 fn invalid_pkgver(#[case] pkgver: &str) {
405 let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
406 panic!("Expected pkgver {pkgver} to be invalid.")
407 };
408
409 let (test_name, _guard) = configure_insta();
410 assert_snapshot!(test_name, err_msg.to_string());
411 }
412
413 #[cfg(feature = "serde")]
415 #[rstest]
416 #[case("1:foo")]
417 #[case("foo-1")]
418 fn package_version_deserialize_error(#[case] input: &str) {
419 let Err(serde_json::Error { .. }) =
420 serde_json::from_str::<PackageVersion>(&format!("\"{input}\""))
421 else {
422 panic!("'{input}' erroneously deserialized as a PackageVersion")
423 };
424 }
425
426 #[rstest]
428 #[case("0")]
429 #[case("1")]
430 #[case("10")]
431 #[case("1.0")]
432 #[case("10.5")]
433 #[case("0.1")]
434 fn valid_pkgrel(#[case] pkgrel: &str) {
435 let parsed = PackageRelease::from_str(pkgrel);
436 assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
437 assert_eq!(
438 parsed.as_ref().unwrap().to_string(),
439 pkgrel,
440 "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
441 parsed.unwrap(),
442 pkgrel
443 );
444 }
445
446 #[rstest]
448 #[case(".1")]
449 #[case("1.")]
450 #[case("1..1")]
451 #[case("-1")]
452 #[case("a")]
453 #[case("1.a")]
454 #[case("1.0.0")]
455 #[case("")]
456 fn invalid_pkgrel(#[case] pkgrel: &str) {
457 let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
458 panic!("'{pkgrel}' erroneously parsed as PackageRelease")
459 };
460
461 let (test_name, _guard) = configure_insta();
462 assert_snapshot!(test_name, err_msg.to_string());
463 }
464
465 #[rstest]
467 #[case("1", "1.0", Ordering::Less)]
468 #[case("1.0", "2", Ordering::Less)]
469 #[case("1", "1.1", Ordering::Less)]
470 #[case("1.0", "1.1", Ordering::Less)]
471 #[case("0", "1.1", Ordering::Less)]
472 #[case("1", "11", Ordering::Less)]
473 #[case("1", "1", Ordering::Equal)]
474 #[case("1.2", "1.2", Ordering::Equal)]
475 #[case("2.0", "2.0", Ordering::Equal)]
476 #[case("2", "1.0", Ordering::Greater)]
477 #[case("1.1", "1", Ordering::Greater)]
478 #[case("1.1", "1.0", Ordering::Greater)]
479 #[case("1.1", "0", Ordering::Greater)]
480 #[case("11", "1", Ordering::Greater)]
481 fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
482 let first = PackageRelease::from_str(first).unwrap();
483 let second = PackageRelease::from_str(second).unwrap();
484 assert_eq!(
485 first.cmp(&second),
486 order,
487 "{first} should be {order:?} to {second}"
488 );
489 }
490}