diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 199d1ca6cb..e32bd3defa 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -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; @@ -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 { let stat = rustix::fs::fstat(file)?; Ok(Self(stat)) @@ -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, dereference: bool) -> IOResult { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] { let stat = if dereference { rustix::fs::stat(path.as_ref()) @@ -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() @@ -124,10 +112,6 @@ impl FileInformation { { self.0.file_size() } - #[cfg(target_os = "wasi")] - { - self.0.len() - } } #[cfg(windows)] @@ -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 { @@ -221,7 +203,7 @@ impl Eq for FileInformation {} impl Hash for FileInformation { fn hash(&self, state: &mut H) { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] { self.0.st_dev.hash(state); self.0.st_ino.hash(state); @@ -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); - } } } @@ -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 } @@ -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 } @@ -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 diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 3a2e274b21..1298fda30f 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -170,7 +170,11 @@ fn metadata_get_change_time(md: &Metadata) -> Option { #[cfg(not(unix))] fn metadata_get_change_time(_md: &Metadata) -> Option { - // 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 } diff --git a/tests/by-util/test_comm.rs b/tests/by-util/test_comm.rs index d082284485..db448d677c 100644 --- a/tests/by-util/test_comm.rs +++ b/tests/by-util/test_comm.rs @@ -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] @@ -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] @@ -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] @@ -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!());