1use {
8 crate::error::{DebianError, Result},
9 std::{
10 cmp::Ordering,
11 fmt::{Display, Formatter},
12 str::FromStr,
13 },
14};
15
16#[derive(Clone, Debug, Eq, PartialEq, Hash)]
43pub struct PackageVersion {
44 epoch: Option<u32>,
45 upstream_version: String,
46 debian_revision: Option<String>,
47}
48
49impl PackageVersion {
50 pub fn parse(s: &str) -> Result<Self> {
52 let (epoch, remainder) = if let Some(pos) = s.find(':') {
56 (Some(&s[0..pos]), &s[pos + 1..])
57 } else {
58 (None, s)
59 };
60
61 let (upstream, debian) = if let Some(pos) = remainder.rfind('-') {
62 (&remainder[0..pos], Some(&remainder[pos + 1..]))
63 } else {
64 (remainder, None)
65 };
66
67 let epoch = if let Some(epoch) = epoch {
71 if !epoch.chars().all(|c| c.is_ascii_digit()) {
72 return Err(DebianError::EpochNonNumeric(s.to_string()));
73 }
74
75 Some(u32::from_str(epoch)?)
76 } else {
77 None
78 };
79
80 if !upstream.chars().all(|c| match c {
84 c if c.is_ascii_alphanumeric() => true,
85 '.' | '+' | '~' => true,
86 '-' => debian.is_some(),
87 _ => false,
88 }) {
89 return Err(DebianError::UpstreamVersionIllegalChar(s.to_string()));
90 }
91
92 let upstream_version = upstream.to_string();
93
94 let debian_revision = if let Some(debian) = debian {
95 if !debian.chars().all(|c| match c {
97 c if c.is_ascii_alphanumeric() => true,
98 '+' | '.' | '~' => true,
99 _ => false,
100 }) {
101 return Err(DebianError::DebianRevisionIllegalChar(s.to_string()));
102 }
103
104 Some(debian.to_string())
105 } else {
106 None
107 };
108
109 Ok(Self {
110 epoch,
111 upstream_version,
112 debian_revision,
113 })
114 }
115
116 pub fn epoch(&self) -> Option<u32> {
120 self.epoch
121 }
122
123 pub fn epoch_assumed(&self) -> u32 {
127 if let Some(epoch) = &self.epoch {
128 *epoch
129 } else {
130 0
131 }
132 }
133
134 pub fn upstream_version(&self) -> &str {
141 &self.upstream_version
142 }
143
144 pub fn debian_revision(&self) -> Option<&str> {
149 self.debian_revision.as_deref()
150 }
151}
152
153impl Display for PackageVersion {
154 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155 write!(
157 f,
158 "{}{}{}{}{}",
159 if let Some(epoch) = self.epoch {
160 format!("{}", epoch)
161 } else {
162 "".to_string()
163 },
164 if self.epoch.is_some() { ":" } else { "" },
165 self.upstream_version,
166 if self.debian_revision.is_some() {
167 "-"
168 } else {
169 ""
170 },
171 if let Some(v) = &self.debian_revision {
172 v
173 } else {
174 ""
175 }
176 )
177 }
178}
179
180fn split_first_digit(s: &str) -> (&str, &str) {
185 let first_nondigit_index = s.chars().position(|c| c.is_ascii_digit());
186
187 match first_nondigit_index {
188 Some(0) => ("", s),
189 Some(pos) => (&s[0..pos], &s[pos..]),
190 None => (s, ""),
191 }
192}
193
194fn split_first_nondigit(s: &str) -> (&str, &str) {
195 let pos = s.chars().position(|c| !c.is_ascii_digit());
196
197 match pos {
198 Some(0) => ("", s),
199 Some(pos) => (&s[0..pos], &s[pos..]),
200 None => (s, ""),
201 }
202}
203
204fn split_first_digit_number(s: &str) -> (u64, &str) {
206 let (digits, remaining) = split_first_nondigit(s);
207
208 let numeric = if digits.is_empty() {
209 0
210 } else {
211 u64::from_str(digits).expect("digits should deserialize to string")
212 };
213
214 (numeric, remaining)
215}
216
217fn lexical_compare(a: &str, b: &str) -> Ordering {
218 let mut a_chars = a.chars();
223 let mut b_chars = b.chars();
224
225 loop {
231 let ord = match (a_chars.next(), b_chars.next()) {
232 (Some('~'), Some('~')) => Ordering::Equal,
233 (Some('~'), _) => Ordering::Less,
234 (Some(_), None) => Ordering::Greater,
235 (None, Some('~')) => Ordering::Greater,
236 (None, Some(_)) => Ordering::Less,
237 (Some(a), Some(b)) if a.is_ascii_alphabetic() && !b.is_ascii_alphabetic() => {
238 Ordering::Less
239 }
240 (Some(a), Some(b)) if !a.is_ascii_alphabetic() && b.is_ascii_alphabetic() => {
241 Ordering::Greater
242 }
243 (Some(a), Some(b)) => a.cmp(&b),
244 (None, None) => break,
245 };
246
247 if ord != Ordering::Equal {
248 return ord;
249 }
250 }
251
252 Ordering::Equal
253}
254
255fn compare_component(a: &str, b: &str) -> Ordering {
257 let mut a_remaining = a;
265 let mut b_remaining = b;
266
267 loop {
268 let a_res = split_first_digit(a_remaining);
269 let a_leading_nondigit = a_res.0;
270 a_remaining = a_res.1;
271
272 let b_res = split_first_digit(b_remaining);
273 let b_leading_nondigit = b_res.0;
274 b_remaining = b_res.1;
275
276 match lexical_compare(a_leading_nondigit, b_leading_nondigit) {
279 Ordering::Equal => {}
280 res => {
281 return res;
282 }
283 }
284
285 let a_res = split_first_digit_number(a_remaining);
293 let a_numeric = a_res.0;
294 a_remaining = a_res.1;
295
296 let b_res = split_first_digit_number(b_remaining);
297 let b_numeric = b_res.0;
298 b_remaining = b_res.1;
299
300 match a_numeric.cmp(&b_numeric) {
301 Ordering::Equal => {}
302 res => {
303 return res;
304 }
305 }
306
307 if a_remaining.is_empty() && b_remaining.is_empty() {
308 return Ordering::Equal;
309 }
310 }
311}
312
313impl PartialOrd<Self> for PackageVersion {
314 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
315 Some(self.cmp(other))
316 }
317}
318
319impl Ord for PackageVersion {
320 fn cmp(&self, other: &Self) -> Ordering {
321 match self.epoch_assumed().cmp(&other.epoch_assumed()) {
325 Ordering::Less => Ordering::Less,
326 Ordering::Greater => Ordering::Greater,
327 Ordering::Equal => {
328 match compare_component(&self.upstream_version, &other.upstream_version) {
329 Ordering::Less => Ordering::Less,
330 Ordering::Greater => Ordering::Greater,
331 Ordering::Equal => {
332 let a = self.debian_revision.as_deref().unwrap_or("0");
333 let b = other.debian_revision.as_deref().unwrap_or("0");
334
335 compare_component(a, b)
336 }
337 }
338 }
339 }
340 }
341}
342
343#[cfg(test)]
344mod test {
345 use super::*;
346
347 #[test]
348 fn parse() -> Result<()> {
349 assert_eq!(
350 PackageVersion::parse("1:4.7.0+dfsg1-2")?,
351 PackageVersion {
352 epoch: Some(1),
353 upstream_version: "4.7.0+dfsg1".into(),
354 debian_revision: Some("2".into()),
355 }
356 );
357 assert_eq!(
358 PackageVersion::parse("3.3.2.final~github")?,
359 PackageVersion {
360 epoch: None,
361 upstream_version: "3.3.2.final~github".into(),
362 debian_revision: None,
363 }
364 );
365 assert_eq!(
366 PackageVersion::parse("3.3.2.final~github-2")?,
367 PackageVersion {
368 epoch: None,
369 upstream_version: "3.3.2.final~github".into(),
370 debian_revision: Some("2".into()),
371 }
372 );
373 assert_eq!(
374 PackageVersion::parse("0.18.0+dfsg-2+b1")?,
375 PackageVersion {
376 epoch: None,
377 upstream_version: "0.18.0+dfsg".into(),
378 debian_revision: Some("2+b1".into())
379 }
380 );
381
382 Ok(())
383 }
384
385 #[test]
386 fn format() -> Result<()> {
387 for s in ["1:4.7.0+dfsg1-2", "3.3.2.final~github", "0.18.0+dfsg-2+b1"] {
388 let v = PackageVersion::parse(s)?;
389 assert_eq!(format!("{}", v), s);
390 }
391
392 Ok(())
393 }
394
395 #[test]
396 fn test_lexical_compare() {
397 assert_eq!(lexical_compare("~~", "~~a"), Ordering::Less);
398 assert_eq!(lexical_compare("~~a", "~~"), Ordering::Greater);
399 assert_eq!(lexical_compare("~~a", "~"), Ordering::Less);
400 assert_eq!(lexical_compare("~", "~~a"), Ordering::Greater);
401 assert_eq!(lexical_compare("~", ""), Ordering::Less);
402 assert_eq!(lexical_compare("", "~"), Ordering::Greater);
403 assert_eq!(lexical_compare("", "a"), Ordering::Less);
404 assert_eq!(lexical_compare("a", ""), Ordering::Greater);
405 assert_eq!(lexical_compare("a", "b"), Ordering::Less);
406 assert_eq!(lexical_compare("b", "a"), Ordering::Greater);
407 assert_eq!(lexical_compare("c", "db"), Ordering::Less);
408 assert_eq!(lexical_compare("b", "+a"), Ordering::Less);
409
410 }
412
413 #[test]
414 fn test_compare_component() {
415 assert_eq!(
416 compare_component("1.0~beta1~svn1245", "1.0~beta1"),
417 Ordering::Less
418 );
419 assert_eq!(compare_component("1.0~beta1", "1.0"), Ordering::Less);
420 }
421
422 #[test]
423 fn compare_version() {
424 assert_eq!(
425 PackageVersion {
426 epoch: Some(1),
427 upstream_version: "ignored".into(),
428 debian_revision: None,
429 }
430 .cmp(&PackageVersion {
431 epoch: Some(0),
432 upstream_version: "ignored".into(),
433 debian_revision: None
434 }),
435 Ordering::Greater
436 );
437 }
438}