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