logo

utils-std

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

mv.c (12230B)


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