git: d514cadacc50 - stable/14 - cp: Add tests for hard link case.

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Wed, 17 Jan 2024 18:28:34 UTC
The branch stable/14 has been updated by des:

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

commit d514cadacc50bfcdef76214b2499bcef464b324e
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2023-12-13 21:31:05 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2024-01-17 18:28:22 +0000

    cp: Add tests for hard link case.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D43052
    
    (cherry picked from commit 1fead66b64822f3f8106ad09bef0b9656836fa1a)
    
    cp: Add tests for symbolic link case.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans, allanjude
    Differential Revision:  https://reviews.freebsd.org/D43054
    
    (cherry picked from commit d3a8e9b43b4cef5b80e3845dfa8fd1fec6e568f9)
    
    cp: Refactor the core logic.
    
    Rewrite `copy_file()` so the lflag and sflag are handled as early as
    possible instead of constantly checking that they're not set and then
    handling them at the end.  This also opens the door to changing the
    failure logic at some future point (for instance, we might decide to
    fall back to copying if `errno` indicates that the file system does not
    support links).
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans, allanjude
    Differential Revision:  https://reviews.freebsd.org/D43055
    
    (cherry picked from commit d002316fd7bf0b359ea2f5518f3c10f6ad89a9ac)
    
    cp: Split the basic_symlink test case in two.
    
    This test case tests two different things: first, that copying a symlink
    results in a file with the same contents as the target of the symlink,
    rather than a second symlink, and second, that cp will refuse to copy a
    file to itself, or to a link to itself, or a link to its target.  Leave
    the first part in basic_symlink, move the second part to a new test case
    named samefile, and slightly expand both cases.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D43062
    
    (cherry picked from commit ac56b9d83c75f548667912ffe422be6bd4f5c27e)
    
    cp: Move the flags around a bit.
    
    - The HLPR flags are grouped together at the beginning because they are
      the standard flags for programs using FTS.  Move the N flag out from
      among them to its correct place in the sequence.
    - The Pflag variable isn't used outside main(), but moving it out lets
      us skip initialization and keeps it with its friends H, L and R.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D43063
    
    (cherry picked from commit 0f4467ce444b201468d2268958130f495951ca3c)
    
    cp: Further simplify the core logic.
    
    If the destination file exists but we decide unlink it, set the dne
    flag.  This means we don't need to re-check the conditions that would
    have caused us to delete the file when we later need to decide whether
    to create or replace it.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans
    Differential Revision:  https://reviews.freebsd.org/D43064
    
    (cherry picked from commit 38509270663f336103273878cc8ddc88a225b9d8)
    
    cp: Move the -N flag in the manual page.
    
    This accidentally got left out of 0f4467ce444b.
    
    MFC after:      1 week
    Sponsored by:   Klara, Inc.
    Reviewed by:    kevans, allanjude
    Differential Revision:  https://reviews.freebsd.org/D43067
    
    (cherry picked from commit 53fc8e190241233d94e183f8a39ec39f2154dfa8)
---
 bin/cp/cp.1             |  10 ++--
 bin/cp/cp.c             |  13 +++---
 bin/cp/tests/cp_test.sh |  87 ++++++++++++++++++++++++++++++++--
 bin/cp/utils.c          | 122 ++++++++++++++++++++++++------------------------
 4 files changed, 154 insertions(+), 78 deletions(-)

diff --git a/bin/cp/cp.1 b/bin/cp/cp.1
index 3862babafe7f..d8d62ef076a1 100644
--- a/bin/cp/cp.1
+++ b/bin/cp/cp.1
@@ -31,7 +31,7 @@
 .\"
 .\"	@(#)cp.1	8.3 (Berkeley) 4/18/94
 .\"
-.Dd December 7, 2023
+.Dd December 14, 2023
 .Dt CP 1
 .Os
 .Sh NAME
@@ -90,10 +90,6 @@ option is specified, symbolic links on the command line are followed.
 If the
 .Fl R
 option is specified, all symbolic links are followed.
-.It Fl N
-When used with
-.Fl p ,
-suppress copying file flags.
 .It Fl P
 No symbolic links are followed.
 This is the default if the
