From 63331ab1e562e01597d4318e9306eda2d07e8842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Zakraj=C5=A1ek?= Date: Sat, 7 Mar 2026 10:10:41 +0100 Subject: [PATCH 1/2] Add tests for repo files sorting --- vault-core/src/repo_files/selectors.rs | 360 +++++++++++++++++++++++++ 1 file changed, 360 insertions(+) diff --git a/vault-core/src/repo_files/selectors.rs b/vault-core/src/repo_files/selectors.rs index 1e38fa36..b51272c8 100644 --- a/vault-core/src/repo_files/selectors.rs +++ b/vault-core/src/repo_files/selectors.rs @@ -373,3 +373,363 @@ pub fn get_unused_name( used_names.contains(&DecryptedNameLower(name.to_lowercase())) })) } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::{ + files::file_category::FileCategory, + repo_files::state::{RepoFile, RepoFileName, RepoFilePath, RepoFileSize, RepoFileType}, + repo_files::state::{RepoFilesSort, RepoFilesSortField}, + sort::state::{SortDirection, SortGrouping}, + types::{ + DecryptedName, DecryptedPath, EncryptedPath, MountId, RemotePath, RepoFileId, RepoId, + }, + }; + + use super::*; + + #[allow(dead_code)] + fn create_repo_file( + typ: RepoFileType, + name: &str, + size: Option, + modified: Option, + ) -> RepoFile { + let id = RepoFileId(format!("r1:/{}", name)); + let path = format!("/{}", name); + + RepoFile { + id, + mount_id: MountId("m1".into()), + remote_path: RemotePath(path.clone()), + repo_id: RepoId("r1".into()), + encrypted_path: EncryptedPath(path.clone()), + path: RepoFilePath::Decrypted { + path: DecryptedPath(path), + }, + name: RepoFileName::Decrypted { + name: DecryptedName(name.into()), + name_lower: name.to_lowercase(), + }, + ext: None, + content_type: None, + typ: typ.clone(), + size: size.map(|size| RepoFileSize::Decrypted { size }), + modified, + tags: None, + unique_name: name.into(), + remote_hash: None, + category: match typ { + RepoFileType::Dir => FileCategory::Folder, + RepoFileType::File => FileCategory::Generic, + }, + } + } + + fn sort_names(files: Vec, sort: RepoFilesSort) -> Vec { + let file_ids: Vec = files.iter().map(|f| f.id.clone()).collect(); + let files_map: HashMap = + files.into_iter().map(|f| (f.id.clone(), f)).collect(); + + select_sorted_files(&files_map, file_ids.iter(), &sort) + .into_iter() + .map(|id| { + files_map + .get(&id) + .unwrap() + .decrypted_name() + .unwrap() + .0 + .clone() + }) + .collect() + } + + #[test] + fn test_select_sorted_files_dirs_first_name() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(30), Some(30)), + create_repo_file(RepoFileType::Dir, "z-dir", Some(50), Some(50)), + create_repo_file(RepoFileType::File, "a.txt", Some(10), Some(10)), + create_repo_file(RepoFileType::Dir, "a-dir", Some(40), Some(40)), + create_repo_file(RepoFileType::File, "b.txt", Some(20), Some(20)), + ]; + + let asc = sort_names( + files.clone(), + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::DirsFirst, + }, + ); + let desc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Desc, + grouping: SortGrouping::DirsFirst, + }, + ); + + assert_eq!(asc, vec!["a-dir", "z-dir", "a.txt", "b.txt", "c.txt"]); + assert_eq!(desc, vec!["z-dir", "a-dir", "c.txt", "b.txt", "a.txt"]); + } + + #[test] + fn test_select_sorted_files_dirs_first_size() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(30), Some(30)), + create_repo_file(RepoFileType::Dir, "z-dir", Some(50), Some(50)), + create_repo_file(RepoFileType::File, "a.txt", Some(10), Some(10)), + create_repo_file(RepoFileType::Dir, "a-dir", Some(40), Some(40)), + create_repo_file(RepoFileType::File, "b.txt", Some(20), Some(20)), + ]; + + let asc = sort_names( + files.clone(), + RepoFilesSort { + field: RepoFilesSortField::Size, + direction: SortDirection::Asc, + grouping: SortGrouping::DirsFirst, + }, + ); + let desc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Size, + direction: SortDirection::Desc, + grouping: SortGrouping::DirsFirst, + }, + ); + + assert_eq!(asc, vec!["a-dir", "z-dir", "a.txt", "b.txt", "c.txt"]); + assert_eq!(desc, vec!["a-dir", "z-dir", "c.txt", "b.txt", "a.txt"]); + } + + #[test] + fn test_select_sorted_files_dirs_first_modified() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(30), Some(30)), + create_repo_file(RepoFileType::Dir, "z-dir", Some(50), Some(50)), + create_repo_file(RepoFileType::File, "a.txt", Some(10), Some(10)), + create_repo_file(RepoFileType::Dir, "a-dir", Some(40), Some(40)), + create_repo_file(RepoFileType::File, "b.txt", Some(20), Some(20)), + ]; + + let asc = sort_names( + files.clone(), + RepoFilesSort { + field: RepoFilesSortField::Modified, + direction: SortDirection::Asc, + grouping: SortGrouping::DirsFirst, + }, + ); + let desc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Modified, + direction: SortDirection::Desc, + grouping: SortGrouping::DirsFirst, + }, + ); + + assert_eq!(asc, vec!["a-dir", "z-dir", "a.txt", "b.txt", "c.txt"]); + assert_eq!(desc, vec!["a-dir", "z-dir", "c.txt", "b.txt", "a.txt"]); + } + + #[test] + fn test_select_sorted_files_no_grouping_name() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(30), Some(30)), + create_repo_file(RepoFileType::Dir, "z-dir", Some(50), Some(50)), + create_repo_file(RepoFileType::File, "a.txt", Some(10), Some(10)), + create_repo_file(RepoFileType::Dir, "a-dir", Some(40), Some(40)), + create_repo_file(RepoFileType::File, "b.txt", Some(20), Some(20)), + ]; + + let asc = sort_names( + files.clone(), + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + let desc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Desc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["a-dir", "a.txt", "b.txt", "c.txt", "z-dir"]); + assert_eq!(desc, vec!["z-dir", "c.txt", "b.txt", "a.txt", "a-dir"]); + } + + #[test] + fn test_select_sorted_files_no_grouping_size() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(30), Some(30)), + create_repo_file(RepoFileType::Dir, "z-dir", Some(50), Some(50)), + create_repo_file(RepoFileType::File, "a.txt", Some(10), Some(10)), + create_repo_file(RepoFileType::Dir, "a-dir", Some(40), Some(40)), + create_repo_file(RepoFileType::File, "b.txt", Some(20), Some(20)), + ]; + + let asc = sort_names( + files.clone(), + RepoFilesSort { + field: RepoFilesSortField::Size, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + let desc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Size, + direction: SortDirection::Desc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["a.txt", "b.txt", "c.txt", "a-dir", "z-dir"]); + assert_eq!(desc, vec!["z-dir", "a-dir", "c.txt", "b.txt", "a.txt"]); + } + + #[test] + fn test_select_sorted_files_no_grouping_modified() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(30), Some(30)), + create_repo_file(RepoFileType::Dir, "z-dir", Some(50), Some(50)), + create_repo_file(RepoFileType::File, "a.txt", Some(10), Some(10)), + create_repo_file(RepoFileType::Dir, "a-dir", Some(40), Some(40)), + create_repo_file(RepoFileType::File, "b.txt", Some(20), Some(20)), + ]; + + let asc = sort_names( + files.clone(), + RepoFilesSort { + field: RepoFilesSortField::Modified, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + let desc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Modified, + direction: SortDirection::Desc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["a.txt", "b.txt", "c.txt", "a-dir", "z-dir"]); + assert_eq!(desc, vec!["z-dir", "a-dir", "c.txt", "b.txt", "a.txt"]); + } + + #[test] + fn test_select_sorted_files_name_case_insensitive() { + let files = vec![ + create_repo_file(RepoFileType::File, "c.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "B.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "a.txt", Some(1), Some(1)), + ]; + + let asc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["a.txt", "B.txt", "c.txt"]); + } + + #[test] + fn test_select_sorted_files_name_numeric_prefix() { + let files = vec![ + create_repo_file(RepoFileType::File, "2foo.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "1foo.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "19foo.txt", Some(1), Some(1)), + ]; + + let asc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["19foo.txt", "1foo.txt", "2foo.txt"]); + } + + #[test] + fn test_select_sorted_files_name_numeric_prefix_with_space() { + let files = vec![ + create_repo_file(RepoFileType::File, "2 foo.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "1 foo.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "19 foo.txt", Some(1), Some(1)), + ]; + + let asc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["1 foo.txt", "19 foo.txt", "2 foo.txt"]); + } + + #[test] + fn test_select_sorted_files_name_numeric_suffix() { + let files = vec![ + create_repo_file(RepoFileType::File, "foo2.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "foo1.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "foo19.txt", Some(1), Some(1)), + ]; + + let asc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["foo1.txt", "foo19.txt", "foo2.txt"]); + } + + #[test] + fn test_select_sorted_files_name_numeric_suffix_with_space() { + let files = vec![ + create_repo_file(RepoFileType::File, "foo 2.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "foo 1.txt", Some(1), Some(1)), + create_repo_file(RepoFileType::File, "foo 19.txt", Some(1), Some(1)), + ]; + + let asc = sort_names( + files, + RepoFilesSort { + field: RepoFilesSortField::Name, + direction: SortDirection::Asc, + grouping: SortGrouping::NoGrouping, + }, + ); + + assert_eq!(asc, vec!["foo 1.txt", "foo 19.txt", "foo 2.txt"]); + } +} From 1ea8a491f1bb9f12e3f20e2d932e683268174134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luka=20Zakraj=C5=A1ek?= Date: Sat, 7 Mar 2026 10:14:30 +0100 Subject: [PATCH 2/2] Use natural ordering for repo file names --- Cargo.lock | 7 +++++++ vault-core/Cargo.toml | 1 + vault-core/src/repo_files/selectors.rs | 14 +++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd7ca393..31b2418f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3181,6 +3181,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + [[package]] name = "ndk" version = "0.9.0" @@ -6652,6 +6658,7 @@ dependencies = [ "lazy_static", "log", "md5", + "natord", "phf 0.11.3", "pin-project-lite", "rand_core 0.6.4", diff --git a/vault-core/Cargo.toml b/vault-core/Cargo.toml index 441ba7a9..67bd924e 100644 --- a/vault-core/Cargo.toml +++ b/vault-core/Cargo.toml @@ -17,6 +17,7 @@ http-body-util = "0.1.3" lazy_static = "1.5.0" log = "0.4.29" md5 = "0.7.0" +natord = "1.0.9" phf = { version = "0.11.3", features = ["macros"] } pin-project-lite = "0.2.16" # rand_core cannot be upgraded to > 0.6 because vault-crypto depends on diff --git a/vault-core/src/repo_files/selectors.rs b/vault-core/src/repo_files/selectors.rs index b51272c8..7a3adddc 100644 --- a/vault-core/src/repo_files/selectors.rs +++ b/vault-core/src/repo_files/selectors.rs @@ -303,10 +303,10 @@ where match field { RepoFilesSortField::Name => { dirs.sort_by(|a, b| { - direction.ordering(a.name_lower_force().cmp(b.name_lower_force())) + direction.ordering(natord::compare(a.name_lower_force(), b.name_lower_force())) }); files.sort_by(|a, b| { - direction.ordering(a.name_lower_force().cmp(b.name_lower_force())) + direction.ordering(natord::compare(a.name_lower_force(), b.name_lower_force())) }); } RepoFilesSortField::Size => { @@ -333,7 +333,7 @@ where match field { RepoFilesSortField::Name => { files.sort_by(|a, b| { - direction.ordering(a.name_lower_force().cmp(b.name_lower_force())) + direction.ordering(natord::compare(a.name_lower_force(), b.name_lower_force())) }); } RepoFilesSortField::Size => { @@ -670,7 +670,7 @@ mod tests { }, ); - assert_eq!(asc, vec!["19foo.txt", "1foo.txt", "2foo.txt"]); + assert_eq!(asc, vec!["1foo.txt", "2foo.txt", "19foo.txt"]); } #[test] @@ -690,7 +690,7 @@ mod tests { }, ); - assert_eq!(asc, vec!["1 foo.txt", "19 foo.txt", "2 foo.txt"]); + assert_eq!(asc, vec!["1 foo.txt", "2 foo.txt", "19 foo.txt"]); } #[test] @@ -710,7 +710,7 @@ mod tests { }, ); - assert_eq!(asc, vec!["foo1.txt", "foo19.txt", "foo2.txt"]); + assert_eq!(asc, vec!["foo1.txt", "foo2.txt", "foo19.txt"]); } #[test] @@ -730,6 +730,6 @@ mod tests { }, ); - assert_eq!(asc, vec!["foo 1.txt", "foo 19.txt", "foo 2.txt"]); + assert_eq!(asc, vec!["foo 1.txt", "foo 2.txt", "foo 19.txt"]); } }