1use std::{
26 cmp::Ordering,
27 fmt::{Display, Formatter},
28 hash::{Hash, Hasher},
29};
30
31use serde::{Deserialize, Serialize};
32use thiserror::Error;
33
34use crate::utils::TryFromStrVisitor;
35pub use crate::ParseError;
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 TryFrom<&str> for PackageVersion {
246 type Error = ParseError;
247
248 fn try_from(mut value: &str) -> Result<Self, Self::Error> {
249 let epoch = if let Some((epoch_str, new_value)) = value.split_once(':') {
250 value = new_value;
251 Some(
252 epoch_str
253 .parse::<u32>()
254 .map_err(|_| ParseError::InvalidVersion(VersionError::InvalidEpoch))?,
255 )
256 } else {
257 None
258 };
259
260 let debian_revision = if let Some((new_value, debian_revision_str)) = value.rsplit_once('-')
261 {
262 value = new_value;
263 Some(debian_revision_str)
264 } else {
265 None
266 };
267
268 Self::new(epoch, value, debian_revision).map_err(ParseError::InvalidVersion)
269 }
270}
271
272impl Display for PackageVersion {
273 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
274 if let Some(epoch) = self.epoch {
275 write!(f, "{epoch}:")?;
276 }
277 write!(f, "{}", self.upstream_version)?;
278 if let Some(debian_revision) = &self.debian_revision {
279 write!(f, "-{debian_revision}")?;
280 }
281 Ok(())
282 }
283}
284
285impl Serialize for PackageVersion {
286 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
287 where
288 S: serde::Serializer,
289 {
290 serializer.serialize_str(&self.to_string())
291 }
292}
293
294impl<'de> Deserialize<'de> for PackageVersion {
295 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
296 where
297 D: serde::Deserializer<'de>,
298 {
299 deserializer.deserialize_str(TryFromStrVisitor::<Self>::new("a package version"))
300 }
301}
302
303impl Hash for PackageVersion {
304 fn hash<H: Hasher>(&self, state: &mut H) {
305 self.epoch_or_0().hash(state);
306 self.upstream_version.hash(state);
307 self.debian_revision.hash(state);
308 }
309}
310
311#[cfg(test)]
312mod test {
313 use super::*;
314
315 #[test]
316 fn conversion() {
317 let version = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
318 assert_eq!(version.epoch, Some(2));
319 assert_eq!(version.upstream_version, "1.0+dfsg");
320 assert_eq!(version.debian_revision, Some("1".into()));
321 }
322
323 #[test]
324 fn invalid_epoch() {
325 assert!(PackageVersion::try_from("-1:1.0-1").is_err());
326 assert!(PackageVersion::try_from(":1.0-1").is_err());
327 assert!(PackageVersion::try_from("a1:1.0-1").is_err());
328 }
329
330 #[test]
331 fn invalid_upstream_version() {
332 assert!(PackageVersion::try_from("-1").is_err());
333 assert!(PackageVersion::try_from("0:-1").is_err());
334 assert!(PackageVersion::new(None, "1:2", None).is_err());
335 assert!(PackageVersion::new(None, "1-2", None).is_err());
336 }
337
338 #[test]
339 fn multi_dash() {
340 let version = PackageVersion::try_from("1.0-2-1").unwrap();
341 assert_eq!(version.epoch, None);
342 assert_eq!(version.upstream_version, "1.0-2");
343 assert_eq!(version.debian_revision.unwrap(), "1");
344 }
345
346 #[test]
347 fn multi_colon() {
348 let version = PackageVersion::try_from("1:1.0:2-1").unwrap();
349 assert_eq!(version.epoch.unwrap(), 1);
350 assert_eq!(version.upstream_version, "1.0:2");
351 assert_eq!(version.debian_revision.unwrap(), "1");
352 }
353
354 #[test]
355 fn binnum() {
356 let version = PackageVersion::try_from("1.0-1").unwrap();
357 assert!(!version.has_binnmu_version());
358 assert_eq!(version.binnmu_version(), None);
359
360 let version = PackageVersion::try_from("1.0-1+b1").unwrap();
361 assert!(version.has_binnmu_version());
362 assert_eq!(version.binnmu_version(), Some(1u32));
363 }
364
365 #[test]
366 fn strip_binnum() {
367 let version = PackageVersion::try_from("1.0-1+b1").unwrap();
368 let version = version.without_binnmu_version();
369 assert_eq!(version, PackageVersion::try_from("1.0-1").unwrap());
370
371 assert!(!version.has_binnmu_version());
372 assert_eq!(version.binnmu_version(), None);
373 }
374
375 #[test]
376 fn compare_non_digits_invariants() {
377 assert_eq!(compare_non_digits("~~", "~~a"), Ordering::Less);
378 assert_eq!(compare_non_digits("~~a", "~"), Ordering::Less);
379 assert_eq!(compare_non_digits("~", ""), Ordering::Less);
380 assert_eq!(compare_non_digits("", "a"), Ordering::Less);
381 }
382
383 #[test]
384 fn epoch_compare() {
385 let version1 = PackageVersion::try_from("2.0-1").unwrap();
386 let version2 = PackageVersion::try_from("2:1.0+dfsg-1").unwrap();
387
388 assert!(version2.has_epoch());
389 assert!(!version1.has_epoch());
390 assert!(version1 < version2);
391 }
392
393 #[test]
394 fn zero_epoch_compare() {
395 let version1 = PackageVersion::try_from("2.0-1").unwrap();
396 let version2 = PackageVersion::try_from("0:2.0-1").unwrap();
397 assert_eq!(version1, version2);
398 }
399
400 #[test]
401 fn equal_compare() {
402 let version1 = PackageVersion::try_from("2.0-1").unwrap();
403 assert_eq!(version1, version1);
404
405 let version1 = PackageVersion::try_from("2a.0-1").unwrap();
406 assert_eq!(version1, version1);
407
408 let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
409 assert_eq!(version1, version1);
410 }
411
412 #[test]
413 fn tilde_plus_compare() {
414 let version1 = PackageVersion::try_from("2.0~dfsg-1").unwrap();
415 let version2 = PackageVersion::try_from("2.0-1").unwrap();
416 assert!(version1 < version2);
417
418 let version2 = PackageVersion::try_from("2.0+dfsg-1").unwrap();
419 assert!(version1 < version2);
420
421 let version1 = PackageVersion::try_from("2.0-1").unwrap();
422 assert!(version1 < version2);
423
424 let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
425 let version2 = PackageVersion::try_from("2+dfsg2-1").unwrap();
426 assert!(version1 < version2);
427
428 let version1 = PackageVersion::try_from("2+dfsg1-1").unwrap();
429 let version2 = PackageVersion::try_from("2.1-1").unwrap();
430 assert!(version1 < version2);
431 }
432
433 #[test]
434 fn letters_compare() {
435 let version1 = PackageVersion::try_from("2dfsg-1").unwrap();
436 let version2 = PackageVersion::try_from("2-1").unwrap();
437 assert!(version1 > version2);
438 }
439
440 #[test]
441 fn less_compare() {
442 let version1 = PackageVersion::try_from("2-1").unwrap();
443 let version2 = PackageVersion::try_from("2.0-1").unwrap();
444 assert!(version1 < version2);
445 }
446
447 #[test]
448 fn native_version_binnmu() {
449 let version1 = PackageVersion::try_from("2+b1").unwrap();
450 let version2 = PackageVersion::try_from("2").unwrap();
451 assert!(version1.has_binnmu_version());
452 assert_eq!(version1.binnmu_version(), Some(1));
453 assert!(!version2.has_binnmu_version());
454 assert_eq!(version1.without_binnmu_version(), version2);
455 }
456}