mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2026-01-17 18:51:05 +00:00
1018 lines
32 KiB
Rust
1018 lines
32 KiB
Rust
// This file is part of the uutils coreutils package.
|
|
//
|
|
// For the full copyright and license information, please view the LICENSE
|
|
// file that was distributed with this source code.
|
|
|
|
// spell-checker:ignore (ToDO) rwxr sourcepath targetpath Isnt uioerror
|
|
|
|
mod mode;
|
|
|
|
use clap::{Arg, ArgAction, ArgMatches, Command};
|
|
use file_diff::diff;
|
|
use filetime::{set_file_times, FileTime};
|
|
use std::fmt::Debug;
|
|
use std::fs::File;
|
|
use std::fs::{self, metadata};
|
|
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
|
|
use std::process;
|
|
use thiserror::Error;
|
|
use uucore::backup_control::{self, BackupMode};
|
|
use uucore::buf_copy::copy_stream;
|
|
use uucore::display::Quotable;
|
|
use uucore::entries::{grp2gid, usr2uid};
|
|
use uucore::error::{FromIo, UError, 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};
|
|
|
|
#[cfg(unix)]
|
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
|
#[cfg(unix)]
|
|
use std::os::unix::prelude::OsStrExt;
|
|
|
|
const DEFAULT_MODE: u32 = 0o755;
|
|
const DEFAULT_STRIP_PROGRAM: &str = "strip";
|
|
|
|
#[allow(dead_code)]
|
|
pub struct Behavior {
|
|
main_function: MainFunction,
|
|
specified_mode: Option<u32>,
|
|
backup_mode: BackupMode,
|
|
suffix: String,
|
|
owner_id: Option<u32>,
|
|
group_id: Option<u32>,
|
|
verbose: bool,
|
|
preserve_timestamps: bool,
|
|
compare: bool,
|
|
strip: bool,
|
|
strip_program: String,
|
|
create_leading: bool,
|
|
target_dir: Option<String>,
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
enum InstallError {
|
|
#[error("Unimplemented feature: {0}")]
|
|
Unimplemented(String),
|
|
|
|
#[error("{} with -d requires at least one argument.", uucore::util_name())]
|
|
DirNeedsArg,
|
|
|
|
#[error("failed to create {0}")]
|
|
CreateDirFailed(PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("failed to chmod {}", .0.quote())]
|
|
ChmodFailed(PathBuf),
|
|
|
|
#[error("failed to chown {}: {}", .0.quote(), .1)]
|
|
ChownFailed(PathBuf, String),
|
|
|
|
#[error("invalid target {}: No such file or directory", .0.quote())]
|
|
InvalidTarget(PathBuf),
|
|
|
|
#[error("target {} is not a directory", .0.quote())]
|
|
TargetDirIsntDir(PathBuf),
|
|
|
|
#[error("cannot backup {0} to {1}")]
|
|
BackupFailed(PathBuf, PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("cannot install {0} to {1}")]
|
|
InstallFailed(PathBuf, PathBuf, #[source] std::io::Error),
|
|
|
|
#[error("strip program failed: {0}")]
|
|
StripProgramFailed(String),
|
|
|
|
#[error("metadata error")]
|
|
MetadataFailed(#[source] std::io::Error),
|
|
|
|
#[error("invalid user: {}", .0.quote())]
|
|
InvalidUser(String),
|
|
|
|
#[error("invalid group: {}", .0.quote())]
|
|
InvalidGroup(String),
|
|
|
|
#[error("omitting directory {}", .0.quote())]
|
|
OmittingDirectory(PathBuf),
|
|
|
|
#[error("failed to access {}: Not a directory", .0.quote())]
|
|
NotADirectory(PathBuf),
|
|
}
|
|
|
|
impl UError for InstallError {
|
|
fn code(&self) -> i32 {
|
|
match self {
|
|
Self::Unimplemented(_) => 2,
|
|
_ => 1,
|
|
}
|
|
}
|
|
|
|
fn usage(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Eq, PartialEq)]
|
|
pub enum MainFunction {
|
|
/// Create directories
|
|
Directory,
|
|
/// Install files to locations (primary functionality)
|
|
Standard,
|
|
}
|
|
|
|
impl Behavior {
|
|
/// Determine the mode for chmod after copy.
|
|
pub fn mode(&self) -> u32 {
|
|
match self.specified_mode {
|
|
Some(x) => x,
|
|
None => DEFAULT_MODE,
|
|
}
|
|
}
|
|
}
|
|
|
|
const ABOUT: &str = help_about!("install.md");
|
|
const USAGE: &str = help_usage!("install.md");
|
|
|
|
static OPT_COMPARE: &str = "compare";
|
|
static OPT_DIRECTORY: &str = "directory";
|
|
static OPT_IGNORED: &str = "ignored";
|
|
static OPT_CREATE_LEADING: &str = "create-leading";
|
|
static OPT_GROUP: &str = "group";
|
|
static OPT_MODE: &str = "mode";
|
|
static OPT_OWNER: &str = "owner";
|
|
static OPT_PRESERVE_TIMESTAMPS: &str = "preserve-timestamps";
|
|
static OPT_STRIP: &str = "strip";
|
|
static OPT_STRIP_PROGRAM: &str = "strip-program";
|
|
static OPT_TARGET_DIRECTORY: &str = "target-directory";
|
|
static OPT_NO_TARGET_DIRECTORY: &str = "no-target-directory";
|
|
static OPT_VERBOSE: &str = "verbose";
|
|
static OPT_PRESERVE_CONTEXT: &str = "preserve-context";
|
|
static OPT_CONTEXT: &str = "context";
|
|
|
|
static ARG_FILES: &str = "files";
|
|
|
|
/// Main install utility function, called from main.rs.
|
|
///
|
|
/// Returns a program return code.
|
|
///
|
|
#[uucore::main]
|
|
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|
let matches = uu_app().try_get_matches_from(args)?;
|
|
|
|
let paths: Vec<String> = matches
|
|
.get_many::<String>(ARG_FILES)
|
|
.map(|v| v.map(ToString::to_string).collect())
|
|
.unwrap_or_default();
|
|
|
|
check_unimplemented(&matches)?;
|
|
|
|
let behavior = behavior(&matches)?;
|
|
|
|
match behavior.main_function {
|
|
MainFunction::Directory => directory(&paths, &behavior),
|
|
MainFunction::Standard => standard(paths, &behavior),
|
|
}
|
|
}
|
|
|
|
pub fn uu_app() -> Command {
|
|
Command::new(uucore::util_name())
|
|
.version(uucore::crate_version!())
|
|
.about(ABOUT)
|
|
.override_usage(format_usage(USAGE))
|
|
.infer_long_args(true)
|
|
.arg(backup_control::arguments::backup())
|
|
.arg(backup_control::arguments::backup_no_args())
|
|
.arg(
|
|
Arg::new(OPT_IGNORED)
|
|
.short('c')
|
|
.help("ignored")
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_COMPARE)
|
|
.short('C')
|
|
.long(OPT_COMPARE)
|
|
.help(
|
|
"compare each pair of source and destination files, and in some cases, \
|
|
do not modify the destination at all",
|
|
)
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_DIRECTORY)
|
|
.short('d')
|
|
.long(OPT_DIRECTORY)
|
|
.help(
|
|
"treat all arguments as directory names. create all components of \
|
|
the specified directories",
|
|
)
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
// TODO implement flag
|
|
Arg::new(OPT_CREATE_LEADING)
|
|
.short('D')
|
|
.help(
|
|
"create all leading components of DEST except the last, then copy \
|
|
SOURCE to DEST",
|
|
)
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_GROUP)
|
|
.short('g')
|
|
.long(OPT_GROUP)
|
|
.help("set group ownership, instead of process's current group")
|
|
.value_name("GROUP"),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_MODE)
|
|
.short('m')
|
|
.long(OPT_MODE)
|
|
.help("set permission mode (as in chmod), instead of rwxr-xr-x")
|
|
.value_name("MODE"),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_OWNER)
|
|
.short('o')
|
|
.long(OPT_OWNER)
|
|
.help("set ownership (super-user only)")
|
|
.value_name("OWNER")
|
|
.value_hint(clap::ValueHint::Username),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_PRESERVE_TIMESTAMPS)
|
|
.short('p')
|
|
.long(OPT_PRESERVE_TIMESTAMPS)
|
|
.help(
|
|
"apply access/modification times of SOURCE files to \
|
|
corresponding destination files",
|
|
)
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_STRIP)
|
|
.short('s')
|
|
.long(OPT_STRIP)
|
|
.help("strip symbol tables (no action Windows)")
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_STRIP_PROGRAM)
|
|
.long(OPT_STRIP_PROGRAM)
|
|
.help("program used to strip binaries (no action Windows)")
|
|
.value_name("PROGRAM")
|
|
.value_hint(clap::ValueHint::CommandName),
|
|
)
|
|
.arg(backup_control::arguments::suffix())
|
|
.arg(
|
|
// TODO implement flag
|
|
Arg::new(OPT_TARGET_DIRECTORY)
|
|
.short('t')
|
|
.long(OPT_TARGET_DIRECTORY)
|
|
.help("move all SOURCE arguments into DIRECTORY")
|
|
.value_name("DIRECTORY")
|
|
.value_hint(clap::ValueHint::DirPath),
|
|
)
|
|
.arg(
|
|
// TODO implement flag
|
|
Arg::new(OPT_NO_TARGET_DIRECTORY)
|
|
.short('T')
|
|
.long(OPT_NO_TARGET_DIRECTORY)
|
|
.help("(unimplemented) treat DEST as a normal file")
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(OPT_VERBOSE)
|
|
.short('v')
|
|
.long(OPT_VERBOSE)
|
|
.help("explain what is being done")
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
// TODO implement flag
|
|
Arg::new(OPT_PRESERVE_CONTEXT)
|
|
.short('P')
|
|
.long(OPT_PRESERVE_CONTEXT)
|
|
.help("(unimplemented) preserve security context")
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
// TODO implement flag
|
|
Arg::new(OPT_CONTEXT)
|
|
.short('Z')
|
|
.long(OPT_CONTEXT)
|
|
.help("(unimplemented) set security context of files and directories")
|
|
.value_name("CONTEXT")
|
|
.action(ArgAction::SetTrue),
|
|
)
|
|
.arg(
|
|
Arg::new(ARG_FILES)
|
|
.action(ArgAction::Append)
|
|
.num_args(1..)
|
|
.value_hint(clap::ValueHint::AnyPath),
|
|
)
|
|
}
|
|
|
|
/// Check for unimplemented command line arguments.
|
|
///
|
|
/// Either return the degenerate Ok value, or an Err with string.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Error datum is a string of the unimplemented argument.
|
|
///
|
|
///
|
|
fn check_unimplemented(matches: &ArgMatches) -> UResult<()> {
|
|
if matches.get_flag(OPT_NO_TARGET_DIRECTORY) {
|
|
Err(InstallError::Unimplemented(String::from("--no-target-directory, -T")).into())
|
|
} else if matches.get_flag(OPT_PRESERVE_CONTEXT) {
|
|
Err(InstallError::Unimplemented(String::from("--preserve-context, -P")).into())
|
|
} else if matches.get_flag(OPT_CONTEXT) {
|
|
Err(InstallError::Unimplemented(String::from("--context, -Z")).into())
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Determine behavior, given command line arguments.
|
|
///
|
|
/// If successful, returns a filled-out Behavior struct.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// In event of failure, returns an integer intended as a program return code.
|
|
///
|
|
fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
|
|
let main_function = if matches.get_flag(OPT_DIRECTORY) {
|
|
MainFunction::Directory
|
|
} else {
|
|
MainFunction::Standard
|
|
};
|
|
|
|
let considering_dir: bool = MainFunction::Directory == main_function;
|
|
|
|
let specified_mode: Option<u32> = if matches.contains_id(OPT_MODE) {
|
|
let x = matches.get_one::<String>(OPT_MODE).ok_or(1)?;
|
|
Some(mode::parse(x, considering_dir, get_umask()).map_err(|err| {
|
|
show_error!("Invalid mode string: {}", err);
|
|
1
|
|
})?)
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let backup_mode = backup_control::determine_backup_mode(matches)?;
|
|
let target_dir = matches.get_one::<String>(OPT_TARGET_DIRECTORY).cloned();
|
|
|
|
let preserve_timestamps = matches.get_flag(OPT_PRESERVE_TIMESTAMPS);
|
|
let compare = matches.get_flag(OPT_COMPARE);
|
|
let strip = matches.get_flag(OPT_STRIP);
|
|
if preserve_timestamps && compare {
|
|
show_error!("Options --compare and --preserve-timestamps are mutually exclusive");
|
|
return Err(1.into());
|
|
}
|
|
if compare && strip {
|
|
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() {
|
|
None
|
|
} else {
|
|
match usr2uid(&owner) {
|
|
Ok(u) => Some(u),
|
|
Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()),
|
|
}
|
|
};
|
|
|
|
let group = matches
|
|
.get_one::<String>(OPT_GROUP)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let group_id = if group.is_empty() {
|
|
None
|
|
} else {
|
|
match grp2gid(&group) {
|
|
Ok(g) => Some(g),
|
|
Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()),
|
|
}
|
|
};
|
|
|
|
Ok(Behavior {
|
|
main_function,
|
|
specified_mode,
|
|
backup_mode,
|
|
suffix: backup_control::determine_backup_suffix(matches),
|
|
owner_id,
|
|
group_id,
|
|
verbose: matches.get_flag(OPT_VERBOSE),
|
|
preserve_timestamps,
|
|
compare,
|
|
strip,
|
|
strip_program: String::from(
|
|
matches
|
|
.get_one::<String>(OPT_STRIP_PROGRAM)
|
|
.map(|s| s.as_str())
|
|
.unwrap_or(DEFAULT_STRIP_PROGRAM),
|
|
),
|
|
create_leading: matches.get_flag(OPT_CREATE_LEADING),
|
|
target_dir,
|
|
})
|
|
}
|
|
|
|
/// Creates directories.
|
|
///
|
|
/// GNU man pages describe this functionality as creating 'all components of
|
|
/// the specified directories'.
|
|
///
|
|
/// Returns a Result type with the Err variant containing the error message.
|
|
///
|
|
fn directory(paths: &[String], b: &Behavior) -> UResult<()> {
|
|
if paths.is_empty() {
|
|
Err(InstallError::DirNeedsArg.into())
|
|
} else {
|
|
for path in paths.iter().map(Path::new) {
|
|
// if the path already exist, don't try to create it again
|
|
if !path.exists() {
|
|
// Special case to match GNU's behavior:
|
|
// install -d foo/. should work and just create foo/
|
|
// std::fs::create_dir("foo/."); fails in pure Rust
|
|
// See also mkdir.rs for another occurrence of this
|
|
let path_to_create = dir_strip_dot_for_creation(path);
|
|
// Differently than the primary functionality
|
|
// (MainFunction::Standard), the directory functionality should
|
|
// create all ancestors (or components) of a directory
|
|
// regardless of the presence of the "-D" flag.
|
|
//
|
|
// NOTE: the GNU "install" sets the expected mode only for the
|
|
// target directory. All created ancestor directories will have
|
|
// the default mode. Hence it is safe to use fs::create_dir_all
|
|
// and then only modify the target's dir mode.
|
|
if let Err(e) = fs::create_dir_all(path_to_create.as_path())
|
|
.map_err_context(|| path_to_create.as_path().maybe_quote().to_string())
|
|
{
|
|
show!(e);
|
|
continue;
|
|
}
|
|
|
|
if b.verbose {
|
|
println!("creating directory {}", path_to_create.quote());
|
|
}
|
|
}
|
|
|
|
if mode::chmod(path, b.mode()).is_err() {
|
|
// Error messages are printed by the mode::chmod function!
|
|
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
|
|
// this return.
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Test if the path is a new file path that can be
|
|
/// created immediately
|
|
fn is_new_file_path(path: &Path) -> bool {
|
|
!path.exists()
|
|
&& (path.parent().map(Path::is_dir).unwrap_or(true)
|
|
|| path.parent().unwrap().as_os_str().is_empty()) // In case of a simple file
|
|
}
|
|
|
|
/// Test if the path is an existing directory or ends with a trailing separator.
|
|
///
|
|
/// Returns true, if one of the conditions above is met; else false.
|
|
///
|
|
#[cfg(unix)]
|
|
fn is_potential_directory_path(path: &Path) -> bool {
|
|
let separator = MAIN_SEPARATOR as u8;
|
|
path.as_os_str().as_bytes().last() == Some(&separator) || path.is_dir()
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn is_potential_directory_path(path: &Path) -> bool {
|
|
let path_str = path.to_string_lossy();
|
|
path_str.ends_with(MAIN_SEPARATOR) || path_str.ends_with('/') || path.is_dir()
|
|
}
|
|
|
|
/// Perform an install, given a list of paths and behavior.
|
|
///
|
|
/// Returns a Result type with the Err variant containing the error message.
|
|
///
|
|
#[allow(clippy::cognitive_complexity)]
|
|
fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
|
|
// first check that paths contains at least one element
|
|
if paths.is_empty() {
|
|
return Err(UUsageError::new(1, "missing file operand"));
|
|
}
|
|
|
|
// get the target from either "-t foo" param or from the last given paths argument
|
|
let target: PathBuf = if let Some(path) = &b.target_dir {
|
|
path.into()
|
|
} else {
|
|
let last_path: PathBuf = paths.pop().unwrap().into();
|
|
|
|
// paths has to contain more elements
|
|
if paths.is_empty() {
|
|
return Err(UUsageError::new(
|
|
1,
|
|
format!(
|
|
"missing destination file operand after '{}'",
|
|
last_path.to_str().unwrap()
|
|
),
|
|
));
|
|
}
|
|
|
|
last_path
|
|
};
|
|
|
|
let sources = &paths.iter().map(PathBuf::from).collect::<Vec<_>>();
|
|
|
|
if b.create_leading {
|
|
// if -t is used in combination with -D, create whole target because it does not include filename
|
|
let to_create: Option<&Path> = if b.target_dir.is_some() {
|
|
Some(target.as_path())
|
|
// if source and target are filenames used in combination with -D, create target's parent
|
|
} else if !(sources.len() > 1 || is_potential_directory_path(&target)) {
|
|
target.parent()
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if let Some(to_create) = to_create {
|
|
// if the path ends in /, remove it
|
|
let to_create = if to_create.to_string_lossy().ends_with('/') {
|
|
Path::new(to_create.to_str().unwrap().trim_end_matches('/'))
|
|
} else {
|
|
to_create
|
|
};
|
|
|
|
if !to_create.exists() {
|
|
if b.verbose {
|
|
let mut result = PathBuf::new();
|
|
// When creating directories with -Dv, show directory creations step by step
|
|
for part in to_create.components() {
|
|
result.push(part.as_os_str());
|
|
if !result.is_dir() {
|
|
// Don't display when the directory already exists
|
|
println!("install: creating directory {}", result.quote());
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Err(e) = fs::create_dir_all(to_create) {
|
|
return Err(InstallError::CreateDirFailed(to_create.to_path_buf(), e).into());
|
|
}
|
|
}
|
|
}
|
|
if b.target_dir.is_some() {
|
|
let p = to_create.unwrap();
|
|
|
|
if !p.exists() || !p.is_dir() {
|
|
return Err(InstallError::NotADirectory(p.to_path_buf()).into());
|
|
}
|
|
}
|
|
}
|
|
|
|
if sources.len() > 1 || is_potential_directory_path(&target) {
|
|
copy_files_into_dir(sources, &target, b)
|
|
} else {
|
|
let source = sources.first().unwrap();
|
|
|
|
if source.is_dir() {
|
|
return Err(InstallError::OmittingDirectory(source.clone()).into());
|
|
}
|
|
|
|
if target.is_file() || is_new_file_path(&target) {
|
|
copy(source, &target, b)
|
|
} else {
|
|
Err(InstallError::InvalidTarget(target).into())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Copy some files into a directory.
|
|
///
|
|
/// Prints verbose information and error messages.
|
|
/// Returns a Result type with the Err variant containing the error message.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// _files_ must all exist as non-directories.
|
|
/// _target_dir_ must be a directory.
|
|
///
|
|
fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UResult<()> {
|
|
if !target_dir.is_dir() {
|
|
return Err(InstallError::TargetDirIsntDir(target_dir.to_path_buf()).into());
|
|
}
|
|
for sourcepath in files {
|
|
if let Err(err) = sourcepath
|
|
.metadata()
|
|
.map_err_context(|| format!("cannot stat {}", sourcepath.quote()))
|
|
{
|
|
show!(err);
|
|
continue;
|
|
}
|
|
|
|
if sourcepath.is_dir() {
|
|
let err = InstallError::OmittingDirectory(sourcepath.clone());
|
|
show!(err);
|
|
continue;
|
|
}
|
|
|
|
let mut targetpath = target_dir.to_path_buf();
|
|
let filename = sourcepath.components().next_back().unwrap();
|
|
targetpath.push(filename);
|
|
|
|
show_if_err!(copy(sourcepath, &targetpath, 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
|
|
// this return.
|
|
Ok(())
|
|
}
|
|
|
|
/// 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
|
|
///
|
|
/// _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<()> {
|
|
// 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(());
|
|
};
|
|
|
|
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(())
|
|
}
|
|
|
|
/// Perform backup before overwriting.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `to` - The destination file path.
|
|
/// * `b` - The behavior configuration.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns an Option containing the backup path, or None if backup is not needed.
|
|
///
|
|
fn perform_backup(to: &Path, b: &Behavior) -> UResult<Option<PathBuf>> {
|
|
if to.exists() {
|
|
if b.verbose {
|
|
println!("removed {}", to.quote());
|
|
}
|
|
let backup_path = backup_control::get_backup_path(b.backup_mode, to, &b.suffix);
|
|
if let Some(ref backup_path) = backup_path {
|
|
// TODO!!
|
|
if let Err(err) = fs::rename(to, backup_path) {
|
|
return Err(
|
|
InstallError::BackupFailed(to.to_path_buf(), backup_path.clone(), err).into(),
|
|
);
|
|
}
|
|
}
|
|
Ok(backup_path)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Copy a non-special file using std::fs::copy.
|
|
///
|
|
/// # Parameters
|
|
/// * `from` - The source file path.
|
|
/// * `to` - The destination file path.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns an empty Result or an error in case of failure.
|
|
fn copy_normal_file(from: &Path, to: &Path) -> UResult<()> {
|
|
if let Err(err) = fs::copy(from, to) {
|
|
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Copy a file from one path to another. Handles the certain cases of special
|
|
/// files (e.g character specials).
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `from` - The source file path.
|
|
/// * `to` - The destination file path.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns an empty Result or an error in case of failure.
|
|
///
|
|
fn copy_file(from: &Path, to: &Path) -> UResult<()> {
|
|
// fs::copy fails if destination is a invalid symlink.
|
|
// so lets just remove all existing files at destination before copy.
|
|
if let Err(e) = fs::remove_file(to) {
|
|
if e.kind() != std::io::ErrorKind::NotFound {
|
|
show_error!(
|
|
"Failed to remove existing file {}. Error: {:?}",
|
|
to.display(),
|
|
e
|
|
);
|
|
}
|
|
}
|
|
|
|
let ft = match metadata(from) {
|
|
Ok(ft) => ft.file_type(),
|
|
Err(err) => {
|
|
return Err(
|
|
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(),
|
|
);
|
|
}
|
|
};
|
|
|
|
// Stream-based copying to get around the limitations of std::fs::copy
|
|
#[cfg(unix)]
|
|
if ft.is_char_device() || ft.is_block_device() || ft.is_fifo() {
|
|
let mut handle = File::open(from)?;
|
|
let mut dest = File::create(to)?;
|
|
copy_stream(&mut handle, &mut dest)?;
|
|
return Ok(());
|
|
}
|
|
|
|
copy_normal_file(from, to)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Strip a file using an external program.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `to` - The destination file path.
|
|
/// * `b` - The behavior configuration.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns an empty Result or an error in case of failure.
|
|
///
|
|
fn strip_file(to: &Path, b: &Behavior) -> UResult<()> {
|
|
// Check if the filename starts with a hyphen and adjust the path
|
|
let to_str = to.as_os_str().to_str().unwrap_or_default();
|
|
let to = if to_str.starts_with('-') {
|
|
let mut new_path = PathBuf::from(".");
|
|
new_path.push(to);
|
|
new_path
|
|
} else {
|
|
to.to_path_buf()
|
|
};
|
|
match process::Command::new(&b.strip_program).arg(&to).status() {
|
|
Ok(status) => {
|
|
if !status.success() {
|
|
// Follow GNU's behavior: if strip fails, removes the target
|
|
let _ = fs::remove_file(to);
|
|
return Err(InstallError::StripProgramFailed(format!(
|
|
"strip process terminated abnormally - exit code: {}",
|
|
status.code().unwrap()
|
|
))
|
|
.into());
|
|
}
|
|
}
|
|
Err(e) => {
|
|
// Follow GNU's behavior: if strip fails, removes the target
|
|
let _ = fs::remove_file(to);
|
|
return Err(InstallError::StripProgramFailed(e.to_string()).into());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Set ownership and permissions on the destination file.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `to` - The destination file path.
|
|
/// * `b` - The behavior configuration.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns an empty Result or an error in case of failure.
|
|
///
|
|
fn set_ownership_and_permissions(to: &Path, b: &Behavior) -> UResult<()> {
|
|
// Silent the warning as we want to the error message
|
|
#[allow(clippy::question_mark)]
|
|
if mode::chmod(to, b.mode()).is_err() {
|
|
return Err(InstallError::ChmodFailed(to.to_path_buf()).into());
|
|
}
|
|
|
|
chown_optional_user_group(to, b)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Preserve timestamps on the destination file.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// * `from` - The source file path.
|
|
/// * `to` - The destination file path.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns an empty Result or an error in case of failure.
|
|
///
|
|
fn preserve_timestamps(from: &Path, to: &Path) -> UResult<()> {
|
|
let meta = match fs::metadata(from) {
|
|
Ok(meta) => meta,
|
|
Err(e) => return Err(InstallError::MetadataFailed(e).into()),
|
|
};
|
|
|
|
let modified_time = FileTime::from_last_modification_time(&meta);
|
|
let accessed_time = FileTime::from_last_access_time(&meta);
|
|
|
|
match set_file_times(to, accessed_time, modified_time) {
|
|
Ok(_) => Ok(()),
|
|
Err(e) => {
|
|
show_error!("{}", e);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Copy one file to a new location, changing metadata.
|
|
///
|
|
/// Returns a Result type with the Err variant containing the error message.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// _from_ must exist as a non-directory.
|
|
/// _to_ must be a non-existent file, whose parent directory exists.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// If the copy system call fails, we print a verbose error and return an empty error value.
|
|
///
|
|
fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
|
|
if b.compare && !need_copy(from, to, b)? {
|
|
return Ok(());
|
|
}
|
|
// Declare the path here as we may need it for the verbose output below.
|
|
let backup_path = perform_backup(to, b)?;
|
|
|
|
copy_file(from, to)?;
|
|
|
|
#[cfg(not(windows))]
|
|
if b.strip {
|
|
strip_file(to, b)?;
|
|
}
|
|
|
|
set_ownership_and_permissions(to, b)?;
|
|
|
|
if b.preserve_timestamps {
|
|
preserve_timestamps(from, to)?;
|
|
}
|
|
|
|
if b.verbose {
|
|
print!("{} -> {}", from.quote(), to.quote());
|
|
match backup_path {
|
|
Some(path) => println!(" (backup: {})", path.quote()),
|
|
None => println!(),
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Return true if a file is necessary to copy. This is the case when:
|
|
///
|
|
/// - _from_ or _to_ is nonexistent;
|
|
/// - either file has a sticky bit or set\[ug\]id bit, or the user specified one;
|
|
/// - either file isn't a regular file;
|
|
/// - the sizes of _from_ and _to_ differ;
|
|
/// - _to_'s owner differs from intended; or
|
|
/// - the contents of _from_ and _to_ differ.
|
|
///
|
|
/// # Parameters
|
|
///
|
|
/// _from_ and _to_, if existent, must be non-directories.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// 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),
|
|
};
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
Ok(false)
|
|
}
|