1use std::{
4 cmp::Ordering,
5 fmt::{Display, Formatter},
6 str::FromStr,
7};
8
9use serde::{Deserialize, Serialize};
10use winnow::{
11 ModalResult,
12 Parser,
13 combinator::{cut_err, eof, opt, preceded, seq, terminated},
14 error::{StrContext, StrContextValue},
15 token::take_till,
16};
17
18use crate::{Epoch, Error, PackageRelease, PackageVersion};
19#[cfg(doc)]
20use crate::{FullVersion, MinimalVersion};
21
22#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct Version {
52 pub pkgver: PackageVersion,
54 pub epoch: Option<Epoch>,
56 pub pkgrel: Option<PackageRelease>,
58}
59
60impl Version {
61 pub fn new(
63 pkgver: PackageVersion,
64 epoch: Option<Epoch>,
65 pkgrel: Option<PackageRelease>,
66 ) -> Self {
67 Version {
68 pkgver,
69 epoch,
70 pkgrel,
71 }
72 }
73
74 pub fn vercmp(a: &Version, b: &Version) -> i8 {
106 match a.cmp(b) {
107 Ordering::Less => -1,
108 Ordering::Equal => 0,
109 Ordering::Greater => 1,
110 }
111 }
112
113 pub fn parser(input: &mut &str) -> ModalResult<Self> {
121 let mut epoch = opt(terminated(take_till(1.., ':'), ':').and_then(
122 cut_err(Epoch::parser),
124 ))
125 .context(StrContext::Expected(StrContextValue::Description(
126 "followed by a ':'",
127 )));
128
129 seq!(Self {
130 epoch: epoch,
131 pkgver: take_till(1.., '-')
132 .context(StrContext::Expected(StrContextValue::Description("pkgver string")))
134 .and_then(PackageVersion::parser),
135 pkgrel: opt(preceded('-', cut_err(PackageRelease::parser))),
136 _: eof.context(StrContext::Expected(StrContextValue::Description("end of version string"))),
137 })
138 .parse_next(input)
139 }
140}
141
142impl FromStr for Version {
143 type Err = Error;
144 fn from_str(s: &str) -> Result<Version, Self::Err> {
152 Ok(Self::parser.parse(s)?)
153 }
154}
155
156impl Display for Version {
157 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
158 if let Some(epoch) = self.epoch {
159 write!(fmt, "{epoch}:")?;
160 }
161
162 write!(fmt, "{}", self.pkgver)?;
163
164 if let Some(pkgrel) = &self.pkgrel {
165 write!(fmt, "-{pkgrel}")?;
166 }
167
168 Ok(())
169 }
170}
171
172impl Ord for Version {
173 fn cmp(&self, other: &Self) -> Ordering {
174 match (self.epoch, other.epoch) {
175 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
176 return self_epoch.cmp(&other_epoch);
177 }
178 (Some(_), None) => return Ordering::Greater,
179 (None, Some(_)) => return Ordering::Less,
180 (_, _) => {}
181 }
182
183 let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
184 if pkgver_cmp.is_ne() {
185 return pkgver_cmp;
186 }
187
188 self.pkgrel.cmp(&other.pkgrel)
189 }
190}
191
192impl PartialOrd for Version {
193 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
194 Some(self.cmp(other))
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use std::num::NonZeroUsize;
201
202 use insta::assert_snapshot;
203 use rstest::rstest;
204
205 use super::*;
206 use crate::configure_insta;
207
208 #[rstest]
210 #[case(
211 "foo",
212 Version {
213 epoch: None,
214 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
215 pkgrel: None
216 },
217 )]
218 #[case(
219 "1:foo-1",
220 Version {
221 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
222 epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
223 pkgrel: Some(PackageRelease::new(1, None))
224 },
225 )]
226 #[case(
227 "1:foo",
228 Version {
229 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
230 epoch: Some(Epoch::new(NonZeroUsize::new(1).unwrap())),
231 pkgrel: None,
232 },
233 )]
234 #[case(
235 "foo-1",
236 Version {
237 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
238 epoch: None,
239 pkgrel: Some(PackageRelease::new(1, None))
240 }
241 )]
242 #[case(
244 ".-1",
245 Version {
246 pkgver: PackageVersion::new(".".to_string()).unwrap(),
247 epoch: None,
248 pkgrel: Some(PackageRelease::new(1, None))
249 }
250 )]
251 fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
252 assert_eq!(
253 Version::from_str(version),
254 Ok(expected),
255 "Expected valid parsing for version {version}"
256 )
257 }
258
259 #[rstest]
261 #[case::two_pkgrel("1:foo-1-1")]
262 #[case::two_epoch("1:1:foo-1")]
263 #[case::no_version("")]
264 #[case::no_version(":")]
265 #[case::invalid_integer("-1foo:1")]
266 #[case::invalid_integer("1-foo:1")]
267 fn parse_error_in_version_from_string(#[case] version: &str) {
268 let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
269 panic!("parsing '{version}' did not fail as expected")
270 };
271
272 let (test_name, _guard) = configure_insta();
273 assert_snapshot!(test_name, err_msg.to_string());
274 }
275
276 #[rstest]
278 #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
279 #[case(Version::from_str("1-1").unwrap(), "1-1")]
280 #[case(Version::from_str("1").unwrap(), "1")]
281 #[case(Version::from_str("1:1").unwrap(), "1:1")]
282 fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
283 assert_eq!(format!("{version}"), to_str);
284 }
285
286 #[rstest]
287 #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
289 #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
290 #[case(
291 Version::from_str("20220102"),
292 Version::from_str("20220202"),
293 Ordering::Less
294 )]
295 #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
297 #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
298 #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
299 #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
300 #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
301 #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
303 #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
304 #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
305 #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
306 #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
307 #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
308 #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
309 #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
311 #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
312 #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
313 #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
314 #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
315 #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
316 #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
317 #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
318 #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
319 #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
320 #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
321 #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
322 #[case(
323 Version::from_str("1.alpha0.0"),
324 Version::from_str("1.alpha.0"),
325 Ordering::Less
326 )]
327 #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
329 #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
331 #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
332 #[case(
333 Version::from_str("1.0.alpha.0"),
334 Version::from_str("1.0."),
335 Ordering::Less
336 )]
337 #[case(
338 Version::from_str("1.a001a.1"),
339 Version::from_str("1.a1a.1"),
340 Ordering::Equal
341 )]
342 fn version_cmp(
343 #[case] version_a: Result<Version, Error>,
344 #[case] version_b: Result<Version, Error>,
345 #[case] expected: Ordering,
346 ) {
347 let version_a = version_a.unwrap();
349 let version_b = version_b.unwrap();
350
351 let vercmp_result = match &expected {
353 Ordering::Equal => 0,
354 Ordering::Greater => 1,
355 Ordering::Less => -1,
356 };
357
358 let ordering = version_a.cmp(&version_b);
359 assert_eq!(
360 ordering, expected,
361 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
362 );
363
364 assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
365
366 #[cfg(feature = "compatibility_tests")]
368 {
369 let output = std::process::Command::new("vercmp")
370 .arg(version_a.to_string())
371 .arg(version_b.to_string())
372 .output()
373 .unwrap();
374 let result = String::from_utf8_lossy(&output.stdout);
375 assert_eq!(result.trim(), vercmp_result.to_string());
376 }
377
378 let reverse_vercmp_result = match &expected {
380 Ordering::Equal => 0,
381 Ordering::Greater => -1,
382 Ordering::Less => 1,
383 };
384 let reverse_expected = match &expected {
385 Ordering::Equal => Ordering::Equal,
386 Ordering::Greater => Ordering::Less,
387 Ordering::Less => Ordering::Greater,
388 };
389
390 let reverse_ordering = version_b.cmp(&version_a);
391 assert_eq!(
392 reverse_ordering, reverse_expected,
393 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
394 );
395
396 assert_eq!(
397 Version::vercmp(&version_b, &version_a),
398 reverse_vercmp_result
399 );
400 }
401}