Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .github/workflows/wasi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
UUTESTS_WASM_RUNNER=wasmtime \
cargo test --test tests -- \
test_base32:: test_base64:: test_basenc:: test_basename:: \
test_cp::test_cp_arg_symlink test_cp::test_cp_preserve_timestamps \
test_comm:: test_cut:: test_dirname:: test_echo:: \
test_expand:: test_factor:: test_false:: test_fold:: \
test_head:: test_link:: test_ln:: test_nl:: test_numfmt:: \
Expand Down
3 changes: 3 additions & 0 deletions src/uu/cp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ windows-sys = { workspace = true, features = [
"Win32_Storage_FileSystem",
] }

[target.'cfg(target_os = "wasi")'.dependencies]
rustix = { workspace = true, features = ["fs"] }

[[bin]]
name = "cp"
path = "src/main.rs"
Expand Down
129 changes: 82 additions & 47 deletions src/uu/cp/src/cp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) copydir fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag
// spell-checker:ignore RDONLY futimens utimensat
// spell-checker:ignore (ToDO) copydir ficlone fiemap filestat ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs utimensat xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// spell-checker:ignore (ToDO) copydir ficlone fiemap filestat ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs utimensat xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag
// spell-checker:ignore (ToDO) copydir fiemap filestat ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs utimensat xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag

part of global config

// spell-checker:ignore RDONLY futimens

use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
Expand All @@ -21,6 +21,7 @@ use uucore::fsxattr::{copy_acls, 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)]
Expand Down Expand Up @@ -1368,9 +1369,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 {
Expand Down Expand Up @@ -1780,6 +1781,66 @@ fn copy_extended_attrs(source: &Path, dest: &Path, skip_selinux: bool) -> CopyRe
Ok(())
}

/// Copy the access and modification timestamps from `source_metadata` onto `dest`.
/// If `dest` is a symlink, the symlink's own timestamps are set rather than the
/// target's.
///
/// On WASI this calls `rustix::fs::utimensat` directly because `filetime`'s
/// WASI backend panics in `from_last_{access,modification}_time`. `SystemTime`
/// values are converted to `Timespec` against `UNIX_EPOCH`, matching WASI's
/// `path_filestat_set_times` contract (unsigned nanosecond count — pre-epoch
/// source times can't be represented).
fn set_timestamps(source_metadata: &Metadata, dest: &Path) -> CopyResult<()> {
#[cfg(target_os = "wasi")]
{
use std::time::UNIX_EPOCH;
let to_timespec = |t: std::time::SystemTime| -> io::Result<rustix::fs::Timespec> {
let d = t
.duration_since(UNIX_EPOCH)
.map_err(|e| io::Error::new(io::ErrorKind::Unsupported, e))?;
Ok(rustix::fs::Timespec {
tv_sec: d.as_secs() as i64,
tv_nsec: d.subsec_nanos() as i32,
})
};
let timestamps = rustix::fs::Timestamps {
last_access: to_timespec(source_metadata.accessed()?)?,
last_modification: to_timespec(source_metadata.modified()?)?,
};
let flags = if dest.is_symlink() {
rustix::fs::AtFlags::SYMLINK_NOFOLLOW
} else {
rustix::fs::AtFlags::empty()
};
rustix::fs::utimensat(rustix::fs::CWD, dest, &timestamps, flags)
.map_err(io::Error::from)?;
Ok(())
}

#[cfg(not(target_os = "wasi"))]
{
let atime = FileTime::from_last_access_time(source_metadata);
let mtime = FileTime::from_last_modification_time(source_metadata);
#[cfg(unix)]
let no_open = {
let ft = source_metadata.file_type();
dest.is_symlink()
|| ft.is_fifo()
|| ft.is_socket()
|| ft.is_char_device()
|| ft.is_block_device()
};
#[cfg(not(unix))]
let no_open = dest.is_symlink();
if no_open {
filetime::set_symlink_file_times(dest, atime, mtime)?;
} else {
filetime::set_file_times(dest, atime, mtime)?;
}
Ok(())
}
}

/// Copy the specified attributes from one path to another.
/// If `skip_selinux_xattr` is true, the security.selinux xattr will not be copied
/// (used when -Z is specified to set the default context instead).
Expand Down Expand Up @@ -1888,32 +1949,8 @@ pub(crate) fn copy_attributes(
Ok(())
})?;

handle_preserve(attributes.timestamps, || -> CopyResult<()> {
let atime = FileTime::from_last_access_time(&source_metadata);
let mtime = FileTime::from_last_modification_time(&source_metadata);
// `set_file_times` opens the destination (O_RDONLY) before calling
// futimens; opening a FIFO or device with no peer blocks forever, and a
// socket cannot be opened at all. For symlinks and these special files
// use the path-based, no-follow variant, which sets the times via
// utimensat without opening.
#[cfg(unix)]
let no_open = {
let ft = source_metadata.file_type();
dest.is_symlink()
|| ft.is_fifo()
|| ft.is_socket()
|| ft.is_char_device()
|| ft.is_block_device()
};
#[cfg(not(unix))]
let no_open = dest.is_symlink();
if no_open {
filetime::set_symlink_file_times(dest, atime, mtime)?;
} else {
filetime::set_file_times(dest, atime, mtime)?;
}

Ok(())
handle_preserve(attributes.timestamps, || {
set_timestamps(&source_metadata, dest)
})?;

#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
Expand Down Expand Up @@ -1959,19 +1996,9 @@ pub(crate) fn copy_attributes(
fn symlink_file(
source: &Path,
dest: &Path,
#[cfg(not(target_os = "wasi"))] symlinked_files: &mut HashSet<FileInformation>,
#[cfg(target_os = "wasi")] _symlinked_files: &mut HashSet<FileInformation>,
symlinked_files: &mut HashSet<FileInformation>,
) -> 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(
Expand All @@ -1993,13 +2020,21 @@ 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);
}
Ok(())
rustix::fs::symlink(source, dest).map_err(|e| {
CpError::IoErrContext(
io::Error::from(e),
translate!("cp-error-cannot-create-symlink",
"dest" => get_filename(dest).unwrap_or("?").quote(),
"source" => get_filename(source).unwrap_or("?").quote()),
)
})?;
}
if let Ok(file_info) = FileInformation::from_path(dest, false) {
symlinked_files.insert(file_info);
}
Ok(())
}

fn context_for(src: &Path, dest: &Path) -> String {
Expand Down
Loading