Skip to content

Commit 4b6a382

Browse files
committed
cp: preserve file timestamps on WASI
1 parent 17ab145 commit 4b6a382

5 files changed

Lines changed: 149 additions & 3 deletions

File tree

.github/workflows/wasi.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ jobs:
7070
cargo test --test tests -- \
7171
test_base32:: test_base64:: test_basenc:: test_basename:: \
7272
test_cp::test_cp_arg_symlink \
73+
test_cp::test_cp_wasi_preserve_file_timestamps \
74+
test_cp::test_cp_wasi_preserve_timestamps_through_destination_symlink \
7375
test_comm:: test_cut:: test_dirname:: test_echo:: \
7476
test_expand:: test_factor:: test_false:: test_fold:: \
7577
test_head:: test_link:: test_ln:: test_nl:: test_numfmt:: \

src/uu/cp/src/cp.rs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ use nix::sys::stat::{Mode, SFlag, dev_t, mknod as nix_mknod, mode_t};
2828
use thiserror::Error;
2929

3030
use platform::copy_on_write;
31+
#[cfg(target_os = "wasi")]
32+
use platform::{SourceTimestamps, set_timestamps};
3133
use uucore::backup_control::backup_would_destroy_source;
3234
use uucore::display::Quotable;
3335
use uucore::error::{UError, UResult, UUsageError, set_exit_code, strip_errno};
@@ -2653,6 +2655,17 @@ fn copy_file(
26532655
})?
26542656
};
26552657

2658+
#[cfg(target_os = "wasi")]
2659+
let source_timestamps = if !source_is_symlink
2660+
&& source_metadata.file_type().is_file()
2661+
&& options.copy_mode != CopyMode::SymLink
2662+
&& matches!(options.attributes.timestamps, Preserve::Yes { .. })
2663+
{
2664+
Some(SourceTimestamps::from_metadata(&source_metadata)?)
2665+
} else {
2666+
None
2667+
};
2668+
26562669
let dest_metadata = dest.symlink_metadata().ok();
26572670

