]> Sergey Matveev's repositories - bfs.git/blob - tests/xtouch.c
Skip mtab
[bfs.git] / tests / xtouch.c
1 // Copyright © Tavian Barnes <tavianator@tavianator.com>
2 // SPDX-License-Identifier: 0BSD
3
4 #include "../src/bfstd.h"
5 #include "../src/config.h"
6 #include "../src/sanity.h"
7 #include "../src/xtime.h"
8 #include <errno.h>
9 #include <fcntl.h>
10 #include <stdio.h>
11 #include <stdlib.h>
12 #include <string.h>
13 #include <sys/stat.h>
14 #include <time.h>
15 #include <unistd.h>
16
17 /** Parsed xtouch arguments. */
18 struct args {
19         /** Simple flags. */
20         enum {
21                 /** Don't create nonexistent files (-c). */
22                 NO_CREATE = 1 << 0,
23                 /** Don't follow symlinks (-h). */
24                 NO_FOLLOW = 1 << 1,
25                 /** Create any missing parent directories (-p). */
26                 CREATE_PARENTS = 1 << 2,
27         } flags;
28
29         /** Timestamps (-r|-t|-d). */
30         struct timespec times[2];
31
32         /** File creation mode (-M; default 0666 & ~umask). */
33         mode_t fmode;
34         /** Directory creation mode (-M; default 0777 & ~umask). */
35         mode_t dmode;
36         /** Parent directory creation mode (0777 & ~umask). */
37         mode_t pmode;
38 };
39
40 /** Open (and maybe create) a single directory. */
41 static int open_dir(const struct args *args, int dfd, const char *path) {
42         int ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
43
44         if (ret < 0 && errno == ENOENT && (args->flags & CREATE_PARENTS)) {
45                 if (mkdirat(dfd, path, args->pmode) == 0 || errno == EEXIST) {
46                         ret = openat(dfd, path, O_SEARCH | O_DIRECTORY);
47                 }
48         }
49
50         return ret;
51 }
52
53 /** Open (and maybe create) the parent directory of the path. */
54 static int open_parent(const struct args *args, const char **path) {
55         size_t max = xbaseoff(*path);
56         if (max == 0) {
57                 return AT_FDCWD;
58         }
59
60         char *dir = strndup(*path, max);
61         if (!dir) {
62                 return -1;
63         }
64
65         // Optimistically try the whole path first
66         int dfd = open_dir(args, AT_FDCWD, dir);
67         if (dfd >= 0) {
68                 goto done;
69         }
70
71         switch (errno) {
72         case ENAMETOOLONG:
73                 break;
74         case ENOENT:
75                 if (args->flags & CREATE_PARENTS) {
76                         break;
77                 } else {
78                         goto err;
79                 }
80         default:
81                 goto err;
82         }
83
84         // Open the parents one-at-a-time
85         dfd = AT_FDCWD;
86         char *cur = dir;
87         while (*cur) {
88                 char *next = cur;
89                 next += strcspn(next, "/");
90                 next += strspn(next, "/");
91
92                 char c = *next;
93                 *next = '\0';
94
95                 int parent = dfd;
96                 dfd = open_dir(args, parent, cur);
97                 if (parent >= 0) {
98                         close_quietly(parent);
99                 }
100                 if (dfd < 0) {
101                         goto err;
102                 }
103
104                 *next = c;
105                 cur = next;
106         }
107
108 done:
109         *path += max;
110 err:
111         free(dir);
112         return dfd;
113 }
114
115 /** Compute flags for fstatat()/utimensat(). */
116 static int at_flags(const struct args *args) {
117         if (args->flags & NO_FOLLOW) {
118                 return AT_SYMLINK_NOFOLLOW;
119         } else {
120                 return 0;
121         }
122 }
123
124 /** Touch one path. */
125 static int xtouch(const struct args *args, const char *path) {
126         int dfd = open_parent(args, &path);
127         if (dfd < 0 && dfd != AT_FDCWD) {
128                 return -1;
129         }
130
131         int ret = utimensat(dfd, path, args->times, at_flags(args));
132         if (ret == 0 || errno != ENOENT) {
133                 goto done;
134         }
135
136         if (args->flags & NO_CREATE) {
137                 ret = 0;
138                 goto done;
139         }
140
141         size_t len = strlen(path);
142         if (len > 0 && path[len - 1] == '/') {
143                 if (mkdirat(dfd, path, args->dmode) == 0) {
144                         ret = utimensat(dfd, path, args->times, at_flags(args));
145                 }
146         } else {
147                 int fd = openat(dfd, path, O_WRONLY | O_CREAT, args->fmode);
148                 if (fd >= 0) {
149                         if (futimens(fd, args->times) == 0) {
150                                 ret = xclose(fd);
151                         } else {
152                                 close_quietly(fd);
153                         }
154                 }
155         }
156
157 done:
158         if (dfd >= 0) {
159                 close_quietly(dfd);
160         }
161         return ret;
162 }
163
164 int main(int argc, char *argv[]) {
165         mode_t mask = umask(0);
166
167         struct args args = {
168                 .flags = 0,
169                 .times = {
170                         { .tv_nsec = UTIME_OMIT },
171                         { .tv_nsec = UTIME_OMIT },
172                 },
173                 .fmode = 0666 & ~mask,
174                 .dmode = 0777 & ~mask,
175                 .pmode = 0777 & ~mask,
176         };
177
178         bool atime = false, mtime = false;
179         const char *darg = NULL;
180         const char *marg = NULL;
181         const char *rarg = NULL;
182
183         const char *cmd = argc > 0 ? argv[0] : "xtouch";
184         int c;
185         while (c = getopt(argc, argv, ":M:acd:hmpr:t:"), c != -1) {
186                 switch (c) {
187                 case 'M':
188                         marg = optarg;
189                         break;
190                 case 'a':
191                         atime = true;
192                         break;
193                 case 'c':
194                         args.flags |= NO_CREATE;
195                         break;
196                 case 'd':
197                 case 't':
198                         darg = optarg;
199                         break;
200                 case 'h':
201                         args.flags |= NO_FOLLOW;
202                         break;
203                 case 'm':
204                         mtime = true;
205                         break;
206                 case 'p':
207                         args.flags |= CREATE_PARENTS;
208                         break;
209                 case 'r':
210                         rarg = optarg;
211                         break;
212                 case ':':
213                         fprintf(stderr, "%s: Missing argument to -%c\n", cmd, optopt);
214                         return EXIT_FAILURE;
215                 case '?':
216                         fprintf(stderr, "%s: Unrecognized option -%c\n", cmd, optopt);
217                         return EXIT_FAILURE;
218                 }
219         }
220
221         if (marg) {
222                 char *end;
223                 long mode = strtol(marg, &end, 8);
224                 // https://github.com/llvm/llvm-project/issues/64946
225                 sanitize_init(&end);
226                 if (*marg && !*end && mode >= 0 && mode < 01000) {
227                         args.fmode = args.dmode = mode;
228                 } else {
229                         fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg);
230                         return EXIT_FAILURE;
231                 }
232         }
233
234         struct timespec times[2];
235
236         if (rarg) {
237                 struct stat buf;
238                 if (fstatat(AT_FDCWD, rarg, &buf, at_flags(&args)) != 0) {
239                         fprintf(stderr, "%s: '%s': %s\n", cmd, rarg, xstrerror(errno));
240                         return EXIT_FAILURE;
241                 }
242                 times[0] = buf.st_atim;
243                 times[1] = buf.st_mtim;
244         } else if (darg) {
245                 if (xgetdate(darg, &times[0]) != 0) {
246                         fprintf(stderr, "%s: Parsing time '%s' failed: %s\n", cmd, darg, xstrerror(errno));
247                         return EXIT_FAILURE;
248                 }
249                 times[1] = times[0];
250         } else {
251                 // Don't use UTIME_NOW, so that multiple paths all get the same timestamp
252                 if (xgettime(&times[0]) != 0) {
253                         perror("xgettime()");
254                         return EXIT_FAILURE;
255                 }
256                 times[1] = times[0];
257         }
258
259         if (!atime && !mtime) {
260                 atime = true;
261                 mtime = true;
262         }
263         if (atime) {
264                 args.times[0] = times[0];
265         }
266         if (mtime) {
267                 args.times[1] = times[1];
268         }
269
270         if (optind >= argc) {
271                 fprintf(stderr, "%s: No files to touch\n", cmd);
272                 return EXIT_FAILURE;
273         }
274
275         int ret = EXIT_SUCCESS;
276         for (; optind < argc; ++optind) {
277                 const char *path = argv[optind];
278                 if (xtouch(&args, path) != 0) {
279                         fprintf(stderr, "%s: '%s': %s\n", cmd, path, xstrerror(errno));
280                         ret = EXIT_FAILURE;
281                 }
282         }
283         return ret;
284 }