logo

utils-std

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

mv.c (10832B)


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