diff --git a/bin/cp/cp.c b/bin/cp/cp.c index a3c8d910639c..c6b34198f20a 100644 --- a/bin/cp/cp.c +++ b/bin/cp/cp.c @@ -1,604 +1,629 @@ /*- * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include "extern.h" static char dot[] = "."; #define END(buf) (buf + sizeof(buf)) PATH_T to = { .dir = -1, .end = to.path }; int Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag; static int Hflag, Lflag, Pflag, Rflag, rflag; 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); 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(argc, argv, "HLPRafilNnprsvx")) != -1) switch (ch) { case 'H': Hflag = 1; Lflag = Pflag = 0; break; case 'L': Lflag = 1; Hflag = Pflag = 0; break; case 'P': Pflag = 1; Hflag = Lflag = 0; break; case 'R': Rflag = 1; break; case 'a': pflag = 1; Rflag = 1; Pflag = 1; Hflag = Lflag = 0; break; case 'f': fflag = 1; iflag = nflag = 0; break; case 'i': iflag = 1; fflag = nflag = 0; break; case 'l': lflag = 1; break; case 'N': Nflag = 1; break; case 'n': nflag = 1; fflag = iflag = 0; break; case 'p': pflag = 1; break; case 'r': rflag = Lflag = 1; Hflag = Pflag = 0; break; case 's': sflag = 1; break; case 'v': vflag = 1; break; case 'x': fts_options |= FTS_XDEV; 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 = 1; 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 + 1 && *(sep - 1) == '/') sep--; *sep = '\0'; } if (strlcpy(to.base, target, sizeof(to.base)) >= sizeof(to.base)) 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 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; int atflags, dne, badcp, len, rval; mode_t mask, mode; bool beneath = Rflag && type != FILE_TO_FILE; bool skipdp = false; /* * 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)); strlcpy(to.base, dot, sizeof(to.base)); } else if (type == FILE_TO_DIR) { to.dir = open(to.base, O_DIRECTORY | O_SEARCH); if (to.dir < 0) err(1, "%s", to.base); } if ((ftsp = fts_open(argv, fts_options, 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: 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)); } /* * If we FTS_SKIP while handling FTS_D, we will * immediately get FTS_DP for the same directory. * If this happens before we've appended the name * to to.path, we need to remember not to perform * the reverse operation. */ skipdp = true; /* 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; } 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; } skipdp = false; /* * 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 (((mode & (S_ISUID | S_ISGID | S_ISTXT)) || - ((mode | S_IRWXU) & mask) != (mode & mask)) && - fchmodat(to.dir, path, mode & mask, 0) != 0) { + if (fchmodat(to.dir, path, mode & mask, 0) != 0) { warn("chmod: %s/%s", to.base, to.path); rval = 1; } } /* are we leaving a directory we failed to enter? */ if (skipdp) continue; /* 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'; } 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; + 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); } if (errno) err(1, "fts_read"); fts_close(ftsp); close(to.dir); free(recpath); return (rval); } static void siginfo(int sig __unused) { info = 1; } diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh index bfc4009580cb..29dce783ffe2 100755 --- a/bin/cp/tests/cp_test.sh +++ b/bin/cp/tests/cp_test.sh @@ -1,596 +1,619 @@ # # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2020 Kyle Evans # # 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. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. # check_size() { file=$1 sz=$2 atf_check -o inline:"$sz\n" stat -f '%z' $file } atf_test_case basic basic_body() { echo "foo" > bar atf_check cp bar baz check_size baz 4 } atf_test_case basic_symlink basic_symlink_body() { echo "foo" > bar ln -s bar baz atf_check cp baz foo atf_check test '!' -L foo atf_check cmp foo bar } atf_test_case chrdev chrdev_body() { echo "foo" > bar check_size bar 4 atf_check cp /dev/null trunc check_size trunc 0 atf_check cp bar trunc check_size trunc 4 atf_check cp /dev/null trunc 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() { # PR235438: `cp -R foo foo` would previously infinitely recurse and # eventually error out. mkdir foo echo "qux" > foo/bar cp foo/bar foo/zoo atf_check cp -R foo foo atf_check -o inline:"qux\n" cat foo/foo/bar atf_check -o inline:"qux\n" cat foo/foo/zoo atf_check -e not-empty -s not-exit:0 stat foo/foo/foo } atf_test_case matching_srctgt_contained matching_srctgt_contained_body() { # Let's do the same thing, except we'll try to recursively copy foo into # one of its subdirectories. mkdir foo ln -s foo coo echo "qux" > foo/bar mkdir foo/moo touch foo/moo/roo cp foo/bar foo/zoo atf_check cp -R foo foo/moo atf_check cp -RH coo foo/moo atf_check -o inline:"qux\n" cat foo/moo/foo/bar atf_check -o inline:"qux\n" cat foo/moo/coo/bar atf_check -o inline:"qux\n" cat foo/moo/foo/zoo atf_check -o inline:"qux\n" cat foo/moo/coo/zoo # We should have copied the contents of foo/moo before foo, coo started # getting copied in. atf_check -o not-empty stat foo/moo/foo/moo/roo atf_check -o not-empty stat foo/moo/coo/moo/roo atf_check -e not-empty -s not-exit:0 stat foo/moo/foo/moo/foo atf_check -e not-empty -s not-exit:0 stat foo/moo/coo/moo/coo } atf_test_case matching_srctgt_link matching_srctgt_link_body() { mkdir foo echo "qux" > foo/bar cp foo/bar foo/zoo atf_check ln -s foo roo atf_check cp -RH roo foo atf_check -o inline:"qux\n" cat foo/roo/bar atf_check -o inline:"qux\n" cat foo/roo/zoo } atf_test_case matching_srctgt_nonexistent matching_srctgt_nonexistent_body() { # We'll copy foo to a nonexistent subdirectory; ideally, we would # skip just the directory and end up with a layout like; # # foo/ # bar # dne/ # bar # zoo # zoo # mkdir foo echo "qux" > foo/bar cp foo/bar foo/zoo atf_check cp -R foo foo/dne atf_check -o inline:"qux\n" cat foo/dne/bar atf_check -o inline:"qux\n" cat foo/dne/zoo atf_check -e not-empty -s not-exit:0 stat foo/dne/foo } atf_test_case pflag_acls pflag_acls_body() { mkdir dir echo "hello" >dir/file if ! setfacl -m g:staff:D::allow dir || ! setfacl -m g:staff:d::allow dir/file ; then atf_skip "file system does not support ACLs" fi atf_check cp -p dir/file dst atf_check -o match:"group:staff:-+d-+" getfacl dst rm dst mkdir dst atf_check cp -rp dir dst atf_check -o not-match:"group:staff:-+D-+" getfacl dst atf_check -o match:"group:staff:-+D-+" getfacl dst/dir atf_check -o match:"group:staff:-+d-+" getfacl dst/dir/file rm -rf dst atf_check cp -rp dir dst atf_check -o match:"group:staff:-+D-+" getfacl dst/ atf_check -o match:"group:staff:-+d-+" getfacl dst/file } atf_test_case pflag_flags pflag_flags_body() { mkdir dir echo "hello" >dir/file if ! chflags nodump dir || ! chflags nodump dir/file ; then atf_skip "file system does not support flags" fi atf_check cp -p dir/file dst atf_check -o match:"nodump" stat -f%Sf dst rm dst mkdir dst atf_check cp -rp dir dst atf_check -o not-match:"nodump" stat -f%Sf dst atf_check -o match:"nodump" stat -f%Sf dst/dir atf_check -o match:"nodump" stat -f%Sf dst/dir/file rm -rf dst atf_check cp -rp dir dst atf_check -o match:"nodump" stat -f%Sf dst atf_check -o match:"nodump" stat -f%Sf dst/file } recursive_link_setup() { extra_cpflag=$1 mkdir -p foo/bar ln -s bar foo/baz mkdir foo-mirror eval "cp -R $extra_cpflag foo foo-mirror" } atf_test_case recursive_link_dflt recursive_link_dflt_body() { recursive_link_setup # -P is the default, so this should work and preserve the link. atf_check cp -R foo foo-mirror atf_check test -L foo-mirror/foo/baz } atf_test_case recursive_link_Hflag recursive_link_Hflag_body() { recursive_link_setup # -H will not follow either, so this should also work and preserve the # link. atf_check cp -RH foo foo-mirror atf_check test -L foo-mirror/foo/baz } atf_test_case recursive_link_Lflag recursive_link_Lflag_body() { recursive_link_setup -L # -L will work, but foo/baz ends up expanded to a directory. atf_check test -d foo-mirror/foo/baz -a \ '(' ! -L foo-mirror/foo/baz ')' atf_check cp -RL foo foo-mirror atf_check test -d foo-mirror/foo/baz -a \ '(' ! -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" } files_are_equal() { atf_check_not_equal "$(stat -f%d,%i "$1")" "$(stat -f%d,%i "$2")" atf_check cmp "$1" "$2" } atf_test_case sparse_leading_hole sparse_leading_hole_body() { # A 16-megabyte hole followed by one megabyte of data truncate -s 16M foo seq -f%015g 65536 >>foo file_is_sparse foo atf_check cp foo bar files_are_equal foo bar file_is_sparse bar } atf_test_case sparse_multiple_holes sparse_multiple_holes_body() { # Three one-megabyte blocks of data preceded, separated, and # followed by 16-megabyte holes truncate -s 16M foo seq -f%015g 65536 >>foo truncate -s 33M foo seq -f%015g 65536 >>foo truncate -s 50M foo seq -f%015g 65536 >>foo truncate -s 67M foo file_is_sparse foo atf_check cp foo bar files_are_equal foo bar file_is_sparse bar } atf_test_case sparse_only_hole sparse_only_hole_body() { # A 16-megabyte hole truncate -s 16M foo file_is_sparse foo atf_check cp foo bar files_are_equal foo bar file_is_sparse bar } atf_test_case sparse_to_dev sparse_to_dev_body() { # Three one-megabyte blocks of data preceded, separated, and # followed by 16-megabyte holes truncate -s 16M foo seq -f%015g 65536 >>foo truncate -s 33M foo seq -f%015g 65536 >>foo truncate -s 50M foo seq -f%015g 65536 >>foo truncate -s 67M foo file_is_sparse foo atf_check -o file:foo cp foo /dev/stdout } atf_test_case sparse_trailing_hole sparse_trailing_hole_body() { # One megabyte of data followed by a 16-megabyte hole seq -f%015g 65536 >foo truncate -s 17M foo file_is_sparse foo atf_check cp foo bar files_are_equal foo bar file_is_sparse bar } atf_test_case standalone_Pflag standalone_Pflag_body() { echo "foo" > bar ln -s bar foo atf_check cp -P foo baz 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_test_case directory_to_symlink directory_to_symlink_body() { mkdir -p foo ln -s .. foo/bar mkdir bar touch bar/baz atf_check -s not-exit:0 -e match:"Not a directory" \ cp -R bar foo atf_check -s not-exit:0 -e match:"Not a directory" \ cp -r bar foo } atf_test_case overwrite_directory overwrite_directory_body() { mkdir -p foo/bar/baz touch bar atf_check -s not-exit:0 -e match:"Is a directory" \ cp bar foo rm bar mkdir bar touch bar/baz atf_check -s not-exit:0 -e match:"Is a directory" \ cp -R bar foo atf_check -s not-exit:0 -e match:"Is a directory" \ cp -r bar foo } atf_test_case to_dir_dne to_dir_dne_body() { mkdir dir echo "foo" >dir/foo atf_check cp -r dir dne atf_check test -d dne atf_check test -f dne/foo atf_check cmp dir/foo dne/foo } atf_test_case to_nondir to_nondir_body() { echo "foo" >foo echo "bar" >bar echo "baz" >baz # This is described as “case 1” in source code comments atf_check cp foo bar atf_check cmp -s foo bar # This is “case 2”, the target must be a directory atf_check -s not-exit:0 -e match:"Not a directory" \ cp foo bar baz } atf_test_case to_deadlink to_deadlink_body() { echo "foo" >foo ln -s bar baz atf_check cp foo baz atf_check cmp -s foo bar } atf_test_case to_deadlink_append to_deadlink_append_body() { echo "foo" >foo mkdir bar ln -s baz bar/foo atf_check cp foo bar atf_check cmp -s foo bar/baz rm -f bar/foo bar/baz ln -s baz bar/foo atf_check cp foo bar/ atf_check cmp -s foo bar/baz rm -f bar/foo bar/baz ln -s $PWD/baz bar/foo atf_check cp foo bar/ atf_check cmp -s foo baz } atf_test_case to_dirlink to_dirlink_body() { mkdir src dir echo "foo" >src/file ln -s dir dst atf_check cp -r src dst atf_check cmp -s src/file dir/src/file rm -r dir/* atf_check cp -r src dst/ atf_check cmp -s src/file dir/src/file rm -r dir/* # If the source is a directory and ends in a slash, our cp has # traditionally copied the contents of the source rather than # the source itself. It is unclear whether this is intended # or simply a consequence of how FTS handles the situation. # Notably, GNU cp does not behave in this manner. atf_check cp -r src/ dst atf_check cmp -s src/file dir/file rm -r dir/* atf_check cp -r src/ dst/ atf_check cmp -s src/file dir/file rm -r dir/* } atf_test_case to_deaddirlink to_deaddirlink_body() { mkdir src echo "foo" >src/file ln -s dir dst # It is unclear which error we should expect in these cases. # Our current implementation always reports ENOTDIR, but one # might be equally justified in expecting EEXIST or ENOENT. # GNU cp reports EEXIST when the destination is given with a # trailing slash and “cannot overwrite non-directory with # directory” otherwise. atf_check -s not-exit:0 -e ignore \ cp -r src dst atf_check -s not-exit:0 -e ignore \ cp -r src dst/ atf_check -s not-exit:0 -e ignore \ cp -r src/ dst atf_check -s not-exit:0 -e ignore \ cp -r src/ dst/ atf_check -s not-exit:0 -e ignore \ cp -R src dst atf_check -s not-exit:0 -e ignore \ cp -R src dst/ atf_check -s not-exit:0 -e ignore \ cp -R src/ dst atf_check -s not-exit:0 -e ignore \ cp -R src/ dst/ } atf_test_case to_link_outside to_link_outside_body() { mkdir dir dst dst/dir echo "foo" >dir/file ln -s ../../file dst/dir/file atf_check \ -s exit:1 \ -e match:"dst/dir/file: Permission denied" \ cp -r dir dst } +atf_test_case dstmode +dstmode_body() +{ + mkdir -m 0755 dir + echo "foo" >dir/file + umask 0177 + #atf_check cp -R dir dst +#begin + # atf-check stupidly refuses to work if the current umask is + # weird, instead of just dealing with the situation + cp -R dir dst >stdout 2>stderr + rc=$? + umask 022 + atf_check_equal 0 $rc + atf_check cat stdout + atf_check cat stderr +#end + atf_check -o inline:"40600\n" stat -f%p dst + atf_check chmod 0750 dst + atf_check cmp dir/file dst/file +} + 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 atf_add_test_case matching_srctgt_nonexistent atf_add_test_case pflag_acls atf_add_test_case pflag_flags 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 atf_add_test_case directory_to_symlink atf_add_test_case overwrite_directory atf_add_test_case to_dir_dne atf_add_test_case to_nondir atf_add_test_case to_deadlink atf_add_test_case to_deadlink_append atf_add_test_case to_dirlink atf_add_test_case to_deaddirlink atf_add_test_case to_link_outside + atf_add_test_case dstmode }