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