1use crate::error::PathError;
24use crate::internal::validation::{is_hidden_name, reject_nul_path};
25use crate::metadata::{EntryKind, FileEntry, SortMode, TraversalErrorPolicy, sort_entries};
26use std::fs;
27use std::path::{Path, PathBuf};
28use walkdir::{DirEntry, WalkDir};
29
30#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct ListOptions {
45 pub recursive: bool,
47 pub follow_symlinks: bool,
49 pub include_files: bool,
51 pub include_directories: bool,
53 pub include_symlinks: bool,
55 pub include_hidden: bool,
57 pub include_other: bool,
59 pub max_depth: Option<usize>,
64 pub max_entries: Option<usize>,
66 pub sort: SortMode,
68 pub error_policy: TraversalErrorPolicy,
70 pub include_root: bool,
72}
73
74impl Default for ListOptions {
75 fn default() -> Self {
76 Self {
77 recursive: false,
78 follow_symlinks: false,
79 include_files: true,
80 include_directories: true,
81 include_symlinks: true,
82 include_hidden: false,
83 include_other: false,
84 max_depth: None,
85 max_entries: None,
86 sort: SortMode::Path,
87 error_policy: TraversalErrorPolicy::FailFast,
88 include_root: false,
89 }
90 }
91}
92
93impl ListOptions {
94 pub fn new() -> Self {
96 Self::default()
97 }
98
99 pub fn recursive(mut self, recursive: bool) -> Self {
101 self.recursive = recursive;
102 self
103 }
104
105 pub fn follow_symlinks(mut self, follow: bool) -> Self {
107 self.follow_symlinks = follow;
108 self
109 }
110
111 pub fn include_files(mut self, include: bool) -> Self {
113 self.include_files = include;
114 self
115 }
116
117 pub fn include_directories(mut self, include: bool) -> Self {
119 self.include_directories = include;
120 self
121 }
122
123 pub fn include_symlinks(mut self, include: bool) -> Self {
125 self.include_symlinks = include;
126 self
127 }
128
129 pub fn include_other(mut self, include: bool) -> Self {
131 self.include_other = include;
132 self
133 }
134
135 pub fn include_hidden(mut self, include: bool) -> Self {
137 self.include_hidden = include;
138 self
139 }
140
141 pub fn max_depth(mut self, depth: Option<usize>) -> Self {
143 self.max_depth = depth;
144 self
145 }
146
147 pub fn max_entries(mut self, max: Option<usize>) -> Self {
149 self.max_entries = max;
150 self
151 }
152
153 pub fn sort(mut self, sort: SortMode) -> Self {
155 self.sort = sort;
156 self
157 }
158
159 pub fn error_policy(mut self, policy: TraversalErrorPolicy) -> Self {
161 self.error_policy = policy;
162 self
163 }
164
165 pub fn include_root(mut self, include: bool) -> Self {
167 self.include_root = include;
168 self
169 }
170}
171
172pub(crate) fn walk_depth_bounds(options: &ListOptions) -> (usize, usize) {
178 if options.recursive {
179 let max = options.max_depth.unwrap_or(usize::MAX);
180 let min = if options.include_root { 0 } else { 1 };
181 let min = min.min(max);
183 (min, max)
184 } else {
185 if options.include_root { (0, 1) } else { (1, 1) }
188 }
189}
190
191pub fn list(root: impl AsRef<Path>, options: &ListOptions) -> Result<Vec<FileEntry>, PathError> {
204 let mut entries = Vec::new();
205 for item in walk(root, options)? {
206 entries.push(item?);
207 if let Some(max) = options.max_entries {
208 if entries.len() >= max {
209 break;
210 }
211 }
212 }
213 sort_entries(&mut entries, options.sort);
214 if let Some(max) = options.max_entries {
216 entries.truncate(max);
217 }
218 Ok(entries)
219}
220
221pub fn walk(root: impl AsRef<Path>, options: &ListOptions) -> Result<WalkIter, PathError> {
240 let root = root.as_ref();
241 reject_nul_path(root)?;
242 ensure_listable_root(root)?;
243
244 let (min_depth, max_depth) = walk_depth_bounds(options);
245 let walker = WalkDir::new(root)
246 .min_depth(min_depth)
247 .max_depth(max_depth)
248 .follow_links(options.follow_symlinks)
249 .same_file_system(false);
250
251 Ok(WalkIter {
252 root: root.to_path_buf(),
253 options: options.clone(),
254 inner: walker.into_iter(),
255 yielded: 0,
256 })
257}
258
259pub struct WalkIter {
264 root: PathBuf,
265 options: ListOptions,
266 inner: walkdir::IntoIter,
267 yielded: usize,
268}
269
270impl Iterator for WalkIter {
271 type Item = Result<FileEntry, PathError>;
272
273 fn next(&mut self) -> Option<Self::Item> {
274 if let Some(max) = self.options.max_entries {
275 if self.yielded >= max {
276 return None;
277 }
278 }
279
280 loop {
281 if let Some(max) = self.options.max_entries {
282 if self.yielded >= max {
283 return None;
284 }
285 }
286
287 let item = self.inner.next()?;
288 let dir_entry = match item {
289 Ok(e) => e,
290 Err(err) => match self.options.error_policy {
291 TraversalErrorPolicy::FailFast => {
292 return Some(Err(PathError::traversal(err.to_string())));
293 }
294 TraversalErrorPolicy::SkipErrors => continue,
295 },
296 };
297
298 match filter_and_build(&self.root, &dir_entry, &self.options) {
299 Ok(Some(entry)) => {
300 self.yielded += 1;
301 return Some(Ok(entry));
302 }
303 Ok(None) => continue,
304 Err(e) => match self.options.error_policy {
305 TraversalErrorPolicy::FailFast => return Some(Err(e)),
306 TraversalErrorPolicy::SkipErrors => continue,
307 },
308 }
309 }
310 }
311}
312
313fn ensure_listable_root(root: &Path) -> Result<(), PathError> {
314 let meta = fs::symlink_metadata(root).map_err(|e| PathError::filesystem(root, e))?;
315 if meta.is_dir() {
316 return Ok(());
317 }
318 if meta.file_type().is_symlink() {
321 match fs::metadata(root) {
322 Ok(m) if m.is_dir() => return Ok(()),
323 Ok(_) => {}
324 Err(e) => return Err(PathError::filesystem(root, e)),
325 }
326 }
327 Err(PathError::invalid(format!(
328 "list root is not a directory: {}",
329 root.to_string_lossy()
330 )))
331}
332
333fn filter_and_build(
334 root: &Path,
335 dir_entry: &DirEntry,
336 options: &ListOptions,
337) -> Result<Option<FileEntry>, PathError> {
338 let path = dir_entry.path();
339 let name = dir_entry.file_name();
340
341 if name == "." || name == ".." {
342 return Ok(None);
343 }
344
345 let is_root = path == root;
348 if !is_root && !options.include_hidden && (is_hidden_name(name) || is_windows_hidden(path)) {
349 return Ok(None);
350 }
351
352 let relative = path.strip_prefix(root).ok().map(Path::to_path_buf);
353 entry_from_dir_entry(path, relative, dir_entry, options)
354}
355
356fn entry_from_dir_entry(
357 path: &Path,
358 relative: Option<PathBuf>,
359 dir_entry: &DirEntry,
360 options: &ListOptions,
361) -> Result<Option<FileEntry>, PathError> {
362 let file_type = dir_entry.file_type();
363 let kind = if file_type.is_symlink() {
364 EntryKind::Symlink
365 } else if file_type.is_dir() {
366 EntryKind::Directory
367 } else if file_type.is_file() {
368 EntryKind::File
369 } else {
370 EntryKind::Other
371 };
372
373 if !kind_allowed(kind, options) {
374 return Ok(None);
375 }
376
377 let (size, modified, readonly) = match fs::symlink_metadata(path) {
378 Ok(meta) => {
379 let size = if meta.is_file() {
380 Some(meta.len())
381 } else {
382 None
383 };
384 let modified = meta.modified().ok();
385 let readonly = Some(meta.permissions().readonly());
386 (size, modified, readonly)
387 }
388 Err(e) => match options.error_policy {
389 TraversalErrorPolicy::FailFast => {
390 return Err(PathError::filesystem(path, e));
391 }
392 TraversalErrorPolicy::SkipErrors => (None, None, None),
393 },
394 };
395
396 Ok(Some(FileEntry {
397 path: path.to_path_buf(),
398 relative_path: relative,
399 kind,
400 size,
401 modified,
402 readonly,
403 }))
404}
405
406fn kind_allowed(kind: EntryKind, options: &ListOptions) -> bool {
407 match kind {
408 EntryKind::File => options.include_files,
409 EntryKind::Directory => options.include_directories,
410 EntryKind::Symlink => options.include_symlinks,
411 EntryKind::Other => options.include_other,
412 }
413}
414
415#[cfg(windows)]
416fn is_windows_hidden(path: &Path) -> bool {
417 use std::os::windows::fs::MetadataExt;
418 const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2;
419 fs::symlink_metadata(path)
420 .map(|m| m.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0)
421 .unwrap_or(false)
422}
423
424#[cfg(not(windows))]
425fn is_windows_hidden(_path: &Path) -> bool {
426 false
427}
428
429#[cfg(feature = "async")]
430pub async fn list_async(
432 root: impl AsRef<Path> + Send + 'static,
433 options: ListOptions,
434) -> Result<Vec<FileEntry>, PathError> {
435 let root = root.as_ref().to_path_buf();
436 tokio::task::spawn_blocking(move || list(root, &options))
437 .await
438 .map_err(|e| PathError::traversal(format!("async listing join failed: {e}")))?
439}