use std::{
cell::RefCell,
ffi::{CStr, CString, OsStr, c_int, c_long, c_void},
fs::{self, File, OpenOptions},
io,
os::unix::{
ffi::OsStrExt,
fs::OpenOptionsExt,
io::{AsRawFd, FromRawFd, IntoRawFd},
},
path::Path,
sync::atomic::{AtomicBool, Ordering},
};
use super::{Listing, defer_entry_stat_error};
const BUFFER_SIZE: usize = 32 * 1024;
const RECORD_LENGTH_OFFSET: usize = 16;
const TYPE_OFFSET: usize = 18;
const NAME_OFFSET: usize = 19;
const DT_DIR: u8 = 4;
const DT_REG: u8 = 8;
const DT_LNK: u8 = 10;
const DT_FIFO: u8 = 1;
const DT_CHR: u8 = 2;
const DT_BLK: u8 = 6;
const DT_SOCK: u8 = 12;
#[cfg(not(target_arch = "arm"))]
const O_DIRECTORY: i32 = 0o200_000;
#[cfg(target_arch = "arm")]
const O_DIRECTORY: i32 = 0o40_000;
static GETDENTS_UNSUPPORTED: AtomicBool = AtomicBool::new(false);
std::thread_local! {
static DIRECTORY_BUFFER: RefCell<Box<[u8; BUFFER_SIZE]>> = RefCell::new(Box::new([0; BUFFER_SIZE]));
}
#[cfg(all(target_arch = "x86_64", target_pointer_width = "64"))]
const SYS_GETDENTS64: c_long = 217;
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
const SYS_GETDENTS64: c_long = 61;
#[cfg(target_arch = "x86")]
const SYS_GETDENTS64: c_long = 220;
#[cfg(target_arch = "arm")]
const SYS_GETDENTS64: c_long = 217;
unsafe extern "C" {
fn syscall(number: c_long, ...) -> c_long;
}
pub(super) fn read_directory(
path: &Path,
refuse_final_symlink: bool,
listing: &mut Listing,
) -> io::Result<()> {
if GETDENTS_UNSUPPORTED.load(Ordering::Relaxed) {
return read_portable_directory_from_path(path, refuse_final_symlink, listing);
}
let used_portable_fallback = DIRECTORY_BUFFER.with(|buffer| {
let mut buffer = buffer.borrow_mut();
read_open_directory_with_portable_fallback(
path,
refuse_final_symlink,
&mut buffer[..],
listing,
read_directory_from_open_directory,
)
});
if used_portable_fallback? {
GETDENTS_UNSUPPORTED.store(true, Ordering::Relaxed);
}
Ok(())
}
fn open_directory(path: &Path, refuse_final_symlink: bool) -> io::Result<File> {
let flags = O_DIRECTORY
| if refuse_final_symlink {
libc::O_NOFOLLOW
} else {
0
};
OpenOptions::new().read(true).custom_flags(flags).open(path)
}
fn read_open_directory_with_portable_fallback(
path: &Path,
refuse_final_symlink: bool,
buffer: &mut [u8],
listing: &mut Listing,
native: impl FnOnce(&File, &Path, &mut [u8], &mut Listing) -> Result<(), NativeDirectoryReadError>,
) -> io::Result<bool> {
let directory = open_directory(path, refuse_final_symlink)?;
match native(&directory, path, buffer, listing) {
Ok(()) => Ok(false),
Err(NativeDirectoryReadError::CapabilityUnavailable) => {
read_portable_directory_from_open_file(directory, path, listing)?;
Ok(true)
}
Err(NativeDirectoryReadError::Io(error)) => Err(error),
}
}
fn read_directory_from_open_directory(
directory: &File,
path: &Path,
buffer: &mut [u8],
listing: &mut Listing,
) -> Result<(), NativeDirectoryReadError> {
read_directory_from_open_directory_with_read_batch(directory, path, buffer, listing, read_batch)
}
fn read_directory_from_open_directory_with_read_batch(
directory: &File,
path: &Path,
buffer: &mut [u8],
listing: &mut Listing,
mut next_batch: impl FnMut(&File, &mut [u8]) -> Result<usize, ReadBatchError>,
) -> Result<(), NativeDirectoryReadError> {
let mut primed = false;
listing.clear();
loop {
let byte_count = match next_batch(directory, buffer) {
Ok(byte_count) => byte_count,
Err(ReadBatchError::CapabilityUnavailable) => {
return Err(NativeDirectoryReadError::CapabilityUnavailable);
}
Err(ReadBatchError::Io(error))
if !primed && error.raw_os_error() == Some(libc::ENOSYS) =>
{
return Err(NativeDirectoryReadError::CapabilityUnavailable);
}
Err(ReadBatchError::Io(error)) => return Err(NativeDirectoryReadError::Io(error)),
};
if byte_count == 0 {
return Ok(());
}
parse_records(path, &buffer[..byte_count], listing)?;
primed = true;
}
}
enum NativeDirectoryReadError {
CapabilityUnavailable,
Io(io::Error),
}
impl From<io::Error> for NativeDirectoryReadError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
#[cfg_attr(
any(
all(target_arch = "x86_64", target_pointer_width = "64"),
target_arch = "aarch64",
target_arch = "riscv64",
target_arch = "x86",
target_arch = "arm"
),
allow(dead_code)
)]
enum ReadBatchError {
CapabilityUnavailable,
Io(io::Error),
}
fn read_portable_directory_from_path(
path: &Path,
refuse_final_symlink: bool,
listing: &mut Listing,
) -> io::Result<()> {
let directory = open_directory(path, refuse_final_symlink)?;
read_portable_directory_from_open_file(directory, path, listing)
}
fn read_portable_directory_from_open_file(
directory: File,
reported_path: &Path,
listing: &mut Listing,
) -> io::Result<()> {
let descriptor = directory.into_raw_fd();
let stream = unsafe { libc::fdopendir(descriptor) };
if stream.is_null() {
let error = io::Error::last_os_error();
unsafe { drop(File::from_raw_fd(descriptor)) };
return Err(error);
}
let stream = DirectoryStream(stream);
listing.clear();
loop {
unsafe { *libc::__errno_location() = 0 };
let entry = unsafe { libc::readdir(stream.0) };
if entry.is_null() {
let error = io::Error::last_os_error();
return if error.raw_os_error() == Some(0) {
Ok(())
} else {
Err(error)
};
}
let entry = unsafe { &*entry };
let name = unsafe { CStr::from_ptr(entry.d_name.as_ptr()) }.to_bytes();
if name.is_empty() || name == b"." || name == b".." {
continue;
}
let name = OsStr::from_bytes(name);
match descriptor_entry_kind(descriptor, name, entry.d_type) {
Ok(Some((is_dir, is_symlink))) => listing.push(name, is_dir, is_symlink),
Ok(None) => {}
Err(error) => defer_entry_stat_error(listing, reported_path.join(name), error)?,
}
}
}
struct DirectoryStream(*mut libc::DIR);
impl Drop for DirectoryStream {
fn drop(&mut self) {
unsafe { libc::closedir(self.0) };
}
}
fn descriptor_entry_kind(
directory: c_int,
name: &OsStr,
directory_type: u8,
) -> io::Result<Option<(bool, bool)>> {
match directory_type {
DT_DIR => Ok(Some((true, false))),
DT_REG | DT_FIFO | DT_CHR | DT_BLK | DT_SOCK => Ok(Some((false, false))),
DT_LNK => Ok(Some((false, true))),
_ => {
let name = CString::new(name.as_bytes()).map_err(|_| malformed_record())?;
let mut metadata = unsafe { std::mem::zeroed::<libc::stat>() };
let result = unsafe {
libc::fstatat(
directory,
name.as_ptr(),
&mut metadata,
libc::AT_SYMLINK_NOFOLLOW,
)
};
if result != 0 {
let error = io::Error::last_os_error();
return if is_vanished_entry(&error) {
Ok(None)
} else {
Err(error)
};
}
let kind = metadata.st_mode & libc::S_IFMT;
Ok(Some((kind == libc::S_IFDIR, kind == libc::S_IFLNK)))
}
}
}
fn read_batch(directory: &File, buffer: &mut [u8]) -> Result<usize, ReadBatchError> {
#[cfg(any(
all(target_arch = "x86_64", target_pointer_width = "64"),
target_arch = "aarch64",
target_arch = "riscv64",
target_arch = "x86",
target_arch = "arm"
))]
loop {
let byte_count = unsafe {
syscall(
SYS_GETDENTS64,
directory.as_raw_fd(),
buffer.as_mut_ptr().cast::<c_void>(),
buffer.len(),
)
};
if byte_count >= 0 {
return Ok(byte_count as usize);
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
if error.raw_os_error() == Some(38) {
return Err(ReadBatchError::Io(error));
}
return Err(ReadBatchError::Io(error));
}
#[cfg(not(any(
all(target_arch = "x86_64", target_pointer_width = "64"),
target_arch = "aarch64",
target_arch = "riscv64",
target_arch = "x86",
target_arch = "arm"
)))]
{
let _ = (directory, buffer);
Err(ReadBatchError::CapabilityUnavailable)
}
}
fn parse_records(directory: &Path, records: &[u8], listing: &mut Listing) -> io::Result<()> {
for_each_record(records, |name, directory_type| {
let name = OsStr::from_bytes(name);
match entry_kind(directory, name, directory_type) {
Ok(Some((is_dir, is_symlink))) => listing.push(name, is_dir, is_symlink),
Ok(None) => {}
Err(error) => defer_entry_stat_error(listing, directory.join(name), error)?,
}
Ok(())
})
}
#[doc(hidden)]
pub fn fuzz_validate_records(records: &[u8]) {
let _ = for_each_record(records, |_, _| Ok(()));
}
fn for_each_record(
records: &[u8],
mut visit: impl FnMut(&[u8], u8) -> io::Result<()>,
) -> io::Result<()> {
let mut offset = 0;
while offset < records.len() {
let record = records.get(offset..).ok_or_else(malformed_record)?;
if record.len() < NAME_OFFSET + 1 {
return Err(malformed_record());
}
let record_length = u16::from_ne_bytes(
record[RECORD_LENGTH_OFFSET..RECORD_LENGTH_OFFSET + std::mem::size_of::<u16>()]
.try_into()
.expect("record length slice has fixed width"),
) as usize;
if record_length < NAME_OFFSET + 1 || record_length > record.len() {
return Err(malformed_record());
}
let name_and_padding = &record[NAME_OFFSET..record_length];
let Some(name_length) = memchr::memchr(0, name_and_padding) else {
return Err(malformed_record());
};
let name = &name_and_padding[..name_length];
offset = offset
.checked_add(record_length)
.ok_or_else(malformed_record)?;
if name.is_empty() || name == b"." || name == b".." {
continue;
}
if memchr::memchr(b'/', name).is_some() {
return Err(malformed_record());
}
visit(name, record[TYPE_OFFSET])?;
}
Ok(())
}
fn entry_kind(
directory: &Path,
name: &OsStr,
directory_type: u8,
) -> io::Result<Option<(bool, bool)>> {
match directory_type {
DT_DIR => Ok(Some((true, false))),
DT_REG | DT_FIFO | DT_CHR | DT_BLK | DT_SOCK => Ok(Some((false, false))),
DT_LNK => Ok(Some((false, true))),
_ => match fs::symlink_metadata(directory.join(name)) {
Ok(metadata) => {
let file_type = metadata.file_type();
Ok(Some((file_type.is_dir(), file_type.is_symlink())))
}
Err(error) if is_vanished_entry(&error) => Ok(None),
Err(error) => Err(error),
},
}
}
fn is_vanished_entry(error: &io::Error) -> bool {
matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
)
}
fn malformed_record() -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, "malformed getdents64 record")
}
#[cfg(test)]
mod tests {
use std::{
fs,
os::unix::{
fs::symlink,
io::{AsRawFd, FromRawFd},
},
path::{Path, PathBuf},
sync::{
Mutex, MutexGuard,
atomic::{AtomicUsize, Ordering},
},
time::{SystemTime, UNIX_EPOCH},
};
use crate::{DirectoryBackend, ErrorPolicy, StdBackend, WalkEntry, WalkOptions, Walker};
use super::{
BUFFER_SIZE, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_REG, DT_SOCK, GETDENTS_UNSUPPORTED,
Listing, NAME_OFFSET, NativeDirectoryReadError, ReadBatchError, TYPE_OFFSET, entry_kind,
for_each_record, open_directory, parse_records, read_directory,
read_directory_from_open_directory_with_read_batch,
read_open_directory_with_portable_fallback, read_portable_directory_from_open_file,
};
static NEXT_FIXTURE: AtomicUsize = AtomicUsize::new(0);
static GETDENTS_LATCH_TEST_LOCK: Mutex<()> = Mutex::new(());
struct GetdentsLatch<'a> {
previous: bool,
_lock: MutexGuard<'a, ()>,
}
impl<'a> GetdentsLatch<'a> {
fn unsupported() -> Self {
let lock = GETDENTS_LATCH_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
Self {
previous: GETDENTS_UNSUPPORTED.swap(true, Ordering::SeqCst),
_lock: lock,
}
}
}
impl Drop for GetdentsLatch<'_> {
fn drop(&mut self) {
GETDENTS_UNSUPPORTED.store(self.previous, Ordering::SeqCst);
}
}
fn getdents_latch_state_for_test() -> bool {
let _lock = GETDENTS_LATCH_TEST_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
GETDENTS_UNSUPPORTED.load(Ordering::SeqCst)
}
fn record(name: &[u8], directory_type: u8) -> Vec<u8> {
let length = (NAME_OFFSET + name.len() + 1 + 7) & !7;
let mut record = vec![0_u8; length];
record[16..18].copy_from_slice(&(length as u16).to_ne_bytes());
record[NAME_OFFSET..NAME_OFFSET + name.len()].copy_from_slice(name);
record[TYPE_OFFSET] = directory_type;
record
}
#[test]
fn parser_skips_dot_entries_and_rejects_malformed_records() {
let mut records = record(b".", DT_DIR);
records.extend(record(b"..", DT_DIR));
records.extend(record(b"regular", DT_REG));
let mut listing = Listing::default();
parse_records(Path::new("/tmp"), &records, &mut listing).expect("dot records parse");
assert_eq!(listing.entries().len(), 1);
assert!(!listing.entries()[0].is_dir());
assert!(!listing.entries()[0].is_symlink());
assert!(parse_records(Path::new("/tmp"), &[0_u8; NAME_OFFSET], &mut listing).is_err());
let mut zero_length = vec![0_u8; NAME_OFFSET + 1];
zero_length[NAME_OFFSET] = DT_REG;
assert!(parse_records(Path::new("/tmp"), &zero_length, &mut listing).is_err());
let mut missing_nul = record(b"name", DT_REG);
for byte in &mut missing_nul[NAME_OFFSET..] {
*byte = b'x';
}
assert!(parse_records(Path::new("/tmp"), &missing_nul, &mut listing).is_err());
}
#[cfg(target_endian = "little")]
#[test]
fn checked_in_linux_native_fuzz_seeds_are_valid_and_reproducible() {
let long_name = [b'x'; 255];
let mut multi = record(b"nested", DT_DIR);
multi.extend(record(b"file", DT_REG));
let seeds: [(&[u8], Vec<u8>); 4] = [
(
include_bytes!("../../../fuzz/corpus/linux_dirent_parser/single-regular"),
record(b"one", DT_REG),
),
(
include_bytes!("../../../fuzz/corpus/linux_dirent_parser/minimal-name"),
record(b"a", DT_REG),
),
(
include_bytes!("../../../fuzz/corpus/linux_dirent_parser/multi-directory-regular"),
multi,
),
(
include_bytes!("../../../fuzz/corpus/linux_dirent_parser/long-name"),
record(&long_name, DT_REG),
),
];
for (seed, expected) in seeds {
assert_eq!(
seed,
expected.as_slice(),
"seed matches the generator record"
);
let mut records = 0;
for_each_record(seed, |_, _| {
records += 1;
Ok(())
})
.expect("seed reaches the parser visitor");
assert!(records > 0);
}
}
#[test]
fn a_vanished_entry_costs_one_entry_and_not_the_listing() {
let missing = format!(
"ferralk-vanished-{}-{}",
std::process::id(),
NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
);
let mut records = record(missing.as_bytes(), 0);
records.extend(record(b"survivor", DT_REG));
let mut listing = Listing::default();
parse_records(Path::new("/tmp"), &records, &mut listing)
.expect("a vanished entry does not end the listing");
assert_eq!(
listing.entries().len(),
1,
"only the vanished entry is dropped"
);
assert_eq!(listing.entries()[0].name(), "survivor");
}
#[test]
fn special_file_types_are_classified_without_a_stat() {
let absent = Path::new("/ferralk-nonexistent-special");
for directory_type in [DT_FIFO, DT_CHR, DT_BLK, DT_SOCK] {
assert_eq!(
entry_kind(absent, "entry".as_ref(), directory_type).expect("no stat is attempted"),
Some((false, false))
);
}
}
#[test]
fn opening_a_non_directory_fails_instead_of_blocking() {
let path = std::env::temp_dir().join(format!(
"ferralk-not-a-directory-{}-{}",
std::process::id(),
NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
));
fs::write(&path, b"fixture").expect("write fixture file");
let error = open_directory(&path, false).expect_err("a regular file is not a directory");
assert_eq!(error.kind(), std::io::ErrorKind::NotADirectory);
let _ = fs::remove_file(&path);
}
#[test]
fn no_follow_directory_open_rejects_a_swapped_symlink_but_follow_opens_it() {
let root = fixture_root("no-follow");
let target = root.join("target");
let link = root.join("scheduled");
fs::create_dir_all(&target).expect("create target directory");
fs::write(target.join("inside"), b"fixture").expect("write target entry");
symlink(&target, &link).expect("replace scheduled directory with a link");
let error = open_directory(&link, true).expect_err("no-follow rejects the replacement");
assert!(matches!(
error.raw_os_error(),
Some(libc::ELOOP | libc::ENOTDIR)
));
let mut listing = Listing::default();
read_directory(&link, false, &mut listing).expect("follow mode still opens through a link");
assert!(listing.contains("inside"));
fs::remove_dir_all(root).expect("remove no-follow fixture");
}
#[test]
fn unsupported_no_follow_descendant_reuses_its_open_descriptor() {
let root = fixture_root("descriptor-fallback");
let descendant = root.join("scheduled");
let moved = root.join("opened-before-swap");
let target = root.join("target");
fs::create_dir_all(descendant.join("nested")).expect("create scheduled subtree");
fs::write(descendant.join("nested/inside"), b"fixture").expect("write nested entry");
fs::create_dir_all(&target).expect("create swapped target");
fs::write(target.join("escaped"), b"fixture").expect("write target entry");
let mut buffer = vec![0_u8; BUFFER_SIZE];
let mut listing = Listing::default();
let used_portable_fallback = read_open_directory_with_portable_fallback(
&descendant,
true,
&mut buffer,
&mut listing,
|_, _, _, _| {
fs::rename(&descendant, &moved).expect("move opened directory");
symlink(&target, &descendant).expect("replace path with a link");
Err(NativeDirectoryReadError::CapabilityUnavailable)
},
)
.expect("unsupported native read degrades through the descriptor");
assert!(used_portable_fallback);
assert!(
listing.contains("nested"),
"the opened descendant remains visible"
);
assert!(
!listing.contains("escaped"),
"the portable fallback never reopens the swapped path"
);
fs::remove_dir_all(root).expect("remove descriptor fallback fixture");
}
#[test]
fn only_pre_batch_enosys_uses_the_portable_descriptor_fallback() {
let root = fixture_root("capability-classification");
fs::create_dir_all(&root).expect("create fixture directory");
fs::write(root.join("portable-entry"), b"fixture").expect("write portable marker");
let mut buffer = vec![0_u8; BUFFER_SIZE];
let mut listing = Listing::default();
let used_portable_fallback = read_open_directory_with_portable_fallback(
&root,
true,
&mut buffer,
&mut listing,
|directory, path, buffer, listing| {
read_directory_from_open_directory_with_read_batch(
directory,
path,
buffer,
listing,
|_, _| {
Err(ReadBatchError::Io(std::io::Error::from_raw_os_error(
libc::ENOSYS,
)))
},
)
},
)
.expect("pre-first-batch ENOSYS is a capability fallback");
assert!(used_portable_fallback);
assert!(listing.contains("portable-entry"));
let mut listing = Listing::default();
let error = read_open_directory_with_portable_fallback(
&root,
true,
&mut buffer,
&mut listing,
|directory, path, buffer, listing| {
read_directory_from_open_directory_with_read_batch(
directory,
path,
buffer,
listing,
|_, _| {
Err(ReadBatchError::Io(std::io::Error::from_raw_os_error(
libc::EOPNOTSUPP,
)))
},
)
},
)
.expect_err("EOPNOTSUPP is an ordinary directory error, not a capability latch");
assert_eq!(error.raw_os_error(), Some(libc::EOPNOTSUPP));
assert!(
listing.entries().is_empty(),
"the portable reader never restarted"
);
let records = record(b"already-read", DT_REG);
let mut calls = 0;
let error = read_open_directory_with_portable_fallback(
&root,
true,
&mut buffer,
&mut listing,
|directory, path, buffer, listing| {
read_directory_from_open_directory_with_read_batch(
directory,
path,
buffer,
listing,
|_, buffer| {
calls += 1;
if calls == 1 {
buffer[..records.len()].copy_from_slice(&records);
Ok(records.len())
} else {
Err(ReadBatchError::Io(std::io::Error::from_raw_os_error(
libc::ENOSYS,
)))
}
},
)
},
)
.expect_err("ENOSYS after an accepted batch does not restart the descriptor");
assert_eq!(error.raw_os_error(), Some(libc::ENOSYS));
assert!(
listing.contains("already-read"),
"the accepted batch is retained"
);
assert!(
!listing.contains("portable-entry"),
"an advanced descriptor never resumes through the portable reader"
);
fs::remove_dir_all(root).expect("remove capability-classification fixture");
}
#[test]
fn portable_descriptor_fallback_closes_the_transferred_descriptor() {
let root = fixture_root("descriptor-close");
fs::create_dir_all(&root).expect("create fixture directory");
fs::write(root.join("entry"), b"fixture").expect("write fixture entry");
let directory = open_directory(&root, true).expect("open fixture directory");
let descriptor = unsafe { libc::fcntl(directory.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 1024) };
assert!(descriptor >= 1024, "duplicate directory descriptor");
drop(directory);
let directory = unsafe { std::fs::File::from_raw_fd(descriptor) };
let mut listing = Listing::default();
read_portable_directory_from_open_file(directory, &root, &mut listing)
.expect("read descriptor fallback");
assert!(listing.contains("entry"));
assert_eq!(unsafe { libc::fcntl(descriptor, libc::F_GETFD) }, -1);
assert_eq!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EBADF)
);
fs::remove_dir_all(root).expect("remove descriptor-close fixture");
}
#[test]
fn latched_unsupported_walks_descendants_in_serial_and_default_parallel_modes() {
let root = fixture_root("latched-walker");
for branch in 0..16 {
let leaf = root.join(format!("branch-{branch}/nested/leaf-{branch}"));
fs::create_dir_all(leaf.parent().expect("leaf has parent"))
.expect("create descendant directory");
fs::write(leaf, b"fixture").expect("write descendant file");
}
let _latch = GetdentsLatch::unsupported();
for walker in [
Walker::new(&root)
.threads(1)
.error_policy(ErrorPolicy::Collect)
.options(WalkOptions::default().sort(true)),
Walker::new(&root)
.error_policy(ErrorPolicy::Collect)
.options(WalkOptions::default().sort(true)),
] {
let result = walker.collect().expect("latched fallback still walks");
assert!(
result.errors().is_empty(),
"fallback reports no descendant errors"
);
assert_eq!(
result.entries().len(),
48,
"all directory and file descendants remain"
);
}
fs::remove_dir_all(root).expect("remove latched-walker fixture");
}
#[test]
fn latched_unsupported_no_follow_failures_reach_the_caller() {
let root = fixture_root("latched-failure");
let target = root.join("target");
let replaced = root.join("scheduled");
fs::create_dir_all(&target).expect("create target directory");
symlink(&target, &replaced).expect("create replacement link");
let _latch = GetdentsLatch::unsupported();
let mut listing = Listing::default();
let error = read_directory(&replaced, true, &mut listing)
.expect_err("a protected fallback must still reject a replacement link");
assert!(matches!(
error.raw_os_error(),
Some(libc::ELOOP | libc::ENOTDIR)
));
assert!(
listing.entries().is_empty(),
"a failed open has no partial listing"
);
fs::remove_dir_all(root).expect("remove latched-failure fixture");
}
#[test]
fn capability_latch_guard_restores_after_a_panic() {
let mut before = None;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let latch = GetdentsLatch::unsupported();
before = Some(latch.previous);
assert!(GETDENTS_UNSUPPORTED.load(Ordering::SeqCst));
panic!("exercise guard unwinding");
}));
assert!(result.is_err(), "the injected panic was caught");
assert_eq!(
getdents_latch_state_for_test(),
before.expect("guard recorded the prior latch state")
);
}
#[test]
fn native_reader_matches_the_portable_reader() {
let root = std::env::temp_dir().join(format!(
"ferralk-linux-native-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock is after epoch")
.as_nanos()
+ NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed) as u128
));
fs::create_dir_all(root.join("nested")).expect("create native fixture");
fs::write(root.join("file.txt"), b"fixture").expect("write native fixture");
let describe = |read: &dyn Fn(&mut Listing)| {
let mut listing = Listing::default();
read(&mut listing);
let mut described = listing
.entries()
.iter()
.map(|entry| {
(
PathBuf::from(entry.name()),
entry.is_dir(),
entry.is_symlink(),
)
})
.collect::<Vec<(PathBuf, bool, bool)>>();
described.sort();
described
};
assert_eq!(
describe(&|listing| {
read_directory(&root, false, listing).expect("native reader succeeds");
}),
describe(&|listing| {
StdBackend
.read_directory(&root, false, false, listing)
.expect("portable reader succeeds");
})
);
fs::remove_dir_all(root).expect("remove native fixture");
}
#[test]
fn native_walker_matches_portable_filters_metadata_and_symlinks() {
let root = fixture_root("walker");
fs::create_dir_all(root.join("src/nested")).expect("create source fixture");
fs::create_dir_all(root.join("ignored")).expect("create ignored fixture");
fs::create_dir_all(root.join(".hidden")).expect("create hidden fixture");
fs::write(root.join("src/lib.rs"), b"library").expect("write source fixture");
fs::write(root.join("src/nested/mod.rs"), b"module").expect("write nested fixture");
fs::write(root.join("src/generated.tmp"), b"temporary").expect("write temp fixture");
fs::write(root.join("ignored/skip.rs"), b"ignored").expect("write ignored fixture");
fs::write(root.join(".hidden/skip.rs"), b"hidden").expect("write hidden fixture");
fs::write(root.join(".gitignore"), b"ignored/\n").expect("write gitignore fixture");
symlink("src", root.join("source-link")).expect("create directory symlink");
let walker = Walker::new(&root)
.threads(1)
.include("**/*")
.expect("valid include")
.exclude("**/*.tmp")
.expect("valid exclude")
.respect_git_ignore(true)
.error_policy(ErrorPolicy::Collect)
.options(
WalkOptions::default()
.sort(true)
.metadata(true)
.skip_hidden(true),
);
let native = walker.clone().collect().expect("native walk succeeds");
let (portable_entries, portable_errors) = collect_with_portable_backend(&walker);
assert!(native.errors().is_empty());
assert!(portable_errors.is_empty());
assert_eq!(
describe_walk_entries(native.entries(), &root),
describe_walk_entries(&portable_entries, &root)
);
fs::remove_dir_all(root).expect("remove walker fixture");
}
type DescribedWalkEntry = (PathBuf, bool, bool, usize, Option<(u64, bool, bool)>);
fn fixture_root(label: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"ferralk-linux-native-{label}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock is after epoch")
.as_nanos()
+ NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed) as u128
))
}
fn collect_with_portable_backend(walker: &Walker) -> (Vec<WalkEntry>, Vec<crate::WalkError>) {
let mut state = crate::WalkState::new(walker, &crate::keep_every_entry);
let root = walker
.roots()
.next()
.expect("a walk has a root")
.to_path_buf();
let (ignores, ignore_errors) = crate::IgnoreScope::for_root(walker, &StdBackend, &root);
let task = crate::DirectoryTask {
path: root.clone(),
depth: 0,
root: 0,
cycle_guard: std::sync::Arc::new(crate::CycleGuard::default()),
ignores,
ignore_errors,
};
state
.walk_directory(&StdBackend, task)
.expect("portable walk succeeds");
if walker.options.sort {
state
.entries
.sort_by(|left, right| left.path.cmp(&right.path));
}
(state.entries, state.errors)
}
fn describe_walk_entries(entries: &[WalkEntry], root: &Path) -> Vec<DescribedWalkEntry> {
entries
.iter()
.map(|entry| {
(
entry
.path()
.strip_prefix(root)
.expect("entry belongs to fixture")
.to_path_buf(),
entry.is_dir(),
entry.is_symlink(),
entry.depth(),
entry.metadata().map(|metadata| {
(
metadata.len(),
metadata.file_type().is_dir(),
metadata.file_type().is_symlink(),
)
}),
)
})
.collect()
}
}