1use std::{
4 cmp::Ordering,
5 fmt::{Display, Formatter},
6 str::FromStr,
7};
8
9use alpm_parsers::traits::{AlpmParser, ParserUntil, ParserUntilInclusive};
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12use winnow::{
13 ModalResult,
14 Parser,
15 combinator::opt,
16 error::{ContextError, ErrMode, StrContext, StrContextValue},
17};
18
19use crate::{Epoch, Error, PackageRelease, PackageVersion};
20#[cfg(doc)]
21use crate::{FullVersion, MinimalVersion};
22
23#[derive(Clone, Debug, Eq, PartialEq)]
52#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
53pub struct Version {
54 pub pkgver: PackageVersion,
56 pub epoch: Option<Epoch>,
58 pub pkgrel: Option<PackageRelease>,
60}
61
62impl Version {
63 pub fn new(
65 pkgver: PackageVersion,
66 epoch: Option<Epoch>,
67 pkgrel: Option<PackageRelease>,
68 ) -> Self {
69 Version {
70 pkgver,
71 epoch,
72 pkgrel,
73 }
74 }
75
76 pub fn vercmp(a: &Version, b: &Version) -> i8 {
108 match a.cmp(b) {
109 Ordering::Less => -1,
110 Ordering::Equal => 0,
111 Ordering::Greater => 1,
112 }
113 }
114}
115
116impl AlpmParser for Version {
117 fn parser(input: &mut &str) -> ModalResult<Self> {
125 let epoch = opt(Epoch::parser_until_inclusive(":")).parse_next(input)?;
130
131 let pkgver = PackageVersion::parser.parse_next(input)?;
134
135 let delimiter = opt('-').parse_next(input)?;
140 let pkgrel = if delimiter.is_some() {
141 Some(PackageRelease::parser.parse_next(input)?)
142 } else {
143 None
144 };
145
146 Ok(Self {
147 epoch,
148 pkgver,
149 pkgrel,
150 })
151 }
152
153 fn delimiter_error_context<'a, O, P>(
154 parser: P,
155 ) -> impl Parser<&'a str, O, ErrMode<ContextError>>
156 where
157 P: Parser<&'a str, O, ErrMode<ContextError>>,
158 {
159 parser
160 .context(StrContext::Label("alpm-package-version"))
161 .context(StrContext::Expected(StrContextValue::Description(
162 "end of the version string",
163 )))
164 }
165}
166
167impl FromStr for Version {
168 type Err = Error;
169 fn from_str(s: &str) -> Result<Version, Self::Err> {
177 Ok(Self::parser_until_eof.parse(s)?)
178 }
179}
180
181impl Display for Version {
182 fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
183 if let Some(epoch) = self.epoch {
184 write!(fmt, "{epoch}:")?;
185 }
186
187 write!(fmt, "{}", self.pkgver)?;
188
189 if let Some(pkgrel) = &self.pkgrel {
190 write!(fmt, "-{pkgrel}")?;
191 }
192
193 Ok(())
194 }
195}
196
197impl Ord for Version {
198 fn cmp(&self, other: &Self) -> Ordering {
199 match (self.epoch, other.epoch) {
200 (Some(self_epoch), Some(other_epoch)) if self_epoch.cmp(&other_epoch).is_ne() => {
201 return self_epoch.cmp(&other_epoch);
202 }
203 (Some(_), None) => return Ordering::Greater,
204 (None, Some(_)) => return Ordering::Less,
205 (_, _) => {}
206 }
207
208 let pkgver_cmp = self.pkgver.cmp(&other.pkgver);
209 if pkgver_cmp.is_ne() {
210 return pkgver_cmp;
211 }
212
213 self.pkgrel.cmp(&other.pkgrel)
214 }
215}
216
217impl PartialOrd for Version {
218 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
219 Some(self.cmp(other))
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use insta::assert_snapshot;
226 use rstest::rstest;
227
228 use super::*;
229 use crate::configure_insta;
230
231 #[rstest]
233 #[case(
234 "foo",
235 Version {
236 epoch: None,
237 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
238 pkgrel: None
239 },
240 )]
241 #[case(
242 "1:foo-1",
243 Version {
244 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
245 epoch: Some(Epoch::new(1)),
246 pkgrel: Some(PackageRelease::new(1, None))
247 },
248 )]
249 #[case(
250 "1:foo",
251 Version {
252 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
253 epoch: Some(Epoch::new(1)),
254 pkgrel: None,
255 },
256 )]
257 #[case(
258 "foo-1",
259 Version {
260 pkgver: PackageVersion::new("foo".to_string()).unwrap(),
261 epoch: None,
262 pkgrel: Some(PackageRelease::new(1, None))
263 }
264 )]
265 #[case(
267 ".-1",
268 Version {
269 pkgver: PackageVersion::new(".".to_string()).unwrap(),
270 epoch: None,
271 pkgrel: Some(PackageRelease::new(1, None))
272 }
273 )]
274 fn valid_version_from_string(#[case] version: &str, #[case] expected: Version) {
275 assert_eq!(
276 Version::from_str(version),
277 Ok(expected),
278 "Expected valid parsing for version {version}"
279 )
280 }
281
282 #[rstest]
284 #[case::two_pkgrel("1:foo-1-1")]
285 #[case::two_epoch("1:1:foo-1")]
286 #[case::no_version("")]
287 #[case::no_version(":")]
288 #[case::invalid_integer("-1foo:1")]
289 #[case::invalid_integer("1-foo:1")]
290 fn parse_error_in_version_from_string(#[case] version: &str) {
291 let Err(Error::ParseError(err_msg)) = Version::from_str(version) else {
292 panic!("parsing '{version}' did not fail as expected")
293 };
294
295 let (test_name, _guard) = configure_insta();
296 assert_snapshot!(test_name, err_msg.to_string());
297 }
298
299 #[rstest]
301 #[case(Version::from_str("1:1-1").unwrap(), "1:1-1")]
302 #[case(Version::from_str("1-1").unwrap(), "1-1")]
303 #[case(Version::from_str("1").unwrap(), "1")]
304 #[case(Version::from_str("1:1").unwrap(), "1:1")]
305 fn version_to_string(#[case] version: Version, #[case] to_str: &str) {
306 assert_eq!(format!("{version}"), to_str);
307 }
308
309 #[rstest]
310 #[case(Version::from_str("1"), Version::from_str("1"), Ordering::Equal)]
312 #[case(Version::from_str("1"), Version::from_str("2"), Ordering::Less)]
313 #[case(
314 Version::from_str("20220102"),
315 Version::from_str("20220202"),
316 Ordering::Less
317 )]
318 #[case(Version::from_str("1"), Version::from_str("1.1"), Ordering::Less)]
320 #[case(Version::from_str("01"), Version::from_str("1"), Ordering::Equal)]
321 #[case(Version::from_str("001a"), Version::from_str("1a"), Ordering::Equal)]
322 #[case(Version::from_str("a1a"), Version::from_str("a1b"), Ordering::Less)]
323 #[case(Version::from_str("foo"), Version::from_str("1.1"), Ordering::Less)]
324 #[case(Version::from_str("1.0"), Version::from_str("1..0"), Ordering::Less)]
326 #[case(Version::from_str("1.1"), Version::from_str("1.1"), Ordering::Equal)]
327 #[case(Version::from_str("1.1"), Version::from_str("1.2"), Ordering::Less)]
328 #[case(Version::from_str("1..0"), Version::from_str("1..0"), Ordering::Equal)]
329 #[case(Version::from_str("1..0"), Version::from_str("1..1"), Ordering::Less)]
330 #[case(Version::from_str("1+0"), Version::from_str("1.0"), Ordering::Equal)]
331 #[case(Version::from_str("1+1"), Version::from_str("1+2"), Ordering::Less)]
332 #[case(Version::from_str("1.1"), Version::from_str("1.1.a"), Ordering::Less)]
334 #[case(Version::from_str("1.1"), Version::from_str("1.11a"), Ordering::Less)]
335 #[case(Version::from_str("1.1"), Version::from_str("1.1_a"), Ordering::Less)]
336 #[case(Version::from_str("1.1a"), Version::from_str("1.1"), Ordering::Less)]
337 #[case(Version::from_str("1.1a1"), Version::from_str("1.1"), Ordering::Less)]
338 #[case(Version::from_str("1.a"), Version::from_str("1.1"), Ordering::Less)]
339 #[case(Version::from_str("1.a"), Version::from_str("1.alpha"), Ordering::Less)]
340 #[case(Version::from_str("1.a1"), Version::from_str("1.1"), Ordering::Less)]
341 #[case(Version::from_str("1.a11"), Version::from_str("1.1"), Ordering::Less)]
342 #[case(Version::from_str("1.a1a"), Version::from_str("1.a1"), Ordering::Less)]
343 #[case(Version::from_str("1.alpha"), Version::from_str("1.b"), Ordering::Less)]
344 #[case(Version::from_str("a.1"), Version::from_str("1.1"), Ordering::Less)]
345 #[case(
346 Version::from_str("1.alpha0.0"),
347 Version::from_str("1.alpha.0"),
348 Ordering::Less
349 )]
350 #[case(Version::from_str("1.0"), Version::from_str("1.0."), Ordering::Less)]
352 #[case(Version::from_str("1.0."), Version::from_str("1.0.0"), Ordering::Less)]
354 #[case(Version::from_str("1.0.."), Version::from_str("1.0."), Ordering::Equal)]
355 #[case(
356 Version::from_str("1.0.alpha.0"),
357 Version::from_str("1.0."),
358 Ordering::Less
359 )]
360 #[case(
361 Version::from_str("1.a001a.1"),
362 Version::from_str("1.a1a.1"),
363 Ordering::Equal
364 )]
365 fn version_cmp(
366 #[case] version_a: Result<Version, Error>,
367 #[case] version_b: Result<Version, Error>,
368 #[case] expected: Ordering,
369 ) {
370 let version_a = version_a.unwrap();
372 let version_b = version_b.unwrap();
373
374 let vercmp_result = match &expected {
376 Ordering::Equal => 0,
377 Ordering::Greater => 1,
378 Ordering::Less => -1,
379 };
380
381 let ordering = version_a.cmp(&version_b);
382 assert_eq!(
383 ordering, expected,
384 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
385 );
386
387 assert_eq!(Version::vercmp(&version_a, &version_b), vercmp_result);
388
389 #[cfg(feature = "_compatibility_tests")]
391 {
392 let output = std::process::Command::new("vercmp")
393 .arg(version_a.to_string())
394 .arg(version_b.to_string())
395 .output()
396 .unwrap();
397 let result = String::from_utf8_lossy(&output.stdout);
398 assert_eq!(result.trim(), vercmp_result.to_string());
399 }
400
401 let reverse_vercmp_result = match &expected {
403 Ordering::Equal => 0,
404 Ordering::Greater => -1,
405 Ordering::Less => 1,
406 };
407 let reverse_expected = match &expected {
408 Ordering::Equal => Ordering::Equal,
409 Ordering::Greater => Ordering::Less,
410 Ordering::Less => Ordering::Greater,
411 };
412
413 let reverse_ordering = version_b.cmp(&version_a);
414 assert_eq!(
415 reverse_ordering, reverse_expected,
416 "Failed to compare '{version_a}' and '{version_b}'. Expected {expected:?} got {ordering:?}"
417 );
418
419 assert_eq!(
420 Version::vercmp(&version_b, &version_a),
421 reverse_vercmp_result
422 );
423 }
424}