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