26582671
let dest_permissions = calculate_dest_permissions(
@@ -2704,9 +2717,11 @@ fn copy_file(
27042717
.ok()
27052718
.filter(|p| p.exists())
27062719
.unwrap_or_else(|| source.to_path_buf());
2707-
copy_attributes(
2720+
copy_attributes_after_copy(
27082721
&src_for_attrs,
27092722
dest,
2723+
#[cfg(target_os = "wasi")]
2724+
source_timestamps,
27102725
&options.attributes,
27112726
false,
27122727
options.set_selinux_context,
@@ -2718,9 +2733,11 @@ fn copy_file(
27182733
// copy function (see `copy_stream` under platform/linux.rs).
27192734
Ok(())
27202735
} else {
2721-
copy_attributes(
2736+
copy_attributes_after_copy(
27222737
source,
27232738
dest,
2739+
#[cfg(target_os = "wasi")]
2740+
source_timestamps,
27242741
&options.attributes,
27252742
false,
27262743
options.set_selinux_context,
@@ -2753,6 +2770,40 @@ fn copy_file(
27532770
Ok(())
27542771
}
27552772

2773+
fn copy_attributes_after_copy(
2774+
source: &Path,
2775+
dest: &Path,
2776+
#[cfg(target_os = "wasi")] source_timestamps: Option<SourceTimestamps>,
2777+
attributes: &Attributes,
2778+
dest_is_freshly_created_dir: bool,
2779+
skip_selinux_xattr: bool,
2780+
) -> CopyResult<()> {
2781+
#[cfg(target_os = "wasi")]
2782+
if let Some(source_timestamps) = source_timestamps {
2783+
let timestamps = attributes.timestamps;
2784+
let mut remaining_attributes = *attributes;
2785+
remaining_attributes.timestamps = Preserve::No { explicit: false };
2786+
copy_attributes(
2787+
source,
2788+
dest,
2789+
&remaining_attributes,
2790+
dest_is_freshly_created_dir,
2791+
skip_selinux_xattr,
2792+
)?;
2793+
return handle_preserve(timestamps, || {
2794+
set_timestamps(source_timestamps, dest).map_err(CpError::from)
2795+
});
2796+
}
2797+
2798+
copy_attributes(
2799+
source,
2800+
dest,
2801+
attributes,
2802+
dest_is_freshly_created_dir,
2803+
skip_selinux_xattr,
2804+
)
2805+
}
2806+
27562807
fn is_stream(metadata: &Metadata) -> bool {
27572808
#[cfg(unix)]
27582809
{

src/uu/cp/src/platform/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ pub(crate) use self::other::copy_on_write;
3737
#[cfg(target_os = "wasi")]
3838
mod wasi;
3939
#[cfg(target_os = "wasi")]
40-
pub(crate) use self::wasi::create_symlink;
40+
pub(crate) use self::wasi::{SourceTimestamps, create_symlink, set_timestamps};

src/uu/cp/src/platform/wasi.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,46 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
use std::fs::Metadata;
67
use std::io;
78
use std::path::Path;
9+
use std::time::{SystemTime, UNIX_EPOCH};
10+
11+
use rustix::fs::{AtFlags, CWD, Timespec, Timestamps, utimensat};
12+
13+
#[derive(Clone, Copy)]
14+
pub(crate) struct SourceTimestamps {
15+
accessed: SystemTime,
16+
modified: SystemTime,
17+
}
18+
19+
impl SourceTimestamps {
20+
pub(crate) fn from_metadata(metadata: &Metadata) -> io::Result<Self> {
21+
Ok(Self {
22+
accessed: metadata.accessed()?,
23+
modified: metadata.modified()?,
24+
})
25+
}
26+
}
827

928
pub(crate) fn create_symlink(source: &Path, dest: &Path) -> io::Result<()> {
1029
rustix::fs::symlink(source, dest).map_err(io::Error::from)
1130
}
31+
32+
pub(crate) fn set_timestamps(source_timestamps: SourceTimestamps, dest: &Path) -> io::Result<()> {
33+
let timestamps = Timestamps {
34+
last_access: to_timespec(source_timestamps.accessed)?,
35+
last_modification: to_timespec(source_timestamps.modified)?,
36+
};
37+
utimensat(CWD, dest, &timestamps, AtFlags::empty()).map_err(io::Error::from)
38+
}
39+
40+
fn to_timespec(time: SystemTime) -> io::Result<Timespec> {
41+
let duration = time
42+
.duration_since(UNIX_EPOCH)
43+
.map_err(|error| io::Error::new(io::ErrorKind::Unsupported, error))?;
44+
Ok(Timespec {
45+
tv_sec: duration.as_secs() as i64,
46+
tv_nsec: duration.subsec_nanos() as i32,
47+
})
48+
}

tests/by-util/test_cp.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2576,6 +2576,62 @@ fn test_cp_preserve_timestamps() {
25762576
assert_eq!(creation, creation2);
25772577
}
25782578

2579+
#[test]
2580+
#[cfg(wasi_runner)]
2581+
fn test_cp_wasi_preserve_file_timestamps() {
2582+
let (at, mut ucmd) = at_and_ucmd!();
2583+
let ts = time::OffsetDateTime::now_utc();
2584+
let previous_atime = FileTime::from_unix_time(ts.unix_timestamp() - 7200, ts.nanosecond());
2585+
let previous_mtime = FileTime::from_unix_time(ts.unix_timestamp() - 3600, ts.nanosecond());
2586+
filetime::set_file_times(
2587+
at.plus_as_string(TEST_HELLO_WORLD_SOURCE),
2588+
previous_atime,
2589+
previous_mtime,
2590+
)
2591+
.unwrap();
2592+
2593+
ucmd.arg(TEST_HELLO_WORLD_SOURCE)
2594+
.arg("--preserve=timestamps")
2595+
.arg(TEST_HOW_ARE_YOU_SOURCE)
2596+
.succeeds();
2597+
2598+
let metadata = std_fs::metadata(at.plus(TEST_HOW_ARE_YOU_SOURCE)).unwrap();
2599+
assert_eq!(FileTime::from_last_access_time(&metadata), previous_atime);
2600+
assert_eq!(
2601+
FileTime::from_last_modification_time(&metadata),
2602+
previous_mtime
2603+
);
2604+
}
2605+
2606+
#[test]
2607+
#[cfg(wasi_runner)]
2608+
fn test_cp_wasi_preserve_timestamps_through_destination_symlink() {
2609+
let (at, mut ucmd) = at_and_ucmd!();
2610+
let ts = time::OffsetDateTime::now_utc();
2611+
let source_atime = FileTime::from_unix_time(ts.unix_timestamp() - 7200, ts.nanosecond());
2612+
let source_mtime = FileTime::from_unix_time(ts.unix_timestamp() - 3600, ts.nanosecond());
2613+
2614+
at.write("source", "new contents");
2615+
at.write("target", "old contents");
2616+
at.relative_symlink_file("target", "destination");
2617+
filetime::set_file_times(at.plus("source"), source_atime, source_mtime).unwrap();
2618+
2619+
ucmd.args(&["--preserve=timestamps", "source", "destination"])
2620+
.succeeds();
2621+
2622+
assert!(at.is_symlink("destination"));
2623+
let target_metadata = std_fs::metadata(at.plus("target")).unwrap();
2624+
assert_eq!(
2625+
FileTime::from_last_access_time(&target_metadata),
2626+
source_atime
2627+
);
2628+
assert_eq!(
2629+
FileTime::from_last_modification_time(&target_metadata),
2630+
source_mtime
2631+
);
2632+
assert_eq!(at.read("target"), "new contents");
2633+
}
2634+
25792635
#[test]
25802636
#[cfg(any(target_os = "linux", target_os = "android"))]
25812637
fn test_cp_no_preserve_timestamps() {

0 commit comments

Comments
 (0)