git: 8ce85300c792 - main - ln: Tweak append logic.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Tue, 15 Apr 2025 17:58:16 UTC
The branch main has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=8ce85300c792ecfcb9a307577e3af96ea9a4544c

commit 8ce85300c792ecfcb9a307577e3af96ea9a4544c
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2025-04-15 17:57:52 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2025-04-15 17:57:52 +0000

    ln: Tweak append logic.
    
    If the target is "." or ends in "/" or "/.", we always want to append
    the source's basename, even in the Fflag case.
    
    MFC after:      never
    Relnotes:       yes
    Sponsored by:   Klara, Inc.
    Reviewed by:    allanjude
    Differential Revision:  https://reviews.freebsd.org/D49842
---
 bin/ln/ln.c             | 34 +++++++++++++++++++++++++++-------
 bin/ln/tests/ln_test.sh |  6 +++++-
 2 files changed, 32 insertions(+), 8 deletions(-)

diff --git a/bin/ln/ln.c b/bin/ln/ln.c
index 6300658effa1..3055c7563cca 100644
--- a/bin/ln/ln.c
+++ b/bin/ln/ln.c
@@ -212,6 +212,13 @@ samedirent(const char *path1, const char *path2)
 	return sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino;
 }
 
+/*
+ * Create a link to source.  If target is a directory (and some additional
+ * conditions apply, see comments within) the link will be created within
+ * target and have the basename of source.  Otherwise, the link will be
+ * named target.  If isdir is true, target has already been determined to
+ * be a directory; otherwise, we will check, if needed.
+ */
 static int
 linkit(const char *source, const char *target, bool isdir)
 {
@@ -221,7 +228,7 @@ linkit(const char *source, const char *target, bool isdir)
 	struct stat sb;
 	const char *p;
 	int ch, first;
-	bool exists;
+	bool append, exists;
 
 	if (!sflag) {
 		/* If source doesn't exist, quit now. */
@@ -238,14 +245,27 @@ linkit(const char *source, const char *target, bool isdir)
 	}
 
 	/*
-	 * If the target is a directory (and not a symlink if hflag),
-	 * append the source's name, unless Fflag is set.
+	 * Append a slash and the source's basename if:
+	 * - the target is "." or ends in "/" or "/.", or
+	 * - the target is a directory (and not a symlink if hflag) and
+         *   Fflag is not set
 	 */
-	if (!Fflag && (isdir ||
-	    (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) ||
-	    (!hflag && stat(target, &sb) == 0 && S_ISDIR(sb.st_mode)))) {
+	if ((p = strrchr(target, '/')) == NULL)
+		p = target;
+	else
+		p++;
+	append = false;
+	if (p[0] == '\0' || (p[0] == '.' && p[1] == '\0')) {
+		append = true;
+	} else if (!Fflag) {
+		if (isdir || (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) ||
+		    (!hflag && stat(target, &sb) == 0 && S_ISDIR(sb.st_mode))) {
+			append = true;
+		}
+	}
+	if (append) {
 		if (strlcpy(bbuf, source, sizeof(bbuf)) >= sizeof(bbuf) ||
-		    (p = basename(bbuf)) == NULL ||
+		    (p = basename(bbuf)) == NULL /* can't happen */ ||
 		    snprintf(path, sizeof(path), "%s/%s", target, p) >=
 		    (ssize_t)sizeof(path)) {
 			errno = ENAMETOOLONG;
diff --git a/bin/ln/tests/ln_test.sh b/bin/ln/tests/ln_test.sh
index 82bc556842d8..78b4074aea18 100644
--- a/bin/ln/tests/ln_test.sh
+++ b/bin/ln/tests/ln_test.sh
@@ -170,11 +170,15 @@ sfF_flag_head()
 }
 sfF_flag_body()
 {
-	atf_check mkdir A B C
+	atf_check mkdir A B C D D/A
 	atf_check ln -sF A C
 	atf_check_symlink_to A C
 	atf_check ln -sfF B C
 	atf_check_symlink_to B C
+	atf_check ln -sfF A D/
+	atf_check_symlink_to A D/A
+	atf_check ln -sfF ../A .
+	atf_check_symlink_to ../A A
 }
 
 atf_test_case s_flag