kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

strings.c (8424B)


      1 #include <kit/core.h>
      2 #include <stddef.h>
      3 #include <stdint.h>
      4 #include <stdio.h>
      5 #include <string.h>
      6 
      7 #include "driver.h"
      8 #include "env.h"
      9 
     10 /* `kit strings` — print sequences of printable characters found in a
     11  * file. Unlike most tools here it is format-agnostic: it scans the raw
     12  * bytes of any input (objects, archives, or arbitrary data), matching
     13  * GNU strings' default whole-file behaviour. All display logic lives
     14  * here; the driver env supplies the path/stdin byte loaders. */
     15 
     16 #define STRINGS_TOOL "strings"
     17 
     18 #define STRINGS_DEFAULT_MIN 4
     19 
     20 typedef enum StringsRadix {
     21   STRINGS_RAD_NONE, /* default: no offset prefix */
     22   STRINGS_RAD_OCT,  /* -t o */
     23   STRINGS_RAD_DEC,  /* -t d */
     24   STRINGS_RAD_HEX,  /* -t x */
     25 } StringsRadix;
     26 
     27 typedef struct StringsOpts {
     28   size_t min_len;     /* -n N / --bytes=N / -N ; default STRINGS_DEFAULT_MIN */
     29   StringsRadix radix; /* -t {o,d,x} */
     30   int print_filename; /* -f / --print-file-name */
     31 } StringsOpts;
     32 
     33 /* A byte is "printable" for the purposes of a run if it is a graphic
     34  * ASCII character (0x20..0x7e) or a horizontal tab, matching GNU
     35  * strings' default (non-locale) classification. */
     36 static int strings_is_printable(uint8_t c) {
     37   return (c >= 0x20 && c <= 0x7e) || c == '\t';
     38 }
     39 
     40 static void strings_emit(const char* path, const StringsOpts* opts,
     41                          size_t offset, const uint8_t* s, size_t len) {
     42   if (opts->print_filename && path) driver_printf("%s: ", path);
     43   switch (opts->radix) {
     44     case STRINGS_RAD_OCT:
     45       driver_printf("%7llo ", (unsigned long long)offset);
     46       break;
     47     case STRINGS_RAD_DEC:
     48       driver_printf("%7llu ", (unsigned long long)offset);
     49       break;
     50     case STRINGS_RAD_HEX:
     51       driver_printf("%7llx ", (unsigned long long)offset);
     52       break;
     53     default:
     54       break;
     55   }
     56   driver_printf("%.*s\n", (int)len, (const char*)s);
     57 }
     58 
     59 /* Scan `data` for printable runs of at least opts->min_len bytes. */
     60 static void strings_scan(const char* path, const StringsOpts* opts,
     61                          const uint8_t* data, size_t size) {
     62   size_t i = 0;
     63   size_t run_start = 0;
     64   size_t run_len = 0;
     65   for (i = 0; i < size; ++i) {
     66     if (strings_is_printable(data[i])) {
     67       if (run_len == 0) run_start = i;
     68       ++run_len;
     69       continue;
     70     }
     71     if (run_len >= opts->min_len) {
     72       strings_emit(path, opts, run_start, data + run_start, run_len);
     73     }
     74     run_len = 0;
     75   }
     76   if (run_len >= opts->min_len) {
     77     strings_emit(path, opts, run_start, data + run_start, run_len);
     78   }
     79 }
     80 
     81 void driver_help_strings(void) {
     82   driver_printf(
     83       "%.*s",
     84       KIT_SLICE_ARG(KIT_SLICE_LIT(
     85           "kit strings — print printable character sequences in a file\n"
     86           "\n"
     87           "USAGE\n"
     88           "  kit strings [OPTIONS] [FILE...]\n"
     89           "\n"
     90           "DESCRIPTION\n"
     91           "  Scans the raw bytes of each input and prints runs of at least\n"
     92           "  N printable characters (default 4). Works on any file, not\n"
     93           "  just object files. With no FILE, or with `-`, reads stdin.\n"
     94           "\n"
     95           "OPTIONS\n"
     96           "  -n N, --bytes=N    minimum run length (default 4)\n"
     97           "  -N                 shorthand for -n N (e.g. -8)\n"
     98           "  -t {o,d,x}         print the byte offset of each string in\n"
     99           "                     octal / decimal / hexadecimal\n"
    100           "  -f, --print-file-name\n"
    101           "                     print the input file name before each string\n"
    102           "  -h, --help         show this help\n"
    103           "\n"
    104           "  Use -- before a leading-dash file name.\n"
    105           "\n"
    106           "EXAMPLES\n"
    107           "  kit strings -n 8 firmware.bin\n"
    108           "  kit strings -t x -f app.o libfoo.a\n"
    109           "  kit strings -n 6 < firmware.bin\n"
    110           "  kit strings -- -firmware.bin\n"
    111           "\n"
    112           "EXIT CODES\n"
    113           "  0   success           1   I/O error           2   bad usage\n")));
    114 }
    115 
    116 /* Parse the argument of -n / --bytes=. Returns 0 and stores the value on
    117  * success, or non-zero on a malformed/zero value. */
    118 static int strings_parse_min(const char* s, size_t* out) {
    119   size_t v = 0;
    120   if (!s || !*s) return 1;
    121   for (; *s; ++s) {
    122     if (*s < '0' || *s > '9') return 1;
    123     v = v * 10u + (size_t)(*s - '0');
    124   }
    125   if (v == 0) return 1;
    126   *out = v;
    127   return 0;
    128 }
    129 
    130 int driver_strings(int argc, char** argv) {
    131   DriverEnv env;
    132   StringsOpts opts;
    133   int i, rc = 1, any_input = 0, options = 1;
    134 
    135   if (driver_argv_wants_help(argc, argv, 1)) {
    136     driver_help_strings();
    137     return 0;
    138   }
    139 
    140   memset(&opts, 0, sizeof opts);
    141   opts.min_len = STRINGS_DEFAULT_MIN;
    142   opts.radix = STRINGS_RAD_NONE;
    143   driver_env_init(&env);
    144 
    145   /* First pass: options. */
    146   for (i = 1; i < argc; ++i) {
    147     const char* a = argv[i];
    148     if (options && driver_streq(a, "--")) {
    149       options = 0;
    150       continue;
    151     }
    152     if (!options) {
    153       any_input = 1;
    154       continue;
    155     }
    156     if (driver_streq(a, "-n") || driver_streq(a, "--bytes")) {
    157       if (i + 1 >= argc || strings_parse_min(argv[++i], &opts.min_len) != 0) {
    158         driver_errf(STRINGS_TOOL, "option %s requires a positive integer", a);
    159         rc = 2;
    160         goto done;
    161       }
    162       continue;
    163     }
    164     if (strncmp(a, "--bytes=", 8) == 0) {
    165       if (strings_parse_min(a + 8, &opts.min_len) != 0) {
    166         driver_errf(STRINGS_TOOL, "invalid --bytes value: %s", a + 8);
    167         rc = 2;
    168         goto done;
    169       }
    170       continue;
    171     }
    172     if (driver_streq(a, "-t")) {
    173       const char* v;
    174       if (i + 1 >= argc) {
    175         driver_errf(STRINGS_TOOL, "option -t requires o, d, or x");
    176         rc = 2;
    177         goto done;
    178       }
    179       v = argv[++i];
    180       if (driver_streq(v, "o")) {
    181         opts.radix = STRINGS_RAD_OCT;
    182       } else if (driver_streq(v, "d")) {
    183         opts.radix = STRINGS_RAD_DEC;
    184       } else if (driver_streq(v, "x")) {
    185         opts.radix = STRINGS_RAD_HEX;
    186       } else {
    187         driver_errf(STRINGS_TOOL, "invalid -t radix: %s (want o, d, or x)", v);
    188         rc = 2;
    189         goto done;
    190       }
    191       continue;
    192     }
    193     if (driver_streq(a, "-f") || driver_streq(a, "--print-file-name")) {
    194       opts.print_filename = 1;
    195       continue;
    196     }
    197     /* `-<digits>` is GNU shorthand for `-n <digits>`. */
    198     if (a[0] == '-' && a[1] >= '0' && a[1] <= '9') {
    199       if (strings_parse_min(a + 1, &opts.min_len) != 0) {
    200         driver_errf(STRINGS_TOOL, "invalid length: %s", a);
    201         rc = 2;
    202         goto done;
    203       }
    204       continue;
    205     }
    206     if (driver_streq(a, "-")) {
    207       any_input = 1; /* stdin, handled in second pass */
    208       continue;
    209     }
    210     if (a[0] == '-' && a[1] != '\0') {
    211       driver_errf(STRINGS_TOOL, "unknown option: %s", a);
    212       rc = 2;
    213       goto done;
    214     }
    215     any_input = 1;
    216   }
    217 
    218   /* No file operands: scan stdin. */
    219   if (!any_input) {
    220     uint8_t* buf = NULL;
    221     size_t n = 0;
    222     if (!driver_read_stdin(&env, &buf, &n)) {
    223       driver_errf(STRINGS_TOOL, "failed to read stdin");
    224       rc = 1;
    225       goto done;
    226     }
    227     strings_scan(NULL, &opts, buf, n);
    228     driver_free(&env, buf, n);
    229     rc = 0;
    230     goto done;
    231   }
    232 
    233   /* Second pass: inputs, in argv order. */
    234   options = 1;
    235   for (i = 1; i < argc; ++i) {
    236     const char* a = argv[i];
    237     if (options && driver_streq(a, "--")) {
    238       options = 0;
    239       continue;
    240     }
    241     /* Skip the options consumed above (and their values). */
    242     if (options && (driver_streq(a, "-n") ||
    243                     driver_streq(a, "--bytes") || driver_streq(a, "-t"))) {
    244       ++i;
    245       continue;
    246     }
    247     if (options &&
    248         (strncmp(a, "--bytes=", 8) == 0 || driver_streq(a, "-f") ||
    249          driver_streq(a, "--print-file-name") ||
    250          (a[0] == '-' && a[1] >= '0' && a[1] <= '9'))) {
    251       continue;
    252     }
    253     if (driver_streq(a, "-")) {
    254       uint8_t* buf = NULL;
    255       size_t n = 0;
    256       if (!driver_read_stdin(&env, &buf, &n)) {
    257         driver_errf(STRINGS_TOOL, "failed to read stdin");
    258         rc = 1;
    259         goto done;
    260       }
    261       strings_scan(opts.print_filename ? "{standard input}" : NULL, &opts, buf,
    262                    n);
    263       driver_free(&env, buf, n);
    264       continue;
    265     }
    266     {
    267       DriverLoad ld = {0};
    268       KitSlice input;
    269       if (driver_load_bytes(&env.file_io, STRINGS_TOOL, a, &ld, &input) != 0) {
    270         rc = 1;
    271         goto done;
    272       }
    273       strings_scan(a, &opts, input.data, input.len);
    274       driver_release_bytes(&env.file_io, &ld);
    275     }
    276   }
    277 
    278   rc = 0;
    279 done:
    280   driver_env_fini(&env);
    281   return rc;
    282 }