logo

utils-std

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

touch.c (6367B)


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