aboutsummaryrefslogtreecommitdiff
path: root/bin/cp/cp.c
diff options
context:
space:
mode:
Diffstat (limited to 'bin/cp/cp.c')
-rw-r--r--bin/cp/cp.c695
1 files changed, 695 insertions, 0 deletions
diff --git a/bin/cp/cp.c b/bin/cp/cp.c
new file mode 100644
index 000000000000..38fe65399d06
--- /dev/null
+++ b/bin/cp/cp.c
@@ -0,0 +1,695 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1988, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ *
+ * This code is derived from software contributed to Berkeley by
+ * David Hitz of Auspex Systems Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Cp copies source files to target files.
+ *
+ * The global PATH_T structure "to" always contains the path to the
+ * current target file. Since fts(3) does not change directories,
+ * this path can be either absolute or dot-relative.
+ *
+ * The basic algorithm is to initialize "to" and use fts(3) to traverse
+ * the file hierarchy rooted in the argument list. A trivial case is the
+ * case of 'cp file1 file2'. The more interesting case is the case of
+ * 'cp file1 file2 ... fileN dir' where the hierarchy is traversed and the
+ * path (relative to the root of the traversal) is appended to dir (stored
+ * in "to") to form the final target path.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <fts.h>
+#include <getopt.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "extern.h"
+
+static char dot[] = ".";
+
+#define END(buf) (buf + sizeof(buf))
+PATH_T to = { .dir = -1, .end = to.path };
+bool Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
+static bool Hflag, Lflag, Pflag, Rflag, rflag, Sflag;
+volatile sig_atomic_t info;
+
+enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
+
+static int copy(char *[], enum op, int, struct stat *);
+static void siginfo(int __unused);
+
+enum {
+ SORT_OPT = CHAR_MAX,
+};
+
+static const struct option long_opts[] =
+{
+ { "archive", no_argument, NULL, 'a' },
+ { "force", no_argument, NULL, 'f' },
+ { "interactive", no_argument, NULL, 'i' },
+ { "dereference", no_argument, NULL, 'L' },
+ { "link", no_argument, NULL, 'l' },
+ { "no-clobber", no_argument, NULL, 'n' },
+ { "no-dereference", no_argument, NULL, 'P' },
+ { "recursive", no_argument, NULL, 'R' },
+ { "symbolic-link", no_argument, NULL, 's' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "one-file-system", no_argument, NULL, 'x' },
+ { "sort", no_argument, NULL, SORT_OPT },
+ { 0 }
+};
+
+int
+main(int argc, char *argv[])
+{
+ struct stat to_stat, tmp_stat;
+ enum op type;
+ int ch, fts_options, r;
+ char *sep, *target;
+ bool have_trailing_slash = false;
+
+ fts_options = FTS_NOCHDIR | FTS_PHYSICAL;
+ while ((ch = getopt_long(argc, argv, "+HLPRafilNnprsvx", long_opts,
+ NULL)) != -1)
+ switch (ch) {
+ case 'H':
+ Hflag = true;
+ Lflag = Pflag = false;
+ break;
+ case 'L':
+ Lflag = true;
+ Hflag = Pflag = false;
+ break;
+ case 'P':
+ Pflag = true;
+ Hflag = Lflag = false;
+ break;
+ case 'R':
+ Rflag = true;
+ break;
+ case 'a':
+ pflag = true;
+ Rflag = true;
+ Pflag = true;
+ Hflag = Lflag = false;
+ break;
+ case 'f':
+ fflag = true;
+ iflag = nflag = false;
+ break;
+ case 'i':
+ iflag = true;
+ fflag = nflag = false;
+ break;
+ case 'l':
+ lflag = true;
+ break;
+ case 'N':
+ Nflag = true;
+ break;
+ case 'n':
+ nflag = true;
+ fflag = iflag = false;
+ break;
+ case 'p':
+ pflag = true;
+ break;
+ case 'r':
+ rflag = Lflag = true;
+ Hflag = Pflag = false;
+ break;
+ case 's':
+ sflag = true;
+ break;
+ case 'v':
+ vflag = true;
+ break;
+ case 'x':
+ fts_options |= FTS_XDEV;
+ break;
+ case SORT_OPT:
+ Sflag = true;
+ break;
+ default:
+ usage();
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 2)
+ usage();
+
+ if (Rflag && rflag)
+ errx(1, "the -R and -r options may not be specified together");
+ if (lflag && sflag)
+ errx(1, "the -l and -s options may not be specified together");
+ if (rflag)
+ Rflag = true;
+ if (Rflag) {
+ if (Hflag)
+ fts_options |= FTS_COMFOLLOW;
+ if (Lflag) {
+ fts_options &= ~FTS_PHYSICAL;
+ fts_options |= FTS_LOGICAL;
+ }
+ } else if (!Pflag) {
+ fts_options &= ~FTS_PHYSICAL;
+ fts_options |= FTS_LOGICAL | FTS_COMFOLLOW;
+ }
+ (void)signal(SIGINFO, siginfo);
+
+ /* Save the target base in "to". */
+ target = argv[--argc];
+ if (*target == '\0') {
+ target = dot;
+ } else if ((sep = strrchr(target, '/')) != NULL && sep[1] == '\0') {
+ have_trailing_slash = true;
+ while (sep > target && *sep == '/')
+ sep--;
+ sep[1] = '\0';
+ }
+ /*
+ * Copy target into to.base, leaving room for a possible separator
+ * which will be appended later in the non-FILE_TO_FILE cases.
+ */
+ if (strlcpy(to.base, target, sizeof(to.base) - 1) >=
+ sizeof(to.base) - 1)
+ errc(1, ENAMETOOLONG, "%s", target);
+
+ /* Set end of argument list for fts(3). */
+ argv[argc] = NULL;
+
+ /*
+ * Cp has two distinct cases:
+ *
+ * cp [-R] source target
+ * cp [-R] source1 ... sourceN directory
+ *
+ * In both cases, source can be either a file or a directory.
+ *
+ * In (1), the target becomes a copy of the source. That is, if the
+ * source is a file, the target will be a file, and likewise for
+ * directories.
+ *
+ * In (2), the real target is not directory, but "directory/source".
+ */
+ r = stat(to.base, &to_stat);
+ if (r == -1 && errno != ENOENT)
+ err(1, "%s", target);
+ if (r == -1 || !S_ISDIR(to_stat.st_mode)) {
+ /*
+ * Case (1). Target is not a directory.
+ */
+ if (argc > 1)
+ errc(1, ENOTDIR, "%s", target);
+
+ /*
+ * Need to detect the case:
+ * cp -R dir foo
+ * Where dir is a directory and foo does not exist, where
+ * we want pathname concatenations turned on but not for
+ * the initial mkdir().
+ */
+ if (r == -1) {
+ if (Rflag && (Lflag || Hflag))
+ stat(*argv, &tmp_stat);
+ else
+ lstat(*argv, &tmp_stat);
+
+ if (S_ISDIR(tmp_stat.st_mode) && Rflag)
+ type = DIR_TO_DNE;
+ else
+ type = FILE_TO_FILE;
+ } else
+ type = FILE_TO_FILE;
+
+ if (have_trailing_slash && type == FILE_TO_FILE) {
+ if (r == -1)
+ errc(1, ENOENT, "%s", target);
+ else
+ errc(1, ENOTDIR, "%s", target);
+ }
+ } else {
+ /*
+ * Case (2). Target is a directory.
+ */
+ type = FILE_TO_DIR;
+ }
+
+ /*
+ * For DIR_TO_DNE, we could provide copy() with the to_stat we've
+ * already allocated on the stack here that isn't being used for
+ * anything. Not doing so, though, simplifies later logic a little bit
+ * as we need to skip checking root_stat on the first iteration and
+ * ensure that we set it with the first mkdir().
+ */
+ exit (copy(argv, type, fts_options, (type == DIR_TO_DNE ? NULL :
+ &to_stat)));
+}
+
+static int
+ftscmp(const FTSENT * const *a, const FTSENT * const *b)
+{
+ return (strcmp((*a)->fts_name, (*b)->fts_name));
+}
+
+static int
+copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
+{
+ char rootname[NAME_MAX];
+ struct stat created_root_stat, to_stat, *curr_stat;
+ FTS *ftsp;
+ FTSENT *curr;
+ char *recpath = NULL, *sep;
+ int atflags, dne, badcp, len, level, rval;
+ mode_t mask, mode;
+ bool beneath = Rflag && type != FILE_TO_FILE;
+
+ /*
+ * Keep an inverted copy of the umask, for use in correcting
+ * permissions on created directories when not using -p.
+ */
+ mask = ~umask(0777);
+ umask(~mask);
+
+ if (type == FILE_TO_FILE) {
+ to.dir = AT_FDCWD;
+ to.end = to.path + strlcpy(to.path, to.base, sizeof(to.path));
+ to.base[0] = '\0';
+ } else if (type == FILE_TO_DIR) {
+ to.dir = open(to.base, O_DIRECTORY | O_SEARCH);
+ if (to.dir < 0)
+ err(1, "%s", to.base);
+ /*
+ * We have previously made sure there is room for this.
+ */
+ if (strcmp(to.base, "/") != 0) {
+ sep = strchr(to.base, '\0');
+ sep[0] = '/';
+ sep[1] = '\0';
+ }
+ } else {
+ /*
+ * We will create the destination directory imminently.
+ */
+ to.dir = -1;
+ }
+
+ level = FTS_ROOTLEVEL;
+ if ((ftsp = fts_open(argv, fts_options, Sflag ? ftscmp : NULL)) == NULL)
+ err(1, "fts_open");
+ for (badcp = rval = 0;
+ (curr = fts_read(ftsp)) != NULL;
+ badcp = 0, *to.end = '\0') {
+ curr_stat = curr->fts_statp;
+ switch (curr->fts_info) {
+ case FTS_NS:
+ case FTS_DNR:
+ case FTS_ERR:
+ if (level > curr->fts_level) {
+ /* leaving a directory; remove its name from to.path */
+ if (type == DIR_TO_DNE &&
+ curr->fts_level == FTS_ROOTLEVEL) {
+ /* this is actually our created root */
+ } else {
+ while (to.end > to.path && *to.end != '/')
+ to.end--;
+ assert(strcmp(to.end + (*to.end == '/'),
+ curr->fts_name) == 0);
+ *to.end = '\0';
+ }
+ level--;
+ }
+ warnc(curr->fts_errno, "%s", curr->fts_path);
+ badcp = rval = 1;
+ continue;
+ case FTS_DC: /* Warn, continue. */
+ warnx("%s: directory causes a cycle", curr->fts_path);
+ badcp = rval = 1;
+ continue;
+ case FTS_D:
+ /*
+ * Stash the root basename off for detecting
+ * recursion later.
+ *
+ * This will be essential if the root is a symlink
+ * and we're rolling with -L or -H. The later
+ * bits will need this bit in particular.
+ */
+ if (curr->fts_level == FTS_ROOTLEVEL) {
+ strlcpy(rootname, curr->fts_name,
+ sizeof(rootname));
+ }
+ /* we must have a destination! */
+ if (type == DIR_TO_DNE &&
+ curr->fts_level == FTS_ROOTLEVEL) {
+ assert(to.dir < 0);
+ assert(root_stat == NULL);
+ mode = curr_stat->st_mode | S_IRWXU;
+ /*
+ * Will our umask prevent us from entering
+ * the directory after we create it?
+ */
+ if (~mask & S_IRWXU)
+ umask(~mask & ~S_IRWXU);
+ if (mkdir(to.base, mode) != 0) {
+ warn("%s", to.base);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ continue;
+ }
+ to.dir = open(to.base, O_DIRECTORY | O_SEARCH);
+ if (to.dir < 0) {
+ warn("%s", to.base);
+ (void)rmdir(to.base);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ continue;
+ }
+ if (fstat(to.dir, &created_root_stat) != 0) {
+ warn("%s", to.base);
+ (void)close(to.dir);
+ (void)rmdir(to.base);
+ fts_set(ftsp, curr, FTS_SKIP);
+ to.dir = -1;
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ continue;
+ }
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ root_stat = &created_root_stat;
+ curr->fts_number = 1;
+ /*
+ * We have previously made sure there is
+ * room for this.
+ */
+ sep = strchr(to.base, '\0');
+ sep[0] = '/';
+ sep[1] = '\0';
+ } else {
+ /* entering a directory; append its name to to.path */
+ len = snprintf(to.end, END(to.path) - to.end, "%s%s",
+ to.end > to.path ? "/" : "", curr->fts_name);
+ if (to.end + len >= END(to.path)) {
+ *to.end = '\0';
+ warnc(ENAMETOOLONG, "%s%s%s%s", to.base,
+ to.path, to.end > to.path ? "/" : "",
+ curr->fts_name);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ continue;
+ }
+ to.end += len;
+ }
+ level++;
+ /*
+ * We're on the verge of recursing on ourselves.
+ * Either we need to stop right here (we knowingly
+ * just created it), or we will in an immediate
+ * descendant. Record the path of the immediate
+ * descendant to make our lives a little less
+ * complicated looking.
+ */
+ if (type != FILE_TO_FILE &&
+ root_stat->st_dev == curr_stat->st_dev &&
+ root_stat->st_ino == curr_stat->st_ino) {
+ assert(recpath == NULL);
+ if (root_stat == &created_root_stat) {
+ /*
+ * This directory didn't exist
+ * when we started, we created it
+ * as part of traversal. Stop
+ * right here before we do
+ * something silly.
+ */
+ fts_set(ftsp, curr, FTS_SKIP);
+ continue;
+ }
+ if (asprintf(&recpath, "%s/%s", to.path,
+ rootname) < 0) {
+ warnc(ENOMEM, NULL);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ continue;
+ }
+ }
+ if (recpath != NULL &&
+ strcmp(recpath, to.path) == 0) {
+ fts_set(ftsp, curr, FTS_SKIP);
+ continue;
+ }
+ break;
+ case FTS_DP:
+ /*
+ * We are nearly finished with this directory. If we
+ * didn't actually copy it, or otherwise don't need to
+ * change its attributes, then we are done.
+ *
+ * If -p is in effect, set all the attributes.
+ * Otherwise, set the correct permissions, limited
+ * by the umask. Optimise by avoiding a chmod()
+ * if possible (which is usually the case if we
+ * made the directory). Note that mkdir() does not
+ * honour setuid, setgid and sticky bits, but we
+ * normally want to preserve them on directories.
+ */
+ if (curr->fts_number && pflag) {
+ int fd = *to.path ? -1 : to.dir;
+ if (setfile(curr_stat, fd, true))
+ rval = 1;
+ if (preserve_dir_acls(curr->fts_accpath,
+ to.path) != 0)
+ rval = 1;
+ } else if (curr->fts_number) {
+ const char *path = *to.path ? to.path : dot;
+ mode = curr_stat->st_mode;
+ if (fchmodat(to.dir, path, mode & mask, 0) != 0) {
+ warn("chmod: %s%s", to.base, to.path);
+ rval = 1;
+ }
+ }
+ if (level > curr->fts_level) {
+ /* leaving a directory; remove its name from to.path */
+ if (type == DIR_TO_DNE &&
+ curr->fts_level == FTS_ROOTLEVEL) {
+ /* this is actually our created root */
+ } else {
+ while (to.end > to.path && *to.end != '/')
+ to.end--;
+ assert(strcmp(to.end + (*to.end == '/'),
+ curr->fts_name) == 0);
+ *to.end = '\0';
+ }
+ level--;
+ }
+ continue;
+ default:
+ /* something else: append its name to to.path */
+ if (type == FILE_TO_FILE)
+ break;
+ len = snprintf(to.end, END(to.path) - to.end, "%s%s",
+ to.end > to.path ? "/" : "", curr->fts_name);
+ if (to.end + len >= END(to.path)) {
+ *to.end = '\0';
+ warnc(ENAMETOOLONG, "%s%s%s%s", to.base,
+ to.path, to.end > to.path ? "/" : "",
+ curr->fts_name);
+ badcp = rval = 1;
+ continue;
+ }
+ /* intentionally do not update to.end */
+ break;
+ }
+
+ /* Not an error but need to remember it happened. */
+ if (to.path[0] == '\0') {
+ /*
+ * This can happen in two cases:
+ * - DIR_TO_DNE; we created the directory and
+ * populated root_stat earlier.
+ * - FILE_TO_DIR if a source has a trailing slash;
+ * the caller populated root_stat.
+ */
+ dne = false;
+ to_stat = *root_stat;
+ } else {
+ atflags = beneath ? AT_RESOLVE_BENEATH : 0;
+ if (curr->fts_info == FTS_D || curr->fts_info == FTS_SL)
+ atflags |= AT_SYMLINK_NOFOLLOW;
+ dne = fstatat(to.dir, to.path, &to_stat, atflags) != 0;
+ }
+
+ /* Check if source and destination are identical. */
+ if (!dne &&
+ to_stat.st_dev == curr_stat->st_dev &&
+ to_stat.st_ino == curr_stat->st_ino) {
+ warnx("%s%s and %s are identical (not copied).",
+ to.base, to.path, curr->fts_path);
+ badcp = rval = 1;
+ if (S_ISDIR(curr_stat->st_mode))
+ fts_set(ftsp, curr, FTS_SKIP);
+ continue;
+ }
+
+ switch (curr_stat->st_mode & S_IFMT) {
+ case S_IFLNK:
+ if ((fts_options & FTS_LOGICAL) ||
+ ((fts_options & FTS_COMFOLLOW) &&
+ curr->fts_level == 0)) {
+ /*
+ * We asked FTS to follow links but got
+ * here anyway, which means the target is
+ * nonexistent or inaccessible. Let
+ * copy_file() deal with the error.
+ */
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ } else {
+ /* Copy the link. */
+ if (copy_link(curr, dne, beneath))
+ badcp = rval = 1;
+ }
+ break;
+ case S_IFDIR:
+ if (!Rflag) {
+ warnx("%s is a directory (not copied).",
+ curr->fts_path);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ break;
+ }
+ /*
+ * If the directory doesn't exist, create the new
+ * one with the from file mode plus owner RWX bits,
+ * modified by the umask. Trade-off between being
+ * able to write the directory (if from directory is
+ * 555) and not causing a permissions race. If the
+ * umask blocks owner writes, we fail.
+ */
+ if (dne) {
+ mode = curr_stat->st_mode | S_IRWXU;
+ /*
+ * Will our umask prevent us from entering
+ * the directory after we create it?
+ */
+ if (~mask & S_IRWXU)
+ umask(~mask & ~S_IRWXU);
+ if (mkdirat(to.dir, to.path, mode) != 0) {
+ warn("%s%s", to.base, to.path);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ break;
+ }
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ } else if (!S_ISDIR(to_stat.st_mode)) {
+ warnc(ENOTDIR, "%s%s", to.base, to.path);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ break;
+ }
+ /*
+ * Arrange to correct directory attributes later
+ * (in the post-order phase) if this is a new
+ * directory, or if the -p flag is in effect.
+ * Note that fts_number may already be set if this
+ * is the newly created destination directory.
+ */
+ curr->fts_number |= pflag || dne;
+ break;
+ case S_IFBLK:
+ case S_IFCHR:
+ if (Rflag && !sflag) {
+ if (copy_special(curr_stat, dne, beneath))
+ badcp = rval = 1;
+ } else {
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ }
+ break;
+ case S_IFSOCK:
+ warnx("%s is a socket (not copied).",
+ curr->fts_path);
+ break;
+ case S_IFIFO:
+ if (Rflag && !sflag) {
+ if (copy_fifo(curr_stat, dne, beneath))
+ badcp = rval = 1;
+ } else {
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ }
+ break;
+ default:
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ break;
+ }
+ if (vflag && !badcp)
+ (void)printf("%s -> %s%s\n", curr->fts_path, to.base, to.path);
+ }
+ assert(level == FTS_ROOTLEVEL);
+ if (errno)
+ err(1, "fts_read");
+ (void)fts_close(ftsp);
+ if (to.dir != AT_FDCWD && to.dir >= 0)
+ (void)close(to.dir);
+ free(recpath);
+ return (rval);
+}
+
+static void
+siginfo(int sig __unused)
+{
+
+ info = 1;
+}