diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 92910650d..e568149ac 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -10,15 +10,6 @@ mod mode; use clap::{crate_version, Arg, ArgAction, ArgMatches, Command}; use file_diff::diff; use filetime::{set_file_times, FileTime}; -use uucore::backup_control::{self, BackupMode}; -use uucore::display::Quotable; -use uucore::entries::{grp2gid, usr2uid}; -use uucore::error::{FromIo, UError, UIoError, UResult, UUsageError}; -use uucore::fs::dir_strip_dot_for_creation; -use uucore::mode::get_umask; -use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel}; -use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err, uio_error}; - use std::error::Error; use std::fmt::{Debug, Display}; use std::fs; @@ -28,8 +19,15 @@ use std::os::unix::fs::MetadataExt; use std::os::unix::prelude::OsStrExt; use std::path::{Path, PathBuf, MAIN_SEPARATOR}; use std::process; -#[cfg(not(target_os = "windows"))] +use uucore::backup_control::{self, BackupMode}; +use uucore::display::Quotable; +use uucore::entries::{grp2gid, usr2uid}; +use uucore::error::{FromIo, UError, UIoError, UResult, UUsageError}; +use uucore::fs::dir_strip_dot_for_creation; +use uucore::mode::get_umask; +use uucore::perms::{wrap_chown, Verbosity, VerbosityLevel}; use uucore::process::{getegid, geteuid}; +use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err, uio_error}; const DEFAULT_MODE: u32 = 0o755; const DEFAULT_STRIP_PROGRAM: &str = "strip"; @@ -665,6 +663,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR /// Handle incomplete user/group parings for chown. /// /// Returns a Result type with the Err variant containing the error message. +/// If the user is root, revert the uid & gid /// /// # Parameters /// @@ -676,23 +675,31 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR /// return an empty error value. /// fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> { - if b.owner_id.is_some() || b.group_id.is_some() { - let meta = match fs::metadata(path) { - Ok(meta) => meta, - Err(e) => return Err(InstallError::MetadataFailed(e).into()), - }; + // GNU coreutils doesn't print chown operations during install with verbose flag. + let verbosity = Verbosity { + groups_only: b.owner_id.is_none(), + level: VerbosityLevel::Normal, + }; - // GNU coreutils doesn't print chown operations during install with verbose flag. - let verbosity = Verbosity { - groups_only: b.owner_id.is_none(), - level: VerbosityLevel::Normal, - }; + // Determine the owner and group IDs to be used for chown. + let (owner_id, group_id) = if b.owner_id.is_some() || b.group_id.is_some() { + (b.owner_id, b.group_id) + } else if geteuid() == 0 { + // Special case for root user. + (Some(0), Some(0)) + } else { + // No chown operation needed. + return Ok(()); + }; - match wrap_chown(path, &meta, b.owner_id, b.group_id, false, verbosity) { - Ok(msg) if b.verbose && !msg.is_empty() => println!("chown: {msg}"), - Ok(_) => {} - Err(e) => return Err(InstallError::ChownFailed(path.to_path_buf(), e).into()), - } + let meta = match fs::metadata(path) { + Ok(meta) => meta, + Err(e) => return Err(InstallError::MetadataFailed(e).into()), + }; + match wrap_chown(path, &meta, owner_id, group_id, false, verbosity) { + Ok(msg) if b.verbose && !msg.is_empty() => println!("chown: {msg}"), + Ok(_) => {} + Err(e) => return Err(InstallError::ChownFailed(path.to_path_buf(), e).into()), } Ok(()) @@ -916,55 +923,74 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { /// Crashes the program if a nonexistent owner or group is specified in _b_. /// fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult { + // Attempt to retrieve metadata for the source file. + // If this fails, assume the file needs to be copied. let from_meta = match fs::metadata(from) { Ok(meta) => meta, Err(_) => return Ok(true), }; + + // Attempt to retrieve metadata for the destination file. + // If this fails, assume the file needs to be copied. let to_meta = match fs::metadata(to) { Ok(meta) => meta, Err(_) => return Ok(true), }; - // setuid || setgid || sticky + // Define special file mode bits (setuid, setgid, sticky). let extra_mode: u32 = 0o7000; + // Define all file mode bits (including permissions). // setuid || setgid || sticky || permissions let all_modes: u32 = 0o7777; + // Check if any special mode bits are set in the specified mode, + // source file mode, or destination file mode. if b.specified_mode.unwrap_or(0) & extra_mode != 0 || from_meta.mode() & extra_mode != 0 || to_meta.mode() & extra_mode != 0 { return Ok(true); } + + // Check if the mode of the destination file differs from the specified mode. if b.mode() != to_meta.mode() & all_modes { return Ok(true); } + // Check if either the source or destination is not a file. if !from_meta.is_file() || !to_meta.is_file() { return Ok(true); } + // Check if the file sizes differ. if from_meta.len() != to_meta.len() { return Ok(true); } // TODO: if -P (#1809) and from/to contexts mismatch, return true. + // Check if the owner ID is specified and differs from the destination file's owner. if let Some(owner_id) = b.owner_id { if owner_id != to_meta.uid() { return Ok(true); } - } else if let Some(group_id) = b.group_id { + } + + // Check if the group ID is specified and differs from the destination file's group. + if let Some(group_id) = b.group_id { if group_id != to_meta.gid() { return Ok(true); } } else { #[cfg(not(target_os = "windows"))] + // Check if the destination file's owner or group + // differs from the effective user/group ID of the process. if to_meta.uid() != geteuid() || to_meta.gid() != getegid() { return Ok(true); } } + // Check if the contents of the source and destination files differ. if !diff(from.to_str().unwrap(), to.to_str().unwrap()) { return Ok(true); } diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index fb360533f..3db25c81f 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -4,9 +4,10 @@ // file that was distributed with this source code. // spell-checker:ignore (words) helloworld nodir objdump n'source -use crate::common::util::{is_ci, TestScenario}; +use crate::common::util::{is_ci, run_ucmd_as_root, TestScenario}; use filetime::FileTime; -use std::os::unix::fs::PermissionsExt; +use std::fs; +use std::os::unix::fs::{MetadataExt, PermissionsExt}; #[cfg(not(any(windows, target_os = "freebsd")))] use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -1613,3 +1614,32 @@ fn test_target_file_ends_with_slash() { .fails() .stderr_contains("failed to access 'dir/target_file/': Not a directory"); } + +#[test] +fn test_install_root_combined() { + let ts = TestScenario::new(util_name!()); + let at = ts.fixtures.clone(); + at.touch("a"); + at.touch("c"); + + let run_and_check = |args: &[&str], target: &str, expected_uid: u32, expected_gid: u32| { + if let Ok(result) = run_ucmd_as_root(&ts, args) { + result.success(); + assert!(at.file_exists(target)); + + let metadata = fs::metadata(at.plus(target)).unwrap(); + assert_eq!(metadata.uid(), expected_uid); + assert_eq!(metadata.gid(), expected_gid); + } else { + print!("Test skipped; requires root user"); + } + }; + + run_and_check(&["-Cv", "-o1", "-g1", "a", "b"], "b", 1, 1); + run_and_check(&["-Cv", "-o2", "-g1", "a", "b"], "b", 2, 1); + run_and_check(&["-Cv", "-o2", "-g2", "a", "b"], "b", 2, 2); + + run_and_check(&["-Cv", "-o2", "c", "d"], "d", 2, 0); + run_and_check(&["-Cv", "c", "d"], "d", 0, 0); + run_and_check(&["-Cv", "c", "d"], "d", 0, 0); +}