diff --git a/src/uu/cp/locales/en-US.ftl b/src/uu/cp/locales/en-US.ftl index bc39d613289..afc56b8d376 100644 --- a/src/uu/cp/locales/en-US.ftl +++ b/src/uu/cp/locales/en-US.ftl @@ -86,6 +86,8 @@ cp-error-selinux-set-context = failed to set the security context of { $path }: cp-error-selinux-get-context = failed to get security context of { $path } cp-error-selinux-error = SELinux error: { $error } cp-error-cannot-create-fifo = cannot create fifo { $path }: File exists +cp-error-cannot-create-char-device = cannot create character device { $path } +cp-error-cannot-create-block-device = cannot create block device { $path } 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 2fea7cf4d7a..ac1d43ba5c6 100644 --- a/src/uu/cp/locales/fr-FR.ftl +++ b/src/uu/cp/locales/fr-FR.ftl @@ -86,6 +86,8 @@ cp-error-selinux-set-context = échec de la définition du contexte de sécurit cp-error-selinux-get-context = échec de l'obtention du contexte de sécurité de { $path } cp-error-selinux-error = Erreur SELinux : { $error } cp-error-cannot-create-fifo = impossible de créer le fifo { $path } : Le fichier existe +cp-error-cannot-create-char-device = impossible de créer le périphérique caractère { $path } +cp-error-cannot-create-block-device = impossible de créer le périphérique bloc { $path } 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 6c544310ca6..de4f5b532c2 100644 --- a/src/uu/cp/src/cp.rs +++ b/src/uu/cp/src/cp.rs @@ -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}; @@ -27,13 +27,13 @@ use thiserror::Error; use platform::copy_on_write; use uucore::display::Quotable; use uucore::error::{UError, UResult, UUsageError, set_exit_code}; -#[cfg(unix)] -use uucore::fs::make_fifo; use uucore::fs::{ FileInformation, MissingHandling, ResolveMode, are_hardlinks_to_same_file, canonicalize, get_filename, is_symlink_loop, normalize_path, path_ends_with_terminator, paths_refer_to_same_file, }; +#[cfg(unix)] +use uucore::fs::{make_block_device, make_char_device, make_fifo}; use uucore::{backup_control, update_control}; // These are exposed for projects (e.g. nushell) that want to create an `Options` value, which // requires these enum. @@ -2082,6 +2082,8 @@ fn handle_copy_mode( source_in_command_line: bool, source_is_fifo: bool, source_is_socket: bool, + source_is_char_device: bool, + source_is_block_device: bool, #[cfg(unix)] source_is_stream: bool, ) -> CopyResult { let source_is_symlink = source_metadata.is_symlink(); @@ -2122,6 +2124,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, source_is_socket, + source_is_char_device, + source_is_block_device, symlinked_files, #[cfg(unix)] source_is_stream, @@ -2145,6 +2149,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, source_is_socket, + source_is_char_device, + source_is_block_device, symlinked_files, #[cfg(unix)] source_is_stream, @@ -2181,6 +2187,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, source_is_socket, + source_is_char_device, + source_is_block_device, symlinked_files, #[cfg(unix)] source_is_stream, @@ -2196,6 +2204,8 @@ fn handle_copy_mode( source_is_symlink, source_is_fifo, source_is_socket, + source_is_char_device, + source_is_block_device, symlinked_files, #[cfg(unix)] source_is_stream, @@ -2424,10 +2434,18 @@ fn copy_file( let source_is_fifo = source_metadata.file_type().is_fifo(); #[cfg(unix)] let source_is_socket = source_metadata.file_type().is_socket(); + #[cfg(unix)] + let source_is_char_device = source_metadata.file_type().is_char_device(); + #[cfg(unix)] + let source_is_block_device = source_metadata.file_type().is_block_device(); #[cfg(not(unix))] let source_is_fifo = false; #[cfg(not(unix))] let source_is_socket = false; + #[cfg(not(unix))] + let source_is_char_device = false; + #[cfg(not(unix))] + let source_is_block_device = false; let source_is_stream = is_stream(&source_metadata); @@ -2441,6 +2459,8 @@ fn copy_file( source_in_command_line, source_is_fifo, source_is_socket, + source_is_char_device, + source_is_block_device, #[cfg(unix)] source_is_stream, )?; @@ -2568,6 +2588,8 @@ fn copy_helper( source_is_symlink: bool, source_is_fifo: bool, source_is_socket: bool, + source_is_char_device: bool, + source_is_block_device: bool, symlinked_files: &mut HashSet, #[cfg(unix)] source_is_stream: bool, ) -> CopyResult<()> { @@ -2586,6 +2608,12 @@ fn copy_helper( } else if source_is_fifo && options.recursive && !options.copy_contents { #[cfg(unix)] copy_fifo(dest, options.overwrite, options.debug)?; + } else if source_is_char_device && options.recursive && !options.copy_contents { + #[cfg(unix)] + copy_char_device(source, dest, options.overwrite, options.debug)?; + } else if source_is_block_device && options.recursive && !options.copy_contents { + #[cfg(unix)] + copy_block_device(source, dest, options.overwrite, options.debug)?; } else if source_is_symlink { copy_link(source, dest, symlinked_files, options)?; } else { @@ -2620,6 +2648,52 @@ fn copy_fifo(dest: &Path, overwrite: OverwriteMode, debug: bool) -> CopyResult<( .map_err(|_| translate!("cp-error-cannot-create-fifo", "path" => dest.quote()).into()) } +// "Copies" a character device by creating a new one with the same major/minor numbers. +#[cfg(unix)] +fn copy_char_device( + source: &Path, + dest: &Path, + overwrite: OverwriteMode, + debug: bool, +) -> CopyResult<()> { + if dest.exists() { + overwrite.verify(dest, debug)?; + fs::remove_file(dest)?; + } + + let source_metadata = fs::metadata(source)?; + let device_id = source_metadata.rdev(); + let major = ((device_id >> 8) & 0xff) as u32; + let minor = (device_id & 0xff) as u32; + + make_char_device(dest, major, minor).map_err(|_| { + translate!("cp-error-cannot-create-char-device", "path" => dest.quote()).into() + }) +} + +// "Copies" a block device by creating a new one with the same major/minor numbers. +#[cfg(unix)] +fn copy_block_device( + source: &Path, + dest: &Path, + overwrite: OverwriteMode, + debug: bool, +) -> CopyResult<()> { + if dest.exists() { + overwrite.verify(dest, debug)?; + fs::remove_file(dest)?; + } + + let source_metadata = fs::metadata(source)?; + let device_id = source_metadata.rdev(); + let major = ((device_id >> 8) & 0xff) as u32; + let minor = (device_id & 0xff) as u32; + + make_block_device(dest, major, minor).map_err(|_| { + translate!("cp-error-cannot-create-block-device", "path" => dest.quote()).into() + }) +} + #[cfg(unix)] fn copy_socket(dest: &Path, overwrite: OverwriteMode, debug: bool) -> CopyResult<()> { if dest.exists() { diff --git a/src/uucore/src/lib/features/fs.rs b/src/uucore/src/lib/features/fs.rs index e112bf730e2..f0b35ff2081 100644 --- a/src/uucore/src/lib/features/fs.rs +++ b/src/uucore/src/lib/features/fs.rs @@ -11,7 +11,7 @@ use libc::{ S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, S_IXOTH, S_IXUSR, - mkfifo, mode_t, + makedev, mkfifo, mknod, mode_t, }; use std::collections::HashSet; use std::collections::VecDeque; @@ -837,6 +837,42 @@ pub fn make_fifo(path: &Path) -> std::io::Result<()> { } } +/// Create a character device file. +/// +/// # Arguments +/// * `path` - The path where the device file should be created +/// * `major` - The major device number +/// * `minor` - The minor device number +#[cfg(unix)] +pub fn make_char_device(path: &Path, major: u32, minor: u32) -> std::io::Result<()> { + let name = CString::new(path.to_str().unwrap()).unwrap(); + let dev = makedev(major, minor); + let err = unsafe { mknod(name.as_ptr(), S_IFCHR | 0o666, dev) }; + if err == -1 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } +} + +/// Create a block device file. +/// +/// # Arguments +/// * `path` - The path where the device file should be created +/// * `major` - The major device number +/// * `minor` - The minor device number +#[cfg(unix)] +pub fn make_block_device(path: &Path, major: u32, minor: u32) -> std::io::Result<()> { + let name = CString::new(path.to_str().unwrap()).unwrap(); + let dev = makedev(major, minor); + let err = unsafe { mknod(name.as_ptr(), S_IFBLK | 0o666, dev) }; + if err == -1 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } +} + #[cfg(test)] mod tests { // Note this useful idiom: importing names from outer (for mod tests) scope. diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 563d127ff7d..e65c23b342c 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -7079,3 +7079,167 @@ fn test_cp_no_dereference_symlink_with_parents() { .succeeds(); assert_eq!(at.resolve_link("x/symlink-to-directory"), "directory"); } + +/// Test for copying character device files with -r flag. +/// This ensures that cp -r creates device files instead of copying content, +/// preventing infinite loops when copying devices like /dev/urandom. +#[cfg(unix)] +#[test] +fn test_cp_char_device() { + use uutests::util::run_ucmd_as_root; + + let scenario = TestScenario::new(util_name!()); + let at = &scenario.fixtures; + + // Create a character device using mknod (requires root) + // Using major=1, minor=9 (same as /dev/urandom for consistency) + if let Ok(result) = run_ucmd_as_root( + &TestScenario::new("mknod"), + &["test_char_dev", "c", "1", "9"], + ) { + result.success(); + + // Test copying the character device with -r + let mut ucmd = scenario.ucmd(); + ucmd.arg("-r") + .arg("test_char_dev") + .arg("copied_char_dev") + .succeeds() + .no_stderr(); + + // Verify the copied file is also a character device + assert!(at.is_char_device("copied_char_dev")); + + // Verify device numbers match + let orig_metadata = std::fs::metadata(at.plus("test_char_dev")).unwrap(); + let copy_metadata = std::fs::metadata(at.plus("copied_char_dev")).unwrap(); + assert_eq!(orig_metadata.rdev(), copy_metadata.rdev()); + } else { + println!("Test skipped; creating character devices requires root privileges"); + } +} + +/// Test for copying block device files with -r flag. +#[cfg(unix)] +#[test] +fn test_cp_block_device() { + use uutests::util::run_ucmd_as_root; + + let scenario = TestScenario::new(util_name!()); + let at = &scenario.fixtures; + + // Create a block device using mknod (requires root) + // Using major=7, minor=0 (similar to loop devices) + if let Ok(result) = run_ucmd_as_root( + &TestScenario::new("mknod"), + &["test_block_dev", "b", "7", "0"], + ) { + result.success(); + + // Test copying the block device with -r + let mut ucmd = scenario.ucmd(); + ucmd.arg("-r") + .arg("test_block_dev") + .arg("copied_block_dev") + .succeeds() + .no_stderr(); + + // Verify the copied file is a block device + // Note: we need a helper method for this, let's check metadata directly + let copy_metadata = std::fs::metadata(at.plus("copied_block_dev")).unwrap(); + assert!(copy_metadata.file_type().is_block_device()); + + // Verify device numbers match + let orig_metadata = std::fs::metadata(at.plus("test_block_dev")).unwrap(); + assert_eq!(orig_metadata.rdev(), copy_metadata.rdev()); + } else { + println!("Test skipped; creating block devices requires root privileges"); + } +} + +/// Test that cp with --copy-contents still copies device content instead of creating device files. +/// This verifies the --copy-contents flag overrides the default device file creation behavior. +#[cfg(unix)] +#[test] +fn test_cp_device_copy_contents() { + use uutests::util::run_ucmd_as_root; + + let scenario = TestScenario::new(util_name!()); + + // Create a character device using mknod (requires root) + if let Ok(result) = run_ucmd_as_root( + &TestScenario::new("mknod"), + &["test_char_dev", "c", "1", "8"], // Using /dev/random major/minor for safety + ) { + result.success(); + + // Test copying with --copy-contents flag + // This should attempt to copy content, not create a device file + // We expect this to succeed and create a regular file (even if empty) + let mut ucmd = scenario.ucmd(); + ucmd.arg("-r") + .arg("--copy-contents") + .arg("test_char_dev") + .arg("copied_content") + .succeeds(); + + // The result should be a regular file, not a character device + let copy_metadata = std::fs::metadata(scenario.fixtures.plus("copied_content")).unwrap(); + assert!(copy_metadata.file_type().is_file()); + assert!(!copy_metadata.file_type().is_char_device()); + } else { + println!("Test skipped; creating character devices requires root privileges"); + } +} + +/// Test error handling when trying to copy devices without sufficient permissions. +#[cfg(unix)] +#[test] +fn test_cp_device_permission_error() { + let scenario = TestScenario::new(util_name!()); + + // Try to copy a system device file to a location where we can't create device files + // This should show our proper error message + scenario + .ucmd() + .arg("-r") + .arg("/dev/null") + .arg("/tmp/test_null_copy") + .fails() + .stderr_contains("cannot create character device"); +} + +/// Test that copying devices preserves permissions when possible. +#[cfg(unix)] +#[test] +fn test_cp_device_preserve_permissions() { + use uutests::util::run_ucmd_as_root; + + let scenario = TestScenario::new(util_name!()); + let at = &scenario.fixtures; + + if let Ok(result) = run_ucmd_as_root( + &TestScenario::new("mknod"), + &["test_char_dev", "c", "1", "9"], + ) { + result.success(); + + // Set specific permissions on the source device + at.set_mode("test_char_dev", 0o640); + + // Copy with permission preservation + let mut ucmd = scenario.ucmd(); + ucmd.arg("-r") + .arg("--preserve=mode") + .arg("test_char_dev") + .arg("copied_char_dev") + .succeeds(); + + // Check that permissions are preserved + let copy_metadata = std::fs::metadata(at.plus("copied_char_dev")).unwrap(); + let permissions = copy_metadata.permissions().mode() & 0o777; + assert_eq!(permissions, 0o640); + } else { + println!("Test skipped; creating character devices requires root privileges"); + } +}