Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions src/uu/du/src/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::mpsc;
use std::thread;
use std::time::SystemTime;
use thiserror::Error;
use uucore::display::{Quotable, print_verbatim};
use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code};
Expand Down Expand Up @@ -82,6 +83,7 @@ struct TraversalOptions {
count_links: bool,
verbose: bool,
excludes: Vec<Pattern>,
time_field: Option<MetadataTimeField>,
}

struct StatPrinter {
Expand Down Expand Up @@ -125,6 +127,7 @@ struct Stat {
inodes: u64,
inode: Option<FileInfo>,
metadata: Metadata,
latest_time: Option<SystemTime>,
}

impl Stat {
Expand Down Expand Up @@ -153,6 +156,9 @@ impl Stat {

let file_info = get_file_info(path, &metadata);
let blocks = get_blocks(path, &metadata);
let latest_time = options
.time_field
.and_then(|field| metadata_get_time(&metadata, field));

Ok(Self {
path: path.to_path_buf(),
Expand All @@ -161,12 +167,17 @@ impl Stat {
inodes: 1,
inode: file_info,
metadata,
latest_time,
})
}

/// Create a Stat using safe traversal methods with `DirFd` for the root directory
#[cfg(all(unix, not(target_os = "redox")))]
fn new_from_dirfd(dir_fd: &DirFd, full_path: &Path) -> std::io::Result<Self> {
fn new_from_dirfd(
dir_fd: &DirFd,
full_path: &Path,
time_field: Option<MetadataTimeField>,
) -> std::io::Result<Self> {
// Get metadata for the directory itself using fstat
let safe_metadata = dir_fd.metadata()?;

Expand All @@ -183,6 +194,7 @@ impl Stat {
// This is still needed for compatibility but should work since we're dealing with
// the root path which should be accessible
let std_metadata = fs::symlink_metadata(full_path)?;
let latest_time = time_field.and_then(|field| metadata_get_time(&std_metadata, field));

Ok(Self {
path: full_path.to_path_buf(),
Expand All @@ -195,6 +207,7 @@ impl Stat {
inodes: 1,
inode: file_info_option,
metadata: std_metadata,
latest_time,
})
}
}
Expand Down Expand Up @@ -288,6 +301,19 @@ fn read_block_size(s: Option<&str>) -> UResult<u64> {
}
}

#[cfg(all(unix, not(target_os = "redox")))]
fn time_from_raw_stat(
entry_stat: &uucore::safe_traversal::Metadata,
time_field: MetadataTimeField,
) -> Option<SystemTime> {
match time_field {
MetadataTimeField::Modification => entry_stat.modified(),
MetadataTimeField::Access => entry_stat.accessed(),
MetadataTimeField::Change => entry_stat.changed(),
MetadataTimeField::Birth => None,
}
}

#[cfg(all(unix, not(target_os = "redox")))]
// Implement safe_du on Unix (except Redox which lacks full stat support)
// This is done for TOCTOU safety
Expand Down Expand Up @@ -322,6 +348,10 @@ fn safe_du(
fs::symlink_metadata("/").expect("root should be accessible")
});

let latest_time = options
.time_field
.and_then(|field| metadata_get_time(&std_metadata, field));

Stat {
path: path.to_path_buf(),
size: if safe_metadata.is_dir() {
Expand All @@ -333,6 +363,7 @@ fn safe_du(
inodes: 1,
inode: file_info_option,
metadata: std_metadata,
latest_time,
}
}
Err(e) => {
Expand Down Expand Up @@ -360,7 +391,7 @@ fn safe_du(
Err(_e) => {
// Try using our new DirFd method for the root directory
match DirFd::open(path, SymlinkBehavior::Follow) {
Ok(dir_fd) => match Stat::new_from_dirfd(&dir_fd, path) {
Ok(dir_fd) => match Stat::new_from_dirfd(&dir_fd, path, options.time_field) {
Ok(s) => s,
Err(e) => {
let error = e.map_err_context(
Expand Down Expand Up @@ -465,6 +496,11 @@ fn safe_du(
dev_id: entry_stat.st_dev as u64,
});

let safe_metadata = uucore::safe_traversal::Metadata::from_stat(entry_stat);
let latest_time = options
.time_field
.and_then(|field| time_from_raw_stat(&safe_metadata, field));

// For safe traversal, we need to handle stats differently
// We can't use std::fs::Metadata since that requires the full path
let this_stat = if is_dir {
Expand All @@ -479,6 +515,7 @@ fn safe_du(
// We need a fake metadata - create one from symlink_metadata of parent
// This is a workaround since we can't get real metadata without the full path
metadata: my_stat.metadata.clone(),
latest_time,
}
} else {
// For files
Expand All @@ -491,6 +528,7 @@ fn safe_du(
inodes: 1,
inode: file_info,
metadata: my_stat.metadata.clone(),
latest_time,
}
};

Expand Down Expand Up @@ -541,6 +579,11 @@ fn safe_du(
my_stat.size += this_stat.size;
my_stat.blocks += this_stat.blocks;
my_stat.inodes += this_stat.inodes;
my_stat.latest_time = match (my_stat.latest_time, this_stat.latest_time) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, None) => a,
(None, b) => b,
}
}
print_tx.send(Ok(StatPrintInfo {
stat: this_stat,
Expand All @@ -550,6 +593,11 @@ fn safe_du(
my_stat.size += this_stat.size;
my_stat.blocks += this_stat.blocks;
my_stat.inodes += 1;
my_stat.latest_time = match (my_stat.latest_time, this_stat.latest_time) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, None) => a,
(None, b) => b,
};
if options.all {
print_tx.send(Ok(StatPrintInfo {
stat: this_stat,
Expand Down Expand Up @@ -702,6 +750,12 @@ fn du_regular(
my_stat.size += this_stat.size;
my_stat.blocks += this_stat.blocks;
my_stat.inodes += this_stat.inodes;
my_stat.latest_time =
match (my_stat.latest_time, this_stat.latest_time) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, None) => a,
(None, b) => b,
};
}
print_tx.send(Ok(StatPrintInfo {
stat: this_stat,
Expand All @@ -711,6 +765,12 @@ fn du_regular(
my_stat.size += this_stat.size;
my_stat.blocks += this_stat.blocks;
my_stat.inodes += 1;
my_stat.latest_time =
match (my_stat.latest_time, this_stat.latest_time) {
(Some(a), Some(b)) => Some(a.max(b)),
(a, None) => a,
(None, b) => b,
};
if options.all {
print_tx.send(Ok(StatPrintInfo {
stat: this_stat,
Expand Down Expand Up @@ -875,8 +935,8 @@ impl StatPrinter {
fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> {
print!("{}\t", self.convert_size(size));

if let Some(md_time) = &self.time {
if let Some(time) = metadata_get_time(&stat.metadata, *md_time) {
if let Some(_md_time) = &self.time {
if let Some(time) = stat.latest_time {
format_system_time(
&mut stdout(),
time,
Expand Down Expand Up @@ -1072,6 +1132,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
count_links,
verbose: matches.get_flag(options::VERBOSE),
excludes: build_exclude_patterns(&matches)?,
time_field: time,
};

let time_format = if time.is_some() {
Expand Down
25 changes: 24 additions & 1 deletion src/uucore/src/lib/features/safe_traversal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ use std::ffi::{CString, OsStr, OsString};
use std::fs;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use nix::dir::Dir;
use nix::fcntl::{OFlag, openat};
Expand Down Expand Up @@ -742,10 +744,31 @@ impl Metadata {
pub fn is_empty(&self) -> bool {
self.len() == 0
}

fn time_from_secs_nsecs(secs: i64, nsecs: i64) -> Option<SystemTime> {
use std::time::{Duration, UNIX_EPOCH};
if secs >= 0 {
UNIX_EPOCH.checked_add(Duration::new(secs as u64, nsecs as u32))
} else {
UNIX_EPOCH.checked_sub(Duration::new((-secs) as u64, 0))
}
}

pub fn modified(&self) -> Option<SystemTime> {
Self::time_from_secs_nsecs(self.mtime(), self.mtime_nsec())
}

pub fn accessed(&self) -> Option<SystemTime> {
Self::time_from_secs_nsecs(self.atime(), self.atime_nsec())
}

pub fn changed(&self) -> Option<SystemTime> {
Self::time_from_secs_nsecs(self.ctime(), self.ctime_nsec())
}
}

// Add MetadataExt trait implementation for compatibility
impl std::os::unix::fs::MetadataExt for Metadata {
impl MetadataExt for Metadata {
// st_dev type varies by platform (i32 on macOS, u64 on Linux)
#[allow(clippy::unnecessary_cast)]
fn dev(&self) -> u64 {
Expand Down
Loading
Loading