use std::{
ffi::OsString,
fs, io,
os::{
fd::{AsRawFd, OwnedFd},
unix::fs::{MetadataExt, OpenOptionsExt},
},
path::{Path, PathBuf},
sync::Arc,
time::SystemTime,
};
mod attributes;
use attributes::{
AlignedBuffer, ParsedRecord, RecordHeader, SF_FIRMLINK, STAT_BLOCK_BYTES, VDIR, VLNK, VNON,
VREG, invalid_data, parse_record, read_record_length, requested_attributes,
};
const DIRECTORY_BUFFER_BYTES: usize = 64 * 1024;
pub struct Entry {
pub depth: usize,
pub file_name: OsString,
pub file_type: FileType,
pub metadata: io::Result<Metadata>,
pub parent_path: Arc<Path>,
}
impl Entry {
pub fn from_path(path: &Path) -> io::Result<Self> {
let metadata = Metadata::from_std(&fs::symlink_metadata(path)?);
Ok(Self {
depth: 0,
file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
file_type: metadata.file_type,
metadata: Ok(metadata),
parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),
})
}
#[must_use]
pub fn path(&self) -> PathBuf {
self.parent_path.join(&self.file_name)
}
}
#[derive(Clone, Copy)]
pub struct FileType {
kind: u32,
}
impl FileType {
fn from_std(file_type: fs::FileType) -> Self {
let kind = if file_type.is_dir() {
VDIR
} else if file_type.is_file() {
VREG
} else if file_type.is_symlink() {
VLNK
} else {
VNON
};
Self { kind }
}
#[must_use]
pub fn is_dir(self) -> bool {
self.kind == VDIR
}
#[must_use]
pub fn is_file(self) -> bool {
self.kind == VREG
}
#[must_use]
pub fn is_symlink(self) -> bool {
self.kind == VLNK
}
}
#[derive(Clone, Copy)]
pub struct Metadata {
len: u64,
allocated_size: u64,
modified: Option<SystemTime>,
dev: u64,
ino: u64,
nlink: u64,
file_type: FileType,
}
impl Metadata {
fn from_std(metadata: &fs::Metadata) -> Self {
let allocated_size = metadata.blocks().saturating_mul(STAT_BLOCK_BYTES);
Self {
len: metadata.len(),
allocated_size,
modified: metadata.modified().ok(),
dev: metadata.dev(),
ino: metadata.ino(),
nlink: metadata.nlink(),
file_type: FileType::from_std(metadata.file_type()),
}
}
#[must_use]
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> u64 {
self.len
}
#[must_use]
pub fn allocated_size(&self) -> u64 {
self.allocated_size
}
#[must_use]
pub fn blocks(&self) -> u64 {
self.allocated_size.div_ceil(STAT_BLOCK_BYTES)
}
pub fn modified(&self) -> io::Result<SystemTime> {
self.modified
.ok_or_else(|| invalid_data("macOS modification time is unavailable"))
}
#[must_use]
pub fn dev(&self) -> u64 {
self.dev
}
#[must_use]
pub fn ino(&self) -> u64 {
self.ino
}
#[must_use]
pub fn nlink(&self) -> u64 {
self.nlink
}
#[must_use]
pub fn is_file(&self) -> bool {
self.file_type.is_file()
}
}
pub(crate) struct ReadDir {
directory: OwnedFd,
fallback: Option<fs::ReadDir>,
buffer: Box<AlignedBuffer<DIRECTORY_BUFFER_BYTES>>,
offset: usize,
remaining: usize,
exhausted: bool,
listing_error: Option<i32>,
parent_path: Arc<Path>,
depth: usize,
}
impl ReadDir {
pub(crate) fn open(path: Arc<Path>, depth: usize) -> io::Result<Self> {
let directory: OwnedFd = fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_DIRECTORY)
.open(&path)?
.into();
Ok(Self {
directory,
fallback: None,
buffer: Box::new(AlignedBuffer::new()),
offset: 0,
remaining: 0,
exhausted: false,
listing_error: None,
parent_path: path,
depth,
})
}
fn refill(&mut self) -> io::Result<bool> {
loop {
let mut attributes = requested_attributes(self.listing_error.is_some());
let count = unsafe {
libc::getattrlistbulk(
self.directory.as_raw_fd(),
(&raw mut attributes).cast(),
self.buffer.as_mut_bytes().as_mut_ptr().cast(),
self.buffer.as_bytes().len(),
0,
)
};
if count > 0 {
self.offset = 0;
self.remaining = usize::try_from(count)
.map_err(|_| invalid_data("macOS directory record count is invalid"))?;
return Ok(true);
}
if count == 0 {
self.exhausted = true;
return Ok(false);
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
if self.listing_error.is_none() && error.raw_os_error() == Some(libc::EACCES) {
self.listing_error = Some(libc::EACCES);
continue;
}
if error.kind() == io::ErrorKind::Unsupported
|| error.raw_os_error() == Some(libc::ENOTSUP)
|| error.raw_os_error() == Some(libc::EOPNOTSUPP)
{
match fs::read_dir(&self.parent_path) {
Ok(entries) => {
self.fallback = Some(entries);
return Ok(true);
}
Err(error) => {
self.exhausted = true;
return Err(error);
}
}
}
self.exhausted = true;
return Err(error);
}
}
fn fallback_entry(&self, entry: fs::DirEntry) -> Entry {
let file_name = entry.file_name();
let metadata =
fs::symlink_metadata(entry.path()).map(|metadata| Metadata::from_std(&metadata));
let file_type = metadata.as_ref().map_or_else(
|_| {
entry
.file_type()
.map_or(FileType { kind: VNON }, FileType::from_std)
},
|metadata| metadata.file_type,
);
Entry {
depth: self.depth,
file_name,
file_type,
metadata,
parent_path: Arc::clone(&self.parent_path),
}
}
fn next_record(&mut self) -> io::Result<Entry> {
let bytes = self.buffer.as_bytes();
let buffer_len = bytes.len();
if self.offset > buffer_len.saturating_sub(size_of::<u32>()) {
self.exhausted = true;
return Err(invalid_data("macOS directory record has no length"));
}
let length = read_record_length(&bytes[self.offset..])?;
let Some(end) = self
.offset
.checked_add(length)
.filter(|end| length >= size_of::<RecordHeader>() && *end <= buffer_len)
else {
self.exhausted = true;
return Err(invalid_data("macOS directory record exceeds its buffer"));
};
let record = &bytes[self.offset..end];
self.offset = end;
self.remaining -= 1;
let mut parsed = parse_record(record)?;
let file_name = parsed
.file_name
.take()
.ok_or_else(|| invalid_data("macOS directory record has no filename"))?;
let metadata_error = if parsed.error != 0 {
Some(parsed.error.cast_signed())
} else {
self.listing_error
};
let file_type = FileType {
kind: parsed.object_type.unwrap_or(VNON),
};
let metadata = if let Some(error) = metadata_error {
Err(io::Error::from_raw_os_error(error))
} else {
if parsed.object_type.is_none() {
return Err(invalid_data("macOS directory record has no object type"));
}
let metadata_from_path = || {
fs::symlink_metadata(self.parent_path.join(&file_name))
.map(|metadata| Metadata::from_std(&metadata))
};
let special_mount = parsed.flags & SF_FIRMLINK != 0
|| parsed.mount_status & libc::DIR_MNTSTATUS_MNTPOINT != 0;
if special_mount {
metadata_from_path()
} else {
parsed.metadata(file_type).or_else(|_| metadata_from_path())
}
};
let file_type = metadata
.as_ref()
.map_or(file_type, |metadata| metadata.file_type);
Ok(Entry {
depth: self.depth,
file_name,
file_type,
metadata,
parent_path: Arc::clone(&self.parent_path),
})
}
}
impl Iterator for ReadDir {
type Item = io::Result<Entry>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(fallback) = &mut self.fallback {
let entry = fallback.next()?;
return Some(entry.map(|entry| self.fallback_entry(entry)));
}
if self.exhausted {
return None;
}
if self.remaining == 0 {
match self.refill() {
Ok(true) => continue,
Ok(false) => return None,
Err(error) => return Some(Err(error)),
}
}
match self.next_record() {
Ok(entry) if entry.file_name == "." || entry.file_name == ".." => {}
Ok(entry) => return Some(Ok(entry)),
Err(error) => return Some(Err(error)),
}
}
}
}
impl ParsedRecord {
fn metadata(&self, file_type: FileType) -> io::Result<Metadata> {
let (len, allocated_size, nlink) = if file_type.is_dir() {
(
self.directory_length
.ok_or_else(|| invalid_data("missing directory length"))?,
self.directory_allocated
.ok_or_else(|| invalid_data("missing directory allocation"))?,
self.directory_links
.ok_or_else(|| invalid_data("missing directory hard-link count"))?,
)
} else {
(
self.file_length
.ok_or_else(|| invalid_data("missing file length"))?,
self.file_allocated
.ok_or_else(|| invalid_data("missing file allocation"))?,
self.file_links
.ok_or_else(|| invalid_data("missing file hard-link count"))?,
)
};
Ok(Metadata {
len,
allocated_size,
modified: Some(
self.modified
.ok_or_else(|| invalid_data("missing modification timestamp"))?,
),
dev: self
.device
.ok_or_else(|| invalid_data("missing device number"))?,
ino: self
.inode
.ok_or_else(|| invalid_data("missing inode number"))?,
nlink,
file_type,
})
}
}
#[cfg(test)]
mod tests;