logo

utils-std

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

mv.c (13692B)


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