1use std::{
12 cmp::Ordering,
13 fmt::{Display, Formatter},
14 num::NonZeroUsize,
15 str::FromStr,
16};
17
18use alpm_parsers::iter_char_context;
19use serde::{Deserialize, Serialize};
20use winnow::{
21 ModalResult,
22 Parser,
23 ascii::{dec_uint, digit1},
24 combinator::{Repeat, cut_err, eof, opt, preceded, repeat, seq, terminated},
25 error::{StrContext, StrContextValue},
26 token::one_of,
27};
28
29#[cfg(doc)]
30use crate::Version;
31use crate::{Error, VersionSegments};
32
33#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
54pub struct Epoch(pub NonZeroUsize);
55
56impl Epoch {
57 pub fn new(epoch: NonZeroUsize) -> Self {
59 Epoch(epoch)
60 }
61
62 pub fn parser(input: &mut &str) -> ModalResult<Self> {
70 terminated(dec_uint, eof)
71 .verify_map(NonZeroUsize::new)
72 .context(StrContext::Label("package epoch"))
73 .context(StrContext::Expected(StrContextValue::Description(
74 "positive non-zero decimal integer",
75 )))
76 .map(Self)
77 .parse_next(input)
78 }
79}
80
81impl FromStr for Epoch {
82 type Err = Error;
83 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 Ok(Self::parser.parse(s)?)
86 }
87}
88
89impl Display for Epoch {
90 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
91 write!(fmt, "{}", self.0)
92 }
93}
94
95#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub struct PackageRelease {
120 pub major: usize,
122 pub minor: Option<usize>,
124}
125
126impl PackageRelease {
127 pub fn new(major: usize, minor: Option<usize>) -> Self {
139 PackageRelease { major, minor }
140 }
141
142 pub fn parser(input: &mut &str) -> ModalResult<Self> {
150 seq!(Self {
151 major: digit1.try_map(FromStr::from_str)
152 .context(StrContext::Label("package release"))
153 .context(StrContext::Expected(StrContextValue::Description(
154 "positive decimal integer",
155 ))),
156 minor: opt(preceded('.', cut_err(digit1.try_map(FromStr::from_str))))
157 .context(StrContext::Label("package release"))
158 .context(StrContext::Expected(StrContextValue::Description(
159 "single '.' followed by positive decimal integer",
160 ))),
161 _: eof.context(StrContext::Expected(StrContextValue::Description(
162 "end of package release value",
163 ))),
164 })
165 .parse_next(input)
166 }
167}
168
169impl FromStr for PackageRelease {
170 type Err = Error;
171 fn from_str(s: &str) -> Result<Self, Self::Err> {
179 Ok(Self::parser.parse(s)?)
180 }
181}
182
183impl Display for PackageRelease {
184 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
185 write!(fmt, "{}", self.major)?;
186 if let Some(minor) = self.minor {
187 write!(fmt, ".{minor}")?;
188 }
189 Ok(())
190 }
191}
192
193impl PartialOrd for PackageRelease {
194 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
195 Some(self.cmp(other))
196 }
197}
198
199impl Ord for PackageRelease {
200 fn cmp(&self, other: &Self) -> Ordering {
201 let major_order = self.major.cmp(&other.major);
202 if major_order != Ordering::Equal {
203 return major_order;
204 }
205
206 match (self.minor, other.minor) {
207 (None, None) => Ordering::Equal,
208 (None, Some(_)) => Ordering::Less,
209 (Some(_), None) => Ordering::Greater,
210 (Some(minor), Some(other_minor)) => minor.cmp(&other_minor),
211 }
212 }
213}
214
215#[derive(Clone, Debug, Deserialize, Eq, Serialize)]
242pub struct PackageVersion(pub(crate) String);
243
244impl PackageVersion {
245 pub fn new(pkgver: String) -> Result<Self, Error> {
247 PackageVersion::from_str(pkgver.as_str())
248 }
249
250 pub fn inner(&self) -> &str {
252 &self.0
253 }
254
255 pub fn segments(&self) -> VersionSegments {
257 VersionSegments::new(&self.0)
258 }
259
260 pub fn parser(input: &mut &str) -> ModalResult<Self> {
268 let alnum = |c: char| c.is_ascii_alphanumeric();
269
270 let first_character = one_of(alnum)
271 .context(StrContext::Label("first pkgver character"))
272 .context(StrContext::Expected(StrContextValue::Description(
273 "ASCII alphanumeric character",
274 )));
275 let special_tail_character = ['_', '+', '.'];
276 let tail_character = one_of((alnum, special_tail_character));
277
278 let tail: Repeat<_, _, _, (), _> = repeat(0.., tail_character);
281
282 (
283 first_character,
284 tail,
285 eof.context(StrContext::Label("pkgver character"))
286 .context(StrContext::Expected(StrContextValue::Description(
287 "ASCII alphanumeric character",
288 )))
289 .context_with(iter_char_context!(special_tail_character)),
290 )
291 .take()
292 .map(|s: &str| Self(s.to_string()))
293 .parse_next(input)
294 }
295}
296
297impl FromStr for PackageVersion {
298 type Err = Error;
299 fn from_str(s: &str) -> Result<Self, Self::Err> {
301 Ok(Self::parser.parse(s)?)
302 }
303}
304
305impl Display for PackageVersion {
306 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
307 write!(fmt, "{}", self.inner())
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use rstest::rstest;
314
315 use super::*;
316
317 #[rstest]
318 #[case("1", Ok(Epoch(NonZeroUsize::new(1).unwrap())))]
319 fn epoch(#[case] version: &str, #[case] result: Result<Epoch, Error>) {
320 assert_eq!(result, Epoch::from_str(version));
321 }
322
323 #[rstest]
324 #[case("0", "expected positive non-zero decimal integer")]
325 #[case("-0", "expected positive non-zero decimal integer")]
326 #[case("z", "expected positive non-zero decimal integer")]
327 fn epoch_parse_failure(#[case] input: &str, #[case] err_snippet: &str) {
328 let Err(Error::ParseError(err_msg)) = Epoch::from_str(input) else {
329 panic!("'{input}' erroneously parsed as Epoch")
330 };
331 assert!(
332 err_msg.contains(err_snippet),
333 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
334 );
335 }
336
337 #[rstest]
339 #[case("foo")]
340 #[case("1.0.0")]
341 fn valid_pkgver(#[case] pkgver: &str) {
342 let parsed = PackageVersion::new(pkgver.to_string());
343 assert!(parsed.is_ok(), "Expected pkgver {pkgver} to be valid.");
344 assert_eq!(
345 parsed.as_ref().unwrap().to_string(),
346 pkgver,
347 "Expected parsed PackageVersion representation '{}' to be identical to input '{}'",
348 parsed.unwrap(),
349 pkgver
350 );
351 }
352
353 #[rstest]
355 #[case("1:foo", "invalid pkgver character")]
356 #[case("foo-1", "invalid pkgver character")]
357 #[case("foo,1", "invalid pkgver character")]
358 #[case(".foo", "invalid first pkgver character")]
359 #[case("_foo", "invalid first pkgver character")]
360 #[case("ß", "invalid first pkgver character")]
362 #[case("1.ß", "invalid pkgver character")]
363 fn invalid_pkgver(#[case] pkgver: &str, #[case] err_snippet: &str) {
364 let Err(Error::ParseError(err_msg)) = PackageVersion::new(pkgver.to_string()) else {
365 panic!("Expected pkgver {pkgver} to be invalid.")
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("0")]
376 #[case("1")]
377 #[case("10")]
378 #[case("1.0")]
379 #[case("10.5")]
380 #[case("0.1")]
381 fn valid_pkgrel(#[case] pkgrel: &str) {
382 let parsed = PackageRelease::from_str(pkgrel);
383 assert!(parsed.is_ok(), "Expected pkgrel {pkgrel} to be valid.");
384 assert_eq!(
385 parsed.as_ref().unwrap().to_string(),
386 pkgrel,
387 "Expected parsed PackageRelease representation '{}' to be identical to input '{}'",
388 parsed.unwrap(),
389 pkgrel
390 );
391 }
392
393 #[rstest]
395 #[case(".1", "expected positive decimal integer")]
396 #[case("1.", "expected single '.' followed by positive decimal integer")]
397 #[case("1..1", "expected single '.' followed by positive decimal integer")]
398 #[case("-1", "expected positive decimal integer")]
399 #[case("a", "expected positive decimal integer")]
400 #[case("1.a", "expected single '.' followed by positive decimal integer")]
401 #[case("1.0.0", "expected end of package release")]
402 #[case("", "expected positive decimal integer")]
403 fn invalid_pkgrel(#[case] pkgrel: &str, #[case] err_snippet: &str) {
404 let Err(Error::ParseError(err_msg)) = PackageRelease::from_str(pkgrel) else {
405 panic!("'{pkgrel}' erroneously parsed as PackageRelease")
406 };
407 assert!(
408 err_msg.contains(err_snippet),
409 "Error:\n=====\n{err_msg}\n=====\nshould contain snippet:\n\n{err_snippet}"
410 );
411 }
412
413 #[rstest]
415 #[case("1", "1.0", Ordering::Less)]
416 #[case("1.0", "2", Ordering::Less)]
417 #[case("1", "1.1", Ordering::Less)]
418 #[case("1.0", "1.1", Ordering::Less)]
419 #[case("0", "1.1", Ordering::Less)]
420 #[case("1", "11", Ordering::Less)]
421 #[case("1", "1", Ordering::Equal)]
422 #[case("1.2", "1.2", Ordering::Equal)]
423 #[case("2.0", "2.0", Ordering::Equal)]
424 #[case("2", "1.0", Ordering::Greater)]
425 #[case("1.1", "1", Ordering::Greater)]
426 #[case("1.1", "1.0", Ordering::Greater)]
427 #[case("1.1", "0", Ordering::Greater)]
428 #[case("11", "1", Ordering::Greater)]
429 fn pkgrel_cmp(#[case] first: &str, #[case] second: &str, #[case] order: Ordering) {
430 let first = PackageRelease::from_str(first).unwrap();
431 let second = PackageRelease::from_str(second).unwrap();
432 assert_eq!(
433 first.cmp(&second),
434 order,
435 "{first} should be {order:?} to {second}"
436 );
437 }
438}