logo

utils-std

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

mv.c (11924B)


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