diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl index fdc1726c442..0a4037ecc4e 100644 --- a/src/uu/install/locales/en-US.ftl +++ b/src/uu/install/locales/en-US.ftl @@ -39,6 +39,7 @@ install-error-invalid-user = invalid user: { $user } install-error-invalid-group = invalid group: { $group } install-error-omitting-directory = omitting directory { $path } install-error-not-a-directory = failed to access { $path }: Not a directory +install-error-target-dir-access = failed to access { $path }: { $error } install-error-existing-file-not-directory = cannot create directory { $path }: File exists install-error-override-directory-failed = cannot overwrite directory { $dir } with non-directory { $file } install-error-same-file = { $file1 } and { $file2 } are the same file diff --git a/src/uu/install/locales/fr-FR.ftl b/src/uu/install/locales/fr-FR.ftl index 6423148c95d..70ae931697b 100644 --- a/src/uu/install/locales/fr-FR.ftl +++ b/src/uu/install/locales/fr-FR.ftl @@ -39,6 +39,7 @@ install-error-invalid-user = utilisateur invalide : { $user } install-error-invalid-group = groupe invalide : { $group } install-error-omitting-directory = omission du répertoire { $path } install-error-not-a-directory = échec de l'accès à { $path } : N'est pas un répertoire +install-error-target-dir-access = échec de l'accès à { $path } : { $error } install-error-existing-file-not-directory = impossible de créer le répertoire { $path } : Le fichier existe install-error-override-directory-failed = impossible d'écraser le répertoire { $dir } avec un non-répertoire { $file } install-error-same-file = { $file1 } et { $file2 } sont le même fichier diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 8a2e9957265..5fc9b146f49 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -117,6 +117,9 @@ enum InstallError { #[error("{}", translate!("install-error-not-a-directory", "path" => .0.quote()))] NotADirectory(PathBuf), + #[error("{}", translate!("install-error-target-dir-access", "path" => .0.quote(), "error" => strip_errno(.1)))] + TargetDirAccess(PathBuf, #[source] std::io::Error), + #[error("{}", translate!("install-error-existing-file-not-directory", "path" => .0.quote()))] ExistingFileNotADirectory(PathBuf), @@ -759,7 +762,9 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { } } - if sources.len() > 1 { + // A `-t` target is always the directory to install into, never the file + // to install as, even with a single source. + if sources.len() > 1 || b.target_dir.is_some() { copy_files_into_dir(sources, &target, b) } else { let source = sources.first().unwrap(); @@ -835,6 +840,13 @@ fn metadata_for_source(path: &Path) -> UResult { /// fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UResult<()> { if !target_dir.is_dir() { + 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()); } let mut installed_destinations: HashSet = HashSet::with_capacity(files.len()); diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 6832c6edbdb..b7cfe1beb53 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -2203,6 +2203,32 @@ fn test_target_file_ends_with_slash() { .stderr_contains("failed to access 'dir/target_file/': Not a directory"); } +/// Without `-D`, a `-t` target that is not a directory is refused, even with a +/// single source, rather than installed over or created as a file. +#[test] +fn test_install_target_dir_not_a_directory() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("source", "new"); + at.write("regular", "old"); + at.symlink_file("nowhere", "dangling"); + + for (target, reason) in [ + ("regular", "Not a directory"), + ("dangling", "No such file or directory"), + ("missing", "No such file or directory"), + ] { + scene + .ucmd() + .args(&["-t", target, "source"]) + .fails() + .stderr_only(format!("install: failed to access '{target}': {reason}\n")); + } + assert_eq!(at.read("regular"), "old"); + assert!(at.is_symlink("dangling")); + assert!(!at.file_exists("missing")); +} + #[test] fn test_install_root_combined() { let ts = TestScenario::new(util_name!());