logo

utils-std

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

touch.c (7307B)


  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 _DEFAULT_SOURCE // tm_gmtoff/tm_zone
  5. #define _XOPEN_SOURCE 700 // strptime (NetBSD)
  6. #define _POSIX_C_SOURCE 200809L // O_NOFOLLOW, st_atim/st_mtim
  7. #ifdef __OpenBSD__ // as seen on OpenBSD 7.7
  8. #define _BSD_SOURCE // O_NOFOLLOW
  9. #endif
  10. #include "../config.h"
  11. #include "../lib/bitmasks.h" /* FIELD_* */
  12. #include "../libutils/datetime_parse.h" /* datetime_parse */
  13. #include "../libutils/getopt_nolong.h"
  14. #include <assert.h>
  15. #include <errno.h> /* errno */
  16. #include <fcntl.h> /* open */
  17. #include <stdbool.h> /* bool */
  18. #include <stdio.h> /* perror */
  19. #include <string.h> /* strrchr, strlen */
  20. #include <sys/stat.h> /* futimens, stat, utimensat */
  21. #include <time.h> /* mktime */
  22. #include <unistd.h> /* getopt, opt*, close */
  23. #ifdef HAS_GETOPT_LONG
  24. #include <getopt.h>
  25. #endif
  26. const char *argv0 = "touch";
  27. // [[CC]YY]MMDDhhmm[.SS]
  28. static struct timespec
  29. opt_t_parse(char *arg, const char **errstr)
  30. {
  31. struct timespec res = {.tv_sec = 0, .tv_nsec = 0};
  32. struct tm tm = {
  33. .tm_year = 0,
  34. .tm_mon = 0,
  35. .tm_mday = 0,
  36. .tm_hour = 0,
  37. .tm_min = 0,
  38. .tm_sec = 0,
  39. .tm_isdst = -1, // unknown if DST is in effect
  40. };
  41. size_t len = strlen(arg);
  42. char *secs = strrchr(arg, '.');
  43. if(secs != NULL)
  44. {
  45. if(&arg[len - 3] != secs)
  46. {
  47. *errstr = "Wrong location of second separator";
  48. return res;
  49. }
  50. secs[0] = 0;
  51. secs++;
  52. len -= 3;
  53. char *s = strptime(secs, "%S", &tm);
  54. if(s == NULL)
  55. {
  56. *errstr = strerror(errno);
  57. errno = 0;
  58. return res;
  59. }
  60. if(s[0] != 0)
  61. {
  62. *errstr = "Extraneous character in seconds";
  63. return res;
  64. }
  65. }
  66. const char *fmt = NULL;
  67. switch(len)
  68. {
  69. case 8:
  70. fmt = "%m%d%H%M"; // MMDDhhmm
  71. break;
  72. case 10:
  73. fmt = "%y%m%d%H%M"; // YYMMDDhhmm
  74. break;
  75. case 12:
  76. fmt = "%Y%m%d%H%M"; // CCYYMMDDhhmm
  77. break;
  78. default:
  79. *errstr = "Invalid datetime";
  80. return res;
  81. }
  82. assert(fmt != NULL);
  83. char *dt = strptime(arg, fmt, &tm);
  84. if(dt == NULL)
  85. {
  86. *errstr = strerror(errno);
  87. errno = 0;
  88. return res;
  89. }
  90. if(dt[0] != 0)
  91. {
  92. *errstr = "Extraneous character in datetime";
  93. return res;
  94. }
  95. res.tv_sec = mktime(&tm);
  96. if(res.tv_sec == (time_t)-1)
  97. {
  98. *errstr = strerror(errno);
  99. errno = 0;
  100. return res;
  101. }
  102. // As observed on FreeBSD 14.0, non-errorneous mktime can still end up setting errno
  103. // cf. https://builds.sr.ht/~lanodan/job/1181509
  104. errno = 0;
  105. return res;
  106. }
  107. int
  108. main(int argc, char *argv[])
  109. {
  110. bool ch_atime = false, ch_mtime = false;
  111. char *ref_file = NULL;
  112. struct timespec times[2] = {
  113. {.tv_sec = 0, .tv_nsec = UTIME_OMIT}, // access
  114. {.tv_sec = 0, .tv_nsec = UTIME_OMIT} // modification
  115. };
  116. struct timespec target = {0, UTIME_NOW};
  117. int open_flags = O_WRONLY | O_CREAT | O_NOCTTY;
  118. int utimensat_flags = 0;
  119. #ifdef HAS_GETOPT_LONG
  120. // Strictly for GNUisms compatibility so no long-only options
  121. // clang-format off
  122. static struct option opts[] = {
  123. {"no-create", no_argument, NULL, 'c'},
  124. {"date", required_argument, NULL, 'd'},
  125. {"no-dereference", no_argument, NULL, 'h'},
  126. {"reference", required_argument, NULL, 'r'},
  127. {0, 0, 0, 0},
  128. };
  129. // clang-format on
  130. // Need + as first character to get POSIX-style option parsing
  131. for(int c = -1; (c = getopt_long(argc, argv, "+:acfhmr:t:d:", opts, NULL)) != -1;)
  132. #else
  133. for(int c = -1; (c = getopt_nolong(argc, argv, ":acfhmr:t:d:")) != -1;)
  134. #endif
  135. {
  136. const char *errstr = NULL;
  137. switch(c)
  138. {
  139. case 'a':
  140. ch_atime = true;
  141. break;
  142. case 'c':
  143. FIELD_CLR(open_flags, O_CREAT);
  144. break;
  145. case 'f':
  146. /* Legacy from BSD, ignored
  147. *
  148. * As of 2024-12-05:
  149. *
  150. * coreutils: Ignored since first commit in 1992
  151. * OpenBSD: Ignored since OpenBSD 3.8 (commit in 2005)
  152. * NetBSD: Ignored since NetBSD 6.0 (commit in 2011)
  153. * FreeBSD: Ignored since FreeBSD 10 (commit in 2012)
  154. * BusyBox: Ignored (and not documented)
  155. * illumos: Ignored in SystemV emulation, supported otherwise
  156. */
  157. break;
  158. case 'h':
  159. FIELD_SET(open_flags, O_NOFOLLOW);
  160. FIELD_SET(utimensat_flags, AT_SYMLINK_NOFOLLOW);
  161. break;
  162. case 'm':
  163. ch_mtime = true;
  164. break;
  165. case 'r':
  166. ref_file = optarg;
  167. break;
  168. case 't':
  169. target = opt_t_parse(optarg, &errstr);
  170. if(errstr != NULL)
  171. {
  172. fprintf(stderr, "touch: error: opt_t_parse(\"%s\", …): %s\n", optarg, errstr);
  173. return 1;
  174. }
  175. break;
  176. case 'd':
  177. {
  178. long nsec = 0;
  179. struct tm iso_res = {
  180. .tm_year = 0,
  181. .tm_mon = 0,
  182. .tm_mday = 0,
  183. .tm_hour = 0,
  184. .tm_min = 0,
  185. .tm_sec = 0,
  186. .tm_isdst = -1, // unknown if DST is in effect
  187. .tm_gmtoff = 0,
  188. .tm_zone = NULL,
  189. };
  190. char *s = datetime_parse(optarg, &iso_res, &nsec, &errstr);
  191. if(errstr != NULL)
  192. {
  193. fprintf(stderr, "touch: error: datetime_parse(\"%s\", …): %s\n", optarg, errstr);
  194. return 1;
  195. }
  196. if(s == NULL)
  197. {
  198. fprintf(stderr, "touch: error: datetime_parse(\"%s\", …) returned NULL\n", optarg);
  199. return 1;
  200. }
  201. target.tv_sec = mktime_tz(&iso_res);
  202. target.tv_nsec = nsec;
  203. if(target.tv_sec == (time_t)-1)
  204. {
  205. fprintf(stderr, "touch: error: mktime: %s\n", strerror(errno));
  206. return 1;
  207. }
  208. errno = 0;
  209. break;
  210. }
  211. case ':':
  212. fprintf(stderr, "touch: error: Missing operand for option: '-%c'\n", optopt);
  213. return 1;
  214. case '?':
  215. GETOPT_UNKNOWN_OPT
  216. return 1;
  217. }
  218. }
  219. argc -= optind;
  220. argv += optind;
  221. // When neither -a nor -m are specified, change both
  222. if(!ch_atime && !ch_mtime)
  223. {
  224. ch_atime = true;
  225. ch_mtime = true;
  226. }
  227. if(ref_file == NULL)
  228. {
  229. if(ch_atime) times[0] = target;
  230. if(ch_mtime) times[1] = target;
  231. }
  232. else
  233. {
  234. struct stat ref;
  235. if(stat(ref_file, &ref) != 0)
  236. {
  237. fprintf(stderr,
  238. "touch: error: Failed getting status of file '%s': %s\n",
  239. ref_file,
  240. strerror(errno));
  241. return 1;
  242. }
  243. if(ch_atime)
  244. {
  245. times[0] = ref.st_atim;
  246. }
  247. if(ch_mtime)
  248. {
  249. times[1] = ref.st_mtim;
  250. }
  251. }
  252. for(int i = 0; i < argc; i++)
  253. {
  254. const char *file = argv[i];
  255. int fd = open(file, open_flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
  256. if(fd == -1)
  257. {
  258. /* BSD nonsense */
  259. #ifdef EMLINK
  260. if(errno == EMLINK) errno = ELOOP;
  261. #endif
  262. #ifdef EFTYPE
  263. if(errno == EFTYPE) errno = ELOOP;
  264. #endif
  265. if(errno == EISDIR || (errno == ELOOP && FIELD_MATCH(open_flags, O_NOFOLLOW)))
  266. {
  267. if(utimensat(AT_FDCWD, file, times, utimensat_flags) != 0)
  268. {
  269. fprintf(stderr,
  270. "touch: error: Failed setting new times on file '%s': %s\n",
  271. file,
  272. strerror(errno));
  273. return 1;
  274. }
  275. continue;
  276. }
  277. if(errno != ENOENT || FIELD_MATCH(open_flags, O_CREAT))
  278. fprintf(stderr, "touch: error: Failed opening file '%s': %s\n", file, strerror(errno));
  279. return 1;
  280. }
  281. if(futimens(fd, times) != 0)
  282. {
  283. fprintf(stderr,
  284. "touch: error: Failed setting new times on file '%s': %s\n",
  285. file,
  286. strerror(errno));
  287. return 1;
  288. }
  289. if(close(fd) != 0)
  290. {
  291. fprintf(stderr,
  292. "touch: error: Failed closing file-descriptor for file '%s': %s\n",
  293. file,
  294. strerror(errno));
  295. return 1;
  296. }
  297. }
  298. return 0;
  299. }