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
4 changes: 4 additions & 0 deletions .github/workflows/wasi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ 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_symlink_timestamps \
test_cp::test_cp_preserve_timestamps \
test_cp::test_cp_preserve_dereferenced_symlink_timestamps \
test_cp::test_cp_preserve_recursive_directory_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
134 changes: 113 additions & 21 deletions src/uu/cp/src/copydir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::convert::identity;
use std::env;
use std::fs::{self, exists};
use std::fs::{self, Metadata, exists};
use std::io;
use std::path::{Path, PathBuf, StripPrefixError};

Expand All @@ -29,8 +29,8 @@ use walkdir::{DirEntry, WalkDir};
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use crate::set_selinux_context;
use crate::{
CopyMode, CopyResult, CpError, Options, aligned_ancestors, context_for, copy_attributes,
copy_file,
CopyMode, CopyResult, CpError, Options, SourceTimestamps, aligned_ancestors, context_for,
copy_attributes, copy_attributes_with_timestamps, copy_file,
};

/// Represents a directory that needs permission fixup after copying its contents.
Expand All @@ -41,6 +41,8 @@ struct DirNeedingPermissions {
dest: PathBuf,
/// Whether this directory was freshly created by the copy operation
was_created: bool,
/// Timestamps captured before this directory was traversed
source_timestamps: Option<Box<SourceTimestamps>>,
}

/// Ensure a Windows path starts with a `\\?`.
Expand Down Expand Up @@ -319,6 +321,7 @@ fn copy_direntry(
copied_files,
created_parent_dirs,
false,
None,
)
{
if preserve_hard_links {
Expand Down Expand Up @@ -372,6 +375,7 @@ pub(crate) fn copy_directory(
copied_files: &mut HashMap<FileInformation, PathBuf>,
created_parent_dirs: &mut HashSet<PathBuf>,
source_in_command_line: bool,
initial_source_metadata: Option<&Metadata>,
) -> CopyResult<()> {
// if no-dereference is enabled and this is a symlink, copy it as a file
if !options.dereference(source_in_command_line) && root.is_symlink() {
Expand All @@ -385,6 +389,7 @@ pub(crate) fn copy_directory(
copied_files,
created_parent_dirs,
source_in_command_line,
initial_source_metadata,
);
}

Expand Down Expand Up @@ -448,6 +453,17 @@ pub(crate) fn copy_directory(

let preserve_hard_links = options.preserve_hard_links();

let preserve_timestamps = matches!(options.attributes.timestamps, crate::Preserve::Yes { .. });
let initial_source_timestamps = if !preserve_timestamps {
None
} else if options.dereference(source_in_command_line) {
fs::metadata(root).ok()
} else {
initial_source_metadata.cloned()
}
.as_ref()
.and_then(|metadata| SourceTimestamps::from_metadata(metadata).ok());

// Collect some paths here that are invariant during the traversal
// of the given directory, like the current working directory and
// the target directory.
Expand All @@ -464,6 +480,11 @@ pub(crate) fn copy_directory(
// Keep track of all directories we've created that need permission fixes
let mut dirs_needing_permissions: Vec<DirNeedingPermissions> = Vec::new();

// WalkDir opens each directory before yielding it, which can update its
// access time. Capture children while their parent is being processed so
// that timestamp preservation uses metadata from before that open.
let mut pending_source_timestamps: HashMap<PathBuf, SourceTimestamps> = HashMap::new();

// Traverse the contents of the directory, copying each one.
for direntry_result in WalkDir::new(root)
.same_file_system(options.one_file_system)
Expand All @@ -473,14 +494,62 @@ pub(crate) fn copy_directory(
Ok(direntry) => {
let direntry_type = direntry.file_type();
let direntry_path = direntry.path();
let source_path_metadata = direntry_path.symlink_metadata().ok();
let (entry_is_symlink, entry_is_dir_no_follow) =
match direntry_path.symlink_metadata() {
Ok(metadata) => {
source_path_metadata.as_ref().map_or_else(
|| (direntry_type.is_symlink(), direntry_type.is_dir()),
|metadata| {
let file_type = metadata.file_type();
(file_type.is_symlink(), file_type.is_dir())
},
);
let source_timestamps = if !preserve_timestamps {
None
} else if direntry.depth() == 0 {
initial_source_timestamps
} else if let Some(timestamps) = pending_source_timestamps.remove(direntry_path) {
Some(timestamps)
} else if options.dereference {
fs::metadata(direntry_path)
.ok()
.as_ref()
.and_then(|metadata| SourceTimestamps::from_metadata(metadata).ok())
} else {
source_path_metadata
.as_ref()
.and_then(|metadata| SourceTimestamps::from_metadata(metadata).ok())
};
let entry_is_dir_for_metadata = if options.dereference {
direntry_type.is_dir()
} else {
entry_is_dir_no_follow
};
if preserve_timestamps
&& entry_is_dir_for_metadata
&& let Ok(children) = fs::read_dir(direntry_path)
{
for child in children.flatten() {
let child_path = child.path();
let Ok(child_type) = child.file_type() else {
continue;
};
let child_metadata = if options.dereference
&& (child_type.is_dir() || child_type.is_symlink())
{
fs::metadata(&child_path)
} else if !options.dereference && child_type.is_dir() {
child.metadata()
} else {
continue;
};
if let Ok(metadata) = child_metadata
&& metadata.file_type().is_dir()
&& let Ok(timestamps) = SourceTimestamps::from_metadata(&metadata)
{
pending_source_timestamps.insert(child_path, timestamps);
}
Err(_) => (direntry_type.is_symlink(), direntry_type.is_dir()),
};
}
}
let entry = Entry::new(&context, direntry_path, options.no_target_dir)?;

let created = copy_direntry(
Expand Down Expand Up @@ -512,20 +581,32 @@ pub(crate) fn copy_directory(
if is_dir_for_permissions {
// For --link mode, copy attributes immediately to avoid O(n) memory
if options.copy_mode == CopyMode::Link {
copy_attributes(
&entry.source_absolute,
&entry.local_to_target,
&options.attributes,
false,
options.set_selinux_context,
)?;
if let Some(timestamps) = source_timestamps {
copy_attributes_with_timestamps(
&entry.source_absolute,
&entry.local_to_target,
timestamps,
&options.attributes,
false,
options.set_selinux_context,
)?;
} else {
copy_attributes(
&entry.source_absolute,
&entry.local_to_target,
&options.attributes,
false,
options.set_selinux_context,
)?;
}
continue;
}
// Add this directory to our list for permission fixing later
dirs_needing_permissions.push(DirNeedingPermissions {
source: entry.source_absolute.clone(),
dest: entry.local_to_target.clone(),
was_created: created,
source_timestamps: source_timestamps.map(Box::new),
});

// If true, last_iter is not a parent of this iter.
Expand Down Expand Up @@ -577,13 +658,24 @@ pub(crate) fn copy_directory(
// Fix permissions for all directories we created
// This ensures that even sibling directories get their permissions fixed
for dir in dirs_needing_permissions {
copy_attributes(
&dir.source,
&dir.dest,
&options.attributes,
dir.was_created,
options.set_selinux_context,
)?;
if let Some(timestamps) = dir.source_timestamps.as_deref().copied() {
copy_attributes_with_timestamps(
&dir.source,
&dir.dest,
timestamps,
&options.attributes,
dir.was_created,
options.set_selinux_context,
)?;
} else {
copy_attributes(
&dir.source,
&dir.dest,
&options.attributes,
dir.was_created,
options.set_selinux_context,
)?;
}

#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
if options.set_selinux_context {
Expand Down
Loading
Loading