logo

utils-std

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

mv.c (11683B)


  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. int c = -1;
  349. while((c = getopt(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. fprintf(stderr, "mv: error: Unrecognised option: '-%c'\n", optopt);
  390. usage();
  391. return 1;
  392. }
  393. }
  394. argc -= optind;
  395. argv += optind;
  396. setlocale(LC_ALL, "");
  397. errno = 0;
  398. consent_init();
  399. stdin_tty = isatty(STDIN_FILENO);
  400. if(!stdin_tty) errno = 0;
  401. if(destdir.fd == AT_FDCWD)
  402. {
  403. if(argc <= 1)
  404. {
  405. fprintf(stderr, "mv: error: Not enough operands, %d given, expect >= 2\n", argc);
  406. return 1;
  407. }
  408. struct stat dest_status;
  409. int ret_stat = fstatat(destdir.fd, argv[1], &dest_status, 0);
  410. if(
  411. // clang-format off
  412. argc == 2 && (
  413. (ret_stat != 0 && errno == ENOENT) ||
  414. (ret_stat == 0 && !S_ISDIR(dest_status.st_mode))
  415. )
  416. // clang-format on
  417. )
  418. {
  419. int ret = do_renameat(srcdir, argv[0], destdir, argv[1]);
  420. consent_finish();
  421. return ret < 0 ? 1 : 0;
  422. }
  423. errno = 0;
  424. argc--;
  425. destdir.name = argv[argc];
  426. destdir.sep = "/";
  427. destdir.fd = open(destdir.name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
  428. if(destdir.fd < 0)
  429. {
  430. fprintf(stderr,
  431. "mv: error: Failed opening destination directory '%s': %s\n",
  432. destdir.name,
  433. strerror(errno));
  434. consent_finish();
  435. return 1;
  436. }
  437. }
  438. for(int i = 0; i < argc; i++)
  439. {
  440. char arg[PATH_MAX] = "";
  441. strcpy(arg, argv[i]);
  442. char *filename = basename(arg);
  443. if(do_renameat(srcdir, argv[i], destdir, filename) < 0)
  444. {
  445. consent_finish();
  446. return 1;
  447. }
  448. }
  449. consent_finish();
  450. if(close(destdir.fd) < 0)
  451. {
  452. fprintf(
  453. stderr, "mv: error: Failed closing directory '%s': %s\n", destdir.name, strerror(errno));
  454. return 1;
  455. }
  456. return 0;
  457. }