@@ -161,6 +157,10 @@ or
 options.)
 .It Fl l
 Create hard links to regular files in a hierarchy instead of copying.
+.It Fl N
+When used with
+.Fl p ,
+suppress copying file flags.
 .It Fl n
 Do not overwrite an existing file.
 (The
diff --git a/bin/cp/cp.c b/bin/cp/cp.c
index 8217a1e5d3c9..852868e65dcb 100644
--- a/bin/cp/cp.c
+++ b/bin/cp/cp.c
@@ -85,7 +85,7 @@ static char emptystring[] = "";
 PATH_T to = { to.p_path, emptystring, "" };
 
 int Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
-static int Hflag, Lflag, Rflag, rflag;
+static int Hflag, Lflag, Pflag, Rflag, rflag;
 volatile sig_atomic_t info;
 
 enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
@@ -98,12 +98,11 @@ main(int argc, char *argv[])
 {
 	struct stat to_stat, tmp_stat;
 	enum op type;
-	int Pflag, ch, fts_options, r, have_trailing_slash;
+	int ch, fts_options, r, have_trailing_slash;
 	char *target;
 
 	fts_options = FTS_NOCHDIR | FTS_PHYSICAL;
-	Pflag = 0;
-	while ((ch = getopt(argc, argv, "HLNPRafilnprsvx")) != -1)
+	while ((ch = getopt(argc, argv, "HLPRafilNnprsvx")) != -1)
 		switch (ch) {
 		case 'H':
 			Hflag = 1;
@@ -113,9 +112,6 @@ main(int argc, char *argv[])
 			Lflag = 1;
 			Hflag = Pflag = 0;
 			break;
-		case 'N':
-			Nflag = 1;
-			break;
 		case 'P':
 			Pflag = 1;
 			Hflag = Lflag = 0;
@@ -140,6 +136,9 @@ main(int argc, char *argv[])
 		case 'l':
 			lflag = 1;
 			break;
+		case 'N':
+			Nflag = 1;
+			break;
 		case 'n':
 			nflag = 1;
 			fflag = iflag = 0;
diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index f995d709cc3c..397c06d75bbb 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -51,10 +51,7 @@ basic_symlink_body()
 	atf_check cp baz foo
 	atf_check test '!' -L foo
 
-	atf_check -e inline:"cp: baz and baz are identical (not copied).\n" \
-	    -s exit:1 cp baz baz
-	atf_check -e inline:"cp: bar and baz are identical (not copied).\n" \
-	    -s exit:1 cp baz bar
+	atf_check cmp foo bar
 }
 
 atf_test_case chrdev
@@ -71,6 +68,35 @@ chrdev_body()
 	check_size trunc 0
 }
 
+atf_test_case hardlink
+hardlink_body()
+{
+	echo "foo" >foo
+	atf_check cp -l foo bar
+	atf_check -o inline:"foo\n" cat bar
+	atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
+}
+
+atf_test_case hardlink_exists
+hardlink_exists_body()
+{
+	echo "foo" >foo
+	echo "bar" >bar
+	atf_check -s not-exit:0 -e match:exists cp -l foo bar
+	atf_check -o inline:"bar\n" cat bar
+	atf_check_not_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
+}
+
+atf_test_case hardlink_exists_force
+hardlink_exists_force_body()
+{
+	echo "foo" >foo
+	echo "bar" >bar
+	atf_check cp -fl foo bar
+	atf_check -o inline:"foo\n" cat bar
+	atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
+}
+
 atf_test_case matching_srctgt
 matching_srctgt_body()
 {
@@ -198,6 +224,22 @@ recursive_link_Lflag_body()
 	    '(' ! -L foo-mirror/foo/baz ')'
 }
 
+atf_test_case samefile
+samefile_body()
+{
+	echo "foo" >foo
+	ln foo bar
+	ln -s bar baz
+	atf_check -e match:"baz and baz are identical" \
+	    -s exit:1 cp baz baz
+	atf_check -e match:"bar and baz are identical" \
+	    -s exit:1 cp baz bar
+	atf_check -e match:"foo and baz are identical" \
+	    -s exit:1 cp baz foo
+	atf_check -e match:"bar and foo are identical" \
+	    -s exit:1 cp foo bar
+}
+
 file_is_sparse()
 {
 	atf_check ${0%/*}/sparse "$1"
@@ -205,7 +247,7 @@ file_is_sparse()
 
 files_are_equal()
 {
-	atf_check test "$(stat -f "%d %i" "$1")" != "$(stat -f "%d %i" "$2")"
+	atf_check_not_equal "$(stat -f%d,%i "$1")" "$(stat -f%d,%i "$2")"
 	atf_check cmp "$1" "$2"
 }
 
@@ -293,11 +335,42 @@ standalone_Pflag_body()
 	atf_check -o inline:'Symbolic Link\n' stat -f %SHT baz
 }
 
+atf_test_case symlink
+symlink_body()
+{
+	echo "foo" >foo
+	atf_check cp -s foo bar
+	atf_check -o inline:"foo\n" cat bar
+	atf_check -o inline:"foo\n" readlink bar
+}
+
+atf_test_case symlink_exists
+symlink_exists_body()
+{
+	echo "foo" >foo
+	echo "bar" >bar
+	atf_check -s not-exit:0 -e match:exists cp -s foo bar
+	atf_check -o inline:"bar\n" cat bar
+}
+
+atf_test_case symlink_exists_force
+symlink_exists_force_body()
+{
+	echo "foo" >foo
+	echo "bar" >bar
+	atf_check cp -fs foo bar
+	atf_check -o inline:"foo\n" cat bar
+	atf_check -o inline:"foo\n" readlink bar
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case basic
 	atf_add_test_case basic_symlink
 	atf_add_test_case chrdev
+	atf_add_test_case hardlink
+	atf_add_test_case hardlink_exists
+	atf_add_test_case hardlink_exists_force
 	atf_add_test_case matching_srctgt
 	atf_add_test_case matching_srctgt_contained
 	atf_add_test_case matching_srctgt_link
@@ -305,10 +378,14 @@ atf_init_test_cases()
 	atf_add_test_case recursive_link_dflt
 	atf_add_test_case recursive_link_Hflag
 	atf_add_test_case recursive_link_Lflag
+	atf_add_test_case samefile
 	atf_add_test_case sparse_leading_hole
 	atf_add_test_case sparse_multiple_holes
 	atf_add_test_case sparse_only_hole
 	atf_add_test_case sparse_to_dev
 	atf_add_test_case sparse_trailing_hole
 	atf_add_test_case standalone_Pflag
+	atf_add_test_case symlink
+	atf_add_test_case symlink_exists
+	atf_add_test_case symlink_exists_force
 }
diff --git a/bin/cp/utils.c b/bin/cp/utils.c
index f43902eab3e6..0bb5157e3f57 100644
--- a/bin/cp/utils.c
+++ b/bin/cp/utils.c
@@ -68,6 +68,11 @@ static char sccsid[] = "@(#)utils.c	8.3 (Berkeley) 4/1/94";
  */
 #define BUFSIZE_SMALL (MAXPHYS)
 
+/*
+ * Prompt used in -i case.
+ */
+#define YESNO "(y/n [n]) "
+
 static ssize_t
 copy_fallback(int from_fd, int to_fd)
 {
@@ -125,7 +130,6 @@ copy_file(const FTSENT *entp, int dne)
 	 * modified by the umask.)
 	 */
 	if (!dne) {
-#define YESNO "(y/n [n]) "
 		if (nflag) {
 			if (vflag)
 				printf("%s not overwritten\n", to.p_path);
@@ -145,70 +149,69 @@ copy_file(const FTSENT *entp, int dne)
 		}
 
 		if (fflag) {
-			/*
-			 * Remove existing destination file name create a new
-			 * file.
-			 */
+			/* remove existing destination file */
 			(void)unlink(to.p_path);
-			if (!lflag && !sflag) {
-				to_fd = open(to.p_path,
-				    O_WRONLY | O_TRUNC | O_CREAT,
-				    fs->st_mode & ~(S_ISUID | S_ISGID));
-			}
-		} else if (!lflag && !sflag) {
-			/* Overwrite existing destination file name. */
-			to_fd = open(to.p_path, O_WRONLY | O_TRUNC, 0);
+			dne = 1;
+		}
+	}
+
+	rval = 0;
+
+	if (lflag) {
+		if (link(entp->fts_path, to.p_path) != 0) {
+			warn("%s", to.p_path);
+			rval = 1;
+		}
+		goto done;
+	}
+
+	if (sflag) {
+		if (symlink(entp->fts_path, to.p_path) != 0) {
+			warn("%s", to.p_path);
+			rval = 1;
 		}
-	} else if (!lflag && !sflag) {
+		goto done;
+	}
+
+	if (!dne) {
+		/* overwrite existing destination file */
+		to_fd = open(to.p_path, O_WRONLY | O_TRUNC, 0);
+	} else {
+		/* create new destination file */
 		to_fd = open(to.p_path, O_WRONLY | O_TRUNC | O_CREAT,
 		    fs->st_mode & ~(S_ISUID | S_ISGID));
 	}
-
-	if (!lflag && !sflag && to_fd == -1) {
+	if (to_fd == -1) {
 		warn("%s", to.p_path);
 		rval = 1;
 		goto done;
 	}
 
-	rval = 0;
-
-	if (!lflag && !sflag) {
-		wtotal = 0;
-		do {
-			if (use_copy_file_range) {
-				wcount = copy_file_range(from_fd, NULL,
-				    to_fd, NULL, SSIZE_MAX, 0);
-				if (wcount < 0 && errno == EINVAL) {
-					/* Prob a non-seekable FD */
-					use_copy_file_range = 0;
-				}
-			}
-			if (!use_copy_file_range) {
-				wcount = copy_fallback(from_fd, to_fd);
+	wtotal = 0;
+	do {
+		if (use_copy_file_range) {
+			wcount = copy_file_range(from_fd, NULL,
+			    to_fd, NULL, SSIZE_MAX, 0);
+			if (wcount < 0 && errno == EINVAL) {
+				/* probably a non-seekable descriptor */
+				use_copy_file_range = 0;
 			}
-			wtotal += wcount;
-			if (info) {
-				info = 0;
-				(void)fprintf(stderr,
-				    "%s -> %s %3d%%\n",
-				    entp->fts_path, to.p_path,
-				    cp_pct(wtotal, fs->st_size));
-			}
-		} while (wcount > 0);
-		if (wcount < 0) {
-			warn("%s", entp->fts_path);
-			rval = 1;
 		}
-	} else if (lflag) {
-		if (link(entp->fts_path, to.p_path)) {
-			warn("%s", to.p_path);
-			rval = 1;
+		if (!use_copy_file_range) {
+			wcount = copy_fallback(from_fd, to_fd);
 		}
-	} else if (sflag) {
-		if (symlink(entp->fts_path, to.p_path)) {
-			warn("%s", to.p_path);
-			rval = 1;
+		wtotal += wcount;
+		if (info) {
+			info = 0;
+			(void)fprintf(stderr,
+			    "%s -> %s %3d%%\n",
+			    entp->fts_path, to.p_path,
+			    cp_pct(wtotal, fs->st_size));
 		}
+	} while (wcount > 0);
+	if (wcount < 0) {
+		warn("%s", entp->fts_path);
+		rval = 1;
 	}
 
 	/*
@@ -217,16 +220,13 @@ copy_file(const FTSENT *entp, int dne)
 	 * or its contents might be irreplaceable.  It would only be safe
 	 * to remove it if we created it and its length is 0.
 	 */
-
-	if (!lflag && !sflag) {
-		if (pflag && setfile(fs, to_fd))
-			rval = 1;
-		if (pflag && preserve_fd_acls(from_fd, to_fd) != 0)
-			rval = 1;
-		if (close(to_fd)) {
-			warn("%s", to.p_path);
-			rval = 1;
-		}
+	if (pflag && setfile(fs, to_fd))
+		rval = 1;
+	if (pflag && preserve_fd_acls(from_fd, to_fd) != 0)
+		rval = 1;
+	if (close(to_fd)) {
+		warn("%s", to.p_path);
+		rval = 1;
 	}
 
 done: