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