use crate::{Throttle, WalkOptions, WalkRoot, crossdev, inodefilter::InodeFilter};
use crossbeam::channel::Receiver;
#[cfg(not(any(windows, target_os = "macos")))]
use filesize::PathExt;
use std::{
borrow::Cow,
collections::HashMap,
fmt, io,
num::NonZeroU32,
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
const NONE: u32 = u32::MAX;
const FLAG_OCCUPIED: u32 = 1 << 0;
const FLAG_DIRECTORY: u32 = 1 << 1;
const FLAG_METADATA_IO_ERROR: u32 = 1 << 2;
const FLAG_ENTRY_COUNT: u32 = 1 << 3;
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct TreeIndex(NonZeroU32);
impl TreeIndex {
#[must_use]
pub fn new(index: usize) -> Self {
Self::from(index)
}
#[must_use]
pub fn index(self) -> usize {
(self.0.get() - 1) as usize
}
fn from_raw(index: u32) -> Self {
debug_assert_ne!(index, NONE);
Self(NonZeroU32::new(index + 1).expect("tree index excludes u32::MAX"))
}
}
impl Default for TreeIndex {
fn default() -> Self {
Self::from_raw(0)
}
}
impl fmt::Debug for TreeIndex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "TreeIndex({})", self.index())
}
}
impl From<u32> for TreeIndex {
fn from(index: u32) -> Self {
assert!(index != NONE, "u32::MAX is reserved for missing tree links");
Self::from_raw(index)
}
}
impl From<usize> for TreeIndex {
fn from(index: usize) -> Self {
let index = u32::try_from(index).expect("tree index exceeds u32::MAX - 1");
assert!(index != NONE, "u32::MAX is reserved for missing tree links");
Self::from_raw(index)
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct EntryData {
pub size: u128,
pub mtime: SystemTime,
pub entry_count: Option<u64>,
pub metadata_io_error: bool,
pub is_dir: bool,
}
impl Default for EntryData {
fn default() -> EntryData {
EntryData {
size: u128::default(),
mtime: UNIX_EPOCH,
entry_count: None,
metadata_io_error: bool::default(),
is_dir: false,
}
}
}
impl fmt::Debug for EntryData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EntryData")
.field("size", &self.size)
.field("entry_count", &self.entry_count)
.field("metadata_io_error", &self.metadata_io_error)
.finish()
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct Entry<'a> {
pub name: Cow<'a, Path>,
pub data: EntryData,
}
impl std::ops::Deref for Entry<'_> {
type Target = EntryData;
fn deref(&self) -> &Self::Target {
&self.data
}
}
#[derive(Clone)]
struct TreeNode {
size: u128,
mtime: SystemTime,
entry_count: u64,
name_start: u32,
name_len: u32,
parent: u32,
first_child: u32,
next_sibling: u32,
flags: u32,
}
impl TreeNode {
fn new(name_start: u32, name_len: u32, data: EntryData) -> Self {
Self {
size: data.size,
mtime: data.mtime,
entry_count: data.entry_count.unwrap_or_default(),
name_start,
name_len,
parent: NONE,
first_child: NONE,
next_sibling: NONE,
flags: FLAG_OCCUPIED
| (u32::from(data.is_dir) * FLAG_DIRECTORY)
| (u32::from(data.metadata_io_error) * FLAG_METADATA_IO_ERROR)
| (u32::from(data.entry_count.is_some()) * FLAG_ENTRY_COUNT),
}
}
fn is_occupied(&self) -> bool {
self.flags & FLAG_OCCUPIED != 0
}
fn data(&self) -> EntryData {
EntryData {
size: self.size,
mtime: self.mtime,
entry_count: (self.flags & FLAG_ENTRY_COUNT != 0).then_some(self.entry_count),
metadata_io_error: self.flags & FLAG_METADATA_IO_ERROR != 0,
is_dir: self.flags & FLAG_DIRECTORY != 0,
}
}
fn set_data(&mut self, data: EntryData) {
self.size = data.size;
self.mtime = data.mtime;
self.entry_count = data.entry_count.unwrap_or_default();
self.flags = FLAG_OCCUPIED
| (u32::from(data.is_dir) * FLAG_DIRECTORY)
| (u32::from(data.metadata_io_error) * FLAG_METADATA_IO_ERROR)
| (u32::from(data.entry_count.is_some()) * FLAG_ENTRY_COUNT);
}
}
#[derive(Debug)]
pub enum TreeError {
Allocation(std::collections::TryReserveError),
Capacity,
InvalidIndex,
AlreadyAttached,
Cycle,
}
impl fmt::Display for TreeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Allocation(err) => err.fmt(f),
Self::Capacity => f.write_str("tree exceeds its u32 storage limit"),
Self::InvalidIndex => f.write_str("tree index does not exist"),
Self::AlreadyAttached => f.write_str("tree node already has a parent"),
Self::Cycle => f.write_str("tree attachment would create a cycle"),
}
}
}
impl std::error::Error for TreeError {}
impl From<std::collections::TryReserveError> for TreeError {
fn from(err: std::collections::TryReserveError) -> Self {
Self::Allocation(err)
}
}
#[derive(Clone)]
pub struct Tree {
nodes: Vec<TreeNode>,
names: Vec<u8>,
free_head: u32,
len: usize,
}
impl Default for Tree {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Tree {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Tree")
.field("nodes", &self.nodes.len())
.field("names", &self.names.len())
.field("len", &self.len)
.finish()
}
}
impl Tree {
#[must_use]
pub const fn new() -> Self {
Self {
nodes: Vec::new(),
names: Vec::new(),
free_head: NONE,
len: 0,
}
}
pub fn add_root(&mut self, name: impl AsRef<Path>, data: EntryData) -> TreeIndex {
self.try_add_root(name, data)
.expect("tree storage can be allocated")
}
pub fn add_detached(&mut self, name: impl AsRef<Path>, data: EntryData) -> TreeIndex {
self.add_root(name, data)
}
pub fn add_child(
&mut self,
parent: TreeIndex,
name: impl AsRef<Path>,
data: EntryData,
) -> TreeIndex {
self.try_add_child(parent, name, data)
.expect("tree storage can be allocated and parent is valid")
}
pub fn try_add_root(
&mut self,
name: impl AsRef<Path>,
data: EntryData,
) -> Result<TreeIndex, TreeError> {
let (name_start, name_len) = self.try_append_name(name.as_ref())?;
self.try_allocate(TreeNode::new(name_start, name_len, data))
}
pub fn try_add_child(
&mut self,
parent: TreeIndex,
name: impl AsRef<Path>,
data: EntryData,
) -> Result<TreeIndex, TreeError> {
if !self.contains(parent) {
return Err(TreeError::InvalidIndex);
}
let child = self.try_add_root(name, data)?;
self.attach(parent, child)?;
Ok(child)
}
pub(crate) fn try_add_child_native(
&mut self,
parent: TreeIndex,
name: &[u8],
data: EntryData,
) -> Result<TreeIndex, TreeError> {
if !self.contains(parent) {
return Err(TreeError::InvalidIndex);
}
let (name_start, name_len) = self.try_append_native_name(name)?;
let child = self.try_allocate(TreeNode::new(name_start, name_len, data))?;
self.attach(parent, child)?;
Ok(child)
}
pub fn attach(&mut self, parent: TreeIndex, child: TreeIndex) -> Result<(), TreeError> {
if !self.contains(parent) || !self.contains(child) {
return Err(TreeError::InvalidIndex);
}
if self.nodes[child.index()].parent != NONE {
return Err(TreeError::AlreadyAttached);
}
let mut ancestor = Some(parent);
while let Some(index) = ancestor {
if index == child {
return Err(TreeError::Cycle);
}
ancestor = self.parent(index);
}
let first_child = self.nodes[parent.index()].first_child;
self.nodes[child.index()].parent = parent.index() as u32;
self.nodes[child.index()].next_sibling = first_child;
self.nodes[parent.index()].first_child = child.index() as u32;
Ok(())
}
#[must_use]
pub fn parent(&self, index: TreeIndex) -> Option<TreeIndex> {
let node = self.node(index)?;
(node.parent != NONE).then(|| TreeIndex::from_raw(node.parent))
}
#[must_use]
pub fn children(&self, index: TreeIndex) -> Children<'_> {
Children {
nodes: &self.nodes,
next: self.node(index).map_or(NONE, |node| node.first_child),
}
}
pub fn indices(&self) -> impl Iterator<Item = TreeIndex> + '_ {
self.nodes
.iter()
.enumerate()
.filter(|(_, node)| node.is_occupied())
.map(|(index, _)| TreeIndex::from(index))
}
#[must_use]
pub fn entry(&self, index: TreeIndex) -> Option<Entry<'_>> {
let node = self.node(index)?;
let name = self.name(index)?;
Some(Entry {
name,
data: node.data(),
})
}
#[must_use]
pub fn data(&self, index: TreeIndex) -> Option<EntryData> {
self.node(index).map(TreeNode::data)
}
pub fn set_data(&mut self, index: TreeIndex, data: EntryData) -> bool {
let Some(node) = self.node_mut(index) else {
return false;
};
node.set_data(data);
true
}
pub fn update(&mut self, index: TreeIndex, edit: impl FnOnce(&mut EntryData)) -> bool {
let Some(mut data) = self.data(index) else {
return false;
};
edit(&mut data);
self.set_data(index, data)
}
#[must_use]
pub fn name(&self, index: TreeIndex) -> Option<Cow<'_, Path>> {
let bytes = self.native_name(index)?;
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt as _;
Some(Cow::Borrowed(Path::new(std::ffi::OsStr::from_bytes(bytes))))
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStringExt as _;
let wide = bytes
.chunks_exact(2)
.map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]))
.collect::<Vec<_>>();
Some(Cow::Owned(PathBuf::from(std::ffi::OsString::from_wide(
&wide,
))))
}
}
pub fn rename(&mut self, index: TreeIndex, name: impl AsRef<Path>) -> Result<(), TreeError> {
if !self.contains(index) {
return Err(TreeError::InvalidIndex);
}
let (start, len) = self.try_append_name(name.as_ref())?;
let node = &mut self.nodes[index.index()];
node.name_start = start;
node.name_len = len;
Ok(())
}
pub fn remove_subtree(&mut self, index: TreeIndex) -> usize {
if !self.contains(index) {
return 0;
}
self.detach(index);
let mut pending = vec![index];
let mut removed = 0;
while let Some(index) = pending.pop() {
let mut child = self.nodes[index.index()].first_child;
while child != NONE {
pending.push(TreeIndex::from_raw(child));
child = self.nodes[child as usize].next_sibling;
}
let node = &mut self.nodes[index.index()];
node.flags = 0;
node.parent = NONE;
node.first_child = NONE;
node.next_sibling = self.free_head;
self.free_head = index.index() as u32;
self.len -= 1;
removed += 1;
}
removed
}
#[must_use]
pub fn contains(&self, index: TreeIndex) -> bool {
self.node(index).is_some()
}
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub(crate) fn native_name(&self, index: TreeIndex) -> Option<&[u8]> {
let node = self.node(index)?;
let start = node.name_start as usize;
Some(&self.names[start..][..node.name_len as usize])
}
fn node(&self, index: TreeIndex) -> Option<&TreeNode> {
self.nodes
.get(index.index())
.filter(|node| node.is_occupied())
}
fn node_mut(&mut self, index: TreeIndex) -> Option<&mut TreeNode> {
self.nodes
.get_mut(index.index())
.filter(|node| node.is_occupied())
}
fn try_allocate(&mut self, node: TreeNode) -> Result<TreeIndex, TreeError> {
let index = if self.free_head == NONE {
let index = u32::try_from(self.nodes.len()).map_err(|_| TreeError::Capacity)?;
if index == NONE {
return Err(TreeError::Capacity);
}
self.nodes.try_reserve(1)?;
self.nodes.push(node);
index
} else {
let index = self.free_head;
self.free_head = self.nodes[index as usize].next_sibling;
self.nodes[index as usize] = node;
index
};
self.len += 1;
Ok(TreeIndex::from_raw(index))
}
fn try_append_name(&mut self, name: &Path) -> Result<(u32, u32), TreeError> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt as _;
self.try_append_native_name(name.as_os_str().as_bytes())
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt as _;
let units = name.as_os_str().encode_wide();
let byte_len = units
.clone()
.count()
.checked_mul(2)
.ok_or(TreeError::Capacity)?;
let start = self.names.len();
let end = start.checked_add(byte_len).ok_or(TreeError::Capacity)?;
let (start, len) = (
u32::try_from(start).map_err(|_| TreeError::Capacity)?,
u32::try_from(byte_len).map_err(|_| TreeError::Capacity)?,
);
u32::try_from(end).map_err(|_| TreeError::Capacity)?;
self.names.try_reserve(byte_len)?;
self.names.extend(units.flat_map(u16::to_le_bytes));
Ok((start, len))
}
}
fn try_append_native_name(&mut self, name: &[u8]) -> Result<(u32, u32), TreeError> {
let start = self.names.len();
let end = start.checked_add(name.len()).ok_or(TreeError::Capacity)?;
let (start, len) = (
u32::try_from(start).map_err(|_| TreeError::Capacity)?,
u32::try_from(name.len()).map_err(|_| TreeError::Capacity)?,
);
u32::try_from(end).map_err(|_| TreeError::Capacity)?;
self.names.try_reserve(name.len())?;
self.names.extend_from_slice(name);
Ok((start, len))
}
fn detach(&mut self, index: TreeIndex) {
let parent = self.nodes[index.index()].parent;
if parent == NONE {
return;
}
let mut link = self.nodes[parent as usize].first_child;
let mut previous = NONE;
while link != NONE {
if link == index.index() as u32 {
let next = self.nodes[link as usize].next_sibling;
if previous == NONE {
self.nodes[parent as usize].first_child = next;
} else {
self.nodes[previous as usize].next_sibling = next;
}
self.nodes[index.index()].parent = NONE;
self.nodes[index.index()].next_sibling = NONE;
return;
}
previous = link;
link = self.nodes[link as usize].next_sibling;
}
}
}
pub struct Children<'a> {
nodes: &'a [TreeNode],
next: u32,
}
impl Iterator for Children<'_> {
type Item = TreeIndex;
fn next(&mut self) -> Option<Self::Item> {
if self.next == NONE {
return None;
}
let index = self.next;
self.next = self.nodes[index as usize].next_sibling;
Some(TreeIndex::from_raw(index))
}
}
#[derive(Debug)]
pub struct Traversal {
pub tree: Tree,
pub root_index: TreeIndex,
pub start_time: Instant,
pub cost: Option<Duration>,
}
impl Default for Traversal {
fn default() -> Self {
Self::new()
}
}
impl Traversal {
#[must_use]
pub fn new() -> Self {
let mut tree = Tree::new();
let root_index = tree.add_root("", EntryData::default());
Self {
tree,
root_index,
start_time: Instant::now(),
cost: None,
}
}
#[must_use]
pub fn is_costly(&self) -> bool {
self.cost.is_none_or(|d| d.as_secs_f32() > 10.0)
}
}
#[derive(Clone, Copy)]
pub struct TraversalStats {
pub entries_traversed: u64,
pub start: std::time::Instant,
pub elapsed: Option<std::time::Duration>,
pub io_errors: u64,
pub total_bytes: Option<u128>,
}
impl Default for TraversalStats {
fn default() -> Self {
Self {
entries_traversed: 0,
start: std::time::Instant::now(),
elapsed: None,
io_errors: 0,
total_bytes: None,
}
}
}
pub struct TraversalEntry(pub(crate) crate::walk::Entry);
pub enum TraversalEvent {
Entry(io::Result<TraversalEntry>, Arc<PathBuf>, u64, usize),
RootError(Arc<PathBuf>, usize),
Finished,
}
pub struct BackgroundTraversal {
walk_options: WalkOptions,
pub root_idx: TreeIndex,
pub stats: TraversalStats,
pub(crate) root_nodes: Vec<Option<TreeIndex>>,
nodes_by_directory: Vec<Option<TreeIndex>>,
inodes: InodeFilter,
throttle: Option<Throttle>,
skip_root: bool,
use_root_path: bool,
retained_depth: Option<usize>,
preexisting_nodes: HashMap<PathBuf, (TreeIndex, bool)>,
pub event_rx: Receiver<TraversalEvent>,
}
impl BackgroundTraversal {
pub fn start(
root_idx: TreeIndex,
walk_options: &WalkOptions,
input: Vec<PathBuf>,
pattern_roots: Option<&[PathBuf]>,
skip_root: bool,
use_root_path: bool,
) -> anyhow::Result<BackgroundTraversal> {
Self::start_inner(
root_idx,
walk_options,
input,
pattern_roots,
skip_root,
use_root_path,
HashMap::new(),
)
}
pub fn start_incremental(
root_idx: TreeIndex,
walk_options: &WalkOptions,
input: Vec<PathBuf>,
pattern_roots: Option<&[PathBuf]>,
skip_root: bool,
use_root_path: bool,
preexisting_nodes: Vec<(PathBuf, TreeIndex, bool)>,
) -> anyhow::Result<BackgroundTraversal> {
let mut walk_options = walk_options.clone();
walk_options.ignore_dirs.extend(
preexisting_nodes
.iter()
.filter_map(|(path, _, _)| gix::path::realpath(path).ok()),
);
Self::start_inner(
root_idx,
&walk_options,
input,
pattern_roots,
skip_root,
use_root_path,
preexisting_nodes
.into_iter()
.map(|(path, index, needs_metadata)| (path, (index, needs_metadata)))
.collect(),
)
}
fn start_inner(
root_idx: TreeIndex,
walk_options: &WalkOptions,
input: Vec<PathBuf>,
pattern_roots: Option<&[PathBuf]>,
skip_root: bool,
use_root_path: bool,
preexisting_nodes: HashMap<PathBuf, (TreeIndex, bool)>,
) -> anyhow::Result<BackgroundTraversal> {
let num_roots = input.len();
let (entry_tx, entry_rx) = crossbeam::channel::bounded(100);
let pattern_roots = pattern_roots.map(<[PathBuf]>::to_owned);
std::thread::Builder::new()
.name("dua-fs-walk-dispatcher".to_string())
.spawn({
let walk_options = walk_options.clone();
move || {
let (mut root_paths, mut root_indices, mut device_ids, mut walk_roots) = (
Vec::with_capacity(input.len()),
Vec::with_capacity(input.len()),
Vec::with_capacity(input.len()),
Vec::with_capacity(input.len()),
);
for (root_idx, root_path) in input.into_iter().enumerate() {
log::info!("Walking {}", root_path.display());
let device_id = if walk_options.cross_filesystems {
0
} else {
let Ok(device_id) = crossdev::init(&root_path) else {
if entry_tx
.send(TraversalEvent::RootError(Arc::new(root_path), root_idx))
.is_err()
{
return;
}
continue;
};
device_id
};
let pattern_root = pattern_roots.as_deref().map(|pattern_roots| {
pattern_roots
.iter()
.filter(|candidate| root_path.starts_with(candidate))
.max_by_key(|candidate| candidate.components().count())
.cloned()
.unwrap_or_else(|| root_path.clone())
});
walk_roots.push(WalkRoot {
index: walk_roots.len(),
pattern_root,
path: root_path.clone(),
#[cfg(any(windows, target_os = "macos"))]
entry: None,
device_id,
});
root_indices.push(root_idx);
device_ids.push(device_id);
root_paths.push(Arc::new(root_path));
}
for (root, event) in walk_options.iter_from_paths(
walk_roots,
skip_root,
crate::walk::Order::ParentFirst,
) {
let crate::walk::RootEvent::Entry(entry) = event else {
continue;
};
if entry_tx
.send(TraversalEvent::Entry(
entry.map(TraversalEntry),
Arc::clone(&root_paths[root]),
device_ids[root],
root_indices[root],
))
.is_err()
{
return;
}
}
if entry_tx.send(TraversalEvent::Finished).is_err() {
log::error!("Failed to send TraversalEvents::Finished event");
}
}
})?;
Ok(Self {
walk_options: walk_options.clone(),
root_idx,
stats: TraversalStats::default(),
root_nodes: vec![None; num_roots],
nodes_by_directory: Vec::new(),
inodes: InodeFilter::default(),
throttle: Some(Throttle::new(Duration::from_millis(250), None)),
skip_root,
use_root_path,
retained_depth: None,
preexisting_nodes,
event_rx: entry_rx,
})
}
#[must_use]
pub fn root_nodes(&self) -> Option<Vec<TreeIndex>> {
self.root_nodes.iter().copied().collect()
}
pub(crate) fn retain_depth(mut self, depth: Option<usize>) -> Self {
self.retained_depth = depth;
self
}
fn record_error_on_root(
&mut self,
traversal: &mut Traversal,
root_idx: usize,
root_path: &Path,
) {
if self.skip_root {
return;
}
if let Some(root) = self.root_nodes[root_idx] {
traversal
.tree
.update(root, |entry| entry.metadata_io_error = true);
return;
}
let name = if self.use_root_path {
root_path.to_owned()
} else {
root_path
.file_name()
.unwrap_or(root_path.as_os_str())
.into()
};
let node = traversal.tree.add_child(
self.root_idx,
name,
EntryData {
metadata_io_error: true,
is_dir: true,
..EntryData::default()
},
);
traversal.tree.update(self.root_idx, |entry| {
*entry.entry_count.get_or_insert(0) += 1;
});
self.root_nodes[root_idx] = Some(node);
}
fn set_directory_node(&mut self, directory_id: usize, node: TreeIndex) {
if self.nodes_by_directory.len() <= directory_id {
self.nodes_by_directory.resize(directory_id + 1, None);
}
self.nodes_by_directory[directory_id] = Some(node);
}
#[expect(
clippy::too_many_lines,
reason = "event integration keeps tree updates atomic"
)]
pub fn integrate_traversal_event(
&mut self,
traversal: &mut Traversal,
event: TraversalEvent,
) -> Option<bool> {
match event {
TraversalEvent::Entry(entry, root_path, device_id, root_idx) => {
self.stats.entries_traversed += 1;
let mut data = EntryData::default();
let Ok(TraversalEntry(entry)) = entry else {
self.stats.io_errors += 1;
self.record_error_on_root(traversal, root_idx, &root_path);
return self
.throttle
.as_ref()
.is_some_and(|t| t.can_update())
.then_some(false);
};
let walk_depth = entry.depth;
let name = if !self.skip_root && walk_depth == 0 && self.use_root_path {
root_path.as_path()
} else {
Path::new(&entry.file_name)
};
let mut file_size = 0u128;
let mut mtime: SystemTime = UNIX_EPOCH;
let mut has_mtime = false;
data.is_dir = entry.file_type.is_dir();
if let Ok(m) = &entry.metadata {
if self.walk_options.count_hard_links
|| self.inodes.add(&entry, m)
&& (self.walk_options.cross_filesystems
|| crossdev::is_same_device(device_id, m))
{
if self.walk_options.apparent_size {
file_size = u128::from(m.len());
} else {
file_size = u128::from(
size_on_disk(
&entry.parent_path,
name,
m,
data.is_dir,
&self.walk_options,
&mut self.inodes,
)
.unwrap_or_else(|_| {
self.stats.io_errors += 1;
data.metadata_io_error = true;
0
}),
);
}
} else {
data.entry_count = Some(0);
}
if let Ok(modified) = m.modified() {
mtime = modified;
has_mtime = true;
} else {
self.stats.io_errors += 1;
data.metadata_io_error = true;
}
} else {
self.stats.io_errors += 1;
data.metadata_io_error = true;
}
data.mtime = mtime;
data.size = file_size;
if data.is_dir {
data.entry_count = Some(1);
}
let entry_count = u64::from(data.is_dir || data.entry_count != Some(0));
let preexisting = if self.preexisting_nodes.is_empty() {
None
} else {
self.preexisting_nodes.remove(&entry.path())
};
if let Some((index, needs_metadata)) = preexisting {
if let Some(directory_id) = entry.directory_id {
self.set_directory_node(directory_id.index(), index);
}
if needs_metadata {
traversal.tree.update(index, |existing| {
existing.size += file_size;
*existing.entry_count.get_or_insert(0) += entry_count;
if has_mtime {
existing.mtime = data.mtime;
}
existing.metadata_io_error |= data.metadata_io_error;
existing.is_dir = data.is_dir;
});
let mut ancestor = traversal.tree.parent(index);
while let Some(ancestor_index) = ancestor {
ancestor = traversal.tree.parent(ancestor_index);
traversal.tree.update(ancestor_index, |entry| {
entry.size += file_size;
*entry.entry_count.get_or_insert(0) += entry_count;
});
}
}
return self
.throttle
.as_ref()
.is_some_and(|t| t.can_update())
.then_some(false);
}
let retain_entry = self.retained_depth.is_none_or(|depth| walk_depth <= depth);
let parent_index = if walk_depth == 0 {
self.root_idx
} else {
let parent_id = entry
.parent_directory_id
.expect("non-root entries have a parent directory identifier");
if self.skip_root && walk_depth == 1 {
self.set_directory_node(parent_id.index(), self.root_idx);
}
self.nodes_by_directory
.get(parent_id.index())
.copied()
.flatten()
.expect("parent entries are emitted before their children")
};
let mut retained_node = None;
if retain_entry {
let entry_index = traversal.tree.add_child(parent_index, name, data);
retained_node = Some(entry_index);
if walk_depth == 0 {
self.root_nodes[root_idx] = Some(entry_index);
}
}
if let Some(directory_id) = entry.directory_id {
self.set_directory_node(
directory_id.index(),
retained_node.unwrap_or(parent_index),
);
}
let mut ancestor = Some(parent_index);
while let Some(index) = ancestor {
ancestor = traversal.tree.parent(index);
traversal.tree.update(index, |entry| {
entry.size += file_size;
*entry.entry_count.get_or_insert(0) += entry_count;
});
}
if self.throttle.as_ref().is_some_and(|t| t.can_update()) {
return Some(false);
}
}
TraversalEvent::RootError(root_path, root_idx) => {
self.stats.io_errors += 1;
self.record_error_on_root(traversal, root_idx, &root_path);
}
TraversalEvent::Finished => {
self.throttle = None;
let root_size = traversal
.tree
.data(self.root_idx)
.expect("traversal root exists")
.size;
self.nodes_by_directory.clear();
self.stats.total_bytes = Some(root_size);
self.stats.elapsed = Some(self.stats.start.elapsed());
return Some(true);
}
}
None
}
}
#[cfg(not(any(windows, target_os = "macos")))]
fn size_on_disk(
_parent: &Path,
name: &Path,
meta: &crate::walk::Metadata,
_is_dir: bool,
_options: &WalkOptions,
_inodes: &mut InodeFilter,
) -> io::Result<u64> {
name.size_on_disk_fast(meta)
}
#[cfg(target_os = "macos")]
#[allow(clippy::unnecessary_wraps)]
fn size_on_disk(
_parent: &Path,
_name: &Path,
meta: &crate::walk::Metadata,
_is_dir: bool,
options: &WalkOptions,
inodes: &mut InodeFilter,
) -> io::Result<u64> {
Ok(if options.metadata_options.apfs_clone_metadata {
inodes.allocated_size(meta)
} else {
meta.allocated_size()
})
}
#[cfg(windows)]
#[allow(clippy::unnecessary_wraps)]
fn size_on_disk(
_parent: &Path,
_name: &Path,
meta: &crate::walk::Metadata,
is_dir: bool,
_options: &WalkOptions,
_inodes: &mut InodeFilter,
) -> io::Result<u64> {
Ok(if is_dir { 0 } else { meta.allocated_size() })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ancestor_sizes_update_before_traversal_finishes() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir(dir.path().join("nested")).unwrap();
std::fs::write(dir.path().join("nested/file"), b"content").unwrap();
let mut traversal = Traversal::new();
let mut background = BackgroundTraversal::start(
traversal.root_index,
&WalkOptions {
threads: 2,
count_hard_links: true,
apparent_size: true,
cross_filesystems: true,
ignore_dirs: std::collections::BTreeSet::default(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions::default(),
},
vec![dir.path().to_owned()],
None,
false,
false,
)
.unwrap();
loop {
let event = background.event_rx.recv().unwrap();
let is_file = matches!(
&event,
TraversalEvent::Entry(Ok(TraversalEntry(entry)), _, _, _)
if entry.file_name == "file"
);
background.integrate_traversal_event(&mut traversal, event);
if is_file {
let root_size = traversal.tree.data(traversal.root_index).unwrap().size;
assert!(
root_size >= 7,
"root size should include the 7-byte nested file, got {root_size}"
);
let nested_size = traversal
.tree
.indices()
.find_map(|index| {
(traversal.tree.name(index).as_deref() == Some(Path::new("nested")))
.then(|| traversal.tree.data(index).unwrap().size)
})
.unwrap();
assert!(
nested_size >= 7,
"nested directory size should include its 7-byte file, got {nested_size}"
);
break;
}
}
}
#[test]
fn duplicate_roots_keep_their_own_children() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("file"), b"content").unwrap();
let mut traversal = Traversal::new();
let mut background = BackgroundTraversal::start(
traversal.root_index,
&WalkOptions {
threads: 1,
count_hard_links: true,
apparent_size: true,
cross_filesystems: true,
ignore_dirs: std::collections::BTreeSet::default(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions::default(),
},
vec![dir.path().to_owned(), dir.path().to_owned()],
None,
false,
false,
)
.unwrap();
while !background
.integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
.unwrap_or(false)
{}
let roots = traversal
.tree
.children(traversal.root_index)
.collect::<Vec<_>>();
assert_eq!(roots.len(), 2);
for root in roots {
assert_eq!(traversal.tree.children(root).count(), 1);
}
}
#[test]
fn retained_depth_rolls_deeper_sizes_into_the_last_kept_node() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("one/two")).unwrap();
std::fs::write(dir.path().join("one/two/file"), b"content").unwrap();
for (depth, expected_nodes) in [(0, 2), (1, 3)] {
let mut traversal = Traversal::new();
let mut background = BackgroundTraversal::start(
traversal.root_index,
&WalkOptions {
threads: 1,
count_hard_links: true,
apparent_size: true,
cross_filesystems: true,
ignore_dirs: std::collections::BTreeSet::default(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions::default(),
},
vec![dir.path().to_owned()],
None,
false,
true,
)
.unwrap()
.retain_depth(Some(depth));
while !background
.integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
.unwrap_or(false)
{}
assert_eq!(traversal.tree.len(), expected_nodes);
assert!(traversal.tree.data(traversal.root_index).unwrap().size >= 7);
let root = traversal
.tree
.children(traversal.root_index)
.next()
.unwrap();
let last_retained = if depth == 0 {
root
} else {
traversal.tree.children(root).next().unwrap()
};
assert!(traversal.tree.data(last_retained).unwrap().size >= 7);
}
}
#[test]
fn descendant_entry_errors_mark_the_retained_root() {
let dir = tempfile::tempdir().unwrap();
let root_path = dir.path().to_owned();
let mut traversal = Traversal::new();
let mut background = BackgroundTraversal::start(
traversal.root_index,
&WalkOptions {
threads: 1,
count_hard_links: true,
apparent_size: true,
cross_filesystems: true,
ignore_dirs: std::collections::BTreeSet::default(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions::default(),
},
vec![root_path.clone()],
None,
false,
true,
)
.unwrap()
.retain_depth(Some(0));
while background.root_nodes[0].is_none() {
let event = background.event_rx.recv().unwrap();
background.integrate_traversal_event(&mut traversal, event);
}
let root = background.root_nodes[0].unwrap();
background.integrate_traversal_event(
&mut traversal,
TraversalEvent::Entry(
Err(io::Error::other("unreadable descendant")),
Arc::new(root_path),
0,
0,
),
);
assert_eq!(background.stats.io_errors, 1);
assert!(
traversal.tree.data(root).unwrap().metadata_io_error,
"a path-less descendant error is reported on its retained root: {:?}",
traversal.tree.entry(root).unwrap()
);
}
#[cfg(target_os = "macos")]
#[test]
fn interactive_traversal_deduplicates_apfs_clones() {
use std::os::unix::fs::MetadataExt as _;
fn total(path: &Path, deduplicate: bool) -> u128 {
let mut traversal = Traversal::new();
let mut background = BackgroundTraversal::start(
traversal.root_index,
&WalkOptions {
threads: 2,
count_hard_links: false,
apparent_size: false,
cross_filesystems: true,
ignore_dirs: std::collections::BTreeSet::default(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions {
apfs_clone_metadata: deduplicate,
},
},
vec![path.to_owned()],
None,
false,
false,
)
.unwrap();
while !background
.integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
.unwrap_or(false)
{}
traversal.tree.data(traversal.root_index).unwrap().size
}
let directory = tempfile::tempdir().unwrap();
let original = directory.path().join("original");
let clone = directory.path().join("clone");
std::fs::write(&original, vec![1; 8192]).unwrap();
std::fs::copy(&original, clone).unwrap();
let data_fork_size = u128::from(std::fs::metadata(original).unwrap().blocks()) * 512;
assert_eq!(
total(directory.path(), false) - total(directory.path(), true),
data_fork_size
);
}
#[cfg(unix)]
#[test]
fn root_device_error_is_reported() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let root = dir.path().join("dangling");
let valid = dir.path().join("valid");
symlink(dir.path().join("missing"), &root).unwrap();
std::fs::write(&valid, b"content").unwrap();
let mut traversal = Traversal::new();
let mut background = BackgroundTraversal::start(
traversal.root_index,
&WalkOptions {
threads: 1,
count_hard_links: true,
apparent_size: true,
cross_filesystems: false,
ignore_dirs: std::collections::BTreeSet::default(),
ignore_patterns: None,
metadata_options: crate::TraversalOptions::default(),
},
vec![root.clone(), valid.clone()],
None,
false,
false,
)
.unwrap();
while !background
.integrate_traversal_event(&mut traversal, background.event_rx.recv().unwrap())
.unwrap_or(false)
{}
assert_eq!(background.stats.io_errors, 1);
let roots = background
.root_nodes
.iter()
.copied()
.collect::<Option<Vec<_>>>()
.unwrap();
assert_eq!(roots.len(), 2, "one node per input root: {roots:?}");
assert_eq!(
traversal
.tree
.data(traversal.root_index)
.unwrap()
.entry_count,
Some(2),
"the synthetic root counts both input roots"
);
assert!(
traversal.tree.data(roots[0]).unwrap().metadata_io_error,
"the failed root records its I/O error: {:?}",
traversal.tree.entry(roots[0]).unwrap()
);
assert_eq!(
traversal.tree.name(roots[0]).unwrap(),
Path::new("dangling"),
"the failed root retains its display name"
);
assert!(
roots
.iter()
.all(|root| traversal.tree.parent(*root) == Some(traversal.root_index)),
"all input roots are children of the synthetic root: {roots:?}"
);
}
#[test]
fn tree_tracks_parents_and_reverse_insertion_order() {
let mut tree = Tree::new();
let root = tree.add_root("root", EntryData::default());
let first = tree.add_child(root, "first", EntryData::default());
let second = tree.add_child(root, "second", EntryData::default());
assert_eq!(tree.parent(first), Some(root));
assert_eq!(tree.parent(second), Some(root));
assert_eq!(tree.children(root).collect::<Vec<_>>(), [second, first]);
}
#[test]
fn tree_removal_is_stable_and_reuses_slots() {
let mut tree = Tree::new();
let root = tree.add_root("root", EntryData::default());
let kept = tree.add_child(root, "kept", EntryData::default());
let removed = tree.add_child(root, "removed", EntryData::default());
let nested = tree.add_child(removed, "nested", EntryData::default());
assert_eq!(tree.remove_subtree(removed), 2);
assert!(tree.contains(kept));
assert_eq!(tree.children(root).collect::<Vec<_>>(), [kept]);
let reused = tree.add_child(root, "reused", EntryData::default());
assert!(
reused == removed || reused == nested,
"a deleted slot is reused"
);
assert_eq!(tree.name(reused).as_deref(), Some(Path::new("reused")));
assert_eq!(tree.children(root).collect::<Vec<_>>(), [reused, kept]);
}
#[test]
fn detached_nodes_can_be_renamed_and_attached() {
let mut tree = Tree::new();
let root = tree.add_root("root", EntryData::default());
let child = tree.add_detached("before", EntryData::default());
tree.rename(child, "after").unwrap();
tree.attach(root, child).unwrap();
assert_eq!(tree.name(child).as_deref(), Some(Path::new("after")));
assert_eq!(tree.parent(child), Some(root));
assert!(matches!(tree.attach(child, root), Err(TreeError::Cycle)));
}
#[cfg(target_pointer_width = "64")]
#[test]
fn tree_nodes_and_optional_indices_are_compact() {
assert_eq!(std::mem::size_of::<TreeNode>(), 64);
assert_eq!(std::mem::size_of::<Option<TreeIndex>>(), 4);
}
}