use crate::error::ZlobError;
use crate::ffi;
use std::ffi::CString;
use std::marker::PhantomData;
use std::os::raw::{c_int, c_void};
use std::path::Path;
use std::sync::Mutex;
const WALK_GITIGNORE: u32 = 1 << 0;
const WALK_SKIP_HIDDEN: u32 = 1 << 1;
const WALK_FOLLOW_SYMLINKS: u32 = 1 << 2;
const WALK_NO_REPORT_DIRS: u32 = 1 << 3;
const WALK_SORT: u32 = 1 << 4;
const WALK_ABORT_ON_ERROR: u32 = 1 << 5;
const WALK_KEEP_GIT_DIR: u32 = 1 << 6;
const META_SIZE: u32 = 1 << 0;
const META_MTIME: u32 = 1 << 1;
const META_ATIME: u32 = 1 << 2;
const META_CTIME: u32 = 1 << 3;
const META_BTIME: u32 = 1 << 4;
const META_INODE: u32 = 1 << 5;
const META_NLINK: u32 = 1 << 6;
const META_MODE: u32 = 1 << 7;
const META_UID: u32 = 1 << 8;
const META_GID: u32 = 1 << 9;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct WalkMetadata {
pub size: bool,
pub modified: bool,
pub accessed: bool,
pub changed: bool,
pub created: bool,
pub inode: bool,
pub nlink: bool,
pub mode: bool,
pub uid: bool,
pub gid: bool,
}
impl WalkMetadata {
pub fn all() -> Self {
Self {
size: true,
modified: true,
accessed: true,
changed: true,
created: true,
inode: true,
nlink: true,
mode: true,
uid: true,
gid: true,
}
}
fn to_mask(self) -> u32 {
let mut m = 0;
if self.size {
m |= META_SIZE;
}
if self.modified {
m |= META_MTIME;
}
if self.accessed {
m |= META_ATIME;
}
if self.changed {
m |= META_CTIME;
}
if self.created {
m |= META_BTIME;
}
if self.inode {
m |= META_INODE;
}
if self.nlink {
m |= META_NLINK;
}
if self.mode {
m |= META_MODE;
}
if self.uid {
m |= META_UID;
}
if self.gid {
m |= META_GID;
}
m
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalkState {
Continue,
SkipDir,
Quit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WalkEntryKind {
File,
Dir,
Symlink,
Unknown,
}
#[derive(Debug, Clone)]
pub struct WalkBuilder {
root: Result<CString, ZlobError>,
flags: u32,
meta: WalkMetadata,
threads: u16,
max_depth: u16,
pattern: Result<Option<CString>, ZlobError>,
pattern_flags: u32,
}
impl WalkBuilder {
pub fn new(root: impl AsRef<Path>) -> Self {
let bytes = path_to_bytes(root.as_ref());
Self {
root: CString::new(bytes).map_err(|_| ZlobError::InvalidInput),
flags: WALK_GITIGNORE | WALK_SKIP_HIDDEN,
meta: WalkMetadata::default(),
threads: 0,
max_depth: 0,
pattern: Ok(None),
pattern_flags: 0,
}
}
fn set(&mut self, bit: u32, on: bool) -> &mut Self {
if on {
self.flags |= bit;
} else {
self.flags &= !bit;
}
self
}
pub fn git_ignore(&mut self, yes: bool) -> &mut Self {
self.set(WALK_GITIGNORE, yes)
}
pub fn hidden(&mut self, skip: bool) -> &mut Self {
self.set(WALK_SKIP_HIDDEN, skip)
}
pub fn follow_links(&mut self, yes: bool) -> &mut Self {
self.set(WALK_FOLLOW_SYMLINKS, yes)
}
pub fn report_dirs(&mut self, yes: bool) -> &mut Self {
self.set(WALK_NO_REPORT_DIRS, !yes)
}
pub fn sort(&mut self, yes: bool) -> &mut Self {
self.set(WALK_SORT, yes)
}
pub fn abort_on_error(&mut self, yes: bool) -> &mut Self {
self.set(WALK_ABORT_ON_ERROR, yes)
}
pub fn keep_git_dir(&mut self, yes: bool) -> &mut Self {
self.set(WALK_KEEP_GIT_DIR, yes)
}
pub fn metadata(&mut self, meta: WalkMetadata) -> &mut Self {
self.meta = meta;
self
}
pub fn glob(&mut self, pattern: impl AsRef<str>) -> &mut Self {
self.pattern = CString::new(pattern.as_ref())
.map(Some)
.map_err(|_| ZlobError::InvalidInput);
self.pattern_flags = 0; self
}
pub fn glob_with_flags(
&mut self,
pattern: impl AsRef<str>,
flags: crate::ZlobFlags,
) -> &mut Self {
self.pattern = CString::new(pattern.as_ref())
.map(Some)
.map_err(|_| ZlobError::InvalidInput);
self.pattern_flags = flags.bits() as u32;
self
}
pub fn threads(&mut self, n: usize) -> &mut Self {
self.threads = n.min(u16::MAX as usize) as u16;
self
}
pub fn max_depth(&mut self, depth: Option<usize>) -> &mut Self {
self.max_depth = depth.unwrap_or(0).min(u16::MAX as usize) as u16;
self
}
fn checked_options(&self) -> Result<(&CString, ffi::zlob_walk_options_t), ZlobError> {
let root = self.root.as_ref().map_err(|e| *e)?;
let pattern = match self.pattern.as_ref().map_err(|e| *e)? {
Some(p) => p.as_ptr(),
None => std::ptr::null(),
};
Ok((
root,
ffi::zlob_walk_options_t {
flags: self.flags,
meta_mask: self.meta.to_mask(),
threads: self.threads,
max_depth: self.max_depth,
errfunc: None,
pattern,
pattern_flags: self.pattern_flags,
},
))
}
pub fn build(&self) -> Result<WalkResults, ZlobError> {
let (root, opts) = self.checked_options()?;
let mut out = ffi::zlob_walk_result_t {
entries: std::ptr::null_mut(),
count: 0,
_storage: std::ptr::null_mut(),
};
let rc = unsafe { ffi::zlob_walk_collect(root.as_ptr(), &opts, &mut out) };
match rc {
0 => Ok(WalkResults { raw: out }),
_ => Err(rc_to_error(rc)),
}
}
pub fn run<F>(&self, visitor: F) -> Result<(), ZlobError>
where
F: Fn(WalkEntry<'_>) -> WalkState + Sync,
{
self.run_inner(self.threads, &move |e| visitor(e))
}
pub fn run_serial<F>(&self, mut visitor: F) -> Result<(), ZlobError>
where
F: FnMut(WalkEntry<'_>) -> WalkState,
{
struct AssertSync<T>(T);
unsafe impl<T> Sync for AssertSync<T> {}
let cell = AssertSync(std::cell::RefCell::new(&mut visitor));
let adapter = &cell;
self.run_inner(1, &move |e| (adapter.0.borrow_mut())(e))
}
fn run_inner<F>(&self, threads: u16, visitor: &F) -> Result<(), ZlobError>
where
F: Fn(WalkEntry<'_>) -> WalkState + Sync,
{
struct VisitCtx<'a, F> {
visitor: &'a F,
panic: Mutex<Option<Box<dyn std::any::Any + Send + 'static>>>,
}
unsafe extern "C" fn trampoline<F>(
entry: *const ffi::zlob_walk_entry_t,
ctx: *mut c_void,
) -> c_int
where
F: Fn(WalkEntry<'_>) -> WalkState + Sync,
{
let ctx = unsafe { &*(ctx as *const VisitCtx<'_, F>) };
let entry = WalkEntry {
raw: unsafe { &*entry },
_marker: PhantomData,
};
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (ctx.visitor)(entry))) {
Ok(WalkState::Continue) => 0,
Ok(WalkState::SkipDir) => 1,
Ok(WalkState::Quit) => 2,
Err(payload) => {
let mut slot = ctx.panic.lock().unwrap_or_else(|p| p.into_inner());
if slot.is_none() {
*slot = Some(payload);
}
2 }
}
}
let (root, mut opts) = self.checked_options()?;
opts.threads = threads;
let ctx = VisitCtx {
visitor,
panic: Mutex::new(None),
};
let rc = unsafe {
ffi::zlob_walk(
root.as_ptr(),
&opts,
Some(trampoline::<F>),
&ctx as *const VisitCtx<'_, F> as *mut c_void,
)
};
if let Some(payload) = ctx.panic.lock().unwrap_or_else(|p| p.into_inner()).take() {
std::panic::resume_unwind(payload);
}
match rc {
0 => Ok(()),
_ => Err(rc_to_error(rc)),
}
}
}
fn rc_to_error(rc: c_int) -> ZlobError {
match ZlobError::from_code(rc) {
Err(e) => e,
Ok(_) => ZlobError::Aborted,
}
}
#[cfg(unix)]
fn path_to_bytes(p: &Path) -> Vec<u8> {
use std::os::unix::ffi::OsStrExt;
p.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
fn path_to_bytes(p: &Path) -> Vec<u8> {
p.to_string_lossy().into_owned().into_bytes()
}
pub struct WalkResults {
raw: ffi::zlob_walk_result_t,
}
unsafe impl Send for WalkResults {}
unsafe impl Sync for WalkResults {}
impl WalkResults {
pub fn len(&self) -> usize {
self.raw.count
}
pub fn is_empty(&self) -> bool {
self.raw.count == 0
}
fn raw_entries(&self) -> &[ffi::zlob_walk_entry_t] {
if self.raw.count == 0 || self.raw.entries.is_null() {
return &[];
}
unsafe { std::slice::from_raw_parts(self.raw.entries, self.raw.count) }
}
pub fn get(&self, index: usize) -> Option<WalkEntry<'_>> {
self.raw_entries().get(index).map(|raw| WalkEntry {
raw,
_marker: PhantomData,
})
}
pub fn iter(&self) -> impl ExactSizeIterator<Item = WalkEntry<'_>> {
self.raw_entries().iter().map(|raw| WalkEntry {
raw,
_marker: PhantomData,
})
}
pub fn ignore_rules(&self) -> Option<IgnoreRules<'_>> {
let handle = unsafe { ffi::zlob_walk_result_ignore_rules(&self.raw) };
if handle.is_null() {
None
} else {
Some(IgnoreRules {
handle,
_marker: PhantomData,
})
}
}
}
#[derive(Clone, Copy)]
pub struct IgnoreRules<'a> {
handle: *mut c_void,
_marker: PhantomData<&'a ()>,
}
unsafe impl Send for IgnoreRules<'_> {}
unsafe impl Sync for IgnoreRules<'_> {}
impl IgnoreRules<'_> {
pub fn is_ignored(&self, path: impl AsRef<Path>) -> bool {
let Ok(cpath) = CString::new(path_to_bytes(path.as_ref())) else {
return false; };
unsafe { ffi::zlob_ignore_rules_match_path(self.handle, cpath.as_ptr()) != 0 }
}
pub fn is_ignored_untrusted(&self, path: impl AsRef<Path>) -> bool {
let Ok(cpath) = CString::new(path_to_bytes(path.as_ref())) else {
return false;
};
unsafe { ffi::zlob_ignore_rules_match_untrusted(self.handle, cpath.as_ptr()) != 0 }
}
}
impl Drop for WalkResults {
fn drop(&mut self) {
unsafe { ffi::zlob_walk_result_free(&mut self.raw) };
}
}
impl std::fmt::Debug for WalkResults {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WalkResults")
.field("count", &self.raw.count)
.finish()
}
}
#[derive(Clone, Copy)]
pub struct WalkEntry<'a> {
raw: &'a ffi::zlob_walk_entry_t,
_marker: PhantomData<&'a ()>,
}
impl<'a> WalkEntry<'a> {
#[inline]
pub fn path_bytes(&self) -> &'a [u8] {
unsafe { std::slice::from_raw_parts(self.raw.path as *const u8, self.raw.path_len) }
}
#[inline]
pub fn path(&self) -> &'a Path {
bytes_to_path(self.path_bytes())
}
#[inline]
pub fn rel_path(&self) -> &'a Path {
bytes_to_path(&self.path_bytes()[self.raw.rel_off as usize..])
}
#[inline]
pub fn file_name(&self) -> &'a Path {
bytes_to_path(&self.path_bytes()[self.raw.basename_off as usize..])
}
#[inline]
pub fn depth(&self) -> usize {
self.raw.depth as usize
}
#[inline]
pub fn kind(&self) -> WalkEntryKind {
match self.raw.kind {
1 => WalkEntryKind::File,
2 => WalkEntryKind::Dir,
3 => WalkEntryKind::Symlink,
_ => WalkEntryKind::Unknown,
}
}
#[inline]
pub fn is_file(&self) -> bool {
self.raw.kind == 1
}
#[inline]
pub fn is_dir(&self) -> bool {
self.raw.kind == 2
}
#[inline]
pub fn is_symlink(&self) -> bool {
self.raw.kind == 3
}
fn meta(&self, bit: u32) -> bool {
self.raw.meta_valid & bit != 0
}
pub fn size(&self) -> Option<u64> {
self.meta(META_SIZE).then_some(self.raw.size)
}
pub fn modified_ns(&self) -> Option<i64> {
self.meta(META_MTIME).then_some(self.raw.mtime_ns)
}
pub fn accessed_ns(&self) -> Option<i64> {
self.meta(META_ATIME).then_some(self.raw.atime_ns)
}
pub fn changed_ns(&self) -> Option<i64> {
self.meta(META_CTIME).then_some(self.raw.ctime_ns)
}
pub fn created_ns(&self) -> Option<i64> {
self.meta(META_BTIME).then_some(self.raw.btime_ns)
}
pub fn inode(&self) -> Option<u64> {
self.meta(META_INODE).then_some(self.raw.inode)
}
pub fn nlink(&self) -> Option<u32> {
self.meta(META_NLINK).then_some(self.raw.nlink)
}
pub fn mode(&self) -> Option<u32> {
self.meta(META_MODE).then_some(self.raw.mode)
}
pub fn uid(&self) -> Option<u32> {
self.meta(META_UID).then_some(self.raw.uid)
}
pub fn gid(&self) -> Option<u32> {
self.meta(META_GID).then_some(self.raw.gid)
}
}
impl std::fmt::Debug for WalkEntry<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WalkEntry")
.field("path", &self.path())
.field("kind", &self.kind())
.field("depth", &self.depth())
.finish()
}
}
#[cfg(unix)]
fn bytes_to_path(b: &[u8]) -> &Path {
use std::os::unix::ffi::OsStrExt;
Path::new(std::ffi::OsStr::from_bytes(b))
}
#[cfg(not(unix))]
fn bytes_to_path(b: &[u8]) -> &Path {
Path::new(std::str::from_utf8(b).unwrap_or(""))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::atomic::{AtomicUsize, Ordering};
fn make_tree() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::create_dir(root.join("target")).unwrap();
fs::write(root.join(".gitignore"), "target/\n*.log\n").unwrap();
fs::write(root.join("Cargo.toml"), "x").unwrap();
fs::write(root.join("debug.log"), "").unwrap();
fs::write(root.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(root.join("target/out.bin"), "bin").unwrap();
dir
}
#[test]
fn build_respects_gitignore() {
let dir = make_tree();
let results = WalkBuilder::new(dir.path())
.threads(1)
.sort(true)
.build()
.unwrap();
let names: Vec<String> = results
.iter()
.map(|e| e.rel_path().to_string_lossy().into_owned())
.collect();
assert_eq!(names, vec!["Cargo.toml", "src", "src/main.rs"]);
}
#[test]
fn build_plain_walkdir_mode() {
let dir = make_tree();
let results = WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.threads(1)
.sort(true)
.build()
.unwrap();
let names: Vec<String> = results
.iter()
.map(|e| e.rel_path().to_string_lossy().into_owned())
.collect();
assert_eq!(
names,
vec![
".gitignore",
"Cargo.toml",
"debug.log",
"src",
"src/main.rs",
"target",
"target/out.bin",
]
);
let main_rs = results
.iter()
.find(|e| e.file_name() == Path::new("main.rs"))
.unwrap();
assert!(main_rs.is_file());
assert_eq!(main_rs.depth(), 2);
assert!(main_rs.path().ends_with("src/main.rs"));
assert_eq!(main_rs.size(), None);
}
#[test]
fn metadata_size_and_mtime() {
let dir = make_tree();
let results = WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.threads(1)
.metadata(WalkMetadata {
size: true,
modified: true,
inode: true,
..Default::default()
})
.build()
.unwrap();
let main_rs = results
.iter()
.find(|e| e.file_name() == Path::new("main.rs"))
.unwrap();
assert_eq!(main_rs.size(), Some(12)); assert!(main_rs.modified_ns().unwrap() > 0);
assert!(main_rs.inode().unwrap() != 0);
}
#[test]
fn parallel_run_visits_everything() {
let dir = make_tree();
let count = AtomicUsize::new(0);
WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.run(|_entry| {
count.fetch_add(1, Ordering::Relaxed);
WalkState::Continue
})
.unwrap();
assert_eq!(count.load(Ordering::Relaxed), 7);
}
#[test]
fn run_serial_accepts_non_sync_fnmut() {
use std::rc::Rc;
let dir = make_tree();
let mut names: Vec<String> = Vec::new();
let not_sync: Rc<u8> = Rc::new(0); WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.threads(8)
.run_serial(|entry| {
let _ = ¬_sync;
names.push(entry.file_name().to_string_lossy().into_owned());
WalkState::Continue
})
.unwrap();
assert_eq!(names.len(), 7);
}
#[test]
fn run_serial_skip_dir_and_quit() {
let dir = make_tree();
let mut count = 0usize;
WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.run_serial(|entry| {
count += 1;
if entry.is_dir() {
WalkState::SkipDir
} else {
WalkState::Continue
}
})
.unwrap();
assert_eq!(count, 5);
}
#[test]
fn run_skip_dir_and_quit() {
let dir = make_tree();
let count = AtomicUsize::new(0);
WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.threads(1)
.run(|entry| {
count.fetch_add(1, Ordering::Relaxed);
if entry.is_dir() {
WalkState::SkipDir
} else {
WalkState::Continue
}
})
.unwrap();
assert_eq!(count.load(Ordering::Relaxed), 5);
let quit_count = AtomicUsize::new(0);
WalkBuilder::new(dir.path())
.threads(1)
.run(|_| {
quit_count.fetch_add(1, Ordering::Relaxed);
WalkState::Quit
})
.unwrap();
assert_eq!(quit_count.load(Ordering::Relaxed), 1);
}
#[test]
fn max_depth_limits() {
let dir = make_tree();
let results = WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.threads(1)
.max_depth(Some(1))
.build()
.unwrap();
assert_eq!(results.len(), 5);
assert!(results.iter().all(|e| e.depth() == 1));
}
#[test]
fn interior_nul_is_an_error_not_a_silent_default() {
let err = WalkBuilder::new("bad\0root").build().unwrap_err();
assert_eq!(err, ZlobError::InvalidInput);
let err = WalkBuilder::new("bad\0root")
.run(|_| WalkState::Continue)
.unwrap_err();
assert_eq!(err, ZlobError::InvalidInput);
let dir = make_tree();
let err = WalkBuilder::new(dir.path())
.glob("*.\0rs")
.build()
.unwrap_err();
assert_eq!(err, ZlobError::InvalidInput);
let ok = WalkBuilder::new(dir.path())
.glob("*.\0rs")
.glob("**/*.rs")
.build();
assert!(ok.is_ok());
}
#[test]
#[should_panic(expected = "visitor exploded")]
fn visitor_panic_propagates_to_caller() {
let dir = make_tree();
let _ = WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.run(|_| panic!("visitor exploded"));
}
#[test]
fn empty_and_missing_roots() {
let dir = tempfile::tempdir().unwrap();
let results = WalkBuilder::new(dir.path()).build().unwrap();
assert!(results.is_empty());
let missing = WalkBuilder::new("/definitely/not/a/real/path/zlob")
.build()
.unwrap();
assert!(missing.is_empty());
let err = WalkBuilder::new("/definitely/not/a/real/path/zlob")
.abort_on_error(true)
.build();
assert!(err.is_err());
}
#[test]
fn retained_ignore_rules_are_reusable() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::write(root.join(".gitignore"), "*.log\nbuild/\n*.tmp\n").unwrap();
fs::write(root.join(".ignore"), "!important.tmp\n").unwrap();
fs::write(root.join("src/.gitignore"), "*.bak\n").unwrap();
fs::write(root.join("src/main.rs"), "").unwrap();
let results = WalkBuilder::new(root).threads(1).build().unwrap();
let rules = results.ignore_rules().expect("rules always present");
assert!(rules.is_ignored("app.log"));
assert!(rules.is_ignored("build/"));
assert!(!rules.is_ignored("build"));
assert!(rules.is_ignored("scratch.tmp"));
assert!(!rules.is_ignored("important.tmp"));
assert!(rules.is_ignored("src/old.bak"));
assert!(!rules.is_ignored("old.bak"));
assert!(!rules.is_ignored("src/main.rs"));
assert!(!rules.is_ignored_untrusted("build")); assert!(rules.is_ignored_untrusted("ghost.tmp")); }
#[test]
fn ignore_rules_always_available() {
let dir = make_tree();
let results = WalkBuilder::new(dir.path()).build().unwrap();
let rules = results.ignore_rules().expect("rules always present");
assert!(rules.is_ignored("target/"));
assert!(rules.is_ignored("debug.log"));
assert!(!rules.is_ignored("Cargo.toml"));
}
#[cfg(unix)]
#[test]
fn permission_denied_surfaces_with_abort_on_error() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let locked = dir.path().join("locked");
fs::create_dir(&locked).unwrap();
fs::write(locked.join("inner.txt"), "x").unwrap();
fs::set_permissions(&locked, fs::Permissions::from_mode(0o000)).unwrap();
struct Restore<'a>(&'a std::path::Path);
impl Drop for Restore<'_> {
fn drop(&mut self) {
let _ = fs::set_permissions(self.0, fs::Permissions::from_mode(0o755));
}
}
let _restore = Restore(&locked);
if std::fs::read_dir(&locked).is_ok() {
return;
}
let ok = WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.build();
assert!(ok.is_ok());
let err = WalkBuilder::new(dir.path())
.git_ignore(false)
.hidden(false)
.threads(1)
.abort_on_error(true)
.build()
.unwrap_err();
assert_eq!(err, ZlobError::PermissionDenied);
}
}
#[cfg(test)]
mod glob_tests {
use super::*;
use std::fs;
#[test]
fn glob_filters_and_prunes() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::create_dir(root.join("docs")).unwrap();
fs::write(root.join("src/main.rs"), "").unwrap();
fs::write(root.join("src/lib.rs"), "").unwrap();
fs::write(root.join("docs/guide.md"), "").unwrap();
fs::write(root.join("top.rs"), "").unwrap();
let results = WalkBuilder::new(root)
.git_ignore(false)
.hidden(false)
.threads(1)
.sort(true)
.glob("**/*.rs")
.build()
.unwrap();
let names: Vec<String> = results
.iter()
.map(|e| e.rel_path().to_string_lossy().into_owned())
.collect();
assert_eq!(names, vec!["src/lib.rs", "src/main.rs", "top.rs"]);
let braced = WalkBuilder::new(root)
.git_ignore(false)
.hidden(false)
.threads(1)
.glob("**/*.{md,rs}")
.build()
.unwrap();
assert_eq!(braced.len(), 4);
let scoped = WalkBuilder::new(root)
.git_ignore(false)
.hidden(false)
.threads(1)
.glob("src/**/*.rs")
.build()
.unwrap();
assert_eq!(scoped.len(), 2);
}
#[test]
fn glob_composes_with_gitignore_and_parallel_run() {
use std::sync::atomic::{AtomicUsize, Ordering};
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::create_dir(root.join("target")).unwrap();
fs::write(root.join(".gitignore"), "target/\n").unwrap();
fs::write(root.join("src/a.rs"), "").unwrap();
fs::write(root.join("target/gen.rs"), "").unwrap();
let count = AtomicUsize::new(0);
WalkBuilder::new(root)
.glob("**/*.rs")
.run(|entry| {
assert!(entry.path().extension().is_some_and(|e| e == "rs"));
count.fetch_add(1, Ordering::Relaxed);
WalkState::Continue
})
.unwrap();
assert_eq!(count.load(Ordering::Relaxed), 1);
}
}