diff --git a/Cargo.lock b/Cargo.lock index f30e487b8..ba385a0d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3293,6 +3293,7 @@ dependencies = [ "fluent", "indicatif", "libc", + "nix", "selinux", "tempfile", "thiserror 2.0.18", diff --git a/src/uu/cp/Cargo.toml b/src/uu/cp/Cargo.toml index 79e00d6bb..bea4afc67 100644 --- a/src/uu/cp/Cargo.toml +++ b/src/uu/cp/Cargo.toml @@ -41,6 +41,7 @@ fluent = { workspace = true } [target.'cfg(unix)'.dependencies] exacl = { workspace = true, optional = true } +nix = { workspace = true, features = ["fs"] } [[bin]] name = "cp" diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl index b51b89dbd..36ab60f25 100644 --- a/src/uu/cp/locales/en-US.ftl +++ b/src/uu/cp/locales/en-US.ftl @@ -87,6 +87,7 @@ cp-error-selinux-get-context = failed to get security context of { $path } cp-error-selinux-error = SELinux error: { $error } cp-error-selinux-context-conflict = cannot combine --context (-Z) with --preserve=context cp-error-cannot-create-fifo = cannot create fifo { $path }: File exists +cp-error-cannot-create-special-file = cannot create special file { $path }: { $error } cp-error-invalid-attribute = invalid attribute { $value } cp-error-failed-to-create-whole-tree = failed to create whole tree cp-error-failed-to-create-directory = Failed to create directory: { $error } diff --git a/src/uu/cp/locales/fr-FR.ftl b/src/uu/cp/locales/fr-FR.ftl index 9e56bf130..76860de03 100644 --- a/src/uu/cp/locales/fr-FR.ftl +++ b/src/uu/cp/locales/fr-FR.ftl @@ -87,6 +87,7 @@ cp-error-selinux-get-context = échec de l'obtention du contexte de sécurité d cp-error-selinux-error = Erreur SELinux : { $error } cp-error-selinux-context-conflict = impossible de combiner --context (-Z) avec --preserve=context cp-error-cannot-create-fifo = impossible de créer le fifo { $path } : Le fichier existe +cp-error-cannot-create-special-file = impossible de créer le fichier spécial { $path } : { $error } cp-error-invalid-attribute = attribut invalide { $value } cp-error-failed-to-create-whole-tree = échec de la création de l'arborescence complète cp-error-failed-to-create-directory = Échec de la création du répertoire : { $error } diff --git a/src/uu/cp/src/cp.rs b/src/uu/cp/src/cp.rs index 3c3b05709..fa6ddcfc6 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO +// spell-checker:ignore (ToDO) copydir ficlone fiemap ftruncate linkgs lstat nlink nlinks pathbuf pwrite reflink strs xattrs symlinked deduplicated advcpmv nushell IRWXG IRWXO IRWXU IRWXUGO IRWXU IRWXG IRWXO IRWXUGO sflag use std::cmp::Ordering; use std::collections::{HashMap, HashSet}; @@ -10,7 +10,7 @@ use std::ffi::OsString; use std::fmt::Display; use std::fs::{self, Metadata, OpenOptions, Permissions}; #[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, PermissionsExt}; +use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; #[cfg(unix)] use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf, StripPrefixError}; @@ -22,6 +22,8 @@ use uucore::translate; use clap::{Arg, ArgAction, ArgMatches, Command, builder::ValueParser, value_parser}; use filetime::FileTime; use indicatif::{ProgressBar, ProgressStyle}; +#[cfg(unix)] +use nix::sys::stat::{Mode, SFlag, dev_t, mknod as nix_mknod, mode_t}; use thiserror::Error; use platform::copy_on_write; @@ -2227,13 +2229,8 @@ fn handle_copy_mode( source_metadata: &Metadata, symlinked_files: &mut HashSet, source_in_command_line: bool, - source_is_fifo: bool, - source_is_socket: bool, created_parent_dirs: &mut HashSet, - #[cfg(unix)] source_is_stream: bool, ) -> CopyResult { - let source_is_symlink = source_metadata.is_symlink(); - match options.copy_mode { CopyMode::Link => { if dest.exists() { @@ -2267,13 +2264,9 @@ fn handle_copy_mode( dest, options, context, - source_is_symlink, - source_is_fifo, - source_is_socket, + source_metadata, symlinked_files, created_parent_dirs, - #[cfg(unix)] - source_is_stream, )?; } CopyMode::SymLink => { @@ -2291,13 +2284,9 @@ fn handle_copy_mode( dest, options, context, - source_is_symlink, - source_is_fifo, - source_is_socket, + source_metadata, symlinked_files, created_parent_dirs, - #[cfg(unix)] - source_is_stream, )?; } UpdateMode::None => { @@ -2328,13 +2317,9 @@ fn handle_copy_mode( dest, options, context, - source_is_symlink, - source_is_fifo, - source_is_socket, + source_metadata, symlinked_files, created_parent_dirs, - #[cfg(unix)] - source_is_stream, )?; } } @@ -2344,13 +2329,9 @@ fn handle_copy_mode( dest, options, context, - source_is_symlink, - source_is_fifo, - source_is_socket, + source_metadata, symlinked_files, created_parent_dirs, - #[cfg(unix)] - source_is_stream, )?; } } @@ -2582,15 +2563,6 @@ fn copy_file( context, ); - #[cfg(unix)] - let source_is_fifo = source_metadata.file_type().is_fifo(); - #[cfg(unix)] - let source_is_socket = source_metadata.file_type().is_socket(); - #[cfg(not(unix))] - let source_is_fifo = false; - #[cfg(not(unix))] - let source_is_socket = false; - let source_is_stream = is_stream(&source_metadata); let performed_action = handle_copy_mode( @@ -2601,11 +2573,7 @@ fn copy_file( &source_metadata, symlinked_files, source_in_command_line, - source_is_fifo, - source_is_socket, created_parent_dirs, - #[cfg(unix)] - source_is_stream, )?; if options.verbose && performed_action != PerformedAction::Skipped { @@ -2741,18 +2709,14 @@ fn handle_no_preserve_mode(options: &Options, org_mode: u32) -> u32 { /// Copy the file from `source` to `dest` either using the normal `fs::copy` or a /// copy-on-write scheme if --reflink is specified and the filesystem supports it. -#[allow(clippy::too_many_arguments)] fn copy_helper( source: &Path, dest: &Path, options: &Options, context: &str, - source_is_symlink: bool, - source_is_fifo: bool, - source_is_socket: bool, + source_metadata: &Metadata, symlinked_files: &mut HashSet, created_parent_dirs: &mut HashSet, - #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { if options.parents { let parent = dest.parent().unwrap_or(dest); @@ -2765,13 +2729,21 @@ fn copy_helper( return Err(CpError::NotADirectory(dest.to_path_buf())); } - if source_is_socket && options.recursive && !options.copy_contents { - #[cfg(unix)] - copy_socket(dest, options.overwrite, options.debug)?; - } else if source_is_fifo && options.recursive && !options.copy_contents { - #[cfg(unix)] - copy_fifo(dest, options.overwrite, options.debug)?; - } else if source_is_symlink { + #[cfg(unix)] + if options.recursive && !options.copy_contents { + let ft = source_metadata.file_type(); + if ft.is_socket() { + return copy_socket(dest, options.overwrite, options.debug); + } + if ft.is_fifo() { + return copy_fifo(dest, options.overwrite, options.debug); + } + if ft.is_char_device() || ft.is_block_device() { + return copy_node(dest, source_metadata, options.overwrite, options.debug); + } + } + + if source_metadata.is_symlink() { copy_link(source, dest, symlinked_files, options)?; } else { let copy_debug = copy_on_write( @@ -2781,7 +2753,7 @@ fn copy_helper( options.sparse_mode, context, #[cfg(unix)] - source_is_stream, + is_stream(source_metadata), )?; if !options.attributes_only && options.debug { @@ -2816,6 +2788,27 @@ fn copy_socket(dest: &Path, overwrite: OverwriteMode, debug: bool) -> CopyResult Ok(()) } +#[cfg(unix)] +fn copy_node( + dest: &Path, + source_metadata: &Metadata, + overwrite: OverwriteMode, + debug: bool, +) -> CopyResult<()> { + if dest.exists() { + overwrite.verify(dest, debug)?; + fs::remove_file(dest)?; + } + let sflag = if source_metadata.file_type().is_char_device() { + SFlag::S_IFCHR + } else { + SFlag::S_IFBLK + }; + let mode = Mode::from_bits_truncate(source_metadata.mode() as mode_t); + nix_mknod(dest, sflag, mode, source_metadata.rdev() as dev_t) + .map_err(|e| translate!("cp-error-cannot-create-special-file", "path" => dest.quote(), "error" => e.desc()).into()) +} + fn copy_link( source: &Path, dest: &Path, diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 6c9b8071e..76c6fcfc3 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -5,6 +5,8 @@ // spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR outfile uufs xattrs // spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel prwx doesnotexist reftests subdirs mksocket srwx +#[cfg(unix)] +use rstest::rstest; use uucore::display::Quotable; #[cfg(feature = "feat_selinux")] use uucore::selinux::get_getfattr_output; @@ -3171,6 +3173,133 @@ fn test_cp_fifo() { assert_eq!(permission, "prwx-wx--x".to_string()); } +#[rstest] +#[case::recursive("-R")] +#[case::archive("-a")] +#[cfg(unix)] +fn test_cp_recursive_char_device(#[case] flag: &str) { + use nix::sys::stat::{Mode, SFlag, mknod as nix_mknod}; + use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; + use uutests::util::run_ucmd_as_root; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let mode = Mode::S_IRUSR | Mode::S_IWUSR | Mode::S_IRGRP; + if nix_mknod( + at.plus("null").as_path(), + SFlag::S_IFCHR, + mode, + uucore::fs::makedev(1, 3), + ) + .is_err() + { + print!("Test skipped; creating a char device node requires CAP_MKNOD"); + return; + } + if let Ok(result) = run_ucmd_as_root(&ts, &[flag, "null", "null2"]) { + result.success(); + let null2_metadata = at.plus("null2").metadata().unwrap(); + assert!(null2_metadata.file_type().is_char_device()); + assert_eq!( + null2_metadata.rdev(), + at.plus("null").metadata().unwrap().rdev() + ); + assert_eq!( + null2_metadata.permissions().mode() & 0o777, + mode.bits() as _ + ); + } else { + print!("Test skipped; copying a char device node requires CAP_MKNOD"); + } +} + +#[rstest] +#[case::recursive("-R")] +#[case::archive("-a")] +#[cfg(unix)] +fn test_cp_recursive_char_device_no_permission(#[case] flag: &str) { + new_ucmd!() + .args(&[flag, "/dev/null", "null2"]) + .fails() + .stderr_is("cp: cannot create special file 'null2': Operation not permitted\n"); +} + +#[test] +#[cfg(unix)] +fn test_cp_recursive_char_device_copy_contents() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-R", "--copy-contents", "/dev/null", "null2"]) + .succeeds() + .no_stderr(); + assert!(at.plus("null2").metadata().unwrap().file_type().is_file()); + assert_eq!(at.read("null2"), ""); +} + +#[test] +#[cfg(unix)] +fn test_cp_char_device() { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["/dev/null", "null2"]).succeeds().no_stderr(); + assert!(at.plus("null2").metadata().unwrap().file_type().is_file()); + assert_eq!(at.read("null2"), ""); +} + +#[rstest] +#[case::recursive("-R")] +#[case::archive("-a")] +#[cfg(unix)] +fn test_cp_recursive_block_device(#[case] flag: &str) { + use nix::sys::stat::{Mode, SFlag, mknod as nix_mknod}; + use std::os::unix::fs::{FileTypeExt, MetadataExt}; + use uutests::util::run_ucmd_as_root; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + if nix_mknod( + at.plus("sda").as_path(), + SFlag::S_IFBLK, + Mode::S_IRUSR | Mode::S_IWUSR, + uucore::fs::makedev(8, 0), + ) + .is_err() + { + print!("Test skipped; creating a block device node requires CAP_MKNOD"); + return; + } + if let Ok(result) = run_ucmd_as_root(&ts, &[flag, "sda", "sda2"]) { + result.success(); + let sda2_metadata = at.plus("sda2").metadata().unwrap(); + assert!(sda2_metadata.file_type().is_block_device()); + assert_eq!( + sda2_metadata.rdev(), + at.plus("sda").metadata().unwrap().rdev() + ); + } else { + print!("Test skipped; copying a block device node requires CAP_MKNOD"); + } +} + +#[test] +#[cfg(unix)] +fn test_cp_block_device_no_permission() { + use nix::sys::stat::{Mode, SFlag, mknod as nix_mknod}; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + if nix_mknod( + at.plus("sda").as_path(), + SFlag::S_IFBLK, + Mode::S_IRUSR | Mode::S_IWUSR, + uucore::fs::makedev(8, 0), + ) + .is_err() + { + print!("Test skipped; creating a block device node requires CAP_MKNOD"); + return; + } + ts.ucmd() + .args(&["-R", "sda", "sda2"]) + .fails() + .stderr_is("cp: cannot create special file 'sda2': Operation not permitted\n"); +} + #[test] #[cfg(unix)] fn test_cp_socket() {