1use crate::walk_platform::{
2 DirectoryIdentity, FileSystemId, PlatformDirectoryInfo, directory_info,
3};
4pub use crate::walk_types::{
5 ErrorPolicy, WalkEntry, WalkError, WalkOperation, WalkOptions, WalkSkipReason,
6};
7use std::collections::{HashSet, VecDeque};
8use std::fs::{self, FileType};
9use std::io;
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13#[derive(Debug)]
14pub(crate) struct PendingDirectory {
15 pub(crate) path: PathBuf,
16 pub(crate) depth: usize,
17 pub(crate) identity: Option<DirectoryIdentity>,
18}
19
20#[allow(clippy::large_enum_variant)]
23pub(crate) enum DirectoryEntries {
24 Open(fs::ReadDir),
25 Buffered(VecDeque<io::Result<fs::DirEntry>>),
26}
27
28impl DirectoryEntries {
29 pub(crate) fn next(&mut self) -> Option<io::Result<fs::DirEntry>> {
30 match self {
31 Self::Open(entries) => entries.next(),
32 Self::Buffered(entries) => entries.pop_front(),
33 }
34 }
35
36 pub(crate) const fn is_open(&self) -> bool {
37 matches!(self, Self::Open(_))
38 }
39}
40
41pub(crate) struct DirectoryFrame {
42 pub(crate) path: PathBuf,
43 pub(crate) depth: usize,
44 pub(crate) entries: DirectoryEntries,
45 pub(crate) identity: Option<DirectoryIdentity>,
46}
47
48pub struct Walker {
55 pub(crate) root: Arc<PathBuf>,
56 pub(crate) root_components: usize,
57 pub(crate) root_file_type: Option<FileType>,
58 pub(crate) root_file_system: Option<FileSystemId>,
59 pub(crate) root_directory_info: Option<PlatformDirectoryInfo>,
60 pub(crate) options: WalkOptions,
61 pub(crate) frames: Vec<DirectoryFrame>,
62 pub(crate) open_handles: usize,
63 pub(crate) yield_root: bool,
64 pub(crate) pending_directory: Option<PendingDirectory>,
65 pub(crate) skip_pending_directory: bool,
66 pub(crate) active_directories: HashSet<DirectoryIdentity>,
67 pub(crate) finished: bool,
68}
69
70impl Walker {
71 pub fn new(root: impl AsRef<Path>) -> Result<Self, WalkError> {
77 Self::with_options(root, WalkOptions::default())
78 }
79
80 pub fn with_options(root: impl AsRef<Path>, options: WalkOptions) -> Result<Self, WalkError> {
86 let requested = root.as_ref();
87 let options = options.normalized();
88 let canonical = if options.follow_links || options.same_file_system {
89 requested.canonicalize().map_err(|source| {
90 WalkError::new(requested, 0, WalkOperation::Canonicalize, source)
91 })?
92 } else if requested.is_absolute() {
93 requested.to_path_buf()
94 } else {
95 std::env::current_dir()
96 .map_err(|source| {
97 WalkError::new(requested, 0, WalkOperation::Canonicalize, source)
98 })?
99 .join(requested)
100 };
101 let metadata = fs::metadata(&canonical)
102 .map_err(|source| WalkError::new(&canonical, 0, WalkOperation::ReadMetadata, source))?;
103 if !metadata.is_dir() {
104 return Err(WalkError::new(
105 &canonical,
106 0,
107 WalkOperation::ReadMetadata,
108 io::Error::new(io::ErrorKind::InvalidInput, "root is not a directory"),
109 ));
110 }
111 let root_directory_info = if options.follow_links || options.same_file_system {
112 Some(directory_info(&canonical, &metadata).map_err(|source| {
113 WalkError::new(&canonical, 0, WalkOperation::ReadMetadata, source)
114 })?)
115 } else {
116 None
117 };
118 let root_file_system = if options.same_file_system {
119 root_directory_info.map(|info| info.file_system)
120 } else {
121 None
122 };
123 let root = Arc::new(canonical.clone());
124 Ok(Self {
125 root: Arc::clone(&root),
126 root_components: canonical.components().count(),
127 root_file_type: Some(metadata.file_type()),
128 root_file_system,
129 root_directory_info,
130 options,
131 frames: Vec::new(),
132 open_handles: 0,
133 yield_root: true,
134 pending_directory: None,
135 skip_pending_directory: false,
136 active_directories: HashSet::new(),
137 finished: false,
138 })
139 }
140
141 pub(crate) fn from_known_directory(
142 root: &Arc<PathBuf>,
143 directory: PathBuf,
144 depth: usize,
145 options: WalkOptions,
146 ) -> Self {
147 let options = options.normalized();
148 let root_components = root.components().count();
149 Self {
150 root: Arc::clone(root),
151 root_components,
152 root_file_type: None,
153 root_file_system: None,
154 root_directory_info: None,
155 options,
156 frames: Vec::new(),
157 open_handles: 0,
158 yield_root: false,
159 pending_directory: Some(PendingDirectory {
160 path: directory,
161 depth,
162 identity: None,
163 }),
164 skip_pending_directory: false,
165 active_directories: HashSet::new(),
166 finished: false,
167 }
168 }
169
170 #[must_use]
171 pub fn root(&self) -> &Path {
172 self.root.as_path()
173 }
174
175 #[must_use]
176 pub const fn options(&self) -> &WalkOptions {
177 &self.options
178 }
179
180 pub fn skip_current_dir(&mut self) {
182 if self.pending_directory.is_some() {
183 self.skip_pending_directory = true;
184 }
185 }
186
187 pub(crate) fn schedule_pending_directory(&mut self) -> Option<WalkError> {
188 let pending = self.pending_directory.take()?;
189 if self.skip_pending_directory {
190 self.skip_pending_directory = false;
191 return None;
192 }
193 if self.open_handles >= self.options.max_open {
194 self.buffer_oldest_open_directory();
195 }
196 match fs::read_dir(&pending.path) {
197 Ok(entries) => {
198 if let Some(identity) = pending.identity {
199 self.active_directories.insert(identity);
200 }
201 self.frames.push(DirectoryFrame {
202 path: pending.path,
203 depth: pending.depth,
204 entries: DirectoryEntries::Open(entries),
205 identity: pending.identity,
206 });
207 self.open_handles += 1;
208 None
209 }
210 Err(source) => Some(WalkError::new(
211 pending.path,
212 pending.depth,
213 WalkOperation::ReadDirectory,
214 source,
215 )),
216 }
217 }
218
219 fn buffer_oldest_open_directory(&mut self) {
220 let Some(index) = self.frames.iter().position(|frame| frame.entries.is_open()) else {
221 return;
222 };
223 let placeholder = DirectoryEntries::Buffered(VecDeque::new());
224 let entries = std::mem::replace(&mut self.frames[index].entries, placeholder);
225 if let DirectoryEntries::Open(entries) = entries {
226 self.frames[index].entries = DirectoryEntries::Buffered(entries.collect());
227 self.open_handles -= 1;
228 }
229 }
230}