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};
20use winnow::{
21 ModalResult,
22 Parser,
23 ascii::{dec_uint, digit1},
24 combinator::opt,
25 error::{ContextError, ErrMode, StrContext, StrContextValue},
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(input: &mut &str) -> ModalResult<Self> {
71 dec_uint
72 .context(StrContext::Label("package epoch"))
73 .context(StrContext::Expected(StrContextValue::Description(
74 "non-negative decimal integer",
75 )))
76 .map(Self)
77 .parse_next(input)
78 }
79
80 fn delimiter_error_context<'a, O, P>(
81 parser: P,
82 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
83 where
84 P: Parser<&'a str, O, ErrMode<ContextError>>,
85 {
86 parser
87 .context(StrContext::Label("package epoch"))
88 .context(StrContext::Expected(StrContextValue::Description(
89 "positive non-zero decimal integer",
90 )))
91 }
92}
93
94impl FromStr for Epoch {
95 type Err = Error;
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 Ok(Self::parser_until_eof.parse(s)?)
99 }
100}
101
102impl Display for Epoch {
103 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
104 write!(fmt, "{}", self.0)
105 }
106}
107
108#[derive(Clone, Debug, Eq, PartialEq)]
132#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
133pub struct PackageRelease {
134 pub major: usize,
136 pub minor: Option<usize>,
138}
139
140impl PackageRelease {
141 pub fn new(major: usize, minor: Option<usize>) -> Self {
153 PackageRelease { major, minor }
154 }
155}
156
157impl AlpmParser for PackageRelease {
158 fn parser(input: &mut &str) -> ModalResult<Self> {
164 let major = digit1
165 .try_map(FromStr::from_str)
166 .context(StrContext::Label("package release"))
167 .context(StrContext::Expected(StrContextValue::Description(
168 "positive decimal integer",
169 )))
170 .parse_next(input)?;
171
172 let minor = if opt('.').parse_next(input)?.is_some() {
174 let minor = digit1
175 .try_map(FromStr::from_str)
176 .context(StrContext::Label("package release"))
177 .context(StrContext::Expected(StrContextValue::Description(
178 "single '.' followed by positive decimal integer",
179 )))
180 .parse_next(input)?;
181
182 Some(minor)
183 } else {
184 None
185 };
186
187 Ok(Self { major, minor })
188 }
189
190 fn delimiter_error_context<'a, O, P>(
191 parser: P,
192 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
193 where
194 P: Parser<&'a str, O, ErrMode<ContextError>>,
195 {
196 parser
197 .context(StrContext::Label("package release"))
198 .context(StrContext::Expected(StrContextValue::Description(
199 "single '.' followed by positive decimal integer",
200 )))
201 }
202}
203
204impl FromStr for PackageRelease {
205 type Err = Error;
206 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 Ok(Self::parser_until_eof.parse(s)?)
215 }
216}
217
218impl Display for PackageRelease {
219 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
220 write!(fmt, "{}", self.major)?;
221 if let Some(minor) = self.minor {
222 write!(fmt, ".{minor}")?;
223 }
224 Ok(())
225 }
226}
227
228impl PartialOrd for PackageRelease {
229 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
230 Some(self.cmp(other))
231 }
232}
233
234impl Ord for PackageRelease {
235 fn cmp(&self, other: &Self) -> Ordering {
236 let major_order = self.major.cmp(&other.major);
237 if major_order != Ordering::Equal {
238 return major_order;
239 }
240
241 match (self.minor, other.minor) {
242 (None, None) => Ordering::Equal,
243 (None, Some(_)) => Ordering::Less,
244 (Some(_), None) => Ordering::Greater,
245 (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
246 }
247 }
248}
249
250#[derive(Clone, Debug, Eq)]
276#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
277pub struct PackageVersion(pub(crate) String);
278
279impl PackageVersion {
280 pub fn new(pkgver: String) -> Result<Self, Error> {
282 PackageVersion::from_str(pkgver.as_str())
283 }
284
285 pub fn inner(&self) -> &str {
287 &self.0
288 }
289
290 pub fn segments(&self) -> VersionSegments<'_> {
292 VersionSegments::new(&self.0)
293 }
294}
295
296impl AlpmParser for PackageVersion {
297 fn parser(input: &mut &str) -> ModalResult<Self> {
305 let allowed = |c: char| {
308 c.is_ascii() && ![':', '/', '-', '<', '>', '='].contains(&c) && !c.is_whitespace()
309 };
310
311 take_while(1.., allowed)
312 .context(StrContext::Label("alpm-pkgver character"))
313 .context(StrContext::Expected(StrContextValue::Description(
314 "an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace characters",
315 )))
316 .map(|s: &str| Self(s.to_string()))
317 .parse_next(input)
318 }
319
320 fn delimiter_error_context<'a, O, P>(
321 parser: P,
322 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
323 where
324 P: Parser<&'a str, O, ErrMode<ContextError>>,
325 {
326 parser.context(StrContext::Label("pkgver character"))
327 .context(StrContext::Expected(StrContextValue::Description(
328 "an ASCII character, except for ':', '/', '-', '<', '>', '=', or any whitespace character",
329 )))
330 }
331}
332
333impl FromStr for PackageVersion {
334 type Err = Error;
335 fn from_str(s: &str) -> Result<Self, Self::Err> {
337 Ok(Self::parser_until_eof.parse(s)?)
338 }
339}
340
341impl Display for PackageVersion {
342 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
343 write!(fmt, "{}", self.inner())
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use insta::assert_snapshot;
350 use rstest::rstest;
351
352 use super::*;
353 use crate::configure_insta;
354
355 #[rstest]
356 #[case("0", Ok(Epoch(0)))]
357 #[case("1", Ok(Epoch(1)))]
358 fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
359 assert_eq!(result, Epoch::from_str(version));
360 }
361
362 #[rstest]
363 #[case("-0", "expected non-negative decimal integer")]
364 #[case("z", "expected non-negative decimal integer")]
365 fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
366 let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
367 panic!("'{input}' erroneously parsed as Epoch")
368 };
369 assert!(
370 err_msg.contains(err_snippet),
371 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
372 );
373 }
374
375 #[rstest]
377 #[case("foo")]
378 #[case("1.0.0")]
379 #[case(".xd")]
381 fn valid_pkgver(#[case] pkgver: &str) {
382 let parsed = PackageVersion::new(pkgver.to_string());
383 assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
384 assert_eq!(
385 parsed.as_ref().unwrap().to_string(),
386 pkgver,
387 "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
388 parsed.unwrap(),
389 pkgver
390 );
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 Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
404 panic!("Expected pkgver {pkgver} to be invalid.")
405 };
406
407 let (test_name, _guard) = configure_insta();
408 assert_snapshot!(test_name, err_msg.to_string());
409 }
410
411 #[rstest]
413 #[case("0")]
414 #[case("1")]
415 #[case("10")]
416 #[case("1.0")]
417 #[case("10.5")]
418 #[case("0.1")]
419 fn valid_pkgrel(#[case] pkgrel: &str) {
420 let parsed = PackageRelease::from_str(pkgrel);
421 assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
422 assert_eq!(
423 parsed.as_ref().unwrap().to_string(),
424 pkgrel,
425 "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
426 parsed.unwrap(),
427 pkgrel
428 );
429 }
430
431 #[rstest]
433 #[case(".1")]
434 #[case("1.")]
435 #[case("1..1")]
436 #[case("-1")]
437 #[case("a")]
438 #[case("1.a")]
439 #[case("1.0.0")]
440 #[case("")]
441 fn invalid_pkgrel(#[case] pkgrel: &str) {
442 let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
443 panic!("'{pkgrel}' erroneously parsed as PackageRelease")
444 };
445
446 let (test_name, _guard) = configure_insta();
447 assert_snapshot!(test_name, err_msg.to_string());
448 }
449
450 #[rstest]
452 #[case("1", "1.0", Ordering::Less)]
453 #[case("1.0", "2", Ordering::Less)]
454 #[case("1", "1.1", Ordering::Less)]
455 #[case("1.0", "1.1", Ordering::Less)]
456 #[case("0", "1.1", Ordering::Less)]
457 #[case("1", "11", Ordering::Less)]
458 #[case("1", "1", Ordering::Equal)]
459 #[case("1.2", "1.2", Ordering::Equal)]
460 #[case("2.0", "2.0", Ordering::Equal)]
461 #[case("2", "1.0", Ordering::Greater)]
462 #[case("1.1", "1", Ordering::Greater)]
463 #[case("1.1", "1.0", Ordering::Greater)]
464 #[case("1.1", "0", Ordering::Greater)]
465 #[case("11", "1", Ordering::Greater)]
466 fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
467 let first = PackageRelease::from_str(first).unwrap();
468 let second = PackageRelease::from_str(second).unwrap();
469 assert_eq!(
470 first.cmp(&second),
471 order,
472 "{first} should be {order:?} to {second}"
473 );
474 }
475}