1use crate::entry::{Entry, EntryKind};
2use crate::options::{ListOptions, SortBy};
3
4pub fn cmp_name(a: &str, b: &str) -> std::cmp::Ordering {
8 cmp_name_with_mode(a, b, false)
9}
10
11pub fn cmp_name_with_mode(a: &str, b: &str, gnu: bool) -> std::cmp::Ordering {
12 if gnu {
13 return a.cmp(b);
15 }
16 let a_key = a.trim_start_matches('.').to_ascii_lowercase();
18 let b_key = b.trim_start_matches('.').to_ascii_lowercase();
19 match a_key.cmp(&b_key) {
20 std::cmp::Ordering::Equal => a.cmp(b),
21 other => other,
22 }
23}
24
25pub fn cmp_version(a: &str, b: &str) -> std::cmp::Ordering {
31 use std::cmp::Ordering;
32 let ab = a.as_bytes();
33 let bb = b.as_bytes();
34 let mut i = 0usize;
35 let mut j = 0usize;
36
37 while i < ab.len() || j < bb.len() {
38 if i >= ab.len() {
40 return if bb[j] == b'~' {
41 Ordering::Greater
42 } else {
43 Ordering::Less
44 };
45 }
46 if j >= bb.len() {
47 return if ab[i] == b'~' {
48 Ordering::Less
49 } else {
50 Ordering::Greater
51 };
52 }
53
54 let a_digit = ab[i].is_ascii_digit();
55 let b_digit = bb[j].is_ascii_digit();
56
57 if a_digit && b_digit {
58 let a_zeros = count_leading(ab, i, b'0');
60 let b_zeros = count_leading(bb, j, b'0');
61 let i0 = i + a_zeros;
62 let j0 = j + b_zeros;
63 let a_digits = count_digits(ab, i0);
64 let b_digits = count_digits(bb, j0);
65
66 match a_digits.cmp(&b_digits) {
68 Ordering::Equal => {
69 let acmp = ab[i0..i0 + a_digits].cmp(&bb[j0..j0 + b_digits]);
70 if acmp != Ordering::Equal {
71 return acmp;
72 }
73 if a_zeros != b_zeros {
75 return b_zeros.cmp(&a_zeros);
76 }
77 }
78 ord => return ord,
79 }
80 i = i0 + a_digits;
81 j = j0 + b_digits;
82 continue;
83 }
84
85 if a_digit != b_digit {
86 return cmp_nondigit_byte(ab[i], bb[j]);
87 }
88
89 while i < ab.len() && j < bb.len() && !ab[i].is_ascii_digit() && !bb[j].is_ascii_digit() {
91 match cmp_nondigit_byte(ab[i], bb[j]) {
92 Ordering::Equal => {
93 i += 1;
94 j += 1;
95 }
96 ord => return ord,
97 }
98 }
99 if i < ab.len() && j < bb.len() {
100 if ab[i].is_ascii_digit() != bb[j].is_ascii_digit() {
102 return cmp_nondigit_byte(ab[i], bb[j]);
103 }
104 }
105 }
106 Ordering::Equal
107}
108
109fn cmp_nondigit_byte(a: u8, b: u8) -> std::cmp::Ordering {
111 use std::cmp::Ordering;
112 if a == b {
113 return Ordering::Equal;
114 }
115 if a == b'~' {
116 return Ordering::Less;
117 }
118 if b == b'~' {
119 return Ordering::Greater;
120 }
121 a.cmp(&b)
122}
123
124fn count_leading(bytes: &[u8], start: usize, ch: u8) -> usize {
125 let mut n = 0;
126 while start + n < bytes.len() && bytes[start + n] == ch {
127 n += 1;
128 }
129 n
130}
131
132fn count_digits(bytes: &[u8], start: usize) -> usize {
133 let mut n = 0;
134 while start + n < bytes.len() && bytes[start + n].is_ascii_digit() {
135 n += 1;
136 }
137 n
138}
139
140pub(crate) fn cmp_entry(a: &Entry, b: &Entry, opts: &ListOptions) -> std::cmp::Ordering {
141 use std::cmp::Ordering;
142
143 if opts.dirs_first {
144 let a_dir = a.kind == EntryKind::Directory;
145 let b_dir = b.kind == EntryKind::Directory;
146 match (a_dir, b_dir) {
147 (true, false) => return Ordering::Less,
148 (false, true) => return Ordering::Greater,
149 _ => {}
150 }
151 }
152
153 let name_cmp = || cmp_name_with_mode(&a.name, &b.name, opts.gnu_mode);
154
155 let primary = match opts.sort_by {
156 SortBy::None => Ordering::Equal,
157 SortBy::Name => name_cmp(),
158 SortBy::Size => b.size.cmp(&a.size).then_with(name_cmp),
160 SortBy::Time => {
161 let ta = a.time_for(opts.time_field);
162 let tb = b.time_for(opts.time_field);
163 tb.cmp(&ta).then_with(name_cmp)
165 }
166 SortBy::Extension => {
167 let ea = a.extension().unwrap_or("");
168 let eb = b.extension().unwrap_or("");
169 if opts.gnu_mode {
170 ea.cmp(eb).then_with(name_cmp)
171 } else {
172 ea.to_ascii_lowercase()
173 .cmp(&eb.to_ascii_lowercase())
174 .then_with(name_cmp)
175 }
176 }
177 SortBy::Version => cmp_version(&a.name, &b.name).then_with(name_cmp),
178 SortBy::Width => display_width(&a.name)
180 .cmp(&display_width(&b.name))
181 .then_with(name_cmp),
182 };
183
184 if opts.reverse {
185 primary.reverse()
186 } else {
187 primary
188 }
189}
190
191fn display_width(s: &str) -> usize {
193 s.chars()
194 .map(|c| {
195 let u = c as u32;
196 if c == '\0' {
197 0
198 } else if u < 0x1100 {
199 1
200 } else if (0x1100..=0x115f).contains(&u)
201 || u == 0x2329
202 || u == 0x232a
203 || (0x2e80..=0xa4cf).contains(&u)
204 || (0xac00..=0xd7a3).contains(&u)
205 || (0xf900..=0xfaff).contains(&u)
206 || (0xfe10..=0xfe19).contains(&u)
207 || (0xfe30..=0xfe6f).contains(&u)
208 || (0xff00..=0xff60).contains(&u)
209 || (0xffe0..=0xffe6).contains(&u)
210 {
211 2
212 } else {
213 1
214 }
215 })
216 .sum()
217}
218
219pub fn sort_entries(entries: &mut [Entry], opts: &ListOptions) {
221 if opts.sort_by == SortBy::None {
222 if opts.reverse {
223 entries.reverse();
224 }
225 return;
226 }
227
228 entries.sort_by(|a, b| match (a.is_dir_header, b.is_dir_header) {
229 (true, false) => std::cmp::Ordering::Less,
230 (false, true) => std::cmp::Ordering::Greater,
231 _ => cmp_entry(a, b, opts),
232 });
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::entry::{Entry, EntryKind, GitStatus};
239 use std::path::PathBuf;
240 use std::time::{Duration, SystemTime};
241
242 fn entry(name: &str, kind: EntryKind, size: u64, secs: Option<u64>) -> Entry {
243 Entry {
244 path: PathBuf::from(name),
245 name: name.to_string(),
246 kind,
247 size,
248 modified: secs.map(|s| SystemTime::UNIX_EPOCH + Duration::from_secs(s)),
249 created: None,
250 accessed: None,
251 changed: secs.map(|s| SystemTime::UNIX_EPOCH + Duration::from_secs(s)),
252 mode: 0,
253 readonly: false,
254 symlink_target: None,
255 depth: 0,
256 git_status: GitStatus::Clean,
257 is_dir_header: false,
258 nlink: 1,
259 uid: 0,
260 gid: 0,
261 inode: 0,
262 blocks: 0,
263 owner: "u".into(),
264 group: "g".into(),
265 author: "u".into(),
266 context: String::new(),
267 }
268 }
269
270 #[test]
271 fn version_tilde_before_digits() {
272 use std::cmp::Ordering;
273 assert_eq!(cmp_version("file~", "file1"), Ordering::Less);
274 assert_eq!(cmp_version("file1", "file2"), Ordering::Less);
275 assert_eq!(cmp_version("file2", "file10"), Ordering::Less);
276 assert_eq!(cmp_version("file~", "file10"), Ordering::Less);
277 }
278
279 #[test]
280 fn width_sort_shorter_first() {
281 let opts = ListOptions {
282 sort_by: SortBy::Width,
283 ..Default::default()
284 };
285 let a = entry("a", EntryKind::File, 0, None);
286 let b = entry("bbbb", EntryKind::File, 0, None);
287 assert_eq!(cmp_entry(&a, &b, &opts), std::cmp::Ordering::Less);
288 }
289}