Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 53 additions & 27 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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
///
Expand All @@ -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(())
Expand Down Expand Up @@ -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<bool> {
// 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);
}
Expand Down
34 changes: 32 additions & 2 deletions tests/by-util/test_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -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);
}