logo

utils-std

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

mv.c (12268B)


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