]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
cp: add --keep-directory-symlink option
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 25 Jan 2024 13:02:32 +0000 (14:02 +0100)
committerPádraig Brady <P@draigBrady.com>
Thu, 22 Feb 2024 14:37:47 +0000 (14:37 +0000)
When recursively copying files into OS trees, it often happens that
some subdirectory of the source directory is a symlink in the target
directory. Currently, cp will fail in that scenario with the error:

"cannot overwrite non-directory %s with directory %s"

However, we'd like cp in this scenario to follow the destination
directory symlink and copy the files into the symlinked directory
instead. Let's support this by adding a new option
--keep-directory-symlink that makes cp follow destination directory
symlinks.

We name the option --keep-directory-symlink to keep consistent with
tar which has the same option with the same effect.

* doc/coreutils.texi (cp invocation): Describe the new option.
* src/copy.h: Add the new setting.
* src/copy.h: Adjust to follow symlinks if setting enabled.
* src/cp.c (usage): Describe the new option.
(main): Accept the new option.
* tests/cp/keep-directory-symlink.sh: A new test.
* tests/local.mk: Reference the new test.
* NEWS: Mention the new feature.

NEWS
doc/coreutils.texi
src/copy.c
src/copy.h
src/cp.c
tests/cp/keep-directory-symlink.sh [new file with mode: 0755]
tests/local.mk

diff --git a/NEWS b/NEWS
index 5b5befd2cb90977eea66289fe837687871248688..36b7fd1feadf06a7165c5829f513fea7b774386e 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -57,6 +57,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   chgrp now accepts the --from=OWNER:GROUP option to restrict changes to files
   with matching current OWNER and/or GROUP, as already supported by chown(1).
 
+  cp now accepts the --keep-directory-symlink option (like tar), to preserve
+  and follow exisiting symlinks to directories in the destination.
+
   od now supports printing IEEE half precision floating point with -t fH,
   or brain 16 bit floating point with -t fB, where supported by the compiler.
 
index 0ef9a098f72b4faf9f3f8a7d1e978d5a4cd8a008..c42126955e5bfa6e4693877423916b60c66edc26 100644 (file)
@@ -10289,6 +10289,14 @@ option is also specified.
 @opindex --verbose
 Print the name of each file before moving it.
 
+@item --keep-directory-symlink
+@opindex --keep-directory-symlink
+Follow existing symlinks to directories when copying. Note that this option
+should only be used when the contents of the destination directory are trusted
+as when this option is enabled, an attacker can place symlinks in the
+destination directory to make @command{cp} write to arbitrary directories in the
+system.
+
 @optStripTrailingSlashes
 
 @optBackupSuffix
index 12845eefd6b144bd5177289ef038ecaa31799eef..8d99f8562258342f003e59a4ee2854fc8a49d0a8 100644 (file)
@@ -2311,7 +2311,8 @@ copy_internal (char const *src_name, char const *dst_name,
           bool use_lstat
             = ((! S_ISREG (src_mode)
                 && (! x->copy_as_regular
-                    || S_ISDIR (src_mode) || S_ISLNK (src_mode)))
+                    || (S_ISDIR (src_mode) && !x->keep_directory_symlink)
+                    || S_ISLNK (src_mode)))
                || x->move_mode || x->symbolic_link || x->hard_link
                || x->backup_type != no_backups
                || x->unlink_dest_before_opening);
index 7eca2e43cc643babc3d87a8e98467eeefbc4b3db..caf8755f95e5b040bd13e3065597eceb6f8a0b0e 100644 (file)
@@ -256,6 +256,9 @@ struct cp_options
   /* If true, display the names of the files before copying them. */
   bool verbose;
 
+  /* If true, follow existing symlinks to directories when copying. */
+  bool keep_directory_symlink;
+
   /* If true, display details of how files were copied.  */
   bool debug;
 
index 42dcb4e0dc23f620aa6176a3cb8d5f1a064d75e8..0355ed97f6f04c52a250e69c7b01648184828a41 100644 (file)
--- a/src/cp.c
+++ b/src/cp.c
@@ -68,7 +68,8 @@ enum
   REFLINK_OPTION,
   SPARSE_OPTION,
   STRIP_TRAILING_SLASHES_OPTION,
-  UNLINK_DEST_BEFORE_OPENING
+  UNLINK_DEST_BEFORE_OPENING,
+  KEEP_DIRECTORY_SYMLINK_OPTION
 };
 
 /* True if the kernel is SELinux enabled.  */
@@ -141,6 +142,8 @@ static struct option const long_opts[] =
   {"target-directory", required_argument, nullptr, 't'},
   {"update", optional_argument, nullptr, 'u'},
   {"verbose", no_argument, nullptr, 'v'},
+  {"keep-directory-symlink", no_argument, nullptr,
+    KEEP_DIRECTORY_SYMLINK_OPTION},
   {GETOPT_SELINUX_CONTEXT_OPTION_DECL},
   {GETOPT_HELP_OPTION_DECL},
   {GETOPT_VERSION_OPTION_DECL},
@@ -230,6 +233,9 @@ Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.\n\
 "), stdout);
       fputs (_("\
   -v, --verbose                explain what is being done\n\
+"), stdout);
+      fputs (_("\
+      --keep-directory-symlink  follow existing symlinks to directories\n\
 "), stdout);
       fputs (_("\
   -x, --one-file-system        stay on this file system\n\
@@ -859,6 +865,7 @@ cp_option_init (struct cp_options *x)
 
   x->update = false;
   x->verbose = false;
+  x->keep_directory_symlink = false;
 
   /* By default, refuse to open a dangling destination symlink, because
      in general one cannot do that safely, give the current semantics of
@@ -1161,6 +1168,10 @@ main (int argc, char **argv)
           x.verbose = true;
           break;
 
+        case KEEP_DIRECTORY_SYMLINK_OPTION:
+          x.keep_directory_symlink = true;
+          break;
+
         case 'x':
           x.one_file_system = true;
           break;
diff --git a/tests/cp/keep-directory-symlink.sh b/tests/cp/keep-directory-symlink.sh
new file mode 100755 (executable)
index 0000000..cf3918b
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+# Test that cp --keep-directory-symlink follows symlinks.
+
+# Copyright (C) 2024 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ cp
+
+mkdir -p a/b b/d/e || framework_failure_
+ln -s b a/d || framework_failure_
+
+cp -RT --copy-contents b a || fail=1
+cp -RT --copy-contents --keep-directory-symlink b a || fail=1
+ls a/b/e || fail=1
index 895a19ae7ac1a2b157873b2ef3e33182bad574fe..2f6fa5b98f63fd7b0ad039f37cb13b6359e49750 100644 (file)
@@ -498,6 +498,7 @@ all_tests =                                 \
   tests/cp/existing-perm-dir.sh                        \
   tests/cp/existing-perm-race.sh               \
   tests/cp/fail-perm.sh                                \
+  tests/cp/keep-directory-symlink.sh           \
   tests/cp/sparse-extents.sh                   \
   tests/cp/copy-FMR.sh                         \
   tests/cp/sparse-perf.sh                      \