logo

utils-std

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

touch.c (6521B)


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