diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 9f023a3937f..f6fcb3311ee 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -20,6 +20,7 @@ use uucore::fsxattr::{copy_xattrs, copy_xattrs_skip_selinux}; use uucore::translate; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser}; +#[cfg(not(target_os = "wasi"))] use filetime::FileTime; use indicatif::{ProgressBar, ProgressStyle}; #[cfg(unix)] @@ -896,7 +897,12 @@ impl Attributes { #[cfg(unix)] ownership: Preserve::Yes { required: true }, mode: Preserve::Yes { required: true }, + // WASI: filetime panics in from_last_{access,modification}_time, + // so timestamps cannot be preserved. Mark as optional so -a works. + #[cfg(not(target_os = "wasi"))] timestamps: Preserve::Yes { required: true }, + #[cfg(target_os = "wasi")] + timestamps: Preserve::Yes { required: false }, context: { #[cfg(feature = "feat_selinux")] { @@ -1335,9 +1341,9 @@ fn parse_path_args( /// Check if an error is ENOTSUP/EOPNOTSUPP (operation not supported). /// This is used to suppress xattr errors on filesystems that don't support them. fn is_enotsup_error(error: &CpError) -> bool { - #[cfg(unix)] + #[cfg(any(unix, target_os = "wasi"))] const EOPNOTSUPP: i32 = libc::EOPNOTSUPP; - #[cfg(not(unix))] + #[cfg(not(any(unix, target_os = "wasi")))] const EOPNOTSUPP: i32 = 95; match error { @@ -1837,15 +1843,24 @@ pub(crate) fn copy_attributes( })?; handle_preserve(attributes.timestamps, || -> CopyResult<()> { - let atime = FileTime::from_last_access_time(&source_metadata); - let mtime = FileTime::from_last_modification_time(&source_metadata); - if dest.is_symlink() { - filetime::set_symlink_file_times(dest, atime, mtime)?; - } else { - filetime::set_file_times(dest, atime, mtime)?; - } + // filetime's WASI backend panics in from_last_{access,modification}_time, + // so return ENOTSUP. handle_preserve silently suppresses ENOTSUP for + // optional preservation (-a) and reports it for required (--preserve=timestamps). + #[cfg(target_os = "wasi")] + return Err(io::Error::from_raw_os_error(libc::EOPNOTSUPP).into()); - Ok(()) + #[cfg(not(target_os = "wasi"))] + { + let atime = FileTime::from_last_access_time(&source_metadata); + let mtime = FileTime::from_last_modification_time(&source_metadata); + if dest.is_symlink() { + filetime::set_symlink_file_times(dest, atime, mtime)?; + } else { + filetime::set_file_times(dest, atime, mtime)?; + } + + Ok(()) + } })?; #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] @@ -1896,19 +1911,9 @@ pub(crate) fn copy_attributes( fn symlink_file( source: &Path, dest: &Path, - #[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet, - #[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet, + symlinked_files: &mut HashSet, ) -> CopyResult<()> { - #[cfg(target_os = "wasi")] - { - Err(CpError::IoErrContext( - io::Error::new(io::ErrorKind::Unsupported, "symlinks not supported"), - translate!("cp-error-cannot-create-symlink", - "dest" => get_filename(dest).unwrap_or("?").quote(), - "source" => get_filename(source).unwrap_or("?").quote()), - )) - } - #[cfg(not(any(windows, target_os = "wasi")))] + #[cfg(unix)] { std::os::unix::fs::symlink(source, dest).map_err(|e| { CpError::IoErrContext( @@ -1930,13 +1935,27 @@ fn symlink_file( ) })?; } - #[cfg(not(target_os = "wasi"))] + #[cfg(target_os = "wasi")] { - if let Ok(file_info) = FileInformation::from_path(dest, false) { - symlinked_files.insert(file_info); + use std::ffi::CString; + use std::os::wasi::ffi::OsStrExt; + let src_c = CString::new(source.as_os_str().as_bytes()) + .map_err(|e| CpError::Error(e.to_string()))?; + let dst_c = + CString::new(dest.as_os_str().as_bytes()).map_err(|e| CpError::Error(e.to_string()))?; + if unsafe { libc::symlink(src_c.as_ptr(), dst_c.as_ptr()) } != 0 { + return Err(CpError::IoErrContext( + io::Error::last_os_error(), + translate!("cp-error-cannot-create-symlink", + "dest" => get_filename(dest).unwrap_or("?").quote(), + "source" => get_filename(source).unwrap_or("?").quote()), + )); } - Ok(()) } + if let Ok(file_info) = FileInformation::from_path(dest, false) { + symlinked_files.insert(file_info); + } + Ok(()) } fn context_for(src: &Path, dest: &Path) -> String { diff --git a/src/uu/env/src/native_int_str.rs b/src/uu/env/src/native_int_str.rs index a52686b9dc1..b5a09033233 100644 --- a/src/uu/env/src/native_int_str.rs +++ b/src/uu/env/src/native_int_str.rs @@ -13,8 +13,10 @@ // this conversion needs to be done only once in the beginning and at the end. use std::ffi::OsString; -#[cfg(not(target_os = "windows"))] +#[cfg(unix)] use std::os::unix::ffi::{OsStrExt, OsStringExt}; +#[cfg(target_os = "wasi")] +use std::os::wasi::ffi::{OsStrExt, OsStringExt}; #[cfg(target_os = "windows")] use std::os::windows::prelude::*; use std::{borrow::Cow, ffi::OsStr}; diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index f0cd78cfe55..59448d86808 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -551,7 +551,7 @@ fn make_temp_dir(dir: &Path, prefix: &str, rand: usize, suffix: &str) -> UResult // The directory is created with these permission at creation time, using mkdir(3) syscall. // This is not relevant on Windows systems. See: https://docs.rs/tempfile/latest/tempfile/#security // `fs` is not imported on Windows anyways. - #[cfg(not(windows))] + #[cfg(unix)] builder.permissions(fs::Permissions::from_mode(0o700)); match builder.tempdir_in(dir) { diff --git a/src/uu/sort/Cargo.toml b/src/uu/sort/Cargo.toml index bf7c514bf3c..6ce91e32879 100644 --- a/src/uu/sort/Cargo.toml +++ b/src/uu/sort/Cargo.toml @@ -32,7 +32,6 @@ compare = { workspace = true } itertools = { workspace = true } memchr = { workspace = true } rand = { workspace = true } -rayon = { workspace = true } self_cell = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } @@ -47,6 +46,9 @@ uucore = { workspace = true, features = [ fluent = { workspace = true } foldhash = { workspace = true } +[target.'cfg(not(all(target_os = "wasi", not(target_feature = "atomics"))))'.dependencies] +rayon = { workspace = true } + [target.'cfg(not(any(target_os = "redox", target_os = "wasi")))'.dependencies] ctrlc = { workspace = true } diff --git a/src/uu/sort/build.rs b/src/uu/sort/build.rs new file mode 100644 index 00000000000..4ff53b18e8b --- /dev/null +++ b/src/uu/sort/build.rs @@ -0,0 +1,15 @@ +fn main() { + // Set a short alias for the WASI-without-threads configuration so that + // source files can use `#[cfg(wasi_no_threads)]` instead of the verbose + // `#[cfg(all(target_os = "wasi", not(target_feature = "atomics")))]`. + println!("cargo::rustc-check-cfg=cfg(wasi_no_threads)"); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let has_atomics = std::env::var("CARGO_CFG_TARGET_FEATURE") + .map(|f| f.split(',').any(|feat| feat == "atomics")) + .unwrap_or(false); + + if target_os == "wasi" && !has_atomics { + println!("cargo::rustc-cfg=wasi_no_threads"); + } +} diff --git a/src/uu/sort/src/check.rs b/src/uu/sort/src/check.rs index dbf598574fe..734d365282d 100644 --- a/src/uu/sort/src/check.rs +++ b/src/uu/sort/src/check.rs @@ -11,14 +11,11 @@ use crate::{ compare_by, open, }; use itertools::Itertools; -use std::{ - cmp::Ordering, - ffi::OsStr, - io::Read, - iter, - sync::mpsc::{Receiver, SyncSender, sync_channel}, - thread, -}; +#[cfg(not(wasi_no_threads))] +use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; +#[cfg(not(wasi_no_threads))] +use std::thread; +use std::{cmp::Ordering, ffi::OsStr, io::Read, iter}; use uucore::error::UResult; /// Check if the file at `path` is ordered. @@ -28,13 +25,35 @@ use uucore::error::UResult; /// The code we should exit with. pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { let max_allowed_cmp = if settings.unique { - // If `unique` is enabled, the previous line must compare _less_ to the next one. Ordering::Less } else { - // Otherwise, the line previous line must compare _less or equal_ to the next one. Ordering::Equal }; let file = open(path)?; + let chunk_size = if settings.buffer_size < 100 * 1024 { + settings.buffer_size + } else { + 100 * 1024 + }; + + #[cfg(not(wasi_no_threads))] + { + check_threaded(path, settings, max_allowed_cmp, file, chunk_size) + } + #[cfg(wasi_no_threads)] + { + check_sync(path, settings, max_allowed_cmp, file, chunk_size) + } +} + +#[cfg(not(wasi_no_threads))] +fn check_threaded( + path: &OsStr, + settings: &GlobalSettings, + max_allowed_cmp: Ordering, + file: Box, + chunk_size: usize, +) -> UResult<()> { let (recycled_sender, recycled_receiver) = sync_channel(2); let (loaded_sender, loaded_receiver) = sync_channel(2); thread::spawn({ @@ -42,13 +61,7 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { move || reader(file, &recycled_receiver, &loaded_sender, &settings) }); for _ in 0..2 { - let _ = recycled_sender.send(RecycledChunk::new(if settings.buffer_size < 100 * 1024 { - // when the buffer size is smaller than 100KiB we choose it instead of the default. - // this improves testability. - settings.buffer_size - } else { - 100 * 1024 - })); + let _ = recycled_sender.send(RecycledChunk::new(chunk_size)); } let mut prev_chunk: Option = None; @@ -56,8 +69,6 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { for chunk in loaded_receiver { line_idx += 1; if let Some(prev_chunk) = prev_chunk.take() { - // Check if the first element of the new chunk is greater than the last - // element from the previous chunk let prev_last = prev_chunk.lines().last().unwrap(); let new_first = chunk.lines().first().unwrap(); @@ -99,6 +110,7 @@ pub fn check(path: &OsStr, settings: &GlobalSettings) -> UResult<()> { } /// The function running on the reader thread. +#[cfg(not(wasi_no_threads))] fn reader( mut file: Box, receiver: &Receiver, @@ -123,3 +135,83 @@ fn reader( } Ok(()) } + +/// Synchronous check for targets without thread support. +#[cfg(wasi_no_threads)] +fn check_sync( + path: &OsStr, + settings: &GlobalSettings, + max_allowed_cmp: Ordering, + mut file: Box, + chunk_size: usize, +) -> UResult<()> { + let separator = settings.line_ending.into(); + let mut carry_over = vec![]; + let mut prev_chunk: Option = None; + let mut spare_recycled: Option = None; + let mut line_idx = 0; + + loop { + let recycled = spare_recycled + .take() + .unwrap_or_else(|| RecycledChunk::new(chunk_size)); + + let (chunk, should_continue) = chunks::read_to_chunk( + recycled, + None, + &mut carry_over, + &mut file, + &mut iter::empty(), + separator, + settings, + )?; + + let Some(chunk) = chunk else { + break; + }; + + line_idx += 1; + if let Some(prev) = prev_chunk.take() { + let prev_last = prev.lines().last().unwrap(); + let new_first = chunk.lines().first().unwrap(); + + if compare_by( + prev_last, + new_first, + settings, + prev.line_data(), + chunk.line_data(), + ) > max_allowed_cmp + { + return Err(SortError::Disorder { + file: path.to_owned(), + line_number: line_idx, + line: String::from_utf8_lossy(new_first.line).into_owned(), + silent: settings.check_silent, + } + .into()); + } + spare_recycled = Some(prev.recycle()); + } + + for (a, b) in chunk.lines().iter().tuple_windows() { + line_idx += 1; + if compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) > max_allowed_cmp { + return Err(SortError::Disorder { + file: path.to_owned(), + line_number: line_idx, + line: String::from_utf8_lossy(b.line).into_owned(), + silent: settings.check_silent, + } + .into()); + } + } + + prev_chunk = Some(chunk); + + if !should_continue { + break; + } + } + Ok(()) +} diff --git a/src/uu/sort/src/chunks.rs b/src/uu/sort/src/chunks.rs index ce6b2118be4..87bb90de493 100644 --- a/src/uu/sort/src/chunks.rs +++ b/src/uu/sort/src/chunks.rs @@ -9,10 +9,11 @@ #![allow(dead_code)] // Ignores non-used warning for `borrow_buffer` in `Chunk` +#[cfg(not(wasi_no_threads))] +use std::sync::mpsc::SyncSender; use std::{ io::{ErrorKind, Read}, ops::Range, - sync::mpsc::SyncSender, }; use memchr::memchr_iter; @@ -157,30 +158,13 @@ impl RecycledChunk { } } -/// Read a chunk, parse lines and send them. +/// Read a chunk from the input, parse lines, and return it directly. /// -/// No empty chunk will be sent. If we reach the end of the input, `false` is returned. -/// However, if this function returns `true`, it is not guaranteed that there is still -/// input left: If the input fits _exactly_ into a buffer, we will only notice that there's -/// nothing more to read at the next invocation. In case there is no input left, nothing will -/// be sent. -/// -/// # Arguments -/// -/// (see also `read_to_chunk` for a more detailed documentation) -/// -/// * `sender`: The sender to send the lines to the sorter. -/// * `recycled_chunk`: The recycled chunk, as returned by `Chunk::recycle`. -/// (i.e. `buffer.len()` should be equal to `buffer.capacity()`) -/// * `max_buffer_size`: How big `buffer` can be. -/// * `carry_over`: The bytes that must be carried over in between invocations. -/// * `file`: The current file. -/// * `next_files`: What `file` should be updated to next. -/// * `separator`: The line separator. -/// * `settings`: The global settings. +/// Returns `(Some(chunk), should_continue)` if data was read, or +/// `(None, false)` if the input was empty. The `should_continue` flag +/// indicates whether more data may remain. #[allow(clippy::too_many_arguments)] -pub fn read( - sender: &SyncSender, +pub fn read_to_chunk( recycled_chunk: RecycledChunk, max_buffer_size: Option, carry_over: &mut Vec, @@ -188,7 +172,7 @@ pub fn read( next_files: &mut impl Iterator>, separator: u8, settings: &GlobalSettings, -) -> UResult { +) -> UResult<(Option, bool)> { let RecycledChunk { lines, selections, @@ -202,7 +186,6 @@ pub fn read( mut buffer, } = recycled_chunk; if buffer.len() < carry_over.len() { - // Separate carry_over and copy them to avoid cost of 0 fill buffer buffer.extend_from_slice(&carry_over[buffer.len()..]); } buffer[..carry_over.len()].copy_from_slice(carry_over); @@ -218,7 +201,7 @@ pub fn read( carry_over.extend_from_slice(&buffer[read..]); if read != 0 { - let payload: UResult = Chunk::try_new(buffer, |buffer| { + let chunk: UResult = Chunk::try_new(buffer, |buffer| { let selections = unsafe { // SAFETY: It is safe to transmute to an empty vector of selections with shorter lifetime. // It was only temporarily transmuted to a Vec> to make recycling possible. @@ -254,7 +237,38 @@ pub fn read( line_count_hint, }) }); - sender.send(payload?).unwrap(); + Ok((Some(chunk?), should_continue)) + } else { + Ok((None, should_continue)) + } +} + +/// Read a chunk, parse lines and send them via channel. +/// +/// Wrapper around [`read_to_chunk`] for the threaded code path. +#[cfg(not(wasi_no_threads))] +#[allow(clippy::too_many_arguments)] +pub fn read( + sender: &SyncSender, + recycled_chunk: RecycledChunk, + max_buffer_size: Option, + carry_over: &mut Vec, + file: &mut T, + next_files: &mut impl Iterator>, + separator: u8, + settings: &GlobalSettings, +) -> UResult { + let (chunk, should_continue) = read_to_chunk( + recycled_chunk, + max_buffer_size, + carry_over, + file, + next_files, + separator, + settings, + )?; + if let Some(chunk) = chunk { + sender.send(chunk).unwrap(); } Ok(should_continue) } @@ -431,32 +445,3 @@ fn read_to_buffer( } } } - -/// Parse a buffer into a `ChunkContents` suitable for `Chunk::try_new`. -/// Used by the WASI single-threaded sort path. -#[cfg(target_os = "wasi")] -pub fn parse_into_chunk<'a>( - buffer: &'a [u8], - separator: u8, - settings: &GlobalSettings, -) -> ChunkContents<'a> { - let mut lines = Vec::new(); - let mut line_data = LineData::default(); - let mut token_buffer = Vec::new(); - let mut line_count_hint = 0; - parse_lines( - buffer, - &mut lines, - &mut line_data, - &mut token_buffer, - &mut line_count_hint, - separator, - settings, - ); - ChunkContents { - lines, - line_data, - token_buffer, - line_count_hint, - } -} diff --git a/src/uu/sort/src/ext_sort/mod.rs b/src/uu/sort/src/ext_sort/mod.rs index 099a4b72e62..e20452f7e8c 100644 --- a/src/uu/sort/src/ext_sort/mod.rs +++ b/src/uu/sort/src/ext_sort/mod.rs @@ -6,15 +6,8 @@ //! External sort: sort large inputs that may not fit in memory. //! //! On most platforms this uses a multi-threaded chunked approach with -//! temporary files. On WASI (no threads) we fall back to an in-memory sort. +//! temporary files. On WASI without atomics, synchronous fallbacks are +//! used instead (selected via `cfg` guards inside the module). -#[cfg(not(target_os = "wasi"))] mod threaded; -#[cfg(not(target_os = "wasi"))] pub use threaded::ext_sort; - -#[cfg(target_os = "wasi")] -mod wasi; -#[cfg(target_os = "wasi")] -// `self::` needed to disambiguate from the `wasi` crate -pub use self::wasi::ext_sort; diff --git a/src/uu/sort/src/ext_sort/threaded.rs b/src/uu/sort/src/ext_sort/threaded.rs index 7dd089d0fe8..2f24bb7a14a 100644 --- a/src/uu/sort/src/ext_sort/threaded.rs +++ b/src/uu/sort/src/ext_sort/threaded.rs @@ -10,14 +10,19 @@ use std::cmp::Ordering; use std::fs::File; use std::io::{Read, Write, stderr}; use std::path::PathBuf; +#[cfg(not(wasi_no_threads))] use std::sync::mpsc::{Receiver, SyncSender}; +#[cfg(not(wasi_no_threads))] use std::thread; use itertools::Itertools; -use uucore::error::{UResult, strip_errno}; +use uucore::error::UResult; +#[cfg(not(wasi_no_threads))] +use uucore::error::strip_errno; use crate::Output; use crate::chunks::RecycledChunk; +#[cfg(not(wasi_no_threads))] use crate::merge::WriteableCompressedTmpFile; use crate::merge::WriteablePlainTmpFile; use crate::merge::WriteableTmpFile; @@ -32,11 +37,174 @@ use crate::{ // Fixed to 8 KiB (equivalent to `std::sys::io::DEFAULT_BUF_SIZE` on most targets) const DEFAULT_BUF_SIZE: usize = 8 * 1024; +/// Synchronous sort for targets without thread support (e.g. wasm32-wasip1). +/// +/// Uses the same chunked sort-write-merge strategy as the threaded version, +/// but reads and sorts each chunk sequentially on the calling thread. +#[cfg(wasi_no_threads)] +pub fn ext_sort( + files: &mut impl Iterator>>, + settings: &GlobalSettings, + output: Output, + tmp_dir: &mut TmpDirWrapper, +) -> UResult<()> { + let separator = settings.line_ending.into(); + let mut buffer_size = match settings.buffer_size { + size if size <= 512 * 1024 * 1024 => size, + size => size / 2, + }; + if !settings.buffer_size_is_explicit { + buffer_size = buffer_size.max(8 * 1024 * 1024); + } + + if settings.compress_prog.is_some() { + let _ = writeln!( + stderr(), + "sort: warning: --compress-program is ignored on this platform" + ); + } + + let mut file = files.next().unwrap()?; + let mut carry_over = vec![]; + + // Read and sort first chunk. + let (first, cont) = chunks::read_to_chunk( + RecycledChunk::new(buffer_size.min(DEFAULT_BUF_SIZE)), + Some(buffer_size), + &mut carry_over, + &mut file, + files, + separator, + settings, + )?; + let Some(mut first) = first else { + return Ok(()); // empty input + }; + first.with_dependent_mut(|_, c| sort_by(&mut c.lines, settings, &c.line_data)); + + if !cont { + // All input fits in one chunk. + return print_chunk(&first, settings, output); + } + + // Read and sort second chunk. + let (second, cont) = chunks::read_to_chunk( + RecycledChunk::new(buffer_size.min(DEFAULT_BUF_SIZE)), + Some(buffer_size), + &mut carry_over, + &mut file, + files, + separator, + settings, + )?; + let Some(mut second) = second else { + return print_chunk(&first, settings, output); + }; + second.with_dependent_mut(|_, c| sort_by(&mut c.lines, settings, &c.line_data)); + + if !cont { + // All input fits in two chunks — merge in memory. + return print_two_chunks(first, second, settings, output); + } + + // More than two chunks: write sorted chunks to temp files, then merge. + let mut tmp_files: Vec<::Closed> = vec![]; + + tmp_files.push(write::( + &first, + tmp_dir.next_file()?, + settings.compress_prog.as_deref(), + separator, + )?); + drop(first); + + tmp_files.push(write::( + &second, + tmp_dir.next_file()?, + settings.compress_prog.as_deref(), + separator, + )?); + let mut recycled = second.recycle(); + + loop { + let (chunk, cont) = chunks::read_to_chunk( + recycled, + None, + &mut carry_over, + &mut file, + files, + separator, + settings, + )?; + let Some(mut chunk) = chunk else { break }; + chunk.with_dependent_mut(|_, c| sort_by(&mut c.lines, settings, &c.line_data)); + tmp_files.push(write::( + &chunk, + tmp_dir.next_file()?, + settings.compress_prog.as_deref(), + separator, + )?); + recycled = chunk.recycle(); + if !cont { + break; + } + } + + merge::merge_with_file_limit::<_, _, WriteablePlainTmpFile>( + tmp_files.into_iter().map(merge::ClosedTmpFile::reopen), + settings, + output, + tmp_dir, + ) +} + +/// Print a single sorted chunk. +#[cfg(wasi_no_threads)] +fn print_chunk(chunk: &Chunk, settings: &GlobalSettings, output: Output) -> UResult<()> { + if settings.unique { + print_sorted( + chunk.lines().iter().dedup_by(|a, b| { + compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Equal + }), + settings, + output, + ) + } else { + print_sorted(chunk.lines().iter(), settings, output) + } +} + +/// Merge two in-memory chunks and print. +#[cfg(wasi_no_threads)] +fn print_two_chunks(a: Chunk, b: Chunk, settings: &GlobalSettings, output: Output) -> UResult<()> { + let merged_iter = a.lines().iter().map(|line| (line, &a)).merge_by( + b.lines().iter().map(|line| (line, &b)), + |(line_a, a), (line_b, b)| { + compare_by(line_a, line_b, settings, a.line_data(), b.line_data()) != Ordering::Greater + }, + ); + if settings.unique { + print_sorted( + merged_iter + .dedup_by(|(line_a, a), (line_b, b)| { + compare_by(line_a, line_b, settings, a.line_data(), b.line_data()) + == Ordering::Equal + }) + .map(|(line, _)| line), + settings, + output, + ) + } else { + print_sorted(merged_iter.map(|(line, _)| line), settings, output) + } +} + /// Sort files by using auxiliary files for storing intermediate chunks (if needed), and output the result. /// /// Two threads cooperate: one reads input and writes temporary chunk files, /// while the other sorts each chunk in memory. Once all chunks are written, /// they are merged back together for final output. +#[cfg(not(wasi_no_threads))] pub fn ext_sort( files: &mut impl Iterator>>, settings: &GlobalSettings, @@ -97,6 +265,7 @@ pub fn ext_sort( } } +#[cfg(not(wasi_no_threads))] fn reader_writer< F: Iterator>>, Tmp: WriteableTmpFile + 'static, @@ -182,6 +351,7 @@ fn reader_writer< } /// The function that is executed on the sorter thread. +#[cfg(not(wasi_no_threads))] fn sorter(receiver: &Receiver, sender: &SyncSender, settings: &GlobalSettings) { while let Ok(mut payload) = receiver.recv() { payload.with_dependent_mut(|_, contents| { @@ -196,6 +366,7 @@ fn sorter(receiver: &Receiver, sender: &SyncSender, settings: &Glo } /// Describes how we read the chunks from the input. +#[cfg(not(wasi_no_threads))] enum ReadResult { /// The input was empty. Nothing was read. EmptyInput, @@ -207,6 +378,7 @@ enum ReadResult { WroteChunksToFile { tmp_files: Vec }, } /// The function that is executed on the reader/writer thread. +#[cfg(not(wasi_no_threads))] fn read_write_loop( mut files: impl Iterator>>, tmp_dir: &mut TmpDirWrapper, diff --git a/src/uu/sort/src/ext_sort/wasi.rs b/src/uu/sort/src/ext_sort/wasi.rs deleted file mode 100644 index 50bd5f63033..00000000000 --- a/src/uu/sort/src/ext_sort/wasi.rs +++ /dev/null @@ -1,59 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -//! WASI single-threaded sort: read all input into memory, sort, and output. -//! Threads are not available on WASI, so we bypass the chunked/threaded path. - -use std::cmp::Ordering; -use std::io::Read; - -use itertools::Itertools; -use uucore::error::UResult; - -use crate::Output; -use crate::chunks::{self, Chunk}; -use crate::tmp_dir::TmpDirWrapper; -use crate::{GlobalSettings, compare_by, print_sorted, sort_by}; - -/// Sort files by reading all input into memory, sorting in a single thread, and outputting directly. -pub fn ext_sort( - files: &mut impl Iterator>>, - settings: &GlobalSettings, - output: Output, - _tmp_dir: &mut TmpDirWrapper, -) -> UResult<()> { - let separator = settings.line_ending.into(); - // Read all input into memory at once. Unlike the threaded path which uses - // chunked buffered reads, WASI has no threads so we accept the memory cost. - // Note: there is no size limit here — WASI targets are expected to handle - // moderately sized inputs; very large files may cause OOM. - let mut input = Vec::new(); - for file in files { - file?.read_to_end(&mut input)?; - } - if input.is_empty() { - return Ok(()); - } - let mut chunk = Chunk::try_new(input, |buffer| { - Ok::<_, Box>(chunks::parse_into_chunk( - buffer, separator, settings, - )) - })?; - chunk.with_dependent_mut(|_, contents| { - sort_by(&mut contents.lines, settings, &contents.line_data); - }); - if settings.unique { - print_sorted( - chunk.lines().iter().dedup_by(|a, b| { - compare_by(a, b, settings, chunk.line_data(), chunk.line_data()) == Ordering::Equal - }), - settings, - output, - )?; - } else { - print_sorted(chunk.lines().iter(), settings, output)?; - } - Ok(()) -} diff --git a/src/uu/sort/src/merge.rs b/src/uu/sort/src/merge.rs index df03a3da612..69de2bea303 100644 --- a/src/uu/sort/src/merge.rs +++ b/src/uu/sort/src/merge.rs @@ -20,12 +20,17 @@ use std::{ path::{Path, PathBuf}, process::{Child, ChildStdin, ChildStdout, Command, Stdio}, rc::Rc, +}; +#[cfg(not(wasi_no_threads))] +use std::{ sync::mpsc::{Receiver, Sender, SyncSender, channel, sync_channel}, thread::{self, JoinHandle}, }; use compare::Compare; -use uucore::error::{FromIo, UResult}; +#[cfg(not(wasi_no_threads))] +use uucore::error::FromIo; +use uucore::error::UResult; use crate::{ GlobalSettings, Output, SortError, @@ -103,6 +108,17 @@ pub fn merge( let files = files .iter() .map(|file| open(file).map(|file| PlainMergeInput { inner: file })); + #[cfg(wasi_no_threads)] + if settings.compress_prog.is_some() { + let _ = writeln!( + std::io::stderr(), + "sort: warning: --compress-program is ignored on this platform" + ); + return merge_with_file_limit::<_, _, WriteablePlainTmpFile>( + files, settings, output, tmp_dir, + ); + } + if settings.compress_prog.is_none() { merge_with_file_limit::<_, _, WriteablePlainTmpFile>(files, settings, output, tmp_dir) } else { @@ -110,6 +126,30 @@ pub fn merge( } } +/// Merge and write to output — dispatches between threaded and synchronous. +fn do_merge_to_output( + files: impl Iterator>, + settings: &GlobalSettings, + output: Output, +) -> UResult<()> { + #[cfg(not(wasi_no_threads))] + return merge_without_limit(files, settings)?.write_all(settings, output); + #[cfg(wasi_no_threads)] + return merge_without_limit_sync(files, settings)?.write_all(settings, output); +} + +/// Merge and write to a writer — dispatches between threaded and synchronous. +fn do_merge_to_writer( + files: impl Iterator>, + settings: &GlobalSettings, + out: &mut impl Write, +) -> UResult<()> { + #[cfg(not(wasi_no_threads))] + return merge_without_limit(files, settings)?.write_all_to(settings, out); + #[cfg(wasi_no_threads)] + return merge_without_limit_sync(files, settings)?.write_all_to(settings, out); +} + // Merge already sorted `MergeInput`s. pub fn merge_with_file_limit< M: MergeInput + 'static, @@ -125,8 +165,7 @@ pub fn merge_with_file_limit< debug_assert!(batch_size >= 2); if files.len() <= batch_size { - let merger = merge_without_limit(files, settings); - merger?.write_all(settings, output) + do_merge_to_output(files, settings, output) } else { let mut temporary_files = vec![]; let mut batch = Vec::with_capacity(batch_size); @@ -134,23 +173,21 @@ pub fn merge_with_file_limit< batch.push(file); if batch.len() >= batch_size { assert_eq!(batch.len(), batch_size); - let merger = merge_without_limit(batch.into_iter(), settings)?; - batch = Vec::with_capacity(batch_size); + let full_batch = std::mem::replace(&mut batch, Vec::with_capacity(batch_size)); let mut tmp_file = Tmp::create(tmp_dir.next_file()?, settings.compress_prog.as_deref())?; - merger.write_all_to(settings, tmp_file.as_write())?; + do_merge_to_writer(full_batch.into_iter(), settings, tmp_file.as_write())?; temporary_files.push(tmp_file.finished_writing()?); } } // Merge any remaining files that didn't get merged in a full batch above. if !batch.is_empty() { assert!(batch.len() < batch_size); - let merger = merge_without_limit(batch.into_iter(), settings)?; let mut tmp_file = Tmp::create(tmp_dir.next_file()?, settings.compress_prog.as_deref())?; - merger.write_all_to(settings, tmp_file.as_write())?; + do_merge_to_writer(batch.into_iter(), settings, tmp_file.as_write())?; temporary_files.push(tmp_file.finished_writing()?); } merge_with_file_limit::<_, _, Tmp>( @@ -171,6 +208,7 @@ pub fn merge_with_file_limit< /// /// It is the responsibility of the caller to ensure that `files` yields only /// as many files as we are allowed to open concurrently. +#[cfg(not(wasi_no_threads))] fn merge_without_limit>>( files: F, settings: &GlobalSettings, @@ -235,6 +273,7 @@ fn merge_without_limit>>( }) } /// The struct on the reader thread representing an input file +#[cfg(not(wasi_no_threads))] struct ReaderFile { file: M, sender: SyncSender, @@ -242,6 +281,7 @@ struct ReaderFile { } /// The function running on the reader thread. +#[cfg(not(wasi_no_threads))] fn reader( recycled_receiver: &Receiver<(usize, RecycledChunk)>, files: &mut [Option>], @@ -276,6 +316,7 @@ fn reader( Ok(()) } /// The struct on the main thread representing an input file +#[cfg(not(wasi_no_threads))] pub struct MergeableFile { current_chunk: Rc, line_idx: usize, @@ -289,10 +330,12 @@ pub struct MergeableFile { struct PreviousLine { chunk: Rc, line_idx: usize, + #[cfg_attr(wasi_no_threads, allow(dead_code))] file_number: usize, } /// Merges files together. This is **not** an iterator because of lifetime problems. +#[cfg(not(wasi_no_threads))] struct FileMerger<'a> { heap: binary_heap_plus::BinaryHeap>, request_sender: Sender<(usize, RecycledChunk)>, @@ -300,6 +343,7 @@ struct FileMerger<'a> { reader_join_handle: JoinHandle>, } +#[cfg(not(wasi_no_threads))] impl FileMerger<'_> { /// Write the merged contents to the output file. fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> { @@ -381,6 +425,7 @@ struct FileComparator<'a> { settings: &'a GlobalSettings, } +#[cfg(not(wasi_no_threads))] impl Compare for FileComparator<'_> { fn compare(&self, a: &MergeableFile, b: &MergeableFile) -> Ordering { let mut cmp = compare_by( @@ -596,3 +641,194 @@ impl MergeInput for PlainMergeInput { &mut self.inner } } + +// --------------------------------------------------------------------------- +// Synchronous merge for targets without thread support (e.g. wasm32-wasip1). +// --------------------------------------------------------------------------- + +#[cfg(wasi_no_threads)] +struct SyncReaderFile { + file: M, + carry_over: Vec, +} + +#[cfg(wasi_no_threads)] +struct SyncMergeableFile { + current_chunk: Rc, + line_idx: usize, + file_number: usize, +} + +#[cfg(wasi_no_threads)] +impl Compare for FileComparator<'_> { + fn compare(&self, a: &SyncMergeableFile, b: &SyncMergeableFile) -> Ordering { + let mut cmp = compare_by( + &a.current_chunk.lines()[a.line_idx], + &b.current_chunk.lines()[b.line_idx], + self.settings, + a.current_chunk.line_data(), + b.current_chunk.line_data(), + ); + if cmp == Ordering::Equal { + cmp = a.file_number.cmp(&b.file_number); + } + cmp.reverse() + } +} + +#[cfg(wasi_no_threads)] +struct SyncFileMerger<'a, M: MergeInput> { + heap: binary_heap_plus::BinaryHeap>, + readers: Vec>>, + prev: Option, + recycled: Option, + settings: &'a GlobalSettings, +} + +#[cfg(wasi_no_threads)] +impl SyncFileMerger<'_, M> { + fn write_all(self, settings: &GlobalSettings, output: Output) -> UResult<()> { + let mut out = output.into_write(); + self.write_all_to(settings, &mut out) + } + + fn write_all_to(mut self, settings: &GlobalSettings, out: &mut impl Write) -> UResult<()> { + while self.write_next(settings, out)? {} + for reader in self.readers.into_iter().flatten() { + reader.file.finished_reading()?; + } + Ok(()) + } + + fn write_next(&mut self, settings: &GlobalSettings, out: &mut impl Write) -> UResult { + if let Some(file) = self.heap.peek() { + let prev = self.prev.replace(PreviousLine { + chunk: file.current_chunk.clone(), + line_idx: file.line_idx, + file_number: file.file_number, + }); + + file.current_chunk.with_dependent(|_, contents| { + let current_line = &contents.lines[file.line_idx]; + if settings.unique { + if let Some(prev) = &prev { + let cmp = compare_by( + &prev.chunk.lines()[prev.line_idx], + current_line, + settings, + prev.chunk.line_data(), + file.current_chunk.line_data(), + ); + if cmp == Ordering::Equal { + return Ok(()); + } + } + } + current_line.print(out, settings) + })?; + + let was_last = file.current_chunk.lines().len() == file.line_idx + 1; + let file_number = file.file_number; + + if was_last { + let separator = self.settings.line_ending.into(); + let recycled = self + .recycled + .take() + .unwrap_or_else(|| RecycledChunk::new(8 * 1024)); + let next_chunk = if let Some(reader) = self.readers[file_number].as_mut() { + let (chunk, should_continue) = chunks::read_to_chunk( + recycled, + None, + &mut reader.carry_over, + reader.file.as_read(), + &mut iter::empty(), + separator, + self.settings, + )?; + if !should_continue { + if let Some(reader) = self.readers[file_number].take() { + reader.file.finished_reading()?; + } + } + chunk + } else { + None + }; + + if let Some(next_chunk) = next_chunk { + let mut file = self.heap.peek_mut().unwrap(); + file.current_chunk = Rc::new(next_chunk); + file.line_idx = 0; + } else { + self.heap.pop(); + } + } else { + self.heap.peek_mut().unwrap().line_idx += 1; + } + + // Recycle the previous chunk if no other reference holds it. + if let Some(prev) = prev { + if let Ok(chunk) = Rc::try_unwrap(prev.chunk) { + self.recycled = Some(chunk.recycle()); + } + } + } + Ok(!self.heap.is_empty()) + } +} + +#[cfg(wasi_no_threads)] +fn merge_without_limit_sync>>( + files: F, + settings: &GlobalSettings, +) -> UResult> { + let separator = settings.line_ending.into(); + let mut readers: Vec>> = Vec::new(); + let mut mergeable_files = Vec::new(); + + for (file_number, file) in files.enumerate() { + let mut reader = SyncReaderFile { + file: file?, + carry_over: vec![], + }; + let recycled = RecycledChunk::new(8 * 1024); + let (chunk, should_continue) = chunks::read_to_chunk( + recycled, + None, + &mut reader.carry_over, + reader.file.as_read(), + &mut iter::empty(), + separator, + settings, + )?; + + if let Some(chunk) = chunk { + mergeable_files.push(SyncMergeableFile { + current_chunk: Rc::new(chunk), + line_idx: 0, + file_number, + }); + if should_continue { + readers.push(Some(reader)); + } else { + reader.file.finished_reading()?; + readers.push(None); + } + } else { + reader.file.finished_reading()?; + readers.push(None); + } + } + + Ok(SyncFileMerger { + heap: binary_heap_plus::BinaryHeap::from_vec_cmp( + mergeable_files, + FileComparator { settings }, + ), + readers, + prev: None, + recycled: None, + settings, + }) +} diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 8b321b55d53..cb8fcb0d27c 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -29,7 +29,7 @@ use foldhash::fast::FoldHasher; use foldhash::{HashMap, SharedSeed}; use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp}; use rand::{RngExt as _, rng}; -#[cfg(not(target_os = "wasi"))] +#[cfg(not(wasi_no_threads))] use rayon::slice::ParallelSliceMut; use std::cmp::Ordering; use std::env; @@ -38,7 +38,7 @@ use std::fs::{File, OpenOptions}; use std::hash::{Hash, Hasher}; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; use std::num::IntErrorKind; -#[cfg(not(target_os = "wasi"))] +#[cfg(not(wasi_no_threads))] use std::num::NonZero; use std::ops::Range; #[cfg(unix)] @@ -2126,7 +2126,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.threads = matches .get_one::(options::PARALLEL) .map_or_else(|| "0".to_string(), String::from); - #[cfg(not(target_os = "wasi"))] + #[cfg(not(wasi_no_threads))] { let num_threads = match settings.threads.parse::() { Ok(0) | Err(_) => std::thread::available_parallelism().map_or(1, NonZero::get), @@ -2148,22 +2148,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { settings.buffer_size_is_explicit = false; } - let mut tmp_dir = TmpDirWrapper::new(matches.get_one::(options::TMP_DIR).map_or_else( - || { - // WASI does not support std::env::temp_dir() — it panics with - // "no filesystem on wasm". Use /tmp as a nominal fallback; - // the WASI ext_sort path never actually creates temp files. - #[cfg(target_os = "wasi")] - { - PathBuf::from("/tmp") - } - #[cfg(not(target_os = "wasi"))] - { + let mut tmp_dir = TmpDirWrapper::new( + matches + .get_one::(options::TMP_DIR) + .map(PathBuf::from) + .or_else(|| env::var_os("TMPDIR").map(PathBuf::from)) + .unwrap_or_else(|| { + // env::temp_dir() panics on WASI; default to /tmp + #[cfg(target_os = "wasi")] + return PathBuf::from("/tmp"); + #[cfg(not(target_os = "wasi"))] env::temp_dir() - } - }, - PathBuf::from, - )); + }), + ); settings.compress_prog = matches .get_one::(options::COMPRESS_PROG) @@ -2623,14 +2620,14 @@ fn sort_by<'a>(unsorted: &mut Vec>, settings: &GlobalSettings, line_dat // WASI does not support threads, so use non-parallel sort to avoid // rayon's thread pool which triggers an unreachable trap. if settings.stable || settings.unique { - #[cfg(not(target_os = "wasi"))] + #[cfg(not(wasi_no_threads))] unsorted.par_sort_by(cmp); - #[cfg(target_os = "wasi")] + #[cfg(wasi_no_threads)] unsorted.sort_by(cmp); } else { - #[cfg(not(target_os = "wasi"))] + #[cfg(not(wasi_no_threads))] unsorted.par_sort_unstable_by(cmp); - #[cfg(target_os = "wasi")] + #[cfg(wasi_no_threads)] unsorted.sort_unstable_by(cmp); } } diff --git a/src/uu/tail/src/platform/mod.rs b/src/uu/tail/src/platform/mod.rs index bb77501fdcb..9c3e8f6e819 100644 --- a/src/uu/tail/src/platform/mod.rs +++ b/src/uu/tail/src/platform/mod.rs @@ -16,7 +16,22 @@ pub use self::windows::{Pid, ProcessChecker, supports_pid_checks}; // WASI has no process management; provide stubs so tail compiles. #[cfg(target_os = "wasi")] -pub type Pid = u64; +pub type Pid = u32; + +#[cfg(target_os = "wasi")] +#[allow(dead_code)] +pub struct ProcessChecker; + +#[cfg(target_os = "wasi")] +#[allow(dead_code)] +impl ProcessChecker { + pub fn new(_pid: Pid) -> Self { + Self + } + pub fn is_dead(&self) -> bool { + true + } +} #[cfg(target_os = "wasi")] pub fn supports_pid_checks(_pid: Pid) -> bool { diff --git a/src/uu/touch/src/error.rs b/src/uu/touch/src/error.rs index 47823cde93d..55e598d0efd 100644 --- a/src/uu/touch/src/error.rs +++ b/src/uu/touch/src/error.rs @@ -28,6 +28,10 @@ pub enum TouchError { #[error("{}", translate!("touch-error-windows-stdout-path-failed", "code" => .0.clone()))] WindowsStdoutPathError(String), + /// A feature that is not available on the current platform + #[error("{0}")] + UnsupportedPlatformFeature(String), + /// An error encountered on a specific file #[error("{error}")] TouchFileError { diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index bcb688ee7b3..e8375aa47fe 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -807,7 +807,10 @@ fn parse_timestamp(s: &str) -> UResult { /// /// On Windows, uses `GetFinalPathNameByHandleW` to attempt to get the path /// from the stdout handle. -#[cfg_attr(not(windows), expect(clippy::unnecessary_wraps))] +#[cfg_attr( + not(any(windows, target_os = "wasi")), + expect(clippy::unnecessary_wraps) +)] fn pathbuf_from_stdout() -> Result { #[cfg(all(unix, not(target_os = "android")))] { @@ -817,6 +820,10 @@ fn pathbuf_from_stdout() -> Result { { Ok(PathBuf::from("/proc/self/fd/1")) } + #[cfg(target_os = "wasi")] + return Err(TouchError::UnsupportedPlatformFeature( + "touch - (stdout) is not supported on WASI".to_string(), + )); #[cfg(windows)] { use std::os::windows::prelude::AsRawHandle; @@ -873,10 +880,6 @@ fn pathbuf_from_stdout() -> Result { .map_err(|e| TouchError::WindowsStdoutPathError(e.to_string()))? .into()) } - #[cfg(target_os = "wasi")] - { - Ok(PathBuf::from("/dev/stdout")) - } } #[cfg(test)] diff --git a/src/uu/wc/src/count_fast.rs b/src/uu/wc/src/count_fast.rs index 637a777e4fe..79c9f822738 100644 --- a/src/uu/wc/src/count_fast.rs +++ b/src/uu/wc/src/count_fast.rs @@ -12,7 +12,7 @@ use super::WordCountable; use std::io::{self, ErrorKind, Read}; #[cfg(unix)] -use libc::{_SC_PAGESIZE, S_IFREG, sysconf}; +use libc::S_IFREG; #[cfg(unix)] use std::io::{Seek, SeekFrom}; #[cfg(unix)] @@ -122,7 +122,7 @@ pub(crate) fn count_bytes_fast(handle: &mut T) -> (usize, Opti && (stat.st_mode as libc::mode_t & S_IFREG) != 0 && stat.st_size > 0 { - let sys_page_size = unsafe { sysconf(_SC_PAGESIZE) as usize }; + let sys_page_size = rustix::param::page_size(); if !(stat.st_size as usize).is_multiple_of(sys_page_size) { // regular file or file from /proc, /sys and similar pseudo-filesystems // with size that is NOT a multiple of system page size diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index 590d0076fed..2e32ea50bef 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -45,8 +45,7 @@ macro_rules! has { pub struct FileInformation( #[cfg(unix)] nix::sys::stat::FileStat, #[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, + #[cfg(target_os = "wasi")] libc::stat, ); impl FileInformation { @@ -64,6 +63,17 @@ impl FileInformation { Ok(Self(info)) } + /// Get information from a currently open file + #[cfg(target_os = "wasi")] + pub fn from_file(file: &fs::File) -> IOResult { + use std::os::fd::AsRawFd; + let mut stat: libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { libc::fstat(file.as_raw_fd(), &raw mut stat) } != 0 { + return Err(Error::last_os_error()); + } + Ok(Self(stat)) + } + /// Get information for a given path. /// /// If `path` points to a symlink and `dereference` is true, information about @@ -93,15 +103,22 @@ 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()) + use std::ffi::CString; + use std::os::wasi::ffi::OsStrExt; + let path_c = CString::new(path.as_ref().as_os_str().as_bytes()) + .map_err(|e| Error::new(ErrorKind::InvalidInput, e))?; + let mut stat: libc::stat = unsafe { std::mem::zeroed() }; + let res = if dereference { + unsafe { libc::stat(path_c.as_ptr(), &raw mut stat) } } else { - fs::symlink_metadata(path.as_ref()) + unsafe { libc::lstat(path_c.as_ptr(), &raw mut stat) } }; - Ok(Self(metadata?)) + if res != 0 { + return Err(Error::last_os_error()); + } + Ok(Self(stat)) } } @@ -117,7 +134,8 @@ impl FileInformation { } #[cfg(target_os = "wasi")] { - self.0.len() + assert!(self.0.st_size >= 0, "File size is negative"); + self.0.st_size.try_into().unwrap() } } @@ -169,9 +187,8 @@ 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; + return self.0.st_nlink; } #[cfg(unix)] @@ -191,20 +208,18 @@ impl PartialEq for FileInformation { } } -// 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")] +#[cfg(target_os = "windows")] impl PartialEq for FileInformation { fn eq(&self, other: &Self) -> bool { - self.0.file_type() == other.0.file_type() && self.0.len() == other.0.len() + self.0.volume_serial_number() == other.0.volume_serial_number() + && self.0.file_index() == other.0.file_index() } } -#[cfg(target_os = "windows")] +#[cfg(target_os = "wasi")] impl PartialEq for FileInformation { fn eq(&self, other: &Self) -> bool { - self.0.volume_serial_number() == other.0.volume_serial_number() - && self.0.file_index() == other.0.file_index() + self.0.st_dev == other.0.st_dev && self.0.st_ino == other.0.st_ino } } @@ -224,8 +239,8 @@ impl Hash for FileInformation { } #[cfg(target_os = "wasi")] { - self.0.len().hash(state); - self.0.file_type().is_dir().hash(state); + self.0.st_dev.hash(state); + self.0.st_ino.hash(state); } } } diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 0ede0f2875f..7f2de568c42 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -428,7 +428,6 @@ fn mount_dev_id(mount_dir: &OsStr) -> String { } } -#[cfg(not(target_os = "wasi"))] use crate::error::UResult; #[cfg(any( target_os = "freebsd", @@ -459,7 +458,7 @@ use std::ptr; use std::slice; /// Read file system list. -#[cfg(not(target_os = "wasi"))] +#[cfg_attr(target_os = "wasi", allow(clippy::unnecessary_wraps))] pub fn read_fs_list() -> UResult> { #[cfg(any(target_os = "linux", target_os = "android", target_os = "cygwin"))] { @@ -540,6 +539,7 @@ pub fn read_fs_list() -> UResult> { target_os = "redox", target_os = "illumos", target_os = "solaris", + target_os = "wasi" ))] { // No method to read mounts on these platforms @@ -547,13 +547,6 @@ pub fn read_fs_list() -> UResult> { } } -/// Read file system list. -#[cfg(target_os = "wasi")] -pub fn read_fs_list() -> Vec { - // No method to read mounts on WASI - Vec::new() -} - #[derive(Debug, Clone)] pub struct FsUsage { pub blocksize: u64, diff --git a/src/uucore/src/lib/mods/io.rs b/src/uucore/src/lib/mods/io.rs index 40bd74324fb..a9e4a7bcbec 100644 --- a/src/uucore/src/lib/mods/io.rs +++ b/src/uucore/src/lib/mods/io.rs @@ -75,14 +75,7 @@ impl OwnedFileDescriptorOrHandle { /// instantiates a corresponding `Stdio` #[cfg(not(target_os = "wasi"))] pub fn into_stdio(self) -> Stdio { - #[cfg(not(target_os = "wasi"))] - { - Stdio::from(self.fx) - } - #[cfg(target_os = "wasi")] - { - Stdio::from(File::from(self.fx)) - } + Stdio::from(self.fx) } /// WASI: Stdio::from(OwnedFd) is not available, convert via File instead.