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 {
30 use std::cmp::Ordering;
31 let ab = a.as_bytes();
32 let bb = b.as_bytes();
33 let mut i = 0usize;
34 let mut j = 0usize;
35
36 while i < ab.len() || j < bb.len() {
37 if i >= ab.len() {
38 return Ordering::Less;
39 }
40 if j >= bb.len() {
41 return Ordering::Greater;
42 }
43
44 let a_digit = ab[i].is_ascii_digit();
45 let b_digit = bb[j].is_ascii_digit();
46
47 if a_digit && b_digit {
48 let a_zeros = count_leading(ab, i, b'0');
50 let b_zeros = count_leading(bb, j, b'0');
51 let i0 = i + a_zeros;
52 let j0 = j + b_zeros;
53 let a_digits = count_digits(ab, i0);
54 let b_digits = count_digits(bb, j0);
55
56 match (a_zeros > 0, b_zeros > 0) {
59 (true, false) => {
60 }
63 (false, true) => {}
64 _ => {}
65 }
66
67 match a_digits.cmp(&b_digits) {
69 Ordering::Equal => {
70 let acmp = ab[i0..i0 + a_digits].cmp(&bb[j0..j0 + b_digits]);
71 if acmp != Ordering::Equal {
72 return acmp;
73 }
74 if a_zeros != b_zeros {
76 return b_zeros.cmp(&a_zeros);
77 }
78 }
79 ord => return ord,
80 }
81 i = i0 + a_digits;
82 j = j0 + b_digits;
83 continue;
84 }
85
86 if a_digit != b_digit {
87 return ab[i].cmp(&bb[j]);
89 }
90
91 while i < ab.len() && j < bb.len() && !ab[i].is_ascii_digit() && !bb[j].is_ascii_digit() {
93 match ab[i].cmp(&bb[j]) {
94 Ordering::Equal => {
95 i += 1;
96 j += 1;
97 }
98 ord => return ord,
99 }
100 }
101 if i < ab.len() && j < bb.len() {
102 if ab[i].is_ascii_digit() != bb[j].is_ascii_digit() {
104 return ab[i].cmp(&bb[j]);
105 }
106 }
107 }
108 Ordering::Equal
109}
110
111fn count_leading(bytes: &[u8], start: usize, ch: u8) -> usize {
112 let mut n = 0;
113 while start + n < bytes.len() && bytes[start + n] == ch {
114 n += 1;
115 }
116 n
117}
118
119fn count_digits(bytes: &[u8], start: usize) -> usize {
120 let mut n = 0;
121 while start + n < bytes.len() && bytes[start + n].is_ascii_digit() {
122 n += 1;
123 }
124 n
125}
126
127pub(crate) fn cmp_entry(a: &Entry, b: &Entry, opts: &ListOptions) -> std::cmp::Ordering {
128 use std::cmp::Ordering;
129
130 if opts.dirs_first {
131 let a_dir = a.kind == EntryKind::Directory;
132 let b_dir = b.kind == EntryKind::Directory;
133 match (a_dir, b_dir) {
134 (true, false) => return Ordering::Less,
135 (false, true) => return Ordering::Greater,
136 _ => {}
137 }
138 }
139
140 let name_cmp = || cmp_name_with_mode(&a.name, &b.name, opts.gnu_mode);
141
142 let primary = match opts.sort_by {
143 SortBy::None => Ordering::Equal,
144 SortBy::Name => name_cmp(),
145 SortBy::Size => b.size.cmp(&a.size).then_with(name_cmp),
147 SortBy::Time => {
148 let ta = a.time_for(opts.time_field);
149 let tb = b.time_for(opts.time_field);
150 tb.cmp(&ta).then_with(name_cmp)
152 }
153 SortBy::Extension => {
154 let ea = a.extension().unwrap_or("");
155 let eb = b.extension().unwrap_or("");
156 if opts.gnu_mode {
157 ea.cmp(eb).then_with(name_cmp)
158 } else {
159 ea.to_ascii_lowercase()
160 .cmp(&eb.to_ascii_lowercase())
161 .then_with(name_cmp)
162 }
163 }
164 SortBy::Version => cmp_version(&a.name, &b.name).then_with(name_cmp),
165 };
166
167 if opts.reverse {
168 primary.reverse()
169 } else {
170 primary
171 }
172}
173
174pub fn sort_entries(entries: &mut [Entry], opts: &ListOptions) {
176 if opts.sort_by == SortBy::None {
177 if opts.reverse {
178 entries.reverse();
179 }
180 return;
181 }
182
183 entries.sort_by(|a, b| match (a.is_dir_header, b.is_dir_header) {
184 (true, false) => std::cmp::Ordering::Less,
185 (false, true) => std::cmp::Ordering::Greater,
186 _ => cmp_entry(a, b, opts),
187 });
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::entry::{Entry, EntryKind, GitStatus, TimeField};
194 use std::path::PathBuf;
195 use std::time::{Duration, SystemTime};
196
197 fn entry(name: &str, kind: EntryKind, size: u64, secs: Option<u64>) -> Entry {
198 Entry {
199 path: PathBuf::from(name),
200 name: name.to_string(),
201 kind,
202 size,
203 modified: secs.map(|s| SystemTime::UNIX_EPOCH + Duration::from_secs(s)),
204 created: None,
205 accessed: None,
206 changed: secs.map(|s| SystemTime::UNIX_EPOCH + Duration::from_secs(s)),
207 mode: 0,
208 readonly: false,
209 symlink_target: None,
210 depth: 0,
211 git_status: GitStatus::Clean,
212 is_dir_header: false,
213 nlink: 1,
214 uid: 0,
215 gid: 0,
216 inode: 0,
217 blocks: 0,
218 owner: "u".into(),
219 group: "g".into(),
220 author: "u".into(),
221 context: String::new(),
222 }
223 }
224
225 #[test]
226 fn sort_by_name_case_insensitive() {
227 let mut entries = vec![
228 entry("Banana", EntryKind::File, 0, None),
229 entry("apple", EntryKind::File, 0, None),
230 entry("Cherry", EntryKind::File, 0, None),
231 ];
232 sort_entries(&mut entries, &ListOptions::default());
233 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
234 assert_eq!(names, vec!["apple", "Banana", "Cherry"]);
235 }
236
237 #[test]
238 fn sort_by_size_largest_first() {
239 let mut entries = vec![
240 entry("big", EntryKind::File, 100, None),
241 entry("small", EntryKind::File, 1, None),
242 entry("mid", EntryKind::File, 50, None),
243 ];
244 let opts = ListOptions {
245 sort_by: SortBy::Size,
246 ..Default::default()
247 };
248 sort_entries(&mut entries, &opts);
249 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
250 assert_eq!(names, vec!["big", "mid", "small"]);
251 }
252
253 #[test]
254 fn sort_by_time_newest_first() {
255 let mut entries = vec![
256 entry("old", EntryKind::File, 0, Some(10)),
257 entry("new", EntryKind::File, 0, Some(100)),
258 entry("mid", EntryKind::File, 0, Some(50)),
259 ];
260 let opts = ListOptions {
261 sort_by: SortBy::Time,
262 ..Default::default()
263 };
264 sort_entries(&mut entries, &opts);
265 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
266 assert_eq!(names, vec!["new", "mid", "old"]);
267 }
268
269 #[test]
270 fn reverse_name_sort() {
271 let mut entries = vec![
272 entry("a", EntryKind::File, 0, None),
273 entry("b", EntryKind::File, 0, None),
274 entry("c", EntryKind::File, 0, None),
275 ];
276 let opts = ListOptions {
277 reverse: true,
278 ..Default::default()
279 };
280 sort_entries(&mut entries, &opts);
281 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
282 assert_eq!(names, vec!["c", "b", "a"]);
283 }
284
285 #[test]
286 fn dirs_first() {
287 let mut entries = vec![
288 entry("zfile", EntryKind::File, 0, None),
289 entry("adir", EntryKind::Directory, 0, None),
290 entry("bfile", EntryKind::File, 0, None),
291 ];
292 let opts = ListOptions {
293 dirs_first: true,
294 ..Default::default()
295 };
296 sort_entries(&mut entries, &opts);
297 assert_eq!(entries[0].name, "adir");
298 }
299
300 #[test]
301 fn sort_by_extension() {
302 let mut entries = vec![
303 entry("a.txt", EntryKind::File, 0, None),
304 entry("b.rs", EntryKind::File, 0, None),
305 entry("c.md", EntryKind::File, 0, None),
306 entry("noext", EntryKind::File, 0, None),
307 ];
308 let opts = ListOptions {
309 sort_by: SortBy::Extension,
310 ..Default::default()
311 };
312 sort_entries(&mut entries, &opts);
313 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
314 assert_eq!(names, vec!["noext", "c.md", "b.rs", "a.txt"]);
315 }
316
317 #[test]
318 fn sort_by_version() {
319 let mut entries = vec![
320 entry("file10.txt", EntryKind::File, 0, None),
321 entry("file2.txt", EntryKind::File, 0, None),
322 entry("file1.txt", EntryKind::File, 0, None),
323 ];
324 let opts = ListOptions {
325 sort_by: SortBy::Version,
326 ..Default::default()
327 };
328 sort_entries(&mut entries, &opts);
329 let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
330 assert_eq!(names, vec!["file1.txt", "file2.txt", "file10.txt"]);
331 }
332
333 #[test]
334 fn cmp_version_basic() {
335 use std::cmp::Ordering;
336 assert_eq!(cmp_version("a2", "a10"), Ordering::Less);
337 assert_eq!(cmp_version("a10", "a2"), Ordering::Greater);
338 assert_eq!(cmp_version("abc", "abc"), Ordering::Equal);
339 assert_eq!(cmp_version("file1", "file01"), Ordering::Greater); assert_eq!(cmp_version("file01", "file1"), Ordering::Less);
342 }
343
344 #[test]
345 fn cmp_name_strips_dot_for_key() {
346 assert!(cmp_name(".foo", "foo").is_le());
347 assert!(cmp_name("apple", "banana").is_lt());
348 }
349
350 #[test]
351 fn gnu_name_sort_is_bytewise() {
352 assert_eq!(cmp_name_with_mode("B", "a", true), std::cmp::Ordering::Less);
353 }
355
356 #[test]
357 fn time_field_access() {
358 let mut e = entry("x", EntryKind::File, 0, Some(50));
359 e.accessed = Some(SystemTime::UNIX_EPOCH + Duration::from_secs(10));
360 assert_eq!(
361 e.time_for(TimeField::Accessed),
362 Some(SystemTime::UNIX_EPOCH + Duration::from_secs(10))
363 );
364 }
365
366 #[test]
367 fn time_field_changed_uses_ctime() {
368 let mut e = entry("x", EntryKind::File, 0, Some(50));
369 e.changed = Some(SystemTime::UNIX_EPOCH + Duration::from_secs(7));
370 assert_eq!(
371 e.time_for(TimeField::Changed),
372 Some(SystemTime::UNIX_EPOCH + Duration::from_secs(7))
373 );
374 }
375}