1use std::path::Path;
8use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
9
10use ignore::{DirEntry, WalkBuilder, WalkState};
11
12use crate::policy;
13
14pub(crate) fn allow_dir_entry(depth: usize, name: &str, is_dir: bool) -> bool {
15 if depth == 0 {
16 return true;
17 }
18 if is_dir {
19 !policy::is_pruned_dir(name)
20 } else {
21 !policy::is_pruned_file(name)
22 }
23}
24
25#[cfg(test)]
26pub(crate) fn rel_is_empty(rel: &str) -> bool {
27 rel.is_empty()
28}
29
30#[cfg(test)]
31pub(crate) fn entry_is_err<T, E>(r: &Result<T, E>) -> bool {
32 r.is_err()
33}
34
35#[derive(Debug, Clone)]
37pub struct WalkEntry {
38 pub rel: Box<str>,
40 pub is_dir: bool,
42 pub size: u64,
44}
45
46#[derive(Debug, Default, Clone, Copy)]
48pub struct WalkStats {
49 pub scanned: usize,
51 pub truncated: bool,
53}
54
55pub fn walk_root(
61 root: &Path,
62 threads: usize,
63 max_entries: usize,
64 scanned: &AtomicUsize,
65 cancel: &AtomicBool,
66 on_entry: &(dyn Fn(WalkEntry) + Sync),
67) -> WalkStats {
68 walk_root_limited(
69 root,
70 threads,
71 max_entries,
72 usize::MAX,
73 scanned,
74 cancel,
75 on_entry,
76 )
77}
78
79pub fn walk_root_limited(
82 root: &Path,
83 threads: usize,
84 max_entries: usize,
85 max_depth: usize,
86 scanned: &AtomicUsize,
87 cancel: &AtomicBool,
88 on_entry: &(dyn Fn(WalkEntry) + Sync),
89) -> WalkStats {
90 let mut builder = WalkBuilder::new(root);
91 builder
92 .hidden(false) .follow_links(false)
94 .git_ignore(true)
95 .git_global(true)
96 .git_exclude(true)
97 .ignore(true)
98 .require_git(false)
99 .max_depth(if max_depth == usize::MAX {
100 None
101 } else {
102 Some(max_depth)
103 })
104 .threads(threads.max(1));
105
106 builder.filter_entry(|entry| {
109 allow_dir_entry(
110 entry.depth(),
111 &entry.file_name().to_string_lossy(),
112 entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false),
113 )
114 });
115
116 let accept = |entry: &DirEntry| -> Option<WalkEntry> {
119 if entry.depth() == 0 {
120 return None; }
122 let ft = entry.file_type()?;
123 let is_symlink = ft.is_symlink();
124 let is_dir = ft.is_dir() && !is_symlink;
125 let rel = entry.path().strip_prefix(root).ok()?;
126 let rel = rel.to_string_lossy().replace('\\', "/");
127 let size = if is_dir || is_symlink {
128 0
129 } else {
130 entry.metadata().map(|m| m.len()).unwrap_or(0)
131 };
132 Some(WalkEntry {
133 rel: rel.into_boxed_str(),
134 is_dir,
135 size,
136 })
137 };
138
139 let delivered = AtomicUsize::new(0);
140 let truncated = AtomicBool::new(false);
141
142 let visit = |entry: Result<DirEntry, ignore::Error>| -> WalkState {
143 if cancel.load(Ordering::Relaxed) {
144 return WalkState::Quit;
145 }
146 let Some(entry) = entry.ok() else {
147 return WalkState::Continue;
148 };
149 let Some(we) = accept(&entry) else {
150 return WalkState::Continue;
151 };
152 if delivered.load(Ordering::Relaxed) >= max_entries {
155 truncated.store(true, Ordering::Relaxed);
156 return WalkState::Quit;
157 }
158 delivered.fetch_add(1, Ordering::Relaxed);
159 scanned.fetch_add(1, Ordering::Relaxed);
160 on_entry(we);
161 WalkState::Continue
162 };
163
164 if threads > 1 {
165 let visit = &visit;
166 builder.build_parallel().run(|| Box::new(visit));
167 } else {
168 let visit = &visit;
170 for entry in builder.build() {
171 if matches!(visit(entry), WalkState::Quit) {
172 break;
173 }
174 }
175 }
176
177 WalkStats {
178 scanned: delivered.load(Ordering::Relaxed),
179 truncated: truncated.load(Ordering::Relaxed),
180 }
181}
182
183#[cfg(test)]
184#[path = "walk_tests.rs"]
185mod tests;