logo

utils-std

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

mv.c (12870B)


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