logo

utils-std

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

mv.c (11681B)


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