#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);
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';
}
if (strlcpy(to.base, target, sizeof(to.base) - 1) >=
sizeof(to.base) - 1)
errc(1, ENAMETOOLONG, "%s", target);
argv[argc] = NULL;
r = stat(to.base, &to_stat);
if (r == -1 && errno != ENOENT)
err(1, "%s", target);
if (r == -1 || !S_ISDIR(to_stat.st_mode)) {
if (argc > 1)
errc(1, ENOTDIR, "%s", target);
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 {
type = FILE_TO_DIR;
}
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;
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);
if (strcmp(to.base, "/") != 0) {
sep = strchr(to.base, '\0');
sep[0] = '/';
sep[1] = '\0';
}
} else {
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) {
if (type == DIR_TO_DNE &&
curr->fts_level == FTS_ROOTLEVEL) {
} 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:
warnx("%s: directory causes a cycle", curr->fts_path);
badcp = rval = 1;
continue;
case FTS_D:
if (curr->fts_level == FTS_ROOTLEVEL) {
strlcpy(rootname, curr->fts_name,
sizeof(rootname));
}
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;
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;
sep = strchr(to.base, '\0');
sep[0] = '/';
sep[1] = '\0';
} else {
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++;
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) {
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:
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) {
if (type == DIR_TO_DNE &&
curr->fts_level == FTS_ROOTLEVEL) {
} 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:
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;
}
break;
}
if (to.path[0] == '\0') {
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;
}
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)) {
if (copy_file(curr, dne, beneath))
badcp = rval = 1;
} else {
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 (dne) {
mode = curr_stat->st_mode | S_IRWXU;
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;
}
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;
}