From a357cfb021cc0ecaff1de81014271977ce1502e7 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 25 Jan 2024 14:02:32 +0100 Subject: [PATCH] cp: add --keep-directory-symlink option 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 | 3 +++ doc/coreutils.texi | 8 ++++++++ src/copy.c | 3 ++- src/copy.h | 3 +++ src/cp.c | 13 ++++++++++++- tests/cp/keep-directory-symlink.sh | 27 +++++++++++++++++++++++++++ tests/local.mk | 1 + 7 files changed, 56 insertions(+), 2 deletions(-) create mode 100755 tests/cp/keep-directory-symlink.sh diff --git a/NEWS b/NEWS index 5b5befd2cb..36b7fd1fea 100644 --- 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. diff --git a/doc/coreutils.texi b/doc/coreutils.texi index 0ef9a098f7..c42126955e 100644 --- a/doc/coreutils.texi +++ b/doc/coreutils.texi @@ -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 diff --git a/src/copy.c b/src/copy.c index 12845eefd6..8d99f85622 100644 --- a/src/copy.c +++ b/src/copy.c @@ -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); diff --git a/src/copy.h b/src/copy.h index 7eca2e43cc..caf8755f95 100644 --- a/src/copy.h +++ b/src/copy.h @@ -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; diff --git a/src/cp.c b/src/cp.c index 42dcb4e0dc..0355ed97f6 100644 --- 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 index 0000000000..cf3918b79c --- /dev/null +++ b/tests/cp/keep-directory-symlink.sh @@ -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 . + +. "${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 diff --git a/tests/local.mk b/tests/local.mk index 895a19ae7a..2f6fa5b98f 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -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 \ -- 2.47.2