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
4 changes: 3 additions & 1 deletion src/uu/install/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ install-error-chmod-failed = failed to chmod { $path }
install-error-chmod-failed-detailed = { $path }: chmod failed with error { $error }
install-error-chown-failed = failed to chown { $path }: { $error }
install-error-invalid-target = invalid target { $path }: No such file or directory
install-error-target-not-dir = target { $path } is not a directory
install-error-target-not-dir = target { $path }: { $error }
install-error-target-stat = cannot stat { $path }: { $error }
install-error-create-regular-file = cannot create regular file { $path }: { $error }
install-error-backup-failed = cannot backup { $from }: { $error }
install-error-install-failed = cannot install { $from } to { $to }: { $error }
install-error-strip-failed = strip program failed: { $error }
Expand Down
2 changes: 1 addition & 1 deletion src/uu/install/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ install-error-chmod-failed = échec du chmod { $path }
install-error-chmod-failed-detailed = { $path } : échec du chmod avec l'erreur { $error }
install-error-chown-failed = échec du chown { $path } : { $error }
install-error-invalid-target = cible invalide { $path } : Aucun fichier ou répertoire de ce type
install-error-target-not-dir = la cible { $path } n'est pas un répertoire
install-error-target-not-dir = la cible { $path } : { $error }
install-error-backup-failed = impossible de sauvegarder { $from } : { $error }
install-error-install-failed = impossible d'installer { $from } vers { $to }: { $error }
install-error-strip-failed = échec du programme strip : { $error }
Expand Down
47 changes: 37 additions & 10 deletions src/uu/install/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,14 @@ enum InstallError {
#[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-target-not-dir", "path" => .0.quote(), "error" => strip_errno(.1)))]
TargetDirIsntDir(PathBuf, #[source] std::io::Error),

#[error("{}", translate!("install-error-target-stat", "path" => .0.quote(), "error" => strip_errno(.1)))]
TargetStatFailed(PathBuf, #[source] std::io::Error),

#[error("{}", translate!("install-error-create-regular-file", "path" => .0.quote(), "error" => strip_errno(.1)))]
CreateRegularFileFailed(PathBuf, #[source] std::io::Error),

#[error("{}", translate!("install-error-backup-failed", "from" => .0.quote(), "error" => strip_errno(.1)))]
BackupFailed(PathBuf, #[source] std::io::Error),
Expand Down Expand Up @@ -660,9 +666,17 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
None
};

// If -t is used, check if target exists as a file before trying to create directories
if b.target_dir.is_some() && target.exists() && !target.is_dir() {
return Err(InstallError::NotADirectory(target).into());
// Reject inaccessible -t targets before trying to create directories.
if b.target_dir.is_some() {
match metadata(&target) {
Ok(meta) if !meta.is_dir() => {
return Err(InstallError::NotADirectory(target).into());
}
Err(e) if e.kind() != std::io::ErrorKind::NotFound => {
return Err(InstallError::TargetDirAccess(target, e).into());
}
_ => {}
}
}

if let Some(to_create) = to_create {
Expand Down Expand Up @@ -840,14 +854,27 @@ fn metadata_for_source(path: &Path) -> UResult<fs::Metadata> {
///
fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UResult<()> {
if !target_dir.is_dir() {
let reason = metadata(target_dir)
.err()
.unwrap_or_else(|| rustix::io::Errno::NOTDIR.into());
if b.target_dir.is_some() {
// GNU names why the `-t` directory cannot be used.
let reason = metadata(target_dir)
.err()
.unwrap_or_else(|| rustix::io::Errno::NOTDIR.into());
return Err(InstallError::TargetDirAccess(target_dir.to_path_buf(), reason).into());
}
return Err(InstallError::TargetDirIsntDir(target_dir.to_path_buf()).into());
if files.len() > 1 {
return Err(InstallError::TargetDirIsntDir(target_dir.to_path_buf(), reason).into());
}
if reason.kind() != std::io::ErrorKind::NotFound {
return Err(InstallError::TargetStatFailed(target_dir.to_path_buf(), reason).into());
}
// A trailing slash requires a directory even when its parent exists.
let create_error = if is_new_file_path(target_dir) {
rustix::io::Errno::NOTDIR.into()
} else {
reason
};
return Err(
InstallError::CreateRegularFileFailed(target_dir.to_path_buf(), create_error).into(),
);
}
let mut installed_destinations: HashSet<PathBuf> = HashSet::with_capacity(files.len());
for sourcepath in files {
Expand Down
57 changes: 52 additions & 5 deletions tests/by-util/test_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ fn test_install_failing_not_dir() {
.arg(file2)
.arg(file3)
.fails()
.stderr_contains("not a directory");
.stderr_contains("Not a directory");
}

#[test]
Expand Down Expand Up @@ -685,7 +685,7 @@ fn test_install_failing_omitting_directory() {
.arg(dir1)
.arg(no_dir2)
.fails()
.stderr_contains("is not a directory");
.stderr_contains("No such file or directory");

// file1 will be copied before install fails on dir1
scene
Expand Down Expand Up @@ -1107,7 +1107,7 @@ fn test_install_creating_leading_dirs_with_single_source_and_target_dir() {
.arg(source1)
.arg(at.plus(target_dir))
.fails()
.stderr_contains("missing_target_dir/' is not a directory");
.stderr_contains("missing_target_dir/': Not a directory");

assert!(!at.dir_exists(target_dir));

Expand Down Expand Up @@ -1143,7 +1143,7 @@ fn test_install_creating_leading_dirs_with_multiple_sources_and_target_dir() {
.arg(source2)
.arg(at.plus(target_dir))
.fails()
.stderr_contains("missing_target_dir' is not a directory");
.stderr_contains("missing_target_dir': No such file or directory");

assert!(!at.dir_exists(target_dir));

Expand Down Expand Up @@ -2243,6 +2243,53 @@ fn test_install_target_dir_not_a_directory() {
assert!(!at.file_exists("missing"));
}

#[test]
fn test_install_unusable_target_diagnostics() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.write("source", "new");
at.write("other", "other source");
at.write("regular", "old");

for (args, message) in [
(
vec!["source", "other", "regular"],
"target 'regular': Not a directory",
),
(
vec!["source", "other", "missing"],
"target 'missing': No such file or directory",
),
(
vec!["source", "regular/"],
"cannot stat 'regular/': Not a directory",
),
(
vec!["source", "missing/"],
"cannot create regular file 'missing/': Not a directory",
),
(
vec!["source", "missing/sub/"],
"cannot create regular file 'missing/sub/': No such file or directory",
),
(
vec!["-D", "-t", "regular/sub", "source"],
"failed to access 'regular/sub': Not a directory",
),
] {
scene
.ucmd()
.args(&args)
.fails_with_code(1)
.stderr_only(format!("install: {message}\n"));
}
assert_eq!(at.read("source"), "new");
assert_eq!(at.read("other"), "other source");
assert_eq!(at.read("regular"), "old");
assert!(!at.dir_exists("missing"));
assert!(!at.file_exists("missing"));
}

#[test]
fn test_install_root_combined() {
let ts = TestScenario::new(util_name!());
Expand Down Expand Up @@ -2435,7 +2482,7 @@ fn test_install_no_target_directory_creating_leading_dirs_with_single_source_and
.arg(source1)
.arg(at.plus(target_dir))
.fails()
.stderr_contains("missing_target_dir/' is not a directory");
.stderr_contains("missing_target_dir/': Not a directory");

assert!(!at.dir_exists(target_dir));
}
Expand Down
Loading