mv.c (11683B)
- // 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 "../lib/consent.h"
- #include "../lib/fs.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
- // 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;
- }
- if(auto_file_copy(in, out, src_status.st_size, 0) < 0) return -1;
- return unlinkat(srcdir.fd, src, 0);
- }
- 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;
- }
- }
- 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(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 = "",
- };
- int c = -1;
- while((c = getopt(argc, argv, ":fint:v")) != -1)
- {
- 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 '?':
- fprintf(stderr, "mv: error: Unrecognised option: '-%c'\n", optopt);
- usage();
- return 1;
- }
- }
- argc -= optind;
- argv += optind;
- setlocale(LC_ALL, "");
- 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++)
- {
- char arg[PATH_MAX] = "";
- strcpy(arg, argv[i]);
- 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;
- }