logo

utils-std

Collection of commonly available Unix tools git clone https://anongit.hacktivis.me/git/utils-std.git/

mv.c (13015B)


  1. // utils-std: Collection of commonly available Unix tools
  2. // SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
  3. // SPDX-License-Identifier: MPL-2.0
  4. #define _POSIX_C_SOURCE 200809L
  5. #define _GNU_SOURCE // copy_file_range
  6. #define _FILE_OFFSET_BITS 64
  7. // NetBSD <10 hides fdopendir behind _NETBSD_SOURCE
  8. #if __NetBSD_Version__ < 1000000000
  9. #define _NETBSD_SOURCE
  10. #endif
  11. #include "../config.h"
  12. #include "../libutils/consent.h"
  13. #include "../libutils/fs.h"
  14. #include "../libutils/getopt_nolong.h"
  15. #include "../libutils/lib_string.h"
  16. #include <dirent.h> // fdopendir
  17. #include <errno.h>
  18. #include <fcntl.h> // open
  19. #include <libgen.h> // basename
  20. #include <limits.h> // PATH_MAX
  21. #include <locale.h> // setlocale
  22. #include <stdbool.h>
  23. #include <stdint.h> // SIZE_MAX
  24. #include <stdio.h> // fprintf, rename
  25. #include <string.h> // strcmp
  26. #include <sys/stat.h> // stat, S_ISDIR
  27. #include <unistd.h> // getopt
  28. #ifdef HAS_GETOPT_LONG
  29. #include <getopt.h>
  30. #endif
  31. // Workaround against GNU glibc
  32. // https://sourceware.org/bugzilla/show_bug.cgi?id=18228
  33. #if defined(__linux__) && !defined(O_SEARCH)
  34. // As defined in musl
  35. #define O_SEARCH O_PATH
  36. #endif
  37. const char *argv0 = "mv";
  38. bool no_clob = false, force = false, interact = false, verbose = false;
  39. static int stdin_tty = 0;
  40. struct named_fd
  41. {
  42. int fd;
  43. const char *name;
  44. const char *sep;
  45. };
  46. static int do_renameat(struct named_fd srcdir,
  47. const char *restrict src,
  48. struct named_fd destdir,
  49. const char *restrict dest);
  50. static int
  51. copy_file_unlink(struct named_fd srcdir,
  52. const char *restrict src,
  53. struct stat src_status,
  54. struct named_fd destdir,
  55. const char *restrict dest)
  56. {
  57. int in = openat(srcdir.fd, src, O_RDONLY | O_NOCTTY);
  58. if(in < 0)
  59. {
  60. fprintf(stderr,
  61. "mv: error: Failed opening source '%s%s%s': %s\n",
  62. srcdir.name,
  63. srcdir.sep,
  64. src,
  65. strerror(errno));
  66. errno = 0;
  67. return -1;
  68. }
  69. int out = openat(destdir.fd, dest, O_WRONLY | O_CREAT | O_NOCTTY, src_status.st_mode);
  70. if(out < 0)
  71. {
  72. fprintf(stderr,
  73. "mv: error: Failed opening destination '%s%s%s': %s\n",
  74. destdir.name,
  75. destdir.sep,
  76. dest,
  77. strerror(errno));
  78. errno = 0;
  79. return -1;
  80. }
  81. const struct timespec times[2] = {src_status.st_atim, src_status.st_mtim};
  82. if(futimens(out, times) != 0)
  83. {
  84. fprintf(stderr,
  85. "mv: warning: Failed copying access & modification times to '%s%s%s': %s\n",
  86. destdir.name,
  87. destdir.sep,
  88. dest,
  89. strerror(errno));
  90. errno = 0;
  91. }
  92. if(fchown(out, src_status.st_uid, src_status.st_gid) != 0)
  93. {
  94. fprintf(stderr,
  95. "mv: warning: Failed copying owner & group to '%s%s%s': %s\n",
  96. destdir.name,
  97. destdir.sep,
  98. dest,
  99. strerror(errno));
  100. errno = 0;
  101. }
  102. posix_fadvise(in, 0, 0, POSIX_FADV_SEQUENTIAL);
  103. posix_fadvise(out, 0, 0, POSIX_FADV_SEQUENTIAL);
  104. errno = 0;
  105. if(auto_file_copy(in, out, src_status.st_size, 0) < 0) return -1;
  106. int err = 0;
  107. err += close(in);
  108. err += close(out);
  109. err += unlinkat(srcdir.fd, src, 0);
  110. return err;
  111. }
  112. static int
  113. rename_dir_entries(struct named_fd srcdir, struct named_fd destdir)
  114. {
  115. DIR *dirsrc = fdopendir(srcdir.fd);
  116. if(dirsrc == NULL)
  117. {
  118. fprintf(stderr,
  119. "mv: error: Failed fd-opening source directory '%s': %s\n",
  120. srcdir.name ? srcdir.name : ".",
  121. strerror(errno));
  122. return -1;
  123. }
  124. while(true)
  125. {
  126. errno = 0;
  127. struct dirent *dirsrc_ent = readdir(dirsrc);
  128. if(dirsrc_ent == NULL)
  129. {
  130. if(errno == 0) break;
  131. fprintf(stderr,
  132. "mv: error: Failed reading source directory '%s': %s\n",
  133. srcdir.name ? srcdir.name : ".",
  134. strerror(errno));
  135. closedir(dirsrc);
  136. errno = 0;
  137. return -1;
  138. }
  139. if(strcmp(dirsrc_ent->d_name, ".") == 0) continue;
  140. if(strcmp(dirsrc_ent->d_name, "..") == 0) continue;
  141. if(do_renameat(srcdir, dirsrc_ent->d_name, destdir, dirsrc_ent->d_name) < 0)
  142. {
  143. closedir(dirsrc);
  144. return -1;
  145. }
  146. }
  147. closedir(dirsrc);
  148. return 0;
  149. }
  150. static int
  151. do_renameat(struct named_fd srcdir,
  152. const char *restrict src,
  153. struct named_fd destdir,
  154. const char *restrict dest)
  155. {
  156. if(destdir.fd == srcdir.fd && strcmp(src, dest) == 0)
  157. {
  158. fprintf(stderr, "mv: error: Passed to both source and destination: '%s'\n", src);
  159. return -1;
  160. }
  161. struct stat src_status;
  162. if(fstatat(srcdir.fd, src, &src_status, AT_SYMLINK_NOFOLLOW) < 0)
  163. {
  164. fprintf(stderr,
  165. "mv: error: Failed getting status for source file '%s%s%s': %s\n",
  166. srcdir.name,
  167. srcdir.sep,
  168. src,
  169. strerror(errno));
  170. return -1;
  171. }
  172. if(S_ISLNK(src_status.st_mode))
  173. {
  174. struct stat src_link_status;
  175. if(fstatat(srcdir.fd, src, &src_link_status, 0) == 0)
  176. {
  177. src_status = src_link_status;
  178. }
  179. }
  180. errno = 0;
  181. struct stat dest_status;
  182. int ret = fstatat(destdir.fd, dest, &dest_status, 0);
  183. if(ret < 0 && errno != ENOENT)
  184. {
  185. fprintf(stderr,
  186. "mv: error: Failed getting status for destination file '%s%s%s': %s\n",
  187. destdir.name,
  188. destdir.sep,
  189. dest,
  190. strerror(errno));
  191. return -1;
  192. }
  193. errno = 0;
  194. if(ret == 0)
  195. {
  196. if(dest_status.st_ino == src_status.st_ino && dest_status.st_dev == src_status.st_dev) return 0;
  197. if(no_clob)
  198. {
  199. fprintf(stderr,
  200. "mv: error: Destination file '%s%s%s' already exists\n",
  201. destdir.name,
  202. destdir.sep,
  203. dest);
  204. return -1;
  205. }
  206. if(!force)
  207. {
  208. if(interact)
  209. {
  210. if(!consentf("mv: Destination file '%s%s%s' already exists, overwrite? [yN] ",
  211. destdir.name,
  212. destdir.sep,
  213. dest))
  214. return 0;
  215. }
  216. else if(stdin_tty)
  217. {
  218. if(faccessat(destdir.fd, dest, W_OK, 0) == 0)
  219. {
  220. if(!consentf(
  221. "mv: error: No write permissions for destination file '%s%s%s', overwrite? [yN] ",
  222. destdir.name,
  223. destdir.sep,
  224. dest))
  225. return 0;
  226. }
  227. else
  228. {
  229. errno = 0;
  230. }
  231. }
  232. }
  233. }
  234. if(renameat(srcdir.fd, src, destdir.fd, dest) < 0)
  235. {
  236. switch(errno)
  237. {
  238. case EXDEV:
  239. errno = 0;
  240. if(S_ISDIR(src_status.st_mode))
  241. {
  242. char child_srcdir_name[PATH_MAX] = "";
  243. snprintf(child_srcdir_name, PATH_MAX, "%s%s%s", srcdir.name, srcdir.sep, src);
  244. struct named_fd child_srcdir = {
  245. .fd = openat(srcdir.fd, src, O_RDONLY | O_DIRECTORY | O_CLOEXEC),
  246. .name = child_srcdir_name,
  247. .sep = "/",
  248. };
  249. if(child_srcdir.fd < 0)
  250. {
  251. fprintf(stderr,
  252. "mv: error: Failed opening source directory '%s%s%s': %s\n",
  253. srcdir.name,
  254. srcdir.sep,
  255. src,
  256. strerror(errno));
  257. return -1;
  258. }
  259. if(mkdirat(destdir.fd, dest, src_status.st_mode) < 0)
  260. {
  261. fprintf(stderr,
  262. "mv: error: Failed creating destination directory '%s%s%s': %s\n",
  263. destdir.name,
  264. destdir.sep,
  265. dest,
  266. strerror(errno));
  267. return -1;
  268. }
  269. char child_destdir_name[PATH_MAX] = "";
  270. snprintf(child_destdir_name, PATH_MAX, "%s%s%s", destdir.name, destdir.sep, dest);
  271. struct named_fd child_destdir = {
  272. .fd = openat(destdir.fd, dest, O_RDONLY | O_DIRECTORY | O_CLOEXEC),
  273. .name = child_destdir_name,
  274. .sep = "/",
  275. };
  276. if(child_destdir.fd < 0)
  277. {
  278. fprintf(stderr,
  279. "mv: error: Failed opening destination directory '%s%s%s': %s\n",
  280. destdir.name,
  281. destdir.sep,
  282. dest,
  283. strerror(errno));
  284. return -1;
  285. }
  286. if(rename_dir_entries(child_srcdir, child_destdir) < 0) return -1;
  287. close(child_srcdir.fd);
  288. close(child_destdir.fd);
  289. if(unlinkat(srcdir.fd, src, AT_REMOVEDIR) < 0)
  290. {
  291. fprintf(stderr,
  292. "mv: error: Failed removing source directory '%s%s%s': %s\n",
  293. srcdir.name,
  294. srcdir.sep,
  295. src,
  296. strerror(errno));
  297. return -1;
  298. }
  299. }
  300. else
  301. {
  302. if(copy_file_unlink(srcdir, src, src_status, destdir, dest) < 0) return -1;
  303. }
  304. break;
  305. case EISDIR:
  306. case ENOTDIR:
  307. if(destdir.fd != AT_FDCWD)
  308. {
  309. fprintf(
  310. stderr, "mv: error: Failed moving '%s' into '%s': %s\n", src, dest, strerror(errno));
  311. return -1;
  312. }
  313. int tmp_destdir = openat(destdir.fd, dest, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
  314. if(tmp_destdir < 0)
  315. {
  316. fprintf(stderr,
  317. "mv: error: Failed opening destination directory '%s': %s\n",
  318. dest,
  319. strerror(errno));
  320. return -1;
  321. }
  322. if(renameat(srcdir.fd, src, tmp_destdir, src) < 0)
  323. {
  324. fprintf(stderr,
  325. "mv: error: Failed moving '%s' into directory '%s': %s\n",
  326. src,
  327. dest,
  328. strerror(errno));
  329. return -1;
  330. }
  331. if(close(tmp_destdir) < 0)
  332. {
  333. fprintf(stderr, "mv: error: Failed closing directory '%s': %s\n", dest, strerror(errno));
  334. return -1;
  335. }
  336. break;
  337. default:
  338. fprintf(stderr,
  339. "mv: error: Failed moving '%s' to '%s%s%s': %s\n",
  340. src,
  341. destdir.name,
  342. destdir.sep,
  343. dest,
  344. strerror(errno));
  345. return -1;
  346. }
  347. }
  348. if(verbose)
  349. fprintf(stderr, "mv: renamed '%s' -> '%s%s%s'\n", src, destdir.name, destdir.sep, dest);
  350. return 0;
  351. }
  352. static void
  353. usage(void)
  354. {
  355. fprintf(stderr, "Usage: mv [-f|-i|-n] [-v] source dest\n");
  356. fprintf(stderr, " mv [-f|-i|-n] [-v] source... destdir\n");
  357. fprintf(stderr, " mv [-f|-i|-n] [-v] -t destdir source...\n");
  358. }
  359. int
  360. main(int argc, char *argv[])
  361. {
  362. struct named_fd destdir = {
  363. .fd = AT_FDCWD,
  364. .name = "",
  365. .sep = "",
  366. };
  367. struct named_fd srcdir = {
  368. .fd = AT_FDCWD,
  369. .name = "",
  370. .sep = "",
  371. };
  372. #ifdef HAS_GETOPT_LONG
  373. // Strictly for GNUisms compatibility so no long-only options
  374. // clang-format off
  375. static struct option opts[] = {
  376. {"force", no_argument, NULL, 'f'},
  377. {"interactive", no_argument, NULL, 'i'},
  378. {"no-clobber", no_argument, NULL, 'n'},
  379. {"target-directory", required_argument, NULL, 't'},
  380. {"verbose", no_argument, NULL, 'v'},
  381. {0, 0, 0, 0},
  382. };
  383. // clang-format on
  384. // Need + as first character to get POSIX-style option parsing
  385. for(int c = -1; (c = getopt_long(argc, argv, "+:fint:v", opts, NULL)) != -1;)
  386. #else
  387. for(int c = -1; (c = getopt_nolong(argc, argv, ":fint:v")) != -1;)
  388. #endif
  389. {
  390. switch(c)
  391. {
  392. case 'f':
  393. force = true;
  394. interact = false;
  395. no_clob = false;
  396. break;
  397. case 'i':
  398. force = false;
  399. interact = true;
  400. no_clob = false;
  401. break;
  402. case 'n':
  403. force = false;
  404. interact = false;
  405. no_clob = true;
  406. break;
  407. case 't':
  408. destdir.name = optarg;
  409. destdir.sep = "/";
  410. destdir.fd = open(optarg, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
  411. if(destdir.fd < 0)
  412. {
  413. fprintf(stderr,
  414. "mv: error: Failed opening destination directory '%s': %s\n",
  415. optarg,
  416. strerror(errno));
  417. return 1;
  418. }
  419. break;
  420. case 'v':
  421. verbose = true;
  422. break;
  423. case ':':
  424. fprintf(stderr, "mv: error: Missing operand for option: '-%c'\n", optopt);
  425. usage();
  426. return 1;
  427. case '?':
  428. GETOPT_UNKNOWN_OPT
  429. usage();
  430. return 1;
  431. }
  432. }
  433. argc -= optind;
  434. argv += optind;
  435. char *lc_all = setlocale(LC_ALL, "");
  436. if(lc_all == NULL)
  437. {
  438. fprintf(stderr,
  439. "%s: warning: Failed loading locales. setlocale(LC_ALL, \"\"): %s\n",
  440. argv0,
  441. strerror(errno));
  442. }
  443. errno = 0;
  444. consent_init();
  445. stdin_tty = isatty(STDIN_FILENO);
  446. if(!stdin_tty) errno = 0;
  447. if(destdir.fd == AT_FDCWD)
  448. {
  449. if(argc <= 1)
  450. {
  451. fprintf(stderr, "mv: error: Not enough operands, %d given, expect >= 2\n", argc);
  452. return 1;
  453. }
  454. struct stat dest_status;
  455. int ret_stat = fstatat(destdir.fd, argv[1], &dest_status, 0);
  456. if(
  457. // clang-format off
  458. argc == 2 && (
  459. (ret_stat != 0 && errno == ENOENT) ||
  460. (ret_stat == 0 && !S_ISDIR(dest_status.st_mode))
  461. )
  462. // clang-format on
  463. )
  464. {
  465. int ret = do_renameat(srcdir, argv[0], destdir, argv[1]);
  466. consent_finish();
  467. return ret < 0 ? 1 : 0;
  468. }
  469. errno = 0;
  470. argc--;
  471. destdir.name = argv[argc];
  472. destdir.sep = "/";
  473. destdir.fd = open(destdir.name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
  474. if(destdir.fd < 0)
  475. {
  476. fprintf(stderr,
  477. "mv: error: Failed opening destination directory '%s': %s\n",
  478. destdir.name,
  479. strerror(errno));
  480. consent_finish();
  481. return 1;
  482. }
  483. }
  484. for(int i = 0; i < argc; i++)
  485. {
  486. static char arg[PATH_MAX] = "";
  487. lib_strlcpy(arg, argv[i], PATH_MAX);
  488. char *filename = basename(arg);
  489. if(do_renameat(srcdir, argv[i], destdir, filename) < 0)
  490. {
  491. consent_finish();
  492. return 1;
  493. }
  494. }
  495. consent_finish();
  496. if(close(destdir.fd) < 0)
  497. {
  498. fprintf(
  499. stderr, "mv: error: Failed closing directory '%s': %s\n", destdir.name, strerror(errno));
  500. return 1;
  501. }
  502. return 0;
  503. }