Skip to main content

oxihuman_core/
directory_scanner.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Recursive directory scanner stub.
6
7/// A directory entry.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct DirEntry {
10    pub path: String,
11    pub is_dir: bool,
12    pub size_bytes: u64,
13}
14
15impl DirEntry {
16    pub fn file(path: &str, size_bytes: u64) -> Self {
17        DirEntry {
18            path: path.to_string(),
19            is_dir: false,
20            size_bytes,
21        }
22    }
23
24    pub fn dir(path: &str) -> Self {
25        DirEntry {
26            path: path.to_string(),
27            is_dir: true,
28            size_bytes: 0,
29        }
30    }
31}
32
33/// Scanner configuration.
34#[derive(Debug, Clone)]
35pub struct ScanConfig {
36    pub max_depth: usize,
37    pub follow_symlinks: bool,
38    pub include_hidden: bool,
39}
40
41impl Default for ScanConfig {
42    fn default() -> Self {
43        ScanConfig {
44            max_depth: 64,
45            follow_symlinks: false,
46            include_hidden: false,
47        }
48    }
49}
50
51/// Directory scanner stub — holds a virtual file tree.
52pub struct DirectoryScanner {
53    pub config: ScanConfig,
54    virtual_tree: Vec<DirEntry>,
55}
56
57impl DirectoryScanner {
58    pub fn new(config: ScanConfig) -> Self {
59        DirectoryScanner {
60            config,
61            virtual_tree: Vec::new(),
62        }
63    }
64
65    pub fn add_entry(&mut self, entry: DirEntry) {
66        self.virtual_tree.push(entry);
67    }
68
69    pub fn scan(&self, _root: &str) -> Vec<DirEntry> {
70        self.virtual_tree.clone()
71    }
72
73    pub fn file_count(&self) -> usize {
74        self.virtual_tree.iter().filter(|e| !e.is_dir).count()
75    }
76
77    pub fn dir_count(&self) -> usize {
78        self.virtual_tree.iter().filter(|e| e.is_dir).count()
79    }
80}
81
82impl Default for DirectoryScanner {
83    fn default() -> Self {
84        Self::new(ScanConfig::default())
85    }
86}
87
88/// Create a default scanner.
89pub fn new_scanner() -> DirectoryScanner {
90    DirectoryScanner::default()
91}
92
93/// Filter entries by extension.
94pub fn filter_by_ext<'a>(entries: &'a [DirEntry], ext: &str) -> Vec<&'a DirEntry> {
95    entries.iter().filter(|e| e.path.ends_with(ext)).collect()
96}
97
98/// Total size of all file entries.
99pub fn total_size(entries: &[DirEntry]) -> u64 {
100    entries.iter().map(|e| e.size_bytes).sum()
101}
102
103/// Find entries whose path contains `needle`.
104pub fn find_by_name<'a>(entries: &'a [DirEntry], needle: &str) -> Vec<&'a DirEntry> {
105    entries.iter().filter(|e| e.path.contains(needle)).collect()
106}
107
108/// Sort entries by path.
109pub fn sort_entries(entries: &mut [DirEntry]) {
110    entries.sort_by(|a, b| a.path.cmp(&b.path));
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_new_scanner_empty() {
119        let s = new_scanner();
120        assert_eq!(s.file_count(), 0);
121        assert_eq!(s.dir_count(), 0);
122    }
123
124    #[test]
125    fn test_add_and_scan() {
126        let mut s = new_scanner();
127        s.add_entry(DirEntry::file("/tmp/a.txt", 100));
128        let entries = s.scan("/tmp");
129        assert_eq!(entries.len(), 1);
130    }
131
132    #[test]
133    fn test_file_dir_count() {
134        let mut s = new_scanner();
135        s.add_entry(DirEntry::file("/tmp/x", 50));
136        s.add_entry(DirEntry::dir("/tmp/subdir"));
137        assert_eq!(s.file_count(), 1);
138        assert_eq!(s.dir_count(), 1);
139    }
140
141    #[test]
142    fn test_filter_by_ext() {
143        let entries = vec![DirEntry::file("/a.rs", 10), DirEntry::file("/b.txt", 20)];
144        let rs = filter_by_ext(&entries, ".rs");
145        assert_eq!(rs.len(), 1);
146    }
147
148    #[test]
149    fn test_total_size() {
150        let entries = vec![DirEntry::file("/a", 100), DirEntry::file("/b", 200)];
151        assert_eq!(total_size(&entries), 300);
152    }
153
154    #[test]
155    fn test_find_by_name() {
156        let entries = vec![
157            DirEntry::file("/foo/bar.rs", 10),
158            DirEntry::file("/baz/qux.rs", 20),
159        ];
160        let found = find_by_name(&entries, "foo");
161        assert_eq!(found.len(), 1);
162    }
163
164    #[test]
165    fn test_sort_entries() {
166        let mut entries = vec![DirEntry::file("/z.txt", 1), DirEntry::file("/a.txt", 1)];
167        sort_entries(&mut entries);
168        assert_eq!(entries[0].path, "/a.txt");
169    }
170
171    #[test]
172    fn test_scan_config_default() {
173        let cfg = ScanConfig::default();
174        assert!(!cfg.follow_symlinks);
175        assert_eq!(cfg.max_depth, 64);
176    }
177
178    #[test]
179    fn test_dir_entry_is_dir_flag() {
180        let d = DirEntry::dir("/mydir");
181        assert!(d.is_dir);
182        let f = DirEntry::file("/myfile", 0);
183        assert!(!f.is_dir);
184    }
185
186    #[test]
187    fn test_total_size_dirs_excluded() {
188        let entries = vec![DirEntry::dir("/subdir"), DirEntry::file("/file", 77)];
189        /* dirs contribute 0 to size */
190        assert_eq!(total_size(&entries), 77);
191    }
192}