mod mode;
use clap::{Arg, ArgAction, ArgMatches, Command};
use file_diff::diff;
use filetime::{FileTime, set_file_times};
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use selinux::SecurityContext;
use std::ffi::OsString;
use std::fmt::Debug;
use std::fs::{self, metadata};
use std::fs::{File, OpenOptions};
use std::io::{Write, stdout};
use std::path::{MAIN_SEPARATOR, Path, PathBuf};
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::perms::{Verbosity, VerbosityLevel, wrap_chown};
use uucore::process::{getegid, geteuid};
#[cfg(unix)]
use uucore::safe_traversal::{DirFd, SymlinkBehavior, create_dir_all_safe};
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use uucore::selinux::{
SeLinuxError, contexts_differ, get_selinux_security_context, is_selinux_enabled,
selinux_error_description, set_selinux_security_context,
};
use uucore::translate;
use uucore::{format_usage, show, show_error, show_if_err};
#[cfg(unix)]
use std::os::unix::fs::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>,
no_target_dir: bool,
preserve_context: bool,
context: Option<String>,
default_context: bool,
unprivileged: bool,
}
#[derive(Error, Debug)]
enum InstallError {
#[error("{}", translate!("install-error-dir-needs-arg", "util_name" => uucore::util_name()))]
DirNeedsArg,
#[error("{}", translate!("install-error-create-dir-failed", "path" => .0.quote()))]
CreateDirFailed(PathBuf, #[source] std::io::Error),
#[error("{}", translate!("install-error-chmod-failed", "path" => .0.quote()))]
ChmodFailed(PathBuf),
#[error("{}", translate!("install-error-chown-failed", "path" => .0.quote(), "error" => .1.clone()))]
ChownFailed(PathBuf, String),
#[error("{}", translate!("install-error-invalid-target", "path" => .0.quote()))]
InvalidTarget(PathBuf),
#[error("{}", translate!("install-error-target-not-dir", "path" => .0.quote()))]
TargetDirIsntDir(PathBuf),
#[error("{}", translate!("install-error-backup-failed", "from" => .0.quote(), "to" => .1.quote()))]
BackupFailed(PathBuf, PathBuf, #[source] std::io::Error),
#[error("{}", translate!("install-error-install-failed", "from" => .0.quote(), "to" => .1.quote(), "error" => .2.clone()))]
InstallFailed(PathBuf, PathBuf, String),
#[error("{}", translate!("install-error-strip-failed", "error" => .0.clone()))]
StripProgramFailed(String),
#[error("{}", translate!("install-error-metadata-failed"))]
MetadataFailed(#[source] std::io::Error),
#[error("{}", translate!("install-error-invalid-user", "user" => .0.quote()))]
InvalidUser(String),
#[error("{}", translate!("install-error-invalid-group", "group" => .0.quote()))]
InvalidGroup(String),
#[error("{}", translate!("install-error-omitting-directory", "path" => .0.quote()))]
OmittingDirectory(PathBuf),
#[error("{}", translate!("install-error-not-a-directory", "path" => .0.quote()))]
NotADirectory(PathBuf),
#[error("{}", translate!("install-error-override-directory-failed", "dir" => .0.quote(), "file" => .1.quote()))]
OverrideDirectoryFailed(PathBuf, PathBuf),
#[error("{}", translate!("install-error-same-file", "file1" => .0.quote(), "file2" => .1.quote()))]
SameFile(PathBuf, PathBuf),
#[error("{}", translate!("install-error-extra-operand", "operand" => .0.quote(), "usage" => .1.clone()))]
ExtraOperand(OsString, String),
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
#[error("{}", .0)]
SelinuxContextFailed(String),
}
impl UError for InstallError {
fn code(&self) -> i32 {
1
}
fn usage(&self) -> bool {
false
}
}
#[derive(Clone, Eq, PartialEq)]
pub enum MainFunction {
Directory,
Standard,
}
impl Behavior {
pub fn mode(&self) -> u32 {
self.specified_mode.unwrap_or(DEFAULT_MODE)
}
}
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 OPT_DEFAULT_CONTEXT: &str = "default-context";
static OPT_UNPRIVILEGED: &str = "unprivileged";
static ARG_FILES: &str = "files";
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
let paths: Vec<OsString> = matches
.get_many::<OsString>(ARG_FILES)
.map(|v| v.cloned().collect())
.unwrap_or_default();
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!())
.help_template(uucore::localized_help_template(uucore::util_name()))
.about(translate!("install-about"))
.override_usage(format_usage(&translate!("install-usage")))
.infer_long_args(true)
.args_override_self(true)
.arg(backup_control::arguments::backup())
.arg(backup_control::arguments::backup_no_args())
.arg(
Arg::new(OPT_IGNORED)
.short('c')
.help(translate!("install-help-ignored"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_COMPARE)
.short('C')
.long(OPT_COMPARE)
.help(translate!("install-help-compare"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_DIRECTORY)
.short('d')
.long(OPT_DIRECTORY)
.help(translate!("install-help-directory"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_CREATE_LEADING)
.short('D')
.help(translate!("install-help-create-leading"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_GROUP)
.short('g')
.long(OPT_GROUP)
.help(translate!("install-help-group"))
.value_name("GROUP"),
)
.arg(
Arg::new(OPT_MODE)
.short('m')
.long(OPT_MODE)
.help(translate!("install-help-mode"))
.value_name("MODE"),
)
.arg(
Arg::new(OPT_OWNER)
.short('o')
.long(OPT_OWNER)
.help(translate!("install-help-owner"))
.value_name("OWNER")
.value_hint(clap::ValueHint::Username),
)
.arg(
Arg::new(OPT_PRESERVE_TIMESTAMPS)
.short('p')
.long(OPT_PRESERVE_TIMESTAMPS)
.help(translate!("install-help-preserve-timestamps"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_STRIP)
.short('s')
.long(OPT_STRIP)
.help(translate!("install-help-strip"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_STRIP_PROGRAM)
.long(OPT_STRIP_PROGRAM)
.help(translate!("install-help-strip-program"))
.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(translate!("install-help-target-directory"))
.value_name("DIRECTORY")
.value_hint(clap::ValueHint::DirPath),
)
.arg(
Arg::new(OPT_NO_TARGET_DIRECTORY)
.short('T')
.long(OPT_NO_TARGET_DIRECTORY)
.help(translate!("install-help-no-target-directory"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_VERBOSE)
.short('v')
.long(OPT_VERBOSE)
.help(translate!("install-help-verbose"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_PRESERVE_CONTEXT)
.short('P')
.long(OPT_PRESERVE_CONTEXT)
.help(translate!("install-help-preserve-context"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_DEFAULT_CONTEXT)
.short('Z')
.help(translate!("install-help-default-context"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(OPT_CONTEXT)
.long(OPT_CONTEXT)
.help(translate!("install-help-context"))
.value_name("CONTEXT")
.value_parser(clap::value_parser!(String))
.num_args(0..=1),
)
.arg(
Arg::new(ARG_FILES)
.action(ArgAction::Append)
.num_args(1..)
.value_hint(clap::ValueHint::AnyPath)
.value_parser(clap::value_parser!(OsString)),
)
.arg(
Arg::new(OPT_UNPRIVILEGED)
.short('U')
.long(OPT_UNPRIVILEGED)
.help(translate!("install-help-unprivileged"))
.action(ArgAction::SetTrue),
)
}
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(uucore::mode::parse(x, considering_dir, 0).map_err(|err| {
show_error!(
"{}",
translate!("install-error-invalid-mode", "error" => 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 no_target_dir = matches.get_flag(OPT_NO_TARGET_DIRECTORY);
if target_dir.is_some() && no_target_dir {
show_error!("{}", translate!("install-error-mutually-exclusive-target"));
return Err(1.into());
}
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!(
"{}",
translate!("install-error-mutually-exclusive-compare-preserve")
);
return Err(1.into());
}
if compare && strip {
show_error!(
"{}",
translate!("install-error-mutually-exclusive-compare-strip")
);
return Err(1.into());
}
if compare {
if let Some(mode) = specified_mode {
let non_permission_bits = 0o7000; if mode & non_permission_bits != 0 {
show_error!("{}", translate!("install-warning-compare-ignored"));
}
}
}
let owner = matches
.get_one::<String>(OPT_OWNER)
.map_or("", |s| s.as_str())
.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_or("", |s| s.as_str())
.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()),
}
};
let context = matches.get_one::<String>(OPT_CONTEXT).cloned();
let default_context = matches.get_flag(OPT_DEFAULT_CONTEXT);
let unprivileged = matches.get_flag(OPT_UNPRIVILEGED);
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_or(DEFAULT_STRIP_PROGRAM, |s| s.as_str()),
),
create_leading: matches.get_flag(OPT_CREATE_LEADING),
target_dir,
no_target_dir,
preserve_context: matches.get_flag(OPT_PRESERVE_CONTEXT),
context,
default_context,
unprivileged,
})
}
fn directory(paths: &[OsString], 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(|| translate!("install-error-create-dir-failed", "path" => path_to_create.as_path().quote()))
{
show!(e);
continue;
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(path_to_create.as_path(), context);
}
if b.verbose {
writeln!(
stdout(),
"{}",
translate!("install-verbose-creating-directory", "path" => path_to_create.quote())
)?;
}
}
if mode::chmod(path, b.mode()).is_err() {
uucore::error::set_exit_code(1);
continue;
}
if !b.unprivileged {
show_if_err!(chown_optional_user_group(path, b));
#[cfg(all(feature = "selinux", target_os = "linux"))]
if b.default_context {
show_if_err!(set_selinux_default_context(path));
} else if b.context.is_some() {
let context = get_context_for_selinux(b);
show_if_err!(set_selinux_security_context(path, context));
}
}
}
Ok(())
}
}
fn is_new_file_path(path: &Path) -> bool {
!path.exists()
&& (path.parent().is_none_or(Path::is_dir) || 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()
}
#[allow(clippy::cognitive_complexity)]
fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
if paths.is_empty() {
return Err(UUsageError::new(
1,
translate!("install-error-missing-file-operand"),
));
}
if b.no_target_dir && paths.len() > 2 {
return Err(InstallError::ExtraOperand(
paths[2].clone(),
format_usage(&translate!("install-usage")),
)
.into());
}
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,
translate!("install-error-missing-destination-operand", "path" => last_path.quote()),
));
}
last_path
};
let sources = &paths.iter().map(PathBuf::from).collect::<Vec<_>>();
#[cfg(unix)]
let mut target_parent_fd: Option<DirFd> = None;
#[cfg(unix)]
let mut target_filename: Option<OsString> = None;
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 b.target_dir.is_some() && target.exists() && !target.is_dir() {
return Err(InstallError::NotADirectory(target.clone()).into());
}
if let Some(to_create) = to_create {
let to_create_original = to_create;
let to_create_owned;
let to_create = match uucore::os_str_as_bytes(to_create.as_os_str()) {
Ok(path_bytes) if path_bytes.ends_with(b"/") => {
let mut trimmed_bytes = path_bytes;
while trimmed_bytes.ends_with(b"/") {
trimmed_bytes = &trimmed_bytes[..trimmed_bytes.len() - 1];
}
let trimmed_os_str = std::ffi::OsStr::from_bytes(trimmed_bytes);
to_create_owned = PathBuf::from(trimmed_os_str);
to_create_owned.as_path()
}
_ => to_create,
};
let dir_exists = if to_create.exists() {
fs::symlink_metadata(to_create)
.is_ok_and(|m| m.is_dir() && !m.file_type().is_symlink())
} else {
false
};
if dir_exists {
#[cfg(unix)]
{
if b.target_dir.is_none()
&& sources.len() == 1
&& !is_potential_directory_path(&target)
{
if let Ok(dir_fd) = DirFd::open(to_create, SymlinkBehavior::NoFollow) {
if let Some(filename) = target.file_name() {
target_parent_fd = Some(dir_fd);
target_filename = Some(filename.to_os_string());
}
}
}
}
} else {
if b.verbose {
let mut result = PathBuf::new();
for part in to_create.components() {
result.push(part.as_os_str());
if !result.is_dir() {
writeln!(
stdout(),
"{}",
translate!("install-verbose-creating-directory-step", "path" => result.quote())
)?;
}
}
}
#[cfg(unix)]
{
match create_dir_all_safe(to_create, DEFAULT_MODE) {
Ok(dir_fd) => {
if b.target_dir.is_none()
&& sources.len() == 1
&& !is_potential_directory_path(&target)
{
if let Some(filename) = target.file_name() {
target_parent_fd = Some(dir_fd);
target_filename = Some(filename.to_os_string());
}
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(to_create, context);
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::AlreadyExists
&& to_create.exists()
&& !to_create.is_dir()
{
return Err(InstallError::NotADirectory(
to_create_original.to_path_buf(),
)
.into());
}
return Err(InstallError::CreateDirFailed(
to_create_original.to_path_buf(),
e,
)
.into());
}
}
}
#[cfg(not(unix))]
{
if let Err(e) = fs::create_dir_all(to_create) {
return Err(
InstallError::CreateDirFailed(to_create.to_path_buf(), e).into()
);
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
if should_set_selinux_context(b) {
let context = get_context_for_selinux(b);
set_selinux_context_for_directories_install(to_create, context);
}
}
}
}
}
if sources.len() > 1 {
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 b.no_target_dir && target.is_dir() {
return Err(
InstallError::OverrideDirectoryFailed(target.clone(), source.clone()).into(),
);
}
if is_potential_directory_path(&target) {
return copy_files_into_dir(sources, &target, b);
}
if target.is_file() || is_new_file_path(&target) {
#[cfg(unix)]
if let (Some(ref parent_fd), Some(ref filename)) = (target_parent_fd, target_filename) {
if b.compare && !need_copy(source, &target, b) {
return Ok(());
}
let backup_path = perform_backup(&target, b)?;
if let Err(e) = parent_fd.unlink_at(filename.as_os_str(), false) {
if e.kind() != std::io::ErrorKind::NotFound {
show_error!(
"{}",
translate!("install-error-failed-to-remove", "path" => target.quote(), "error" => format!("{e:?}"))
);
}
}
copy_file_safe(source, parent_fd, filename.as_os_str())?;
finalize_installed_file(source, &target, b, backup_path)
} else {
copy(source, &target, b)
}
#[cfg(not(unix))]
{
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 {
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));
}
Ok(())
}
fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> {
let verbosity = Verbosity {
groups_only: b.owner_id.is_none(),
level: VerbosityLevel::Normal,
};
let (owner_id, group_id) = if b.owner_id.is_some() || b.group_id.is_some() {
(b.owner_id, b.group_id)
} else {
return Ok(());
};
let meta = match 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() => writeln!(stdout(), "chown: {msg}")?,
Ok(_) => {}
Err(e) => return Err(InstallError::ChownFailed(path.to_path_buf(), e).into()),
}
Ok(())
}
fn perform_backup(to: &Path, b: &Behavior) -> UResult<Option<PathBuf>> {
if to.exists() {
if b.verbose {
writeln!(
stdout(),
"{}",
translate!("install-verbose-removed", "path" => to.quote())
)?;
}
let backup_path = backup_control::get_backup_path(b.backup_mode, to, &b.suffix);
if let Some(ref backup_path) = backup_path {
fs::rename(to, backup_path).map_err(|err| {
InstallError::BackupFailed(to.to_path_buf(), backup_path.clone(), err)
})?;
}
Ok(backup_path)
} else {
Ok(None)
}
}
#[cfg(unix)]
fn copy_file_safe(from: &Path, to_parent_fd: &DirFd, to_filename: &std::ffi::OsStr) -> UResult<()> {
let from_meta = metadata(from)?;
if let Ok(to_stat) = to_parent_fd.stat_at(to_filename, SymlinkBehavior::Follow) {
#[allow(clippy::unnecessary_cast)]
if from_meta.dev() == to_stat.st_dev as u64 && from_meta.ino() == to_stat.st_ino as u64 {
return Err(
InstallError::SameFile(from.to_path_buf(), PathBuf::from(to_filename)).into(),
);
}
}
let mut src = File::open(from)?;
let mut dst = to_parent_fd.open_file_at(to_filename)?;
copy_stream(&mut src, &mut dst)?;
Ok(())
}
fn copy_file(from: &Path, to: &Path) -> UResult<()> {
use std::os::unix::fs::OpenOptionsExt;
if let Ok(to_abs) = to.canonicalize() {
if from.canonicalize()? == to_abs {
return Err(InstallError::SameFile(from.to_path_buf(), to.to_path_buf()).into());
}
}
if to.is_dir() && !from.is_dir() {
return Err(InstallError::OverrideDirectoryFailed(
to.to_path_buf().clone(),
from.to_path_buf().clone(),
)
.into());
}
if let Err(e) = fs::remove_file(to) {
if e.kind() != std::io::ErrorKind::NotFound {
show_error!(
"{}",
translate!("install-error-failed-to-remove", "path" => to.quote(), "error" => format!("{e:?}"))
);
}
}
let mut handle = File::open(from)?;
let mut dest = OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(to)?;
copy_stream(&mut handle, &mut dest).map_err(|err| {
InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err.to_string())
})?;
Ok(())
}
fn strip_file(to: &Path, b: &Behavior) -> UResult<()> {
let to_str = to.to_string_lossy();
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() {
let _ = fs::remove_file(to);
return Err(InstallError::StripProgramFailed(
translate!("install-error-strip-abnormal", "code" => status.code().unwrap()),
)
.into());
}
}
Err(e) => {
let _ = fs::remove_file(to);
return Err(InstallError::StripProgramFailed(e.to_string()).into());
}
}
Ok(())
}
fn set_ownership_and_permissions(to: &Path, b: &Behavior) -> UResult<()> {
#[allow(clippy::question_mark)]
if mode::chmod(to, b.mode()).is_err() {
return Err(InstallError::ChmodFailed(to.to_path_buf()).into());
}
if !b.unprivileged {
chown_optional_user_group(to, b)?;
}
Ok(())
}
fn preserve_timestamps(from: &Path, to: &Path) -> UResult<()> {
let meta = match 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);
if let Err(e) = set_file_times(to, accessed_time, modified_time) {
show_error!("{e}");
}
Ok(())
}
fn finalize_installed_file(
from: &Path,
to: &Path,
b: &Behavior,
backup_path: Option<PathBuf>,
) -> UResult<()> {
#[cfg(not(windows))]
if b.strip {
strip_file(to, b)?;
}
set_ownership_and_permissions(to, b)?;
if b.preserve_timestamps {
preserve_timestamps(from, to)?;
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
if !b.unprivileged {
if b.preserve_context {
uucore::selinux::preserve_security_context(from, to)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
} else if b.default_context {
set_selinux_default_context(to)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
} else if b.context.is_some() {
let context = get_context_for_selinux(b);
set_selinux_security_context(to, context)
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
}
}
if b.verbose {
write!(
stdout(),
"{}",
translate!("install-verbose-copy", "from" => from.quote(), "to" => to.quote())
)?;
match backup_path {
Some(path) => writeln!(
stdout(),
" {}",
translate!("install-verbose-backup", "backup" => path.quote())
)?,
None => writeln!(stdout())?,
}
}
Ok(())
}
fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
if b.compare && !need_copy(from, to, b) {
return Ok(());
}
let backup_path = perform_backup(to, b)?;
copy_file(from, to)?;
finalize_installed_file(from, to, b, backup_path)
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn get_context_for_selinux(b: &Behavior) -> Option<&String> {
if b.default_context {
None
} else {
b.context.as_ref()
}
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
fn should_set_selinux_context(b: &Behavior) -> bool {
!b.unprivileged && (b.context.is_some() || b.default_context)
}
fn needs_copy_for_ownership(to: &Path, to_meta: &fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
if to_meta.uid() != geteuid() {
return true;
}
let expected_gid = to
.parent()
.and_then(|parent| metadata(parent).ok())
.filter(|parent_meta| parent_meta.mode() & 0o2000 != 0)
.map_or(getegid(), |parent_meta| parent_meta.gid());
to_meta.gid() != expected_gid
}
fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
let Ok(from_meta) = metadata(from) else {
return true;
};
let Ok(to_meta) = metadata(to) else {
return true;
};
if let Ok(to_symlink_meta) = fs::symlink_metadata(to) {
if to_symlink_meta.file_type().is_symlink() {
return true;
}
}
let extra_mode: u32 = 0o7000;
let all_modes: u32 = 0o7777;
if b.mode() & extra_mode != 0
|| from_meta.mode() & extra_mode != 0
|| to_meta.mode() & extra_mode != 0
{
return true;
}
if b.mode() != to_meta.mode() & all_modes {
return true;
}
if !from_meta.is_file() || !to_meta.is_file() {
return true;
}
if from_meta.len() != to_meta.len() {
return true;
}
#[cfg(all(feature = "selinux", target_os = "linux"))]
if !b.unprivileged && b.preserve_context && contexts_differ(from, to) {
return true;
}
if let Some(owner_id) = b.owner_id {
if !b.unprivileged && owner_id != to_meta.uid() {
return true;
}
}
if let Some(group_id) = b.group_id {
if !b.unprivileged && group_id != to_meta.gid() {
return true;
}
} else if !b.unprivileged && needs_copy_for_ownership(to, &to_meta) {
return true;
}
if !diff(&from.to_string_lossy(), &to.to_string_lossy()) {
return true;
}
false
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
pub fn set_selinux_default_context(path: &Path) -> Result<(), SeLinuxError> {
if !is_selinux_enabled() {
return Err(SeLinuxError::SELinuxNotEnabled);
}
match get_default_context_for_path(path) {
Ok(Some(default_ctx)) => {
set_selinux_security_context(path, Some(&default_ctx))
}
Ok(None) | Err(_) => {
SecurityContext::set_default_for_path(path).map_err(|e| {
SeLinuxError::ContextSetFailure(String::new(), selinux_error_description(&e))
})
}
}
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn get_default_context_for_path(path: &Path) -> Result<Option<String>, SeLinuxError> {
if !is_selinux_enabled() {
return Err(SeLinuxError::SELinuxNotEnabled);
}
let mut current_path = path;
loop {
if current_path.exists() {
if let Ok(parent_context) = get_selinux_security_context(current_path, false) {
if !parent_context.is_empty() {
return Ok(Some(derive_context_from_parent(&parent_context)));
}
}
}
if let Some(parent) = current_path.parent() {
if parent == current_path {
break; }
current_path = parent;
} else {
break;
}
if current_path == Path::new("/") || current_path == Path::new("") {
break;
}
}
Ok(None)
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn derive_context_from_parent(parent_context: &str) -> String {
let parts: Vec<&str> = parent_context.split(':').collect();
if parts.len() >= 3 {
let user = parts[0];
let role = parts[1];
let parent_type = parts[2];
let level = if parts.len() > 3 { parts[3] } else { "" };
let derived_type = if parent_type.contains("tmp") {
"user_home_t"
} else {
parent_type
};
if level.is_empty() {
format!("{user}:{role}:{derived_type}")
} else {
format!("{user}:{role}:{derived_type}:{level}")
}
} else {
parent_context.to_string()
}
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn collect_paths_for_context_setting(starting_path: &Path) -> Vec<&Path> {
let mut paths: Vec<&Path> = starting_path
.ancestors()
.take_while(|p| p.exists())
.collect();
paths.reverse();
paths
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
fn set_selinux_context_for_directories(target_path: &Path, context: Option<&String>) {
for path in collect_paths_for_context_setting(target_path) {
show_if_err!(set_selinux_security_context(path, context));
}
}
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
pub fn set_selinux_context_for_directories_install(target_path: &Path, context: Option<&String>) {
if context.is_some() {
set_selinux_context_for_directories(target_path, context);
} else {
for path in collect_paths_for_context_setting(target_path) {
show_if_err!(set_selinux_default_context(path));
}
}
}
#[cfg(test)]
mod tests {
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
use super::derive_context_from_parent;
#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))]
#[test]
fn test_derive_context_from_parent() {
let test_cases = [
(
"unconfined_u:object_r:tmp_t:s0",
"regular_file",
"unconfined_u:object_r:user_home_t:s0",
"tmp_t transformation",
),
(
"unconfined_u:object_r:tmp_t:s0",
"directory",
"unconfined_u:object_r:user_home_t:s0",
"tmp_t directory transformation",
),
(
"unconfined_u:object_r:tmp_t:s0",
"other",
"unconfined_u:object_r:user_home_t:s0",
"tmp_t other file type transformation",
),
(
"unconfined_u:object_r:user_tmp_t:s0",
"regular_file",
"unconfined_u:object_r:user_home_t:s0",
"user_tmp_t transformation",
),
(
"root:object_r:admin_tmp_t:s0",
"directory",
"root:object_r:user_home_t:s0",
"admin_tmp_t transformation",
),
(
"unconfined_u:object_r:user_home_t:s0",
"regular_file",
"unconfined_u:object_r:user_home_t:s0",
"user_home_t preservation",
),
(
"system_u:object_r:bin_t:s0",
"directory",
"system_u:object_r:bin_t:s0",
"bin_t preservation",
),
(
"system_u:object_r:lib_t:s0",
"regular_file",
"system_u:object_r:lib_t:s0",
"lib_t preservation",
),
(
"unconfined_u:object_r:tmp_t",
"regular_file",
"unconfined_u:object_r:user_home_t",
"tmp_t no level transformation",
),
(
"unconfined_u:object_r:user_home_t",
"directory",
"unconfined_u:object_r:user_home_t",
"user_home_t no level preservation",
),
(
"root:system_r:tmp_t:s0",
"regular_file",
"root:system_r:user_home_t:s0",
"root user tmp transformation",
),
(
"staff_u:staff_r:tmp_t:s0-s0:c0.c1023",
"directory",
"staff_u:staff_r:user_home_t:s0-s0",
"complex MLS level truncation with tmp transformation",
),
(
"unconfined_u:unconfined_r:tmp_t:s0-s0:c0.c1023",
"regular_file",
"unconfined_u:unconfined_r:user_home_t:s0-s0",
"user session tmp context transformation",
),
(
"system_u:system_r:tmp_t:s0",
"directory",
"system_u:system_r:user_home_t:s0",
"system tmp context transformation",
),
(
"unconfined_u:unconfined_r:user_home_t:s0",
"regular_file",
"unconfined_u:unconfined_r:user_home_t:s0",
"already correct home context",
),
(
"invalid",
"regular_file",
"invalid",
"invalid context passthrough",
),
("", "regular_file", "", "empty context passthrough"),
(
"user:role",
"regular_file",
"user:role",
"insufficient parts passthrough",
),
(
"user:role:type:level:extra:parts",
"regular_file",
"user:role:type:level",
"extra parts truncation",
),
(
"user:role:tmp_t:s0:extra",
"regular_file",
"user:role:user_home_t:s0",
"tmp transformation with extra parts",
),
];
for (input_context, file_type, expected_output, description) in test_cases {
let result = derive_context_from_parent(input_context);
assert_eq!(
result, expected_output,
"Failed test case: {description} - Input: '{input_context}', File type: '{file_type}', Expected: '{expected_output}', Got: '{result}'"
);
}
let tmp_context = "unconfined_u:object_r:tmp_t:s0";
let expected = "unconfined_u:object_r:user_home_t:s0";
let file_types = ["regular_file", "directory", "other", "custom_type"];
for file_type in file_types {
let result = derive_context_from_parent(tmp_context);
assert_eq!(
result, expected,
"File type independence test failed - file_type: '{file_type}', Expected: '{expected}', Got: '{result}'"
);
}
}
}