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