mv.c (13015B)
- // utils-std: Collection of commonly available Unix tools
 - // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
 - // SPDX-License-Identifier: MPL-2.0
 - #define _POSIX_C_SOURCE 200809L
 - #define _GNU_SOURCE // copy_file_range
 - #define _FILE_OFFSET_BITS 64
 - // NetBSD <10 hides fdopendir behind _NETBSD_SOURCE
 - #if __NetBSD_Version__ < 1000000000
 - #define _NETBSD_SOURCE
 - #endif
 - #include "../config.h"
 - #include "../libutils/consent.h"
 - #include "../libutils/fs.h"
 - #include "../libutils/getopt_nolong.h"
 - #include "../libutils/lib_string.h"
 - #include <dirent.h> // fdopendir
 - #include <errno.h>
 - #include <fcntl.h> // open
 - #include <libgen.h> // basename
 - #include <limits.h> // PATH_MAX
 - #include <locale.h> // setlocale
 - #include <stdbool.h>
 - #include <stdint.h> // SIZE_MAX
 - #include <stdio.h> // fprintf, rename
 - #include <string.h> // strcmp
 - #include <sys/stat.h> // stat, S_ISDIR
 - #include <unistd.h> // getopt
 - #ifdef HAS_GETOPT_LONG
 - #include <getopt.h>
 - #endif
 - // Workaround against GNU glibc
 - // https://sourceware.org/bugzilla/show_bug.cgi?id=18228
 - #if defined(__linux__) && !defined(O_SEARCH)
 - // As defined in musl
 - #define O_SEARCH O_PATH
 - #endif
 - const char *argv0 = "mv";
 - bool no_clob = false, force = false, interact = false, verbose = false;
 - static int stdin_tty = 0;
 - struct named_fd
 - {
 - int fd;
 - const char *name;
 - const char *sep;
 - };
 - static int do_renameat(struct named_fd srcdir,
 - const char *restrict src,
 - struct named_fd destdir,
 - const char *restrict dest);
 - static int
 - copy_file_unlink(struct named_fd srcdir,
 - const char *restrict src,
 - struct stat src_status,
 - struct named_fd destdir,
 - const char *restrict dest)
 - {
 - int in = openat(srcdir.fd, src, O_RDONLY | O_NOCTTY);
 - if(in < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening source '%s%s%s': %s\n",
 - srcdir.name,
 - srcdir.sep,
 - src,
 - strerror(errno));
 - errno = 0;
 - return -1;
 - }
 - int out = openat(destdir.fd, dest, O_WRONLY | O_CREAT | O_NOCTTY, src_status.st_mode);
 - if(out < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening destination '%s%s%s': %s\n",
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - errno = 0;
 - return -1;
 - }
 - const struct timespec times[2] = {src_status.st_atim, src_status.st_mtim};
 - if(futimens(out, times) != 0)
 - {
 - fprintf(stderr,
 - "mv: warning: Failed copying access & modification times to '%s%s%s': %s\n",
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - errno = 0;
 - }
 - if(fchown(out, src_status.st_uid, src_status.st_gid) != 0)
 - {
 - fprintf(stderr,
 - "mv: warning: Failed copying owner & group to '%s%s%s': %s\n",
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - errno = 0;
 - }
 - posix_fadvise(in, 0, 0, POSIX_FADV_SEQUENTIAL);
 - posix_fadvise(out, 0, 0, POSIX_FADV_SEQUENTIAL);
 - errno = 0;
 - if(auto_file_copy(in, out, src_status.st_size, 0) < 0) return -1;
 - int err = 0;
 - err += close(in);
 - err += close(out);
 - err += unlinkat(srcdir.fd, src, 0);
 - return err;
 - }
 - static int
 - rename_dir_entries(struct named_fd srcdir, struct named_fd destdir)
 - {
 - DIR *dirsrc = fdopendir(srcdir.fd);
 - if(dirsrc == NULL)
 - {
 - fprintf(stderr,
 - "mv: error: Failed fd-opening source directory '%s': %s\n",
 - srcdir.name ? srcdir.name : ".",
 - strerror(errno));
 - return -1;
 - }
 - while(true)
 - {
 - errno = 0;
 - struct dirent *dirsrc_ent = readdir(dirsrc);
 - if(dirsrc_ent == NULL)
 - {
 - if(errno == 0) break;
 - fprintf(stderr,
 - "mv: error: Failed reading source directory '%s': %s\n",
 - srcdir.name ? srcdir.name : ".",
 - strerror(errno));
 - closedir(dirsrc);
 - errno = 0;
 - return -1;
 - }
 - if(strcmp(dirsrc_ent->d_name, ".") == 0) continue;
 - if(strcmp(dirsrc_ent->d_name, "..") == 0) continue;
 - if(do_renameat(srcdir, dirsrc_ent->d_name, destdir, dirsrc_ent->d_name) < 0)
 - {
 - closedir(dirsrc);
 - return -1;
 - }
 - }
 - closedir(dirsrc);
 - return 0;
 - }
 - static int
 - do_renameat(struct named_fd srcdir,
 - const char *restrict src,
 - struct named_fd destdir,
 - const char *restrict dest)
 - {
 - if(destdir.fd == srcdir.fd && strcmp(src, dest) == 0)
 - {
 - fprintf(stderr, "mv: error: Passed to both source and destination: '%s'\n", src);
 - return -1;
 - }
 - struct stat src_status;
 - if(fstatat(srcdir.fd, src, &src_status, AT_SYMLINK_NOFOLLOW) < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed getting status for source file '%s%s%s': %s\n",
 - srcdir.name,
 - srcdir.sep,
 - src,
 - strerror(errno));
 - return -1;
 - }
 - if(S_ISLNK(src_status.st_mode))
 - {
 - struct stat src_link_status;
 - if(fstatat(srcdir.fd, src, &src_link_status, 0) == 0)
 - {
 - src_status = src_link_status;
 - }
 - }
 - errno = 0;
 - struct stat dest_status;
 - int ret = fstatat(destdir.fd, dest, &dest_status, 0);
 - if(ret < 0 && errno != ENOENT)
 - {
 - fprintf(stderr,
 - "mv: error: Failed getting status for destination file '%s%s%s': %s\n",
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - return -1;
 - }
 - errno = 0;
 - if(ret == 0)
 - {
 - if(dest_status.st_ino == src_status.st_ino && dest_status.st_dev == src_status.st_dev) return 0;
 - if(no_clob)
 - {
 - fprintf(stderr,
 - "mv: error: Destination file '%s%s%s' already exists\n",
 - destdir.name,
 - destdir.sep,
 - dest);
 - return -1;
 - }
 - if(!force)
 - {
 - if(interact)
 - {
 - if(!consentf("mv: Destination file '%s%s%s' already exists, overwrite? [yN] ",
 - destdir.name,
 - destdir.sep,
 - dest))
 - return 0;
 - }
 - else if(stdin_tty)
 - {
 - if(faccessat(destdir.fd, dest, W_OK, 0) == 0)
 - {
 - if(!consentf(
 - "mv: error: No write permissions for destination file '%s%s%s', overwrite? [yN] ",
 - destdir.name,
 - destdir.sep,
 - dest))
 - return 0;
 - }
 - else
 - {
 - errno = 0;
 - }
 - }
 - }
 - }
 - if(renameat(srcdir.fd, src, destdir.fd, dest) < 0)
 - {
 - switch(errno)
 - {
 - case EXDEV:
 - errno = 0;
 - if(S_ISDIR(src_status.st_mode))
 - {
 - char child_srcdir_name[PATH_MAX] = "";
 - snprintf(child_srcdir_name, PATH_MAX, "%s%s%s", srcdir.name, srcdir.sep, src);
 - struct named_fd child_srcdir = {
 - .fd = openat(srcdir.fd, src, O_RDONLY | O_DIRECTORY | O_CLOEXEC),
 - .name = child_srcdir_name,
 - .sep = "/",
 - };
 - if(child_srcdir.fd < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening source directory '%s%s%s': %s\n",
 - srcdir.name,
 - srcdir.sep,
 - src,
 - strerror(errno));
 - return -1;
 - }
 - if(mkdirat(destdir.fd, dest, src_status.st_mode) < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed creating destination directory '%s%s%s': %s\n",
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - return -1;
 - }
 - char child_destdir_name[PATH_MAX] = "";
 - snprintf(child_destdir_name, PATH_MAX, "%s%s%s", destdir.name, destdir.sep, dest);
 - struct named_fd child_destdir = {
 - .fd = openat(destdir.fd, dest, O_RDONLY | O_DIRECTORY | O_CLOEXEC),
 - .name = child_destdir_name,
 - .sep = "/",
 - };
 - if(child_destdir.fd < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening destination directory '%s%s%s': %s\n",
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - return -1;
 - }
 - if(rename_dir_entries(child_srcdir, child_destdir) < 0) return -1;
 - close(child_srcdir.fd);
 - close(child_destdir.fd);
 - if(unlinkat(srcdir.fd, src, AT_REMOVEDIR) < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed removing source directory '%s%s%s': %s\n",
 - srcdir.name,
 - srcdir.sep,
 - src,
 - strerror(errno));
 - return -1;
 - }
 - }
 - else
 - {
 - if(copy_file_unlink(srcdir, src, src_status, destdir, dest) < 0) return -1;
 - }
 - break;
 - case EISDIR:
 - case ENOTDIR:
 - if(destdir.fd != AT_FDCWD)
 - {
 - fprintf(
 - stderr, "mv: error: Failed moving '%s' into '%s': %s\n", src, dest, strerror(errno));
 - return -1;
 - }
 - int tmp_destdir = openat(destdir.fd, dest, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
 - if(tmp_destdir < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening destination directory '%s': %s\n",
 - dest,
 - strerror(errno));
 - return -1;
 - }
 - if(renameat(srcdir.fd, src, tmp_destdir, src) < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed moving '%s' into directory '%s': %s\n",
 - src,
 - dest,
 - strerror(errno));
 - return -1;
 - }
 - if(close(tmp_destdir) < 0)
 - {
 - fprintf(stderr, "mv: error: Failed closing directory '%s': %s\n", dest, strerror(errno));
 - return -1;
 - }
 - break;
 - default:
 - fprintf(stderr,
 - "mv: error: Failed moving '%s' to '%s%s%s': %s\n",
 - src,
 - destdir.name,
 - destdir.sep,
 - dest,
 - strerror(errno));
 - return -1;
 - }
 - }
 - if(verbose)
 - fprintf(stderr, "mv: renamed '%s' -> '%s%s%s'\n", src, destdir.name, destdir.sep, dest);
 - return 0;
 - }
 - static void
 - usage(void)
 - {
 - fprintf(stderr, "Usage: mv [-f|-i|-n] [-v] source dest\n");
 - fprintf(stderr, " mv [-f|-i|-n] [-v] source... destdir\n");
 - fprintf(stderr, " mv [-f|-i|-n] [-v] -t destdir source...\n");
 - }
 - int
 - main(int argc, char *argv[])
 - {
 - struct named_fd destdir = {
 - .fd = AT_FDCWD,
 - .name = "",
 - .sep = "",
 - };
 - struct named_fd srcdir = {
 - .fd = AT_FDCWD,
 - .name = "",
 - .sep = "",
 - };
 - #ifdef HAS_GETOPT_LONG
 - // Strictly for GNUisms compatibility so no long-only options
 - // clang-format off
 - static struct option opts[] = {
 - {"force", no_argument, NULL, 'f'},
 - {"interactive", no_argument, NULL, 'i'},
 - {"no-clobber", no_argument, NULL, 'n'},
 - {"target-directory", required_argument, NULL, 't'},
 - {"verbose", no_argument, NULL, 'v'},
 - {0, 0, 0, 0},
 - };
 - // clang-format on
 - // Need + as first character to get POSIX-style option parsing
 - for(int c = -1; (c = getopt_long(argc, argv, "+:fint:v", opts, NULL)) != -1;)
 - #else
 - for(int c = -1; (c = getopt_nolong(argc, argv, ":fint:v")) != -1;)
 - #endif
 - {
 - switch(c)
 - {
 - case 'f':
 - force = true;
 - interact = false;
 - no_clob = false;
 - break;
 - case 'i':
 - force = false;
 - interact = true;
 - no_clob = false;
 - break;
 - case 'n':
 - force = false;
 - interact = false;
 - no_clob = true;
 - break;
 - case 't':
 - destdir.name = optarg;
 - destdir.sep = "/";
 - destdir.fd = open(optarg, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
 - if(destdir.fd < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening destination directory '%s': %s\n",
 - optarg,
 - strerror(errno));
 - return 1;
 - }
 - break;
 - case 'v':
 - verbose = true;
 - break;
 - case ':':
 - fprintf(stderr, "mv: error: Missing operand for option: '-%c'\n", optopt);
 - usage();
 - return 1;
 - case '?':
 - GETOPT_UNKNOWN_OPT
 - usage();
 - return 1;
 - }
 - }
 - argc -= optind;
 - argv += optind;
 - char *lc_all = setlocale(LC_ALL, "");
 - if(lc_all == NULL)
 - {
 - fprintf(stderr,
 - "%s: warning: Failed loading locales. setlocale(LC_ALL, \"\"): %s\n",
 - argv0,
 - strerror(errno));
 - }
 - errno = 0;
 - consent_init();
 - stdin_tty = isatty(STDIN_FILENO);
 - if(!stdin_tty) errno = 0;
 - if(destdir.fd == AT_FDCWD)
 - {
 - if(argc <= 1)
 - {
 - fprintf(stderr, "mv: error: Not enough operands, %d given, expect >= 2\n", argc);
 - return 1;
 - }
 - struct stat dest_status;
 - int ret_stat = fstatat(destdir.fd, argv[1], &dest_status, 0);
 - if(
 - // clang-format off
 - argc == 2 && (
 - (ret_stat != 0 && errno == ENOENT) ||
 - (ret_stat == 0 && !S_ISDIR(dest_status.st_mode))
 - )
 - // clang-format on
 - )
 - {
 - int ret = do_renameat(srcdir, argv[0], destdir, argv[1]);
 - consent_finish();
 - return ret < 0 ? 1 : 0;
 - }
 - errno = 0;
 - argc--;
 - destdir.name = argv[argc];
 - destdir.sep = "/";
 - destdir.fd = open(destdir.name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
 - if(destdir.fd < 0)
 - {
 - fprintf(stderr,
 - "mv: error: Failed opening destination directory '%s': %s\n",
 - destdir.name,
 - strerror(errno));
 - consent_finish();
 - return 1;
 - }
 - }
 - for(int i = 0; i < argc; i++)
 - {
 - static char arg[PATH_MAX] = "";
 - lib_strlcpy(arg, argv[i], PATH_MAX);
 - char *filename = basename(arg);
 - if(do_renameat(srcdir, argv[i], destdir, filename) < 0)
 - {
 - consent_finish();
 - return 1;
 - }
 - }
 - consent_finish();
 - if(close(destdir.fd) < 0)
 - {
 - fprintf(
 - stderr, "mv: error: Failed closing directory '%s': %s\n", destdir.name, strerror(errno));
 - return 1;
 - }
 - return 0;
 - }