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, show, show_error, show_if_err, uio_error};
use libc::{getegid, geteuid};
use std::error::Error;
use std::fmt::{Debug, Display};
use std::fs;
use std::fs::File;
use std::os::unix::fs::MetadataExt;
#[cfg(unix)]
use std::os::unix::prelude::OsStrExt;
use std::path::{Path, PathBuf, MAIN_SEPARATOR};
use std::process;
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: String,
group: String,
verbose: bool,
preserve_timestamps: bool,
compare: bool,
strip: bool,
strip_program: String,
create_leading: bool,
target_dir: Option<String>,
}
#[derive(Debug)]
enum InstallError {
Unimplemented(String),
DirNeedsArg(),
CreateDirFailed(PathBuf, std::io::Error),
ChmodFailed(PathBuf),
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),
OmittingDirectory(PathBuf),
}
impl UError for InstallError {
fn code(&self) -> i32 {
match self {
Self::Unimplemented(_) => 2,
_ => 1,
}
}
fn usage(&self) -> bool {
false
}
}
impl Error for InstallError {}
impl Display for InstallError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unimplemented(opt) => write!(f, "Unimplemented feature: {}", opt),
Self::DirNeedsArg() => {
write!(
f,
"{} with -d requires at least one argument.",
uucore::util_name()
)
}
Self::CreateDirFailed(dir, e) => {
Display::fmt(&uio_error!(e, "failed to create {}", dir.quote()), f)
}
Self::ChmodFailed(file) => write!(f, "failed to chmod {}", file.quote()),
Self::InvalidTarget(target) => write!(
f,
"invalid target {}: No such file or directory",
target.quote()
),
Self::TargetDirIsntDir(target) => {
write!(f, "target {} is not a directory", target.quote())
}
Self::BackupFailed(from, to, e) => Display::fmt(
&uio_error!(e, "cannot backup {} to {}", from.quote(), to.quote()),
f,
),
Self::InstallFailed(from, to, e) => Display::fmt(
&uio_error!(e, "cannot install {} to {}", from.quote(), to.quote()),
f,
),
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::OmittingDirectory(dir) => write!(f, "omitting directory {}", dir.quote()),
}
}
}
#[derive(Clone, Eq, PartialEq)]
pub enum MainFunction {
Directory,
Standard,
}
impl Behavior {
pub fn mode(&self) -> u32 {
match self.specified_mode {
Some(x) => x,
None => DEFAULT_MODE,
}
}
}
static ABOUT: &str = "Copy SOURCE to DEST or multiple SOURCE(s) to the existing
DIRECTORY, while setting permission modes and owner/group";
const USAGE: &str = "{} [OPTION]... [FILE]...";
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";
#[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(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(
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(
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(
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(
Arg::new(OPT_PRESERVE_CONTEXT)
.short('P')
.long(OPT_PRESERVE_CONTEXT)
.help("(unimplemented) preserve security context")
.action(ArgAction::SetTrue),
)
.arg(
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),
)
}
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(())
}
}
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)
.map(|d| d.to_owned());
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());
}
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(),
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,
})
}
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 !path.exists() {
let path_to_create = dir_strip_dot_for_creation(path);
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() {
uucore::error::set_exit_code(1);
continue;
}
}
Ok(())
}
}
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()) }
#[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()
}
fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
if paths.is_empty() {
return Err(UUsageError::new(1, "missing file operand"));
}
let target: PathBuf = if let Some(path) = &b.target_dir {
path.into()
} else {
let last_path: PathBuf = paths.pop().unwrap().into();
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 {
let to_create: Option<&Path> = if b.target_dir.is_some() {
Some(target.as_path())
} else if !(sources.len() > 1 || is_potential_directory_path(&target)) {
target.parent()
} else {
None
};
if let Some(to_create) = to_create {
if !to_create.exists() {
if b.verbose {
let mut result = PathBuf::new();
for part in to_create.components() {
result.push(part.as_os_str());
if !result.is_dir() {
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());
}
#[allow(clippy::question_mark)]
if mode::chmod(to_create, b.mode()).is_err() {
return Err(InstallError::ChmodFailed(to_create.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.to_path_buf()).into());
}
if target.is_file() || is_new_file_path(&target) {
copy(source, &target, b)
} else {
Err(InstallError::InvalidTarget(target).into())
}
}
}
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.iter() {
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.to_path_buf());
show!(err);
continue;
}
let mut targetpath = target_dir.to_path_buf();
let filename = sourcepath.components().last().unwrap();
targetpath.push(filename);
show_if_err!(copy(sourcepath, &targetpath, b));
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
if b.compare && !need_copy(from, to, b)? {
return Ok(());
}
let mut backup_path = None;
if to.exists() {
if b.verbose {
println!("removed {}", to.quote());
}
backup_path = backup_control::get_backup_path(b.backup_mode, to, &b.suffix);
if let Some(ref backup_path) = backup_path {
if let Err(err) = fs::rename(to, backup_path) {
return Err(InstallError::BackupFailed(
to.to_path_buf(),
backup_path.to_path_buf(),
err,
)
.into());
}
}
}
if from.as_os_str() == "/dev/null" {
if let Err(err) = File::create(to) {
return Err(
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(),
);
}
} else if let Err(err) = fs::copy(from, to) {
return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into());
}
if b.strip && cfg!(not(windows)) {
match process::Command::new(&b.strip_program).arg(to).output() {
Ok(o) => {
if !o.status.success() {
let _ = fs::remove_file(to);
return Err(InstallError::StripProgramFailed(
String::from_utf8(o.stderr).unwrap_or_default(),
)
.into());
}
}
Err(e) => {
let _ = fs::remove_file(to);
return Err(InstallError::StripProgramFailed(e.to_string()).into());
}
}
}
#[allow(clippy::question_mark)]
if mode::chmod(to, b.mode()).is_err() {
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),
}
}
if b.preserve_timestamps {
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(_) => {}
Err(e) => show_error!("{}", e),
}
}
if b.verbose {
print!("{} -> {}", from.quote(), to.quote());
match backup_path {
Some(path) => println!(" (backup: {})", path.quote()),
None => println!(),
}
}
Ok(())
}
fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult<bool> {
let from_meta = match fs::metadata(from) {
Ok(meta) => meta,
Err(_) => return Ok(true),
};
let to_meta = match fs::metadata(to) {
Ok(meta) => meta,
Err(_) => return Ok(true),
};
let extra_mode: u32 = 0o7000;
let all_modes: u32 = 0o7777;
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);
}
if b.mode() != to_meta.mode() & all_modes {
return Ok(true);
}
if !from_meta.is_file() || !to_meta.is_file() {
return Ok(true);
}
if from_meta.len() != to_meta.len() {
return Ok(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 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()),
};
if group_id != to_meta.gid() {
return Ok(true);
}
} else {
#[cfg(not(target_os = "windows"))]
unsafe {
if to_meta.uid() != geteuid() || to_meta.gid() != getegid() {
return Ok(true);
}
}
}
if !diff(from.to_str().unwrap(), to.to_str().unwrap()) {
return Ok(true);
}
Ok(false)
}