xref: /freebsd/usr.bin/find/printf.c (revision cb2887746f8b9dd4ad6b1e757cdc053a08b25a2e)
1 /*-
2  * Copyright (c) 2023, Netflix, Inc
3  *
4  * SPDX-License-Identifier: BSD-2-Clause
5  */
6 
7 #include <sys/types.h>
8 
9 #include <err.h>
10 #include <errno.h>
11 #include <fts.h>
12 #include <grp.h>
13 #include <pwd.h>
14 #include <stdbool.h>
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <time.h>
19 #include <unistd.h>
20 
21 #include "find.h"
22 
23 /* translate \X to proper escape, or to itself if no special meaning */
24 static const char *esc = "\a\bcde\fghijklm\nopq\rs\tu\v";
25 
26 static inline bool
27 isoct(char c)
28 {
29 	return (c >= '0' && c <= '7');
30 }
31 
32 static inline bool
33 isesc(char c)
34 {
35 	return (c >= 'a' && c <= 'v' && esc[c - 'a'] != c);
36 }
37 
38 static char *
39 escape(const char *str, bool *flush, bool *warned)
40 {
41 	char c;
42 	int value;
43 	char *tmpstr;
44 	size_t tmplen;
45 	FILE *fp;
46 
47 	fp = open_memstream(&tmpstr, &tmplen);
48 
49 	/*
50 	 * Copy the str string into a new struct sbuf and return that expanding
51 	 * the different ANSI escape sequences.
52 	 */
53 	*flush = false;
54 	for (c = *str++; c; c = *str++) {
55 		if (c != '\\') {
56 			putc(c, fp);
57 			continue;
58 		}
59 		c = *str++;
60 
61 		/*
62 		 * User error \ at end of string
63 		 */
64 		if (c == '\0') {
65 			putc('\\', fp);
66 			break;
67 		}
68 
69 		/*
70 		 * \c terminates output now and is supposed to flush the output
71 		 * too...
72 		 */
73 		if (c == 'c') {
74 			*flush = true;
75 			break;
76 		}
77 
78 		/*
79 		 * Is it octal? If so, decode up to 3 octal characters.
80 		 */
81 		if (isoct(c)) {
82 			value = 0;
83 			for (int i = 3; i-- > 0 && isoct(c);
84 			     c = *str++) {
85 				value <<= 3;
86 				value += c - '0';
87 			}
88 			str--;
89 			putc((char)value, fp);
90 			continue;
91 		}
92 
93 		/*
94 		 * It's an ANSI X3.159-1989 escape, use the mini-escape lookup
95 		 * table to translate.
96 		 */
97 		if (isesc(c)) {
98 			putc(esc[c - 'a'], fp);
99 			continue;
100 		}
101 
102 		/*
103 		 * Otherwise, it's self inserting. gnu find specifically says
104 		 * not to rely on this behavior though. gnu find will issue
105 		 * a warning here, while printf(1) won't.
106 		 */
107 		if (!*warned) {
108 			warn("Unknown character %c after \\.", c);
109 			*warned = true;
110 		}
111 		putc(c, fp);
112 	}
113 	fclose(fp);
114 
115 	return (tmpstr);
116 }
117 
118 static void
119 fp_ctime(FILE *fp, time_t t)
120 {
121 	char s[26];
122 
123 	ctime_r(&t, s);
124 	s[24] = '\0';	/* kill newline, though gnu find info silent on issue */
125 	fputs(s, fp);
126 }
127 
128 /*
129  * Assumes all times are displayed in UTC rather than local time, gnu find info
130  * page silent on the issue.
131  *
132  * Also assumes that gnu find doesn't support multiple character escape sequences,
133  * which it's info page is also silent on.
134  */
135 static void
136 fp_strftime(FILE *fp, time_t t, char mod)
137 {
138 	struct tm tm;
139 	char buffer[128];
140 	char fmt[3] = "% ";
141 
142 	/*
143 	 * Gnu libc extension we don't yet support -- seconds since epoch
144 	 * Used in Linux kernel build, so we kinda have to support it here
145 	 */
146 	if (mod == '@')	{
147 		fprintf(fp, "%ju", (uintmax_t)t);
148 		return;
149 	}
150 
151 	gmtime_r(&t, &tm);
152 	fmt[1] = mod;
153 	if (strftime(buffer, sizeof(buffer), fmt, &tm) == 0)
154 		errx(1, "Format bad or data too long for buffer"); /* Can't really happen ??? */
155 	fputs(buffer, fp);
156 }
157 
158 void
159 do_printf(PLAN *plan, FTSENT *entry, FILE *fout)
160 {
161 	char buf[4096];
162 	struct stat sb;
163 	struct stat *sp;
164 	const char *path, *pend;
165 	char *all, *fmt;
166 	ssize_t ret;
167 	int c;
168 	bool flush, warned;
169 
170 	warned = (plan->flags & F_HAS_WARNED) != 0;
171 	all = fmt = escape(plan->c_data, &flush, &warned);
172 	if (warned)
173 		plan->flags |= F_HAS_WARNED;
174 	for (c = *fmt++; c; c = *fmt++) {
175 		sp = entry->fts_statp;
176 		if (c != '%') {
177 			putc(c, fout);
178 			continue;
179 		}
180 		c = *fmt++;
181 		/* Style(9) deviation: case order same as gnu find info doc */
182 		switch (c) {
183 		case '%':
184 			putc(c, fout);
185 			break;
186 		case 'p': /* Path to file */
187 			fputs(entry->fts_path, fout);
188 			break;
189 		case 'f': /* filename w/o dirs */
190 			fputs(entry->fts_name, fout);
191 			break;
192 		case 'h':
193 			/*
194 			 * path, relative to the starting point, of the file, or
195 			 * '.' if that's empty for some reason.
196 			 */
197 			path = entry->fts_path;
198 			pend = strrchr(path, '/');
199 			if (pend == NULL)
200 				putc('.', fout);
201 			else
202 				fwrite(path, pend - path, 1, fout);
203 			break;
204 		case 'P': /* file with command line arg rm'd -- HOW? fts_parent? */
205 			errx(1, "%%%c is unimplemented", c);
206 		case 'H': /* Command line arg -- HOW? */
207 			errx(1, "%%%c is unimplemented", c);
208 		case 'g': /* gid human readable */
209 			fputs(group_from_gid(sp->st_gid, 0), fout);
210 			break;
211 		case 'G': /* gid numeric */
212 			fprintf(fout, "%d", sp->st_gid);
213 			break;
214 		case 'u': /* uid human readable */
215 			fputs(user_from_uid(sp->st_uid, 0), fout);
216 			break;
217 		case 'U': /* uid numeric */
218 			fprintf(fout, "%d", sp->st_uid);
219 			break;
220 		case 'm': /* mode in octal */
221 			fprintf(fout, "%o", sp->st_mode & 07777);
222 			break;
223 		case 'M': /* Mode in ls-standard form */
224 			strmode(sp->st_mode, buf);
225 			fwrite(buf, 10, 1, fout);
226 			break;
227 		case 'k': /* kbytes used by file */
228 			fprintf(fout, "%jd", (intmax_t)sp->st_blocks / 2);
229 			break;
230 		case 'b': /* blocks used by file */
231 			fprintf(fout, "%jd", (intmax_t)sp->st_blocks);
232 			break;
233 		case 's': /* size in bytes of file */
234 			fprintf(fout, "%ju", (uintmax_t)sp->st_size);
235 			break;
236 		case 'S': /* sparseness of file */
237 			fprintf(fout, "%3.1f",
238 			    (float)sp->st_blocks * 512 / (float)sp->st_size);
239 			break;
240 		case 'd': /* Depth in tree */
241 			fprintf(fout, "%ld", entry->fts_level);
242 			break;
243 		case 'D': /* device number */
244 			fprintf(fout, "%ju", (uintmax_t)sp->st_dev);
245 			break;
246 		case 'F': /* Filesystem type */
247 			errx(1, "%%%c is unimplemented", c);
248 		case 'l': /* object of symbolic link */
249 			ret = readlink(entry->fts_accpath, buf, sizeof(buf));
250 			if (ret > 0)
251 				fwrite(buf, ret, 1, fout);
252 			break;
253 		case 'i': /* inode # */
254 			fprintf(fout, "%ju", (uintmax_t)sp->st_ino);
255 			break;
256 		case 'n': /* number of hard links */
257 			fprintf(fout, "%ju", (uintmax_t)sp->st_nlink);
258 			break;
259 		case 'Y': /* -type of file, following 'l' types L loop ? error */
260 			if (S_ISLNK(sp->st_mode)) {
261 				if (stat(entry->fts_accpath, &sb) != 0) {
262 					switch (errno) {
263 					case ELOOP:
264 						putc('L', fout);
265 						break;
266 					case ENOENT:
267 						putc('N', fout);
268 						break;
269 					default:
270 						putc('?', fout);
271 						break;
272 					}
273 					break;
274 				}
275 				sp = &sb;
276 			}
277 			/* FALLTHROUGH */
278 		case 'y': /* -type of file, incl 'l' */
279 			switch (sp->st_mode & S_IFMT) {
280 			case S_IFIFO:
281 				putc('p', fout);
282 				break;
283 			case S_IFCHR:
284 				putc('c', fout);
285 				break;
286 			case S_IFDIR:
287 				putc('d', fout);
288 				break;
289 			case S_IFBLK:
290 				putc('b', fout);
291 				break;
292 			case S_IFREG:
293 				putc('f', fout);
294 				break;
295 			case S_IFLNK:
296 				putc('l', fout);
297 				break;
298 			case S_IFSOCK:
299 				putc('s', fout);
300 				break;
301 			case S_IFWHT:
302 				putc('w', fout);
303 				break;
304 			default:
305 				putc('U', fout);
306 				break;
307 			}
308 			break;
309 		case 'a': /* access time ctime */
310 			fp_ctime(fout, sp->st_atime);
311 			break;
312 		case 'A': /* access time with next char strftime format */
313 			fp_strftime(fout, sp->st_atime, *fmt++);
314 			break;
315 		case 'B': /* birth time with next char strftime format */
316 #ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME
317 			if (sp->st_birthtime != 0)
318 				fp_strftime(fout, sp->st_birthtime, *fmt);
319 #endif
320 			fmt++;
321 			break;	/* blank on systems that don't support it */
322 		case 'c': /* status change time ctime */
323 			fp_ctime(fout, sp->st_ctime);
324 			break;
325 		case 'C': /* status change time with next char strftime format */
326 			fp_strftime(fout, sp->st_ctime, *fmt++);
327 			break;
328 		case 't': /* modification change time ctime */
329 			fp_ctime(fout, sp->st_mtime);
330 			break;
331 		case 'T': /* modification time with next char strftime format */
332 			fp_strftime(fout, sp->st_mtime, *fmt++);
333 			break;
334 		case 'Z': /* empty string for compat SELinux context string */
335 			break;
336 		/* Modifier parsing here, but also need to modify above somehow */
337 		case '#': case '-': case '0': case '1': case '2': case '3': case '4':
338 		case '5': case '6': case '7': case '8': case '9': case '.':
339 			errx(1, "Format modifier %c not yet supported: '%s'", c, all);
340 		/* Any FeeeBSD-specific modifications here -- none yet */
341 		default:
342 			errx(1, "Unknown format %c '%s'", c, all);
343 		}
344 	}
345 	if (flush)
346 		fflush(fout);
347 	free(all);
348 }
349