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}