rune/library/filevercmp.rs
1use std::cmp::{Ord, Ordering};
2
3/// Return the length of a prefix of S that corresponds to the suffix
4/// defined by this extended regular expression in the C locale:
5/// (\.[A-Za-z~][A-Za-z0-9~]*)*$
6/// Use the longest suffix matching this regular expression,
7/// except do not use all of S as a suffix if S is nonempty.
8/// If *LEN is -1, S is a string; set *LEN to S's length.
9/// Otherwise, *LEN should be nonnegative, S is a char array,
10/// and *LEN does not change.
11fn prefix_len(s: &[u8]) -> (usize, usize) {
12 let mut prefix_len = 0;
13
14 let mut i = 0;
15 while i < s.len() {
16 i += 1;
17 prefix_len = i;
18
19 while i + 1 < s.len()
20 && s[i] == b'.'
21 && (s[i + 1].is_ascii_alphabetic() || s[i + 1] == b'~')
22 {
23 i += 2;
24 while i < s.len() && (s[i].is_ascii_alphanumeric() || s[i] == b'~') {
25 i += 1;
26 }
27 }
28 }
29 (prefix_len, i)
30}
31
32/// Return a version sort comparison value for S's byte at position POS. If POS
33/// == LEN, sort before all non-'~' bytes.
34fn order(s: &[u8], pos: usize) -> i32 {
35 if pos == s.len() {
36 return -1;
37 }
38
39 let c = s[pos];
40
41 match c {
42 b'0'..=b'9' => 0,
43 b'A'..=b'Z' | b'a'..=b'z' => c as i32,
44 b'~' => -2,
45 _ => c as i32 + 256,
46 }
47}
48
49/// slightly modified verrevcmp function from dpkg
50/// S1, S2 - compared char array
51///
52/// This implements the algorithm for comparison of version strings
53/// specified by Debian and now widely adopted. The detailed
54/// specification can be found in the Debian Policy Manual in the
55/// section on the 'Version' control field. This version of the code
56/// implements that from s5.6.12 of [Debian Policy v3.8.0.1](https://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version)
57fn verrevcmp(s1: &[u8], s2: &[u8]) -> i32 {
58 let mut s1_pos = 0;
59 let mut s2_pos = 0;
60
61 while s1_pos < s1.len() && s2_pos < s2.len() {
62 let mut first_diff: i32 = 0;
63
64 while (s1_pos < s1.len() && !s1[s1_pos].is_ascii_digit())
65 || (s2_pos < s2.len() && !s2[s2_pos].is_ascii_digit())
66 {
67 let s1_c = order(s1, s1_pos);
68 let s2_c = order(s2, s2_pos);
69 if s1_c != s2_c {
70 return s1_c - s2_c;
71 }
72 s1_pos += 1;
73 s2_pos += 1;
74 }
75
76 while s1_pos < s1.len() && s1[s1_pos] == b'0' {
77 s1_pos += 1;
78 }
79 while s2_pos < s2.len() && s2[s2_pos] == b'0' {
80 s2_pos += 1;
81 }
82 while s1_pos < s1.len()
83 && s2_pos < s2.len()
84 && s1[s1_pos].is_ascii_digit()
85 && s2[s2_pos].is_ascii_digit()
86 {
87 if first_diff == 0 {
88 first_diff = s1[s1_pos] as i32 - s2[s2_pos] as i32;
89 }
90 s1_pos += 1;
91 s2_pos += 1;
92 }
93 if s1_pos < s1.len() && s1[s1_pos].is_ascii_digit() {
94 return 1;
95 }
96 if s2_pos < s2.len() && s2[s2_pos].is_ascii_digit() {
97 return -1;
98 }
99 if first_diff != 0 {
100 return first_diff;
101 }
102 }
103 0
104}
105
106/// Compare strings A and B as file names containing version numbers,
107/// and return an integer that is negative, zero, or positive depending
108/// on whether A compares less than, equal to, or greater than B.
109///
110/// Use the following version sort algorithm:
111///
112/// 1. Compare the strings' maximal-length non-digit prefixes lexically.
113/// If there is a difference return that difference.
114/// Otherwise discard the prefixes and continue with the next step.
115///
116/// 2. Compare the strings' maximal-length digit prefixes, using
117/// numeric comparison of the numbers represented by each prefix.
118/// (Treat an empty prefix as zero; this can happen only at string end.)
119/// If there is a difference, return that difference.
120/// Otherwise discard the prefixes and continue with the next step.
121///
122/// 3. If both strings are empty, return 0. Otherwise continue with step 1.
123///
124/// In version sort, lexical comparison is left to right, byte by byte,
125/// using the byte's numeric value (0-255), except that:
126///
127/// 1. ASCII letters sort before other bytes.
128/// 2. A tilde sorts before anything, even an empty string.
129///
130/// In addition to the version sort rules, the following strings have
131/// special priority and sort before all other strings (listed in order):
132///
133/// 1. The empty string.
134/// 2. ".".
135/// 3. "..".
136/// 4. Strings starting with "." sort before other strings.
137///
138/// Before comparing two strings where both begin with non-".",
139/// or where both begin with "." but neither is "." or "..",
140/// suffixes matching the C-locale extended regular expression
141/// (\.[A-Za-z~][A-Za-z0-9~]*)*$ are removed and the strings compared
142/// without them, using version sort without special priority;
143/// if they do not compare equal, this comparison result is used and
144/// the suffixes are effectively ignored. Otherwise, the entire
145/// strings are compared using version sort. When removing a suffix
146/// from a nonempty string, remove the maximal-length suffix such that
147/// the remaining string is nonempty.
148///
149/// This function is intended to be a replacement for strverscmp.
150pub(crate) fn filevercmp(a: &[u8], b: &[u8]) -> Ordering {
151 const D: u8 = b'.';
152 match (a, b) {
153 ([], []) | ([D], [D]) | ([D, D], [D, D]) => return Ordering::Equal,
154 ([], [..]) | ([D], [_, ..]) | ([D, D], [_, _, ..]) => return Ordering::Less,
155 ([..], []) | ([_, ..], [D]) | ([_, _, ..], [D, D]) => return Ordering::Greater,
156 _ => {}
157 }
158
159 let (a_prefix_len, a_len) = prefix_len(a);
160 let (b_prefix_len, b_len) = prefix_len(b);
161
162 let one_pass_only = a_prefix_len == a_len && b_prefix_len == b_len;
163
164 let result = verrevcmp(&a[..a_prefix_len], &b[..b_prefix_len]);
165
166 if result != 0 || one_pass_only {
167 result.cmp(&0)
168 } else {
169 let result = verrevcmp(&a[..a_len], &b[..b_len]);
170 result.cmp(&0)
171 }
172}