| /* Copyright 2008 Rob Landley <rob@landley.net> |
| * |
| * See http://opengroup.org/onlinepubs/9699919799/utilities/cp.html |
| * And http://opengroup.org/onlinepubs/9699919799/utilities/mv.html |
| * And http://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic.html#INSTALL |
| * |
| * Posix says "cp -Rf dir file" shouldn't delete file, but our -f does. |
| * |
| * Deviations from posix: -adlnrsvF, --preserve... about half the |
| * functionality in this cp isn't in posix. Posix is stuck in the 1970's. |
| * |
| * TODO: --preserve=links |
| * TODO: what's this _CP_mode system.posix_acl_ business? We chmod()? |
| |
| // options shared between mv/cp must be in same order (right to left) |
| // for FLAG macros to work out right in shared infrastructure. |
| |
| USE_CP(NEWTOY(cp, "<2"USE_CP_PRESERVE("(preserve):;")"RHLPprdaslvnF(remove-destination)fi[-HLPd][-ni]", TOYFLAG_BIN)) |
| USE_MV(NEWTOY(mv, "<2vnF(remove-destination)fi[-ni]", TOYFLAG_BIN)) |
| USE_INSTALL(NEWTOY(install, "<1cdDpsvm:o:g:", TOYFLAG_USR|TOYFLAG_BIN)) |
| |
| config CP |
| bool "cp" |
| default y |
| help |
| usage: cp [-adlnrsvfipRHLP] SOURCE... DEST |
| |
| Copy files from SOURCE to DEST. If more than one SOURCE, DEST must |
| be a directory. |
| |
| -f delete destination files we can't write to |
| -F delete any existing destination file first (--remove-destination) |
| -i interactive, prompt before overwriting existing DEST |
| -p preserve timestamps, ownership, and mode |
| -R recurse into subdirectories (DEST must be a directory) |
| -H Follow symlinks listed on command line |
| -L Follow all symlinks |
| -P Do not follow symlinks [default] |
| -a same as -dpr |
| -d don't dereference symlinks |
| -l hard link instead of copy |
| -n no clobber (don't overwrite DEST) |
| -r synonym for -R |
| -s symlink instead of copy |
| -v verbose |
| |
| config CP_PRESERVE |
| bool "cp --preserve support" |
| default y |
| depends on CP |
| help |
| usage: cp [--preserve=motcxa] |
| |
| --preserve takes either a comma separated list of attributes, or the first |
| letter(s) of: |
| |
| mode - permissions (ignore umask for rwx, copy suid and sticky bit) |
| ownership - user and group |
| timestamps - file creation, modification, and access times. |
| context - security context |
| xattr - extended attributes |
| all - all of the above |
| |
| config MV |
| bool "mv" |
| default y |
| help |
| usage: mv [-fivn] SOURCE... DEST" |
| |
| -f force copy by deleting destination file |
| -i interactive, prompt before overwriting existing DEST |
| -v verbose |
| -n no clobber (don't overwrite DEST) |
| |
| config INSTALL |
| bool "install" |
| default y |
| help |
| usage: install [-dDpsv] [-o USER] [-g GROUP] [-m MODE] [SOURCE...] DEST |
| |
| Copy files and set attributes. |
| |
| -d Act like mkdir -p |
| -D Create leading directories for DEST |
| -g Make copy belong to GROUP |
| -m Set permissions to MODE |
| -o Make copy belong to USER |
| -p Preserve timestamps |
| -s Call "strip -p" |
| -v Verbose |
| */ |
| |
| #define FORCE_FLAGS |
| #define FOR_cp |
| #include "toys.h" |
| |
| GLOBALS( |
| union { |
| struct { |
| // install's options |
| char *group; |
| char *user; |
| char *mode; |
| } i; |
| struct { |
| char *preserve; |
| } c; |
| }; |
| |
| char *destname; |
| struct stat top; |
| int (*callback)(struct dirtree *try); |
| uid_t uid; |
| gid_t gid; |
| int pflags; |
| ) |
| |
| struct cp_preserve { |
| char *name; |
| } static const cp_preserve[] = TAGGED_ARRAY(CP, |
| {"mode"}, {"ownership"}, {"timestamps"}, {"context"}, {"xattr"}, |
| ); |
| |
| // Callback from dirtree_read() for each file/directory under a source dir. |
| |
| static int cp_node(struct dirtree *try) |
| { |
| int fdout = -1, cfd = try->parent ? try->parent->extra : AT_FDCWD, |
| tfd = dirtree_parentfd(try); |
| unsigned flags = toys.optflags; |
| char *catch = try->parent ? try->name : TT.destname, *err = "%s"; |
| struct stat cst; |
| |
| if (!dirtree_notdotdot(try)) return 0; |
| |
| // If returning from COMEAGAIN, jump straight to -p logic at end. |
| if (S_ISDIR(try->st.st_mode) && try->again) { |
| fdout = try->extra; |
| err = 0; |
| } else { |
| |
| // -d is only the same as -r for symlinks, not for directories |
| if (S_ISLNK(try->st.st_mode) && (flags & FLAG_d)) flags |= FLAG_r; |
| |
| // Detect recursive copies via repeated top node (cp -R .. .) or |
| // identical source/target (fun with hardlinks). |
| if ((TT.top.st_dev == try->st.st_dev && TT.top.st_ino == try->st.st_ino |
| && (catch = TT.destname)) |
| || (!fstatat(cfd, catch, &cst, 0) && cst.st_dev == try->st.st_dev |
| && cst.st_ino == try->st.st_ino)) |
| { |
| error_msg("'%s' is '%s'", catch, err = dirtree_path(try, 0)); |
| free(err); |
| |
| return 0; |
| } |
| |
| // Handle -invF |
| |
| if (!faccessat(cfd, catch, F_OK, 0) && !S_ISDIR(cst.st_mode)) { |
| char *s; |
| |
| if (S_ISDIR(try->st.st_mode)) { |
| error_msg("dir at '%s'", s = dirtree_path(try, 0)); |
| free(s); |
| return 0; |
| } else if ((flags & FLAG_F) && unlinkat(cfd, catch, 0)) { |
| error_msg("unlink '%s'", catch); |
| return 0; |
| } else if (flags & FLAG_n) return 0; |
| else if (flags & FLAG_i) { |
| fprintf(stderr, "%s: overwrite '%s'", toys.which->name, |
| s = dirtree_path(try, 0)); |
| free(s); |
| if (!yesno(1)) return 0; |
| } |
| } |
| |
| if (flags & FLAG_v) { |
| char *s = dirtree_path(try, 0); |
| printf("%s '%s'\n", toys.which->name, s); |
| free(s); |
| } |
| |
| // Loop for -f retry after unlink |
| do { |
| |
| // directory, hardlink, symlink, mknod (char, block, fifo, socket), file |
| |
| // Copy directory |
| |
| if (S_ISDIR(try->st.st_mode)) { |
| struct stat st2; |
| |
| if (!(flags & (FLAG_a|FLAG_r|FLAG_R))) { |
| err = "Skipped dir '%s'"; |
| catch = try->name; |
| break; |
| } |
| |
| // Always make directory writeable to us, so we can create files in it. |
| // |
| // Yes, there's a race window between mkdir() and open() so it's |
| // possible that -p can be made to chown a directory other than the one |
| // we created. The closest we can do to closing this is make sure |
| // that what we open _is_ a directory rather than something else. |
| |
| if (!mkdirat(cfd, catch, try->st.st_mode | 0200) || errno == EEXIST) |
| if (-1 != (try->extra = openat(cfd, catch, O_NOFOLLOW))) |
| if (!fstat(try->extra, &st2) && S_ISDIR(st2.st_mode)) |
| return DIRTREE_COMEAGAIN |
| | (DIRTREE_SYMFOLLOW*!!(toys.optflags&FLAG_L)); |
| |
| // Hardlink |
| |
| } else if (flags & FLAG_l) { |
| if (!linkat(tfd, try->name, cfd, catch, 0)) err = 0; |
| |
| // Copy tree as symlinks. For non-absolute paths this involves |
| // appending the right number of .. entries as you go down the tree. |
| |
| } else if (flags & FLAG_s) { |
| char *s; |
| struct dirtree *or; |
| int dotdots = 0; |
| |
| s = dirtree_path(try, 0); |
| for (or = try; or->parent; or = or->parent) dotdots++; |
| |
| if (*or->name == '/') dotdots = 0; |
| if (dotdots) { |
| char *s2 = xmprintf("%*c%s", 3*dotdots, ' ', s); |
| free(s); |
| s = s2; |
| while(dotdots--) { |
| memcpy(s2, "../", 3); |
| s2 += 3; |
| } |
| } |
| if (!symlinkat(s, cfd, catch)) { |
| err = 0; |
| fdout = AT_FDCWD; |
| } |
| free(s); |
| |
| // Do something _other_ than copy contents of a file? |
| } else if (!S_ISREG(try->st.st_mode) |
| && (try->parent || (flags & (FLAG_a|FLAG_r)))) |
| { |
| int i; |
| |
| // make symlink, or make block/char/fifo/socket |
| if (S_ISLNK(try->st.st_mode) |
| ? ((i = readlinkat0(tfd, try->name, toybuf, sizeof(toybuf))) && |
| !symlinkat(toybuf, cfd, catch)) |
| : !mknodat(cfd, catch, try->st.st_mode, try->st.st_rdev)) |
| { |
| err = 0; |
| fdout = AT_FDCWD; |
| } |
| |
| // Copy contents of file. |
| } else { |
| int fdin; |
| |
| fdin = openat(tfd, try->name, O_RDONLY); |
| if (fdin < 0) { |
| catch = try->name; |
| break; |
| } |
| fdout = openat(cfd, catch, O_RDWR|O_CREAT|O_TRUNC, try->st.st_mode); |
| if (fdout >= 0) { |
| xsendfile(fdin, fdout); |
| err = 0; |
| } |
| |
| // We only copy xattrs for files because there's no flistxattrat() |
| if (TT.pflags&(_CP_xattr|_CP_context)) { |
| ssize_t listlen = flistxattr(fdin, 0, 0), len; |
| char *name, *value, *list; |
| |
| if (listlen>0) { |
| list = xmalloc(listlen); |
| flistxattr(fdin, list, listlen); |
| list[listlen-1] = 0; // I do not trust this API. |
| for (name = list; name-list < listlen; name += strlen(name)+1) { |
| if (!(TT.pflags&_CP_xattr) && strncmp(name, "security.", 9)) |
| continue; |
| if ((len = fgetxattr(fdin, name, 0, 0))>0) { |
| value = xmalloc(len); |
| if (len == fgetxattr(fdin, name, value, len)) |
| if (fsetxattr(fdout, name, value, len, 0)) |
| perror_msg("%s setxattr(%s=%s)", catch, name, value); |
| free(value); |
| } |
| } |
| free(list); |
| } |
| } |
| |
| close(fdin); |
| } |
| } while (err && (flags & (FLAG_f|FLAG_n)) && !unlinkat(cfd, catch, 0)); |
| } |
| |
| // Did we make a thing? |
| if (fdout != -1) { |
| int rc; |
| |
| // Inability to set --preserve isn't fatal, some require root access. |
| |
| // ownership |
| if (TT.pflags & _CP_ownership) { |
| |
| // permission bits already correct for mknod and don't apply to symlink |
| // If we can't get a filehandle to the actual object, use racy functions |
| if (fdout == AT_FDCWD) |
| rc = fchownat(cfd, catch, try->st.st_uid, try->st.st_gid, |
| AT_SYMLINK_NOFOLLOW); |
| else rc = fchown(fdout, try->st.st_uid, try->st.st_gid); |
| if (rc && !geteuid()) { |
| char *pp; |
| |
| perror_msg("chown '%s'", pp = dirtree_path(try, 0)); |
| free(pp); |
| } |
| } |
| |
| // timestamp |
| if (TT.pflags & _CP_timestamps) { |
| struct timespec times[] = {try->st.st_atim, try->st.st_mtim}; |
| |
| if (fdout == AT_FDCWD) utimensat(cfd, catch, times, AT_SYMLINK_NOFOLLOW); |
| else futimens(fdout, times); |
| } |
| |
| // mode comes last because other syscalls can strip suid bit |
| if (fdout != AT_FDCWD) { |
| if (TT.pflags & _CP_mode) fchmod(fdout, try->st.st_mode); |
| xclose(fdout); |
| } |
| |
| if (CFG_MV && toys.which->name[0] == 'm') |
| if (unlinkat(tfd, try->name, S_ISDIR(try->st.st_mode) ? AT_REMOVEDIR :0)) |
| err = "%s"; |
| } |
| |
| if (err) { |
| char *f = 0; |
| |
| if (catch == try->name) { |
| f = dirtree_path(try, 0); |
| while (try->parent) try = try->parent; |
| catch = xmprintf("%s%s", TT.destname, f+strlen(try->name)); |
| free(f); |
| f = catch; |
| } |
| perror_msg(err, catch); |
| free(f); |
| } |
| return 0; |
| } |
| |
| void cp_main(void) |
| { |
| char *destname = toys.optargs[--toys.optc]; |
| int i, destdir = !stat(destname, &TT.top) && S_ISDIR(TT.top.st_mode); |
| |
| if (toys.optc>1 && !destdir) error_exit("'%s' not directory", destname); |
| |
| if (toys.optflags & (FLAG_a|FLAG_p)) { |
| TT.pflags = CP_mode|CP_ownership|CP_timestamps; |
| umask(0); |
| } |
| // Not using comma_args() (yet?) because interpeting as letters. |
| if (CFG_CP_PRESERVE && (toys.optflags & FLAG_preserve)) { |
| char *pre = xstrdup(TT.c.preserve), *s; |
| |
| if (comma_scan(pre, "all", 1)) TT.pflags = ~0; |
| for (i=0; i<ARRAY_LEN(cp_preserve); i++) |
| if (comma_scan(pre, cp_preserve[i].name, 1)) TT.pflags |= 1<<i; |
| if (*pre) { |
| |
| // Try to interpret as letters, commas won't set anything this doesn't. |
| for (s = TT.c.preserve; *s; s++) { |
| for (i=0; i<ARRAY_LEN(cp_preserve); i++) |
| if (*s == *cp_preserve[i].name) break; |
| if (i == ARRAY_LEN(cp_preserve)) { |
| if (*s == 'a') TT.pflags = ~0; |
| else break; |
| } else TT.pflags |= 1<<i; |
| } |
| |
| if (*s) error_exit("bad --preserve=%s", pre); |
| } |
| free(pre); |
| } |
| if (!TT.callback) TT.callback = cp_node; |
| |
| // Loop through sources |
| for (i=0; i<toys.optc; i++) { |
| char *src = toys.optargs[i]; |
| int rc = 1; |
| |
| if (destdir) TT.destname = xmprintf("%s/%s", destname, basename(src)); |
| else TT.destname = destname; |
| |
| errno = EXDEV; |
| if (CFG_MV && toys.which->name[0] == 'm') { |
| int force = toys.optflags & FLAG_f, no_clobber = toys.optflags & FLAG_n; |
| |
| if (!force || no_clobber) { |
| struct stat st; |
| int exists = !stat(TT.destname, &st); |
| |
| // Prompt if -i or file isn't writable. Technically "is writable" is |
| // more complicated (022 is not writeable by the owner, just everybody |
| // _else_) but I don't care. |
| if (exists && ((toys.optflags & FLAG_i) || !(st.st_mode & 0222))) { |
| fprintf(stderr, "%s: overwrite '%s'", toys.which->name, TT.destname); |
| if (!yesno(1)) rc = 0; |
| else unlink(TT.destname); |
| } |
| // if -n and dest exists, don't try to rename() or copy |
| if (exists && no_clobber) rc = 0; |
| } |
| if (rc) rc = rename(src, TT.destname); |
| } |
| |
| // Copy if we didn't mv, skipping nonexistent sources |
| if (rc) { |
| if (errno!=EXDEV || dirtree_flagread(src, DIRTREE_SHUTUP+ |
| DIRTREE_SYMFOLLOW*!!(toys.optflags&(FLAG_H|FLAG_L)), TT.callback)) |
| perror_msg("bad '%s'", src); |
| } |
| if (destdir) free(TT.destname); |
| } |
| } |
| |
| void mv_main(void) |
| { |
| toys.optflags |= FLAG_d|FLAG_p|FLAG_R; |
| |
| cp_main(); |
| } |
| |
| // Export cp flags into install's flag context. |
| |
| static inline int cp_flag_F(void) { return FLAG_F; }; |
| static inline int cp_flag_p(void) { return FLAG_p; }; |
| static inline int cp_flag_v(void) { return FLAG_v; }; |
| |
| // Switch to install's flag context |
| #define CLEANUP_cp |
| #define FOR_install |
| #include <generated/flags.h> |
| |
| static int install_node(struct dirtree *try) |
| { |
| try->st.st_mode = (TT.i.mode) |
| ? string_to_mode(TT.i.mode, try->st.st_mode) : 0755; |
| if (TT.i.group) try->st.st_gid = TT.gid; |
| if (TT.i.user) try->st.st_uid = TT.uid; |
| |
| // Always returns 0 because no -r |
| cp_node(try); |
| |
| // No -r so always one level deep, so destname as set by cp_node() is correct |
| if (toys.optflags & FLAG_s) |
| if (xrun((char *[]){"strip", "-p", TT.destname, 0})) toys.exitval = 1; |
| |
| return 0; |
| } |
| |
| void install_main(void) |
| { |
| char **ss; |
| int flags = toys.optflags; |
| |
| if (flags & FLAG_d) { |
| for (ss = toys.optargs; *ss; ss++) { |
| if (mkpathat(AT_FDCWD, *ss, 0777, 3)) perror_msg_raw(*ss); |
| if (flags & FLAG_v) printf("%s\n", *ss); |
| } |
| |
| return; |
| } |
| |
| if (toys.optflags & FLAG_D) { |
| TT.destname = toys.optargs[toys.optc-1]; |
| if (mkpathat(AT_FDCWD, TT.destname, 0, 2)) |
| perror_exit("-D '%s'", TT.destname); |
| if (toys.optc == 1) return; |
| } |
| if (toys.optc < 2) error_exit("needs 2 args"); |
| |
| // Translate flags from install to cp |
| toys.optflags = cp_flag_F(); |
| if (flags & FLAG_v) toys.optflags |= cp_flag_v(); |
| if (flags & (FLAG_p|FLAG_o|FLAG_g)) toys.optflags |= cp_flag_p(); |
| |
| if (TT.i.user) TT.uid = xgetuid(TT.i.user); |
| if (TT.i.group) TT.gid = xgetgid(TT.i.group); |
| |
| TT.callback = install_node; |
| cp_main(); |
| } |