Merge pull request #4365 from kylemanna/km/fix-install-directory-chown

install: add missing directory chown
This commit is contained in:
Sylvestre Ledru 2023-02-22 21:49:22 +01:00 committed by GitHub
commit 41018859bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 176 additions and 86 deletions

View file

@ -41,8 +41,8 @@ pub struct Behavior {
specified_mode: Option<u32>,
backup_mode: BackupMode,
suffix: String,
owner: String,
group: String,
owner_id: Option<u32>,
group_id: Option<u32>,
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<Behavior> {
show_error!("Options --compare and --strip are mutually exclusive");
return Err(1.into());
}
let owner = matches
.get_one::<String>(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::<String>(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::<String>(OPT_OWNER)
.map(|s| s.as_str())
.unwrap_or("")
.to_string(),
group: matches
.get_one::<String>(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<bool> {
// 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);
}

View file

@ -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!());