logo

utils-std

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

mv.c (11767B)


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