From d1610c33b0106d3c894d07e4c79b2a88d863d054 Mon Sep 17 00:00:00 2001 From: aweinstock Date: Mon, 2 Mar 2026 14:26:57 -0500 Subject: [PATCH] ln: Restore backup to destination if linking fails. --- src/uu/ln/src/ln.rs | 39 +++++++++++++++++++++++++-------------- tests/by-util/test_ln.rs | 16 ++++++++++++++++ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index d4158beca88..f1b7ff2bfec 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -438,23 +438,34 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { } } - if settings.symbolic { - symlink(&source, dst)?; - } else { - let p = if settings.logical && source.is_symlink() { - fs::canonicalize(&source) - .map_err_context(|| translate!("ln-failed-to-access", "file" => source.quote()))? + let res = (|| -> UResult<()> { + if settings.symbolic { + Ok(symlink(&source, dst)?) } else { - source.to_path_buf() - }; - if let Err(e) = fs::hard_link(&p, dst) { - if p.is_dir() { - return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); + let p = if settings.logical && source.is_symlink() { + fs::canonicalize(&source).map_err_context( + || translate!("ln-failed-to-access", "file" => source.quote()), + )? + } else { + source.to_path_buf() + }; + if let Err(e) = fs::hard_link(&p, dst) { + if p.is_dir() { + return Err(LnError::FailedToCreateHardLinkDir(source.to_path_buf()).into()); + } + return Err(e).map_err_context(|| { + translate!("ln-failed-to-create-hard-link", "source" => source.quote(), "dest" => dst.quote()) + }); } - return Err(e).map_err_context(|| { - translate!("ln-failed-to-create-hard-link", "source" => source.quote(), "dest" => dst.quote()) - }); + Ok(()) + } + })(); + if res.is_err() { + if let Some(ref p) = backup_path { + fs::rename(p, dst) + .map_err_context(|| translate!("ln-cannot-backup", "file" => dst.quote()))?; } + res?; } if settings.verbose { diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index e379e76d679..560a7364f2f 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -1065,3 +1065,19 @@ fn test_ln_no_dereference_symbolic() { assert!(at.is_symlink("b~")); } } + +#[test] +fn test_ln_backup_nonexistent_rollback() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + at.touch("dst"); + + scene + .ucmd() + .args(&["--backup", "/non/existent/path", "dst"]) + .fails(); + + assert!(!at.file_exists("dst~")); + assert!(at.file_exists("dst")); +}