logo

utils-std

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

touch.c (6843B)


  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. char *s = datetime_parse(optarg, &target.tv_sec, &target.tv_nsec, &errstr);
  179. if(errstr != NULL)
  180. {
  181. fprintf(stderr, "touch: error: datetime_parse(\"%s\", …): %s\n", optarg, errstr);
  182. return 1;
  183. }
  184. if(s == NULL)
  185. {
  186. fprintf(stderr, "touch: error: datetime_parse(\"%s\", …) returned NULL\n", optarg);
  187. return 1;
  188. }
  189. break;
  190. }
  191. case ':':
  192. fprintf(stderr, "touch: error: Missing operand for option: '-%c'\n", optopt);
  193. return 1;
  194. case '?':
  195. GETOPT_UNKNOWN_OPT
  196. return 1;
  197. }
  198. }
  199. argc -= optind;
  200. argv += optind;
  201. // When neither -a nor -m are specified, change both
  202. if(!ch_atime && !ch_mtime)
  203. {
  204. ch_atime = true;
  205. ch_mtime = true;
  206. }
  207. if(ref_file == NULL)
  208. {
  209. if(ch_atime) times[0] = target;
  210. if(ch_mtime) times[1] = target;
  211. }
  212. else
  213. {
  214. struct stat ref;
  215. if(stat(ref_file, &ref) != 0)
  216. {
  217. fprintf(stderr,
  218. "touch: error: Failed getting status of file '%s': %s\n",
  219. ref_file,
  220. strerror(errno));
  221. return 1;
  222. }
  223. if(ch_atime)
  224. {
  225. times[0] = ref.st_atim;
  226. }
  227. if(ch_mtime)
  228. {
  229. times[1] = ref.st_mtim;
  230. }
  231. }
  232. for(int i = 0; i < argc; i++)
  233. {
  234. const char *file = argv[i];
  235. int fd = open(file, open_flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
  236. if(fd == -1)
  237. {
  238. /* BSD nonsense */
  239. #ifdef EMLINK
  240. if(errno == EMLINK) errno = ELOOP;
  241. #endif
  242. #ifdef EFTYPE
  243. if(errno == EFTYPE) errno = ELOOP;
  244. #endif
  245. if(errno == EISDIR || (errno == ELOOP && FIELD_MATCH(open_flags, O_NOFOLLOW)))
  246. {
  247. if(utimensat(AT_FDCWD, file, times, utimensat_flags) != 0)
  248. {
  249. fprintf(stderr,
  250. "touch: error: Failed setting new times on file '%s': %s\n",
  251. file,
  252. strerror(errno));
  253. return 1;
  254. }
  255. continue;
  256. }
  257. if(errno != ENOENT || FIELD_MATCH(open_flags, O_CREAT))
  258. fprintf(stderr, "touch: error: Failed opening file '%s': %s\n", file, strerror(errno));
  259. return 1;
  260. }
  261. if(futimens(fd, times) != 0)
  262. {
  263. fprintf(stderr,
  264. "touch: error: Failed setting new times on file '%s': %s\n",
  265. file,
  266. strerror(errno));
  267. return 1;
  268. }
  269. if(close(fd) != 0)
  270. {
  271. fprintf(stderr,
  272. "touch: error: Failed closing file-descriptor for file '%s': %s\n",
  273. file,
  274. strerror(errno));
  275. return 1;
  276. }
  277. }
  278. return 0;
  279. }