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
90 changes: 45 additions & 45 deletions src/uucore/src/lib/features/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::fs::read_dir;
use std::hash::Hash;
use std::io::Stdin;
use std::io::{Error, ErrorKind, Result as IOResult};
#[cfg(any(unix, all(target_os = "wasi", target_env = "p2")))]
#[cfg(any(unix, target_os = "wasi"))]
use std::os::fd::AsFd;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
Expand Down Expand Up @@ -52,15 +52,13 @@ macro_rules! has {
/// Information to uniquely identify a file
#[derive(Clone)]
pub struct FileInformation(
#[cfg(unix)] rustix::fs::Stat,
#[cfg(any(unix, target_os = "wasi"))] rustix::fs::Stat,
#[cfg(windows)] winapi_util::file::Information,
// WASI does not have nix::sys::stat, so we store std::fs::Metadata instead.
#[cfg(target_os = "wasi")] fs::Metadata,
);

impl FileInformation {
/// Get information from a currently open file
#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
pub fn from_file(file: &impl AsFd) -> IOResult<Self> {
let stat = rustix::fs::fstat(file)?;
Ok(Self(stat))
Expand All @@ -78,7 +76,7 @@ impl FileInformation {
/// If `path` points to a symlink and `dereference` is true, information about
/// the link's target will be returned.
pub fn from_path(path: impl AsRef<Path>, dereference: bool) -> IOResult<Self> {
#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
{
let stat = if dereference {
rustix::fs::stat(path.as_ref())
Expand All @@ -102,20 +100,10 @@ impl FileInformation {
let file = open_options.read(true).open(path.as_ref())?;
Self::from_file(&file)
}
// WASI: use std::fs::metadata / symlink_metadata since nix is not available
#[cfg(target_os = "wasi")]
{
let metadata = if dereference {
fs::metadata(path.as_ref())
} else {
fs::symlink_metadata(path.as_ref())
};
Ok(Self(metadata?))
}
}

pub fn file_size(&self) -> u64 {
#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
{
assert!(self.0.st_size >= 0, "File size is negative");
self.0.st_size.try_into().unwrap()
Expand All @@ -124,10 +112,6 @@ impl FileInformation {
{
self.0.file_size()
}
#[cfg(target_os = "wasi")]
{
self.0.len()
}
}

#[cfg(windows)]
Expand Down Expand Up @@ -178,37 +162,35 @@ impl FileInformation {
return self.0.st_nlink.try_into().unwrap();
#[cfg(windows)]
return self.0.number_of_links();
// WASI: nlink is not available in std::fs::Metadata, return 1
#[cfg(target_os = "wasi")]
return 1;
#[allow(clippy::useless_conversion)]
return self.0.st_nlink.into();
}

#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
pub fn inode(&self) -> u64 {
#[cfg(all(not(any(target_os = "netbsd")), target_pointer_width = "64"))]
#[cfg(all(
not(any(target_os = "netbsd", target_os = "wasi")),
target_pointer_width = "64"
))]
return self.0.st_ino;
#[cfg(any(target_os = "netbsd", not(target_pointer_width = "64")))]
#[cfg(any(
target_os = "netbsd",
target_os = "wasi",
not(target_pointer_width = "64")
))]
#[allow(clippy::useless_conversion)]
return self.0.st_ino.into();
}
}

#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
impl PartialEq for FileInformation {
fn eq(&self, other: &Self) -> bool {
self.0.st_dev == other.0.st_dev && self.0.st_ino == other.0.st_ino
}
}

// WASI: compare by file type and size as a basic heuristic since
// device/inode numbers are not available through std::fs::Metadata.
#[cfg(target_os = "wasi")]
impl PartialEq for FileInformation {
fn eq(&self, other: &Self) -> bool {
self.0.file_type() == other.0.file_type() && self.0.len() == other.0.len()
}
}

#[cfg(target_os = "windows")]
impl PartialEq for FileInformation {
fn eq(&self, other: &Self) -> bool {
Expand All @@ -221,7 +203,7 @@ impl Eq for FileInformation {}

impl Hash for FileInformation {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
#[cfg(unix)]
#[cfg(any(unix, target_os = "wasi"))]
{
self.0.st_dev.hash(state);
self.0.st_ino.hash(state);
Expand All @@ -231,11 +213,6 @@ impl Hash for FileInformation {
self.0.volume_serial_number().hash(state);
self.0.file_index().hash(state);
}
#[cfg(target_os = "wasi")]
{
self.0.len().hash(state);
self.0.file_type().is_dir().hash(state);
}
}
}

Expand Down Expand Up @@ -704,8 +681,8 @@ pub fn is_symlink_loop(path: &Path) -> bool {
false
}

#[cfg(not(unix))]
// Hard link comparison is not supported on non-Unix platforms
#[cfg(not(any(unix, target_os = "wasi")))]
// Hard link comparison is not supported on non-Unix, non-WASI platforms
pub fn are_hardlinks_to_same_file(_source: &Path, _target: &Path) -> bool {
false
}
Expand All @@ -731,7 +708,20 @@ pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool {
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
}

#[cfg(not(unix))]
// `std::os::unix::fs::MetadataExt` is unavailable on WASI (`std::os::wasi` is
// nightly-only). `rustix::fs::stat` exposes the same st_ino/st_dev fields and
// works on stable for both wasip1 and wasip2.
#[cfg(target_os = "wasi")]
pub fn are_hardlinks_to_same_file(source: &Path, target: &Path) -> bool {
let (Ok(source_stat), Ok(target_stat)) = (rustix::fs::lstat(source), rustix::fs::lstat(target))
else {
return false;
};

source_stat.st_ino == target_stat.st_ino && source_stat.st_dev == target_stat.st_dev
}

#[cfg(not(any(unix, target_os = "wasi")))]
pub fn are_hardlinks_or_one_way_symlink_to_same_file(_source: &Path, _target: &Path) -> bool {
false
}
Expand All @@ -757,6 +747,16 @@ pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Pat
source_metadata.ino() == target_metadata.ino() && source_metadata.dev() == target_metadata.dev()
}

#[cfg(target_os = "wasi")]
pub fn are_hardlinks_or_one_way_symlink_to_same_file(source: &Path, target: &Path) -> bool {
let (Ok(source_stat), Ok(target_stat)) = (rustix::fs::stat(source), rustix::fs::lstat(target))
else {
return false;
};

source_stat.st_ino == target_stat.st_ino && source_stat.st_dev == target_stat.st_dev
}

/// Returns true if the passed `path` ends with a path terminator.
///
/// This function examines the last character of the path to determine
Expand Down
6 changes: 5 additions & 1 deletion src/uucore/src/lib/features/fsext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ fn metadata_get_change_time(md: &Metadata) -> Option<SystemTime> {

#[cfg(not(unix))]
fn metadata_get_change_time(_md: &Metadata) -> Option<SystemTime> {
// Not available.
// Not available: `std::fs::Metadata` has no ctime accessor without
// `std::os::unix::fs::MetadataExt` (unix-only) or `std::os::wasi::fs::MetadataExt`
// (WASI, but nightly-only). Getting ctime on WASI would require get stat of
// the path directly via `rustix::fs::stat` instead of going through
// `Metadata`, which isn't available at this call site.
None
}

Expand Down
64 changes: 27 additions & 37 deletions tests/by-util/test_comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,17 +455,12 @@ fn test_sorted() {
let at = &scene.fixtures;
at.write("comm1", "1\n3");
at.write("comm2", "3\n2");
let cmd = scene.ucmd().args(&["comm1", "comm2"]).run();
// WASI's strcoll (C locale only) may not detect unsorted input,
// but the comparison output is still correct.
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
cmd.success().stdout_is("1\n\t\t3\n\t2\n");
} else {
cmd.failure()
.code_is(1)
.stdout_is("1\n\t\t3\n\t2\n")
.stderr_is("comm: file 2 is not in sorted order\ncomm: input is not in sorted order\n");
}
scene
.ucmd()
.args(&["comm1", "comm2"])
.fails_with_code(1)
.stdout_is("1\n\t\t3\n\t2\n")
.stderr_is("comm: file 2 is not in sorted order\ncomm: input is not in sorted order\n");
}

#[test]
Expand All @@ -492,19 +487,16 @@ fn test_both_inputs_out_of_order() {
at.write("file_a", "3\n1\n0\n");
at.write("file_b", "3\n2\n0\n");

let cmd = scene.ucmd().args(&["file_a", "file_b"]).run();
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
cmd.success().stdout_is("\t\t3\n1\n0\n\t2\n\t0\n");
} else {
cmd.failure()
.code_is(1)
.stdout_is("\t\t3\n1\n0\n\t2\n\t0\n")
.stderr_is(
"comm: file 1 is not in sorted order\n\
comm: file 2 is not in sorted order\n\
comm: input is not in sorted order\n",
);
}
scene
.ucmd()
.args(&["file_a", "file_b"])
.fails_with_code(1)
.stdout_is("\t\t3\n1\n0\n\t2\n\t0\n")
.stderr_is(
"comm: file 1 is not in sorted order\n\
comm: file 2 is not in sorted order\n\
comm: input is not in sorted order\n",
);
}

#[test]
Expand All @@ -514,19 +506,16 @@ fn test_both_inputs_out_of_order_last_pair() {
at.write("file_a", "3\n1\n");
at.write("file_b", "3\n2\n");

let cmd = scene.ucmd().args(&["file_a", "file_b"]).run();
if std::env::var("UUTESTS_WASM_RUNNER").is_ok() {
cmd.success().stdout_is("\t\t3\n1\n\t2\n");
} else {
cmd.failure()
.code_is(1)
.stdout_is("\t\t3\n1\n\t2\n")
.stderr_is(
"comm: file 1 is not in sorted order\n\
comm: file 2 is not in sorted order\n\
comm: input is not in sorted order\n",
);
}
scene
.ucmd()
.args(&["file_a", "file_b"])
.fails_with_code(1)
.stdout_is("\t\t3\n1\n\t2\n")
.stderr_is(
"comm: file 1 is not in sorted order\n\
comm: file 2 is not in sorted order\n\
comm: input is not in sorted order\n",
);
}

#[test]
Expand Down Expand Up @@ -740,6 +729,7 @@ fn test_read_error() {

#[test]
#[cfg(target_os = "linux")]
#[cfg_attr(wasi_runner, ignore = "WASI P2: /dev/full filesystem not available")]
fn test_comm_write_error_dev_full() {
use std::fs::OpenOptions;
let scene = TestScenario::new(util_name!());
Expand Down
Loading