logo

utils-std

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

mv.c (12957B)


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