logo

utils-std

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

mv.c (14100B)


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