1use std::{
26 cmp::Ordering,
27 fmt::{Display, Formatter},
28 hash::{Hash, Hasher},
29};
30
31use serde::{Deserialize, Serialize};
32use thiserror::Error;
33
34pub use crate::ParseError;
35use crate::utils::TryFromStrVisitor;
36
37fn compare_non_digits(mut lhs: &str, mut rhs: &str) -> Ordering {
41 while !lhs.is_empty() || !rhs.is_empty() {
42 let (lhs_tilde, lhs_found) = lhs.find('~').map_or((lhs.len(), false), |i| (i, true));
43 let (rhs_tilde, rhs_found) = rhs.find('~').map_or((rhs.len(), false), |i| (i, true));
44 let c = lhs[..lhs_tilde].cmp(&rhs[..rhs_tilde]);
45 if c != Ordering::Equal {
46 return c;
47 }
48
49 if lhs_found && rhs_found {
50 lhs = &lhs[lhs_tilde + 1..];
51 rhs = &rhs[rhs_tilde + 1..];
52 } else if lhs_found {
53 return Ordering::Less;
54 } else if rhs_found {
55 return Ordering::Greater;
56 } else {
57 return Ordering::Equal;
58 }
59 }
60
61 Ordering::Equal
63}
64
65fn compare_parts(mut lhs: &str, mut rhs: &str) -> Ordering {
67 while !lhs.is_empty() || !rhs.is_empty() {
68 let lhs_digit_start = lhs.find(|c| char::is_ascii_digit(&c)).unwrap_or(lhs.len());
70 let rhs_digit_start = rhs.find(|c| char::is_ascii_digit(&c)).unwrap_or(rhs.len());
71 let c = compare_non_digits(&lhs[..lhs_digit_start], &rhs[..rhs_digit_start]);
72 if c != Ordering::Equal {
73 return c;
74 }
75 lhs = &lhs[lhs_digit_start..];
76 rhs = &rhs[rhs_digit_start..];
77
78 let lhs_digit_end = lhs.find(|c| !char::is_ascii_digit(&c)).unwrap_or(lhs.len());
80 let rhs_digit_end = rhs.find(|c| !char::is_ascii_digit(&c)).unwrap_or(rhs.len());
81 let c = lhs[..lhs_digit_end]
82 .parse::<u64>()
83 .unwrap_or(0)
84 .cmp(&rhs[..rhs_digit_end].parse::<u64>().unwrap_or(0));
85 if c != Ordering::Equal {
86 return c;
87 }
88 lhs = &lhs[lhs_digit_end..];
89 rhs = &rhs[rhs_digit_end..];
90 }
91
92 Ordering::Equal
94}
95
96#[derive(Clone, Copy, Debug, Error)]
98pub enum VersionError {
99 #[error("invalid epoch")]
100 InvalidEpoch,
102 #[error("invalid upstream version")]
103 InvalidUpstreamVersion,
105 #[error("invalid Debian revision")]
106 InvalidDebianRevision,
108}
109
110#[derive(Clone, Debug)]
117pub struct PackageVersion {
118 pub(crate) epoch: Option<u32>,
120 pub(crate) upstream_version: String,
122 pub(crate) debian_revision: Option<String>,
124}
125
126impl PackageVersion {
127 pub fn new(
129 epoch: Option<u32>,
130 upstream_version: &str,
131 debian_revision: Option<&str>,
132 ) -> Result<Self, VersionError> {
133 if upstream_version.is_empty()
135 || upstream_version.chars().any(|c| {
136 !(c.is_alphanumeric()
137 || ".+~".contains(c)
138 || (debian_revision.is_some() && c == '-')
139 || (epoch.is_some() && c == ':'))
140 })
141 {
142 return Err(VersionError::InvalidUpstreamVersion);
143 }
144
145 if let Some(rev) = debian_revision {
147 if rev.is_empty()
148 || rev
149 .chars()
150 .any(|c| !c.is_alphanumeric() && !".+~".contains(c))
151 {
152 return Err(VersionError::InvalidDebianRevision);
153 }
154 }
155
156 Ok(Self {
157 epoch,
158 upstream_version: String::from(upstream_version),
159 debian_revision: debian_revision.map(String::from),
160 })
161 }
162
163 pub fn is_native(&self) -> bool {
165 self.debian_revision.is_none()
166 }
167
168 pub fn has_epoch(&self) -> bool {
170 self.epoch.is_some()
171 }
172
173 pub fn epoch_or_0(&self) -> u32 {
175 self.epoch.unwrap_or(0)
176 }
177
178 pub fn has_binnmu_version(&self) -> bool {
180 self.binnmu_version().is_some()
181 }
182
183 pub fn binnmu_version(&self) -> Option<u32> {
185 self.debian_revision
186 .as_ref()
187 .map_or(&self.upstream_version, |v| v)
188 .rsplit_once("+b")
189 .and_then(|(_, binnmu_version)| binnmu_version.parse().ok())
190 }
191
192 pub fn without_binnmu_version(mut self) -> Self {
194 if let Some(revision) = self.debian_revision.as_mut() {
195 if let Some(index) = revision.rfind("+b") {
196 revision.truncate(index);
197 }
198 } else if let Some(index) = self.upstream_version.rfind("+b") {
199 self.upstream_version.truncate(index);
200 }
201 self
202 }
203
204 pub fn clone_without_binnmu_version(&self) -> Self {
206 self.clone().without_binnmu_version()
207 }
208}
209
210impl PartialOrd for PackageVersion {
211 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
212 Some(self.cmp(other))
213 }
214}
215
216impl Ord for PackageVersion {
217 fn cmp(&self, other: &Self) -> Ordering {
218 match self.epoch_or_0().cmp(&other.epoch_or_0()) {
219 Ordering::Equal => {}
220 v => return v,
221 };
222
223 match compare_parts(&self.upstream_version, &other.upstream_version) {
224 Ordering::Equal => {}
225 v => return v,
226 };
227
228 match (&self.debian_revision, &other.debian_revision) {
229 (None, None) => Ordering::Equal,
230 (None, Some(_)) => Ordering::Less,
231 (Some(_), None) => Ordering::Greater,
232 (Some(lhs), Some(rhs)) => compare_parts(lhs, rhs),
233 }
234 }
235}
236
237impl PartialEq for PackageVersion {
238 fn eq(&self, other: &Self) -> bool {
239 self.cmp(other) == Ordering::Equal
240 }
241}
242
243impl Eq for PackageVersion {}
244
245impl PartialEq<&str> for PackageVersion {
246 fn eq(&self, other: &&str) -> bool {
247 self.partial_cmp(other) == Some(Ordering::Equal)
248 }
249}
250
251impl PartialOrd<&str> for PackageVersion {
252 fn partial_cmp(&self, other: &&str) -> Option<Ordering> {
253 let rhs = Self::try_from(*other);
254 match rhs {
255 Err(_) => None,
256 Ok(rhs) => self.partial_cmp(&rhs),
257 }
258 }
259}
260
261impl TryFrom<&str> for PackageVersion {
262 type Error = ParseError;
263
264 fn try_from(mut value: &str) -> Result<Self, Self::Error> {
265 let epoch = if let Some((epoch_str, new_value)) = value.split_once(':') {
266 value = new_value;
267 Some(
268 epoch_str
269 .parse::<u32>()
270 .map_err(|_| ParseError::InvalidVersion(VersionError::InvalidEpoch))?,
271 )
272 } else {
273 None
274 };
275
276 let debian_revision = if let Some((new_value, debian_revision_str)) = value.rsplit_once('-')
277 {
278 value = new_value;
279 Some(debian_revision_str)
280 } else {
281 None
282 };
283
284 Self::new(epoch, value, debian_revision).map_err(ParseError::InvalidVersion)
285 }
286}
287
288impl Display for PackageVersion {
289 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
290 if let Some(epoch) = self.epoch {
291 write!(f, "{epoch}:")?;
292 }
293 write!(f, "{}", self.upstream_version)?;
294 if let Some(debian_revision) = &self.debian_revision {
295 write!(f, "-{debian_revision}")?;
296 }
297 Ok(())
298 }
299}
300
301impl Serialize for PackageVersion {
302 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
303 where
304 S: serde::Serializer,
305 {
306 serializer.serialize_str(&self.to_string())
307 }
308}
309
310impl<'de> Deserialize<'de> for PackageVersion {
311 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312 where
313 D: serde::Deserializer<'de>,
314 {
315 deserializer.deserialize_str(TryFromStrVisitor::new("a package version"))
316 }
317}
318
319impl Hash for PackageVersion {
320 fn hash<H: Hasher>(&self, state: &mut H) {
321 self.epoch_or_0().hash(state);
322 self.upstream_version.hash(state);
323 self.debian_revision.hash(state);
324 }
325}
326
327#[cfg(test)]
328mod test {
329 use super::*;
330
331 #[test]
332 fn conversion() {
333 let version = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
334 assert_eq!(version.epoch, Some(2));
335 assert_eq!(version.upstream_version, "1.0+dfsg");
336 assert_eq!(version.debian_revision, Some("1".into()));
337 }
338
339 #[test]
340 fn invalid_epoch() {
341 assert!(PackageVersion::try_from("-1:1.0-1").is_err());
342 assert!(PackageVersion::try_from(":1.0-1").is_err());
343 assert!(PackageVersion::try_from("a1:1.0-1").is_err());
344 }
345
346 #[test]
347 fn invalid_upstream_version() {
348 assert!(PackageVersion::try_from("-1").is_err());
349 assert!(PackageVersion::try_from("0:-1").is_err());
350 assert!(PackageVersion::new(None, "1:2", None).is_err());
351 assert!(PackageVersion::new(None, "1-2", None).is_err());
352 }
353
354 #[test]
355 fn multi_dash() {
356 let version = PackageVersion::try_from("1.0-2-1").unwrap();
357 assert_eq!(version.epoch, None);
358 assert_eq!(version.upstream_version, "1.0-2");
359 assert_eq!(version.debian_revision.unwrap(), "1");
360 }
361
362 #[test]
363 fn multi_colon() {
364 let version = PackageVersion::try_from("1:1.0:2-1").unwrap();
365 assert_eq!(version.epoch.unwrap(), 1);
366 assert_eq!(version.upstream_version, "1.0:2");
367 assert_eq!(version.debian_revision.unwrap(), "1");
368 }
369
370 #[test]
371 fn binnum() {
372 let version = PackageVersion::try_from("1.0-1").unwrap();
373 assert!(!version.has_binnmu_version());
374 assert_eq!(version.binnmu_version(), None);
375
376 let version = PackageVersion::try_from("1.0-1+b1").unwrap();
377 assert!(version.has_binnmu_version());
378 assert_eq!(version.binnmu_version(), Some(1u32));
379 }
380
381 #[test]
382 fn strip_binnum() {
383 let version = PackageVersion::try_from("1.0-1+b1").unwrap();
384 let version = version.without_binnmu_version();
385 assert_eq!(version, PackageVersion::try_from("1.0-1").unwrap());
386
387 assert!(!version.has_binnmu_version());
388 assert_eq!(version.binnmu_version(), None);
389 }
390
391 #[test]
392 fn compare_non_digits_invariants() {
393 assert_eq!(compare_non_digits("~~", "~~a"), Ordering::Less);
394 assert_eq!(compare_non_digits("~~a", "~"), Ordering::Less);
395 assert_eq!(compare_non_digits("~", ""), Ordering::Less);
396 assert_eq!(compare_non_digits("", "a"), Ordering::Less);
397 }
398
399 #[test]
400 fn epoch_compare() {
401 let version1 = PackageVersion::try_from("2.0-1").unwrap();
402 let version2 = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
403
404 assert!(version2.has_epoch());
405 assert!(!version1.has_epoch());
406 assert!(version1 < version2);
407 }
408
409 #[test]
410 fn zero_epoch_compare() {
411 let version1 = PackageVersion::try_from("2.0-1").unwrap();
412 let version2 = PackageVersion::try_from("0:2.0-1").unwrap();
413 assert_eq!(version1, version2);
414 }
415
416 #[test]
417 fn equal_compare() {
418 let version1 = PackageVersion::try_from("2.0-1").unwrap();
419 assert_eq!(version1, version1);
420
421 let version1 = PackageVersion::try_from("2a.0-1").unwrap();
422 assert_eq!(version1, version1);
423
424 let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
425 assert_eq!(version1, version1);
426 }
427
428 #[test]
429 fn tilde_plus_compare() {
430 let version1 = PackageVersion::try_from("2.0~dfsg-1").unwrap();
431 let version2 = PackageVersion::try_from("2.0-1").unwrap();
432 assert!(version1 < version2);
433
434 let version2 = PackageVersion::try_from("2.0+dfsg-1").unwrap();
435 assert!(version1 < version2);
436
437 let version1 = PackageVersion::try_from("2.0-1").unwrap();
438 assert!(version1 < version2);
439
440 let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
441 let version2 = PackageVersion::try_from("2+dfsg2-1").unwrap();
442 assert!(version1 < version2);
443
444 let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
445 let version2 = PackageVersion::try_from("2.1-1").unwrap();
446 assert!(version1 < version2);
447 }
448
449 #[test]
450 fn letters_compare() {
451 let version1 = PackageVersion::try_from("2dfsg-1").unwrap();
452 let version2 = PackageVersion::try_from("2-1").unwrap();
453 assert!(version1 > version2);
454 }
455
456 #[test]
457 fn less_compare() {
458 let version1 = PackageVersion::try_from("2-1").unwrap();
459 let version2 = PackageVersion::try_from("2.0-1").unwrap();
460 assert!(version1 < version2);
461 }
462
463 #[test]
464 fn native_version_binnmu() {
465 let version1 = PackageVersion::try_from("2+b1").unwrap();
466 let version2 = PackageVersion::try_from("2").unwrap();
467 assert!(version1.has_binnmu_version());
468 assert_eq!(version1.binnmu_version(), Some(1));
469 assert!(!version2.has_binnmu_version());
470 assert_eq!(version1.without_binnmu_version(), version2);
471 }
472}