From 4b02a38be5ed4ee604a78a48fa9345320587dab3 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Wed, 19 Nov 2025 20:36:26 -0300 Subject: [PATCH 1/4] ln: add error handling for hard link creation on directories --- src/uu/ln/locales/en-US.ftl | 1 + src/uu/ln/locales/fr-FR.ftl | 1 + src/uu/ln/src/ln.rs | 8 ++++++++ tests/by-util/test_ln.rs | 16 ++++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/src/uu/ln/locales/en-US.ftl b/src/uu/ln/locales/en-US.ftl index 85315070d09..54755c7dc14 100644 --- a/src/uu/ln/locales/en-US.ftl +++ b/src/uu/ln/locales/en-US.ftl @@ -35,4 +35,5 @@ ln-prompt-replace = replace {$file}? ln-cannot-backup = cannot backup {$file} ln-failed-to-access = failed to access {$file} ln-failed-to-create-hard-link = failed to create hard link {$source} => {$dest} +ln-failed-to-create-hard-link-dir = {$source}: hard link not allowed for directory ln-backup = backup: {$backup} diff --git a/src/uu/ln/locales/fr-FR.ftl b/src/uu/ln/locales/fr-FR.ftl index 483f15c9255..f037528c672 100644 --- a/src/uu/ln/locales/fr-FR.ftl +++ b/src/uu/ln/locales/fr-FR.ftl @@ -36,4 +36,5 @@ ln-prompt-replace = remplacer {$file} ? ln-cannot-backup = impossible de sauvegarder {$file} ln-failed-to-access = échec d'accès à {$file} ln-failed-to-create-hard-link = échec de création du lien physique {$source} => {$dest} +ln-failed-to-create-hard-link-dir = {$source} : lien physique non autorisé pour un répertoire ln-backup = sauvegarde : {$backup} diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index a3fde8f4add..e287dfa9773 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -62,6 +62,9 @@ enum LnError { #[error("{}", translate!("ln-error-extra-operand", "operand" => _0.to_string_lossy(), "program" => _1.clone()))] ExtraOperand(OsString, String), + + #[error("{}", translate!("ln-failed-to-create-hard-link-dir", "source" => _0.to_string_lossy()))] + FailedToCreateHardLinkDir(PathBuf), } impl UError for LnError { @@ -431,6 +434,11 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { if settings.symbolic { symlink(&source, dst)?; } else { + // Cannot create hard link to a directory + if src.is_dir() { + return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); + } + let p = if settings.logical && source.is_symlink() { // if we want to have an hard link, // source is a symlink and -L is passed diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index d5a7bbfbb7c..bfcbc4e71a7 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -934,3 +934,19 @@ fn test_ln_non_utf8_paths() { let symlink_path = at.plus(symlink_name); assert!(symlink_path.is_symlink()); } + +#[test] +fn test_ln_hard_link_dir() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.mkdir("dir"); + + let result = scene.ucmd().args(&["dir", "dir_link"]).fails(); + + assert!( + result + .stderr_str() + .contains("hard link not allowed for directory") + ); +} From 0a5441fb0667e841ed6e6d18545c5d6c85966e2f Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 20 Nov 2025 09:12:39 -0300 Subject: [PATCH 2/4] ln: simplify hard link directory test assertion --- tests/by-util/test_ln.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index bfcbc4e71a7..bc103a62980 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -942,11 +942,9 @@ fn test_ln_hard_link_dir() { at.mkdir("dir"); - let result = scene.ucmd().args(&["dir", "dir_link"]).fails(); - - assert!( - result - .stderr_str() - .contains("hard link not allowed for directory") - ); + scene + .ucmd() + .args(&["dir", "dir_link"]) + .fails() + .stderr_contains("hard link not allowed for directory"); } From 2b8e67bee2ebf9285d6370b606e2a8b17008e52e Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 20 Nov 2025 10:56:55 -0300 Subject: [PATCH 3/4] ln: Allow hard links to soft links even when they poin to a directory --- src/uu/ln/src/ln.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index e287dfa9773..094106383b1 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -434,8 +434,9 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { if settings.symbolic { symlink(&source, dst)?; } else { - // Cannot create hard link to a directory - if src.is_dir() { + // Cannot create hard link to a directory directly + // We can however create hard link to a symlink that points to a directory, so long as -L is not passed + if src.is_dir() && (!src.is_symlink() || settings.logical) { return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); } From e1f2ba1c18468f9bfccf81a5ef11cb6bc33a7b72 Mon Sep 17 00:00:00 2001 From: FidelSch Date: Thu, 20 Nov 2025 12:36:50 -0300 Subject: [PATCH 4/4] ln: More helpful error message when trying to hard link to a directory --- util/build-gnu.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/util/build-gnu.sh b/util/build-gnu.sh index 3d2b509a2f7..f6874137d4d 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -268,9 +268,6 @@ sed -i -e "s/cat opts/sed -i -e \"s| <.\*$||g\" opts/" tests/misc/usage_vs_getop # for some reasons, some stuff are duplicated, strip that sed -i -e "s/provoked error./provoked error\ncat pat |sort -u > pat/" tests/misc/usage_vs_getopt.sh -# Update the GNU error message to match ours -sed -i -e "s/link-to-dir: hard link not allowed for directory/failed to create hard link 'link-to-dir' =>/" -e "s|link-to-dir/: hard link not allowed for directory|failed to create hard link 'link-to-dir/' =>|" tests/ln/hard-to-sym.sh - # install verbose messages shows ginstall as command sed -i -e "s/ginstall: creating directory/install: creating directory/g" tests/install/basic-1.sh