diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 3178df8e1..c4c8f095b 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -41,8 +41,8 @@ pub struct Behavior { specified_mode: Option, backup_mode: BackupMode, suffix: String, - owner: String, - group: String, + owner_id: Option, + group_id: Option, verbose: bool, preserve_timestamps: bool, compare: bool, @@ -58,14 +58,15 @@ enum InstallError { DirNeedsArg(), CreateDirFailed(PathBuf, std::io::Error), ChmodFailed(PathBuf), + ChownFailed(PathBuf, String), InvalidTarget(PathBuf), TargetDirIsntDir(PathBuf), BackupFailed(PathBuf, PathBuf, std::io::Error), InstallFailed(PathBuf, PathBuf, std::io::Error), StripProgramFailed(String), MetadataFailed(std::io::Error), - NoSuchUser(String), - NoSuchGroup(String), + InvalidUser(String), + InvalidGroup(String), OmittingDirectory(PathBuf), } @@ -99,6 +100,7 @@ impl Display for InstallError { Display::fmt(&uio_error!(e, "failed to create {}", dir.quote()), f) } Self::ChmodFailed(file) => write!(f, "failed to chmod {}", file.quote()), + Self::ChownFailed(file, msg) => write!(f, "failed to chown {}: {}", file.quote(), msg), Self::InvalidTarget(target) => write!( f, "invalid target {}: No such file or directory", @@ -117,8 +119,8 @@ impl Display for InstallError { ), Self::StripProgramFailed(msg) => write!(f, "strip program failed: {msg}"), Self::MetadataFailed(e) => Display::fmt(&uio_error!(e, ""), f), - Self::NoSuchUser(user) => write!(f, "no such user: {}", user.maybe_quote()), - Self::NoSuchGroup(group) => write!(f, "no such group: {}", group.maybe_quote()), + Self::InvalidUser(user) => write!(f, "invalid user: {}", user.quote()), + Self::InvalidGroup(group) => write!(f, "invalid group: {}", group.quote()), Self::OmittingDirectory(dir) => write!(f, "omitting directory {}", dir.quote()), } } @@ -391,21 +393,44 @@ fn behavior(matches: &ArgMatches) -> UResult { show_error!("Options --compare and --strip are mutually exclusive"); return Err(1.into()); } + + let owner = matches + .get_one::(OPT_OWNER) + .map(|s| s.as_str()) + .unwrap_or("") + .to_string(); + + let owner_id = if !owner.is_empty() { + match usr2uid(&owner) { + Ok(u) => Some(u), + Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()), + } + } else { + None + }; + + let group = matches + .get_one::(OPT_GROUP) + .map(|s| s.as_str()) + .unwrap_or("") + .to_string(); + + let group_id = if !group.is_empty() { + match grp2gid(&group) { + Ok(g) => Some(g), + Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()), + } + } else { + None + }; + Ok(Behavior { main_function, specified_mode, backup_mode, suffix: backup_control::determine_backup_suffix(matches), - owner: matches - .get_one::(OPT_OWNER) - .map(|s| s.as_str()) - .unwrap_or("") - .to_string(), - group: matches - .get_one::(OPT_GROUP) - .map(|s| s.as_str()) - .unwrap_or("") - .to_string(), + owner_id, + group_id, verbose: matches.get_flag(OPT_VERBOSE), preserve_timestamps, compare, @@ -466,6 +491,8 @@ fn directory(paths: &[String], b: &Behavior) -> UResult<()> { uucore::error::set_exit_code(1); continue; } + + show_if_err!(chown_optional_user_group(path, b)); } // If the exit code was set, or show! has been called at least once // (which sets the exit code as well), function execution will end after @@ -620,6 +647,42 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR Ok(()) } +/// Handle incomplete user/group parings for chown. +/// +/// Returns a Result type with the Err variant containing the error message. +/// +/// # Parameters +/// +/// _path_ must exist. +/// +/// # Errors +/// +/// If the owner or group are invalid or copy system call fails, we print a verbose error and +/// 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, + }; + + 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()), + } + } + + Ok(()) +} + /// Copy one file to a new location, changing metadata. /// /// Returns a Result type with the Err variant containing the error message. @@ -702,66 +765,7 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { return Err(InstallError::ChmodFailed(to.to_path_buf()).into()); } - if !b.owner.is_empty() { - let meta = match fs::metadata(to) { - Ok(meta) => meta, - Err(e) => return Err(InstallError::MetadataFailed(e).into()), - }; - - let owner_id = match usr2uid(&b.owner) { - Ok(g) => g, - _ => return Err(InstallError::NoSuchUser(b.owner.clone()).into()), - }; - let gid = meta.gid(); - match wrap_chown( - to, - &meta, - Some(owner_id), - Some(gid), - false, - Verbosity { - groups_only: false, - level: VerbosityLevel::Normal, - }, - ) { - Ok(n) => { - if !n.is_empty() { - show_error!("{}", n); - } - } - Err(e) => show_error!("{}", e), - } - } - - if !b.group.is_empty() { - let meta = match fs::metadata(to) { - Ok(meta) => meta, - Err(e) => return Err(InstallError::MetadataFailed(e).into()), - }; - - let group_id = match grp2gid(&b.group) { - Ok(g) => g, - _ => return Err(InstallError::NoSuchGroup(b.group.clone()).into()), - }; - match wrap_chown( - to, - &meta, - Some(group_id), - None, - false, - Verbosity { - groups_only: true, - level: VerbosityLevel::Normal, - }, - ) { - Ok(n) => { - if !n.is_empty() { - show_error!("{}", n); - } - } - Err(e) => show_error!("{}", e), - } - } + chown_optional_user_group(to, b)?; if b.preserve_timestamps { let meta = match fs::metadata(from) { @@ -841,19 +845,11 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult { // TODO: if -P (#1809) and from/to contexts mismatch, return true. - if !b.owner.is_empty() { - let owner_id = match usr2uid(&b.owner) { - Ok(id) => id, - _ => return Err(InstallError::NoSuchUser(b.owner.clone()).into()), - }; + if let Some(owner_id) = b.owner_id { if owner_id != to_meta.uid() { return Ok(true); } - } else if !b.group.is_empty() { - let group_id = match grp2gid(&b.group) { - Ok(id) => id, - _ => return Err(InstallError::NoSuchGroup(b.group.clone()).into()), - }; + } else if let Some(group_id) = b.group_id { if group_id != to_meta.gid() { return Ok(true); } diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index e041fb081..f5c65b352 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -1400,6 +1400,100 @@ fn test_install_dir_req_verbose() { .stdout_contains("install: creating directory 'sub5/a'\ninstall: creating directory 'sub5/a/b'\ninstall: creating directory 'sub5/a/b/c'\n'source_file1' -> 'sub5/a/b/c/file'"); } +#[test] +fn test_install_chown_file_invalid() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let file_1 = "source_file1"; + at.touch(file_1); + + scene + .ucmd() + .arg("-o") + .arg("test_invalid_user") + .arg(file_1) + .arg("target_file1") + .fails() + .stderr_contains("install: invalid user: 'test_invalid_user'"); + + scene + .ucmd() + .arg("-g") + .arg("test_invalid_group") + .arg(file_1) + .arg("target_file1") + .fails() + .stderr_contains("install: invalid group: 'test_invalid_group'"); + + scene + .ucmd() + .arg("-o") + .arg("test_invalid_user") + .arg("-g") + .arg("test_invalid_group") + .arg(file_1) + .arg("target_file1") + .fails() + .stderr_contains("install: invalid user: 'test_invalid_user'"); + + scene + .ucmd() + .arg("-g") + .arg("test_invalid_group") + .arg("-o") + .arg("test_invalid_user") + .arg(file_1) + .arg("target_file1") + .fails() + .stderr_contains("install: invalid user: 'test_invalid_user'"); +} + +#[test] +fn test_install_chown_directory_invalid() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("-o") + .arg("test_invalid_user") + .arg("-d") + .arg("dir1/dir2") + .fails() + .stderr_contains("install: invalid user: 'test_invalid_user'"); + + scene + .ucmd() + .arg("-g") + .arg("test_invalid_group") + .arg("-d") + .arg("dir1/dir2") + .fails() + .stderr_contains("install: invalid group: 'test_invalid_group'"); + + scene + .ucmd() + .arg("-o") + .arg("test_invalid_user") + .arg("-g") + .arg("test_invalid_group") + .arg("-d") + .arg("dir1/dir2") + .fails() + .stderr_contains("install: invalid user: 'test_invalid_user'"); + + scene + .ucmd() + .arg("-g") + .arg("test_invalid_group") + .arg("-o") + .arg("test_invalid_user") + .arg("-d") + .arg("dir1/dir2") + .fails() + .stderr_contains("install: invalid user: 'test_invalid_user'"); +} + #[test] fn test_install_compare_option() { let scene = TestScenario::new(util_name!());