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