kit

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

xxd.c (13218B)


      1 #include <kit/core.h>
      2 #include <stddef.h>
      3 #include <stdint.h>
      4 #include <string.h>
      5 
      6 #include "driver.h"
      7 #include "env.h"
      8 
      9 /* `kit xxd` — hex dump any file, and reverse a dump back to binary. A GNU
     10  * xxd subset: default hex+ASCII dump, plus -r (reverse), -p (plain hex),
     11  * -i (C array), -c/-g (columns/grouping), -s/-l (seek/length), -u (uppercase).
     12  * Unlike `objdump -s` it is format-agnostic: it dumps raw bytes of any input.
     13  * With no FILE, or `-`, reads stdin; output goes to stdout or -o FILE. */
     14 
     15 #define XXD_TOOL "xxd"
     16 
     17 #define XXD_COLS_DUMP 16u
     18 #define XXD_COLS_PLAIN 30u
     19 #define XXD_COLS_INCLUDE 12u
     20 
     21 typedef enum XxdMode {
     22   XXD_DUMP = 0, /* default hex+ASCII */
     23   XXD_PLAIN,    /* -p: continuous hex */
     24   XXD_INCLUDE,  /* -i: C array */
     25 } XxdMode;
     26 
     27 typedef struct XxdOpts {
     28   XxdMode mode;
     29   int reverse;     /* -r */
     30   int uppercase;   /* -u */
     31   size_t cols;     /* -c; 0 = mode default */
     32   size_t group;    /* -g; 0 = default (2) */
     33   uint64_t seek;   /* -s */
     34   uint64_t limit;  /* -l; 0 = no limit */
     35   int have_limit;  /* whether -l was given */
     36   const char* in;  /* input path, or NULL = stdin */
     37   const char* out; /* output path, or NULL = stdout */
     38 } XxdOpts;
     39 
     40 /* Streaming output buffer over a KitWriter; flushes when full so cols can be
     41  * arbitrarily large. */
     42 typedef struct Xb {
     43   char buf[8192];
     44   size_t n;
     45   KitWriter* w;
     46   int err;
     47 } Xb;
     48 
     49 static void xb_flush(Xb* b) {
     50   if (b->n) {
     51     if (kit_writer_write(b->w, b->buf, b->n) != KIT_OK) b->err = 1;
     52     b->n = 0;
     53   }
     54 }
     55 static void xb_c(Xb* b, char c) {
     56   if (b->n == sizeof b->buf) xb_flush(b);
     57   b->buf[b->n++] = c;
     58 }
     59 static void xb_s(Xb* b, const char* s) {
     60   while (*s) xb_c(b, *s++);
     61 }
     62 static void xb_hex2(Xb* b, uint8_t v, const char* hx) {
     63   xb_c(b, hx[v >> 4]);
     64   xb_c(b, hx[v & 0x0f]);
     65 }
     66 static void xb_hexnum(Xb* b, uint64_t v, int width, const char* hx) {
     67   char t[16];
     68   int i;
     69   if (width > 16) width = 16;
     70   for (i = width - 1; i >= 0; --i) {
     71     t[i] = hx[v & 0x0f];
     72     v >>= 4;
     73   }
     74   for (i = 0; i < width; ++i) xb_c(b, t[i]);
     75 }
     76 static void xb_dec(Xb* b, uint64_t v) {
     77   char t[24];
     78   int i = 0;
     79   if (v == 0) {
     80     xb_c(b, '0');
     81     return;
     82   }
     83   while (v) {
     84     t[i++] = (char)('0' + (int)(v % 10));
     85     v /= 10;
     86   }
     87   while (i) xb_c(b, t[--i]);
     88 }
     89 
     90 void driver_help_xxd(void) {
     91   driver_printf(
     92       "%.*s",
     93       KIT_SLICE_ARG(KIT_SLICE_LIT(
     94           "kit xxd — hex dump a file (and reverse a dump back to binary)\n"
     95           "\n"
     96           "USAGE\n"
     97           "  kit xxd [OPTIONS] [INFILE [OUTFILE]]\n"
     98           "\n"
     99           "DESCRIPTION\n"
    100           "  Dumps the raw bytes of INFILE (or stdin) as a hex+ASCII table.\n"
    101           "  Works on any file, not just objects. `xxd f | xxd -r` "
    102           "round-trips\n"
    103           "  contiguous data back to the original bytes.\n"
    104           "\n"
    105           "OPTIONS\n"
    106           "  -r            reverse: read a hex dump, write binary\n"
    107           "  -p            plain hex dump (continuous, no offsets/ASCII)\n"
    108           "  -i            output a C `unsigned char[]` array\n"
    109           "  -c N          N bytes per line (default 16; -p 30; -i 12)\n"
    110           "  -g N          group hex into N-byte columns (default 2)\n"
    111           "  -s OFF        start at byte OFF (decimal or 0x-hex)\n"
    112           "  -l LEN        dump at most LEN bytes\n"
    113           "  -u            uppercase hex digits\n"
    114           "  -o FILE       write output to FILE instead of stdout\n"
    115           "  -h, --help    show this help\n"
    116           "\n"
    117           "NOTE\n"
    118           "  -r reconstructs contiguous data; leading offsets are read for\n"
    119           "  context but not used to seek/pad sparse output.\n"
    120           "  Use -- before a leading-dash input.\n"
    121           "\n"
    122           "EXAMPLES\n"
    123           "  kit xxd firmware.bin\n"
    124           "  kit xxd -p -u firmware.bin > firmware.hex\n"
    125           "  kit xxd -i -o firmware.inc firmware.bin\n"
    126           "  kit xxd firmware.bin | kit xxd -r -o firmware.copy\n"
    127           "  printf '4142430a\\n' | kit xxd -r -p > abc.txt\n"
    128           "\n"
    129           "EXIT CODES\n"
    130           "  0   success           1   I/O error           2   bad usage\n")));
    131 }
    132 
    133 static int xxd_is_hex(int c) {
    134   return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
    135          (c >= 'A' && c <= 'F');
    136 }
    137 
    138 /* Derive a C identifier from a file path: basename, non-alnum -> '_', a leading
    139  * digit gets an '_' prefix. Writes into out (cap bytes). */
    140 static void xxd_ident(const char* path, char* out, size_t cap) {
    141   const char* base = driver_basename(path);
    142   size_t i = 0;
    143   if (cap == 0) return;
    144   if (base[0] >= '0' && base[0] <= '9' && i + 1 < cap) out[i++] = '_';
    145   for (; *base && i + 1 < cap; ++base) {
    146     char c = *base;
    147     int ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
    148              (c >= '0' && c <= '9');
    149     out[i++] = ok ? c : '_';
    150   }
    151   out[i] = '\0';
    152 }
    153 
    154 static void xxd_emit_dump(Xb* b, const uint8_t* data, size_t len, uint64_t base,
    155                           size_t cols, size_t group, const char* hx) {
    156   size_t off;
    157   for (off = 0; off < len; off += cols) {
    158     size_t linelen = (len - off < cols) ? (len - off) : cols;
    159     size_t j;
    160     xb_hexnum(b, base + off, 8, hx);
    161     xb_s(b, ": ");
    162     for (j = 0; j < cols; ++j) {
    163       if (j < linelen)
    164         xb_hex2(b, data[off + j], hx);
    165       else {
    166         xb_c(b, ' ');
    167         xb_c(b, ' ');
    168       }
    169       if ((j + 1) % group == 0) xb_c(b, ' ');
    170     }
    171     if (cols % group != 0) xb_c(b, ' ');
    172     xb_c(b, ' ');
    173     for (j = 0; j < linelen; ++j) {
    174       uint8_t c = data[off + j];
    175       xb_c(b, (c >= 0x20 && c <= 0x7e) ? (char)c : '.');
    176     }
    177     xb_c(b, '\n');
    178   }
    179 }
    180 
    181 static void xxd_emit_plain(Xb* b, const uint8_t* data, size_t len, size_t cols,
    182                            const char* hx) {
    183   size_t i;
    184   for (i = 0; i < len; ++i) {
    185     xb_hex2(b, data[i], hx);
    186     if ((i + 1) % cols == 0) xb_c(b, '\n');
    187   }
    188   if (len == 0 || len % cols != 0) xb_c(b, '\n');
    189 }
    190 
    191 static void xxd_emit_include(Xb* b, const uint8_t* data, size_t len,
    192                              const char* ident, size_t cols, const char* hx) {
    193   size_t i;
    194   xb_s(b, "unsigned char ");
    195   if (ident && ident[0]) xb_s(b, ident);
    196   xb_s(b, "[] = {\n");
    197   for (i = 0; i < len; ++i) {
    198     if (i % cols == 0) xb_s(b, "  ");
    199     xb_s(b, "0x");
    200     xb_hex2(b, data[i], hx);
    201     if (i + 1 < len) xb_c(b, ',');
    202     if ((i + 1) % cols == 0 || i + 1 == len)
    203       xb_c(b, '\n');
    204     else
    205       xb_c(b, ' ');
    206   }
    207   xb_s(b, "};\n");
    208   xb_s(b, "unsigned int ");
    209   if (ident && ident[0]) xb_s(b, ident);
    210   xb_s(b, "_len = ");
    211   xb_dec(b, len);
    212   xb_s(b, ";\n");
    213 }
    214 
    215 /* Reverse a hex dump back to raw bytes. plain=1 treats the whole input as
    216  * continuous hex; otherwise each line is `[offset:] hex...  ascii` and parsing
    217  * stops at the double-space gutter. */
    218 static void xxd_reverse(Xb* b, const uint8_t* data, size_t len, int plain) {
    219   size_t i = 0;
    220   while (i < len) {
    221     size_t eol = i;
    222     size_t s, p;
    223     int hi = -1;
    224     while (eol < len && data[eol] != '\n') ++eol;
    225     s = i;
    226     if (!plain) {
    227       size_t c;
    228       for (c = i; c < eol; ++c) {
    229         if (data[c] == ':') {
    230           s = c + 1;
    231           break;
    232         }
    233       }
    234     }
    235     for (p = s; p < eol; ++p) {
    236       unsigned char ch = data[p];
    237       if (ch == ' ' || ch == '\t') {
    238         if (!plain && p + 1 < eol &&
    239             (data[p + 1] == ' ' || data[p + 1] == '\t'))
    240           break; /* gutter before the ASCII column */
    241         continue;
    242       }
    243       if (xxd_is_hex(ch)) {
    244         if (hi < 0) {
    245           hi = driver_hex_nibble((char)ch);
    246         } else {
    247           xb_c(b, (char)((hi << 4) | driver_hex_nibble((char)ch)));
    248           hi = -1;
    249         }
    250         continue;
    251       }
    252       break; /* non-hex, non-space: rest of line is ASCII/junk */
    253     }
    254     i = (eol < len) ? eol + 1 : eol;
    255   }
    256 }
    257 
    258 /* Extract the value for a short option that may be attached (-c16) or separate
    259  * (-c 16). On success advances *i past a consumed separate arg and returns the
    260  * value string; returns NULL on a missing argument. */
    261 static const char* xxd_optval(const char* a, int argc, char** argv, int* i) {
    262   if (a[2] != '\0') return a + 2;
    263   if (*i + 1 >= argc) return NULL;
    264   return argv[++(*i)];
    265 }
    266 
    267 int driver_xxd(int argc, char** argv) {
    268   DriverEnv env;
    269   KitContext ctx;
    270   XxdOpts o;
    271   Xb b;
    272   KitWriter* w = NULL;
    273   const uint8_t* data = NULL;
    274   size_t len = 0;
    275   DriverLoad ld = {0};
    276   uint8_t* sbuf = NULL;
    277   size_t sbuf_len = 0;
    278   int loaded = 0, npos = 0, rc = 2, owned_writer = 0, options = 1;
    279   int i;
    280   size_t cols, group;
    281   const char* hx;
    282   char ident[256];
    283 
    284   if (driver_argv_wants_help(argc, argv, 1)) {
    285     driver_help_xxd();
    286     return 0;
    287   }
    288 
    289   memset(&o, 0, sizeof o);
    290   driver_env_init(&env);
    291   ctx = driver_env_to_context(&env);
    292 
    293   for (i = 1; i < argc; ++i) {
    294     const char* a = argv[i];
    295     if (options && driver_streq(a, "--")) {
    296       options = 0;
    297       continue;
    298     }
    299     if (!options) {
    300       if (npos == 0)
    301         o.in = a;
    302       else if (npos == 1 && !o.out)
    303         o.out = a;
    304       else {
    305         driver_errf(XXD_TOOL, "too many operands: %s", a);
    306         goto done;
    307       }
    308       ++npos;
    309       continue;
    310     }
    311     if (driver_streq(a, "-r")) {
    312       o.reverse = 1;
    313       continue;
    314     }
    315     if (driver_streq(a, "-p")) {
    316       o.mode = XXD_PLAIN;
    317       continue;
    318     }
    319     if (driver_streq(a, "-i")) {
    320       o.mode = XXD_INCLUDE;
    321       continue;
    322     }
    323     if (driver_streq(a, "-u")) {
    324       o.uppercase = 1;
    325       continue;
    326     }
    327     if (a[0] == '-' && a[1] == 'c') {
    328       const char* v = xxd_optval(a, argc, argv, &i);
    329       uint64_t n;
    330       if (!v || driver_parse_u64(v, &n) != 0 || n == 0) {
    331         driver_errf(XXD_TOOL, "-c requires a positive integer");
    332         goto done;
    333       }
    334       o.cols = (size_t)n;
    335       continue;
    336     }
    337     if (a[0] == '-' && a[1] == 'g') {
    338       const char* v = xxd_optval(a, argc, argv, &i);
    339       uint64_t n;
    340       if (!v || driver_parse_u64(v, &n) != 0 || n == 0) {
    341         driver_errf(XXD_TOOL, "-g requires a positive integer");
    342         goto done;
    343       }
    344       o.group = (size_t)n;
    345       continue;
    346     }
    347     if (a[0] == '-' && a[1] == 's') {
    348       const char* v = xxd_optval(a, argc, argv, &i);
    349       if (!v || driver_parse_u64(v, &o.seek) != 0) {
    350         driver_errf(XXD_TOOL, "-s requires a byte offset");
    351         goto done;
    352       }
    353       continue;
    354     }
    355     if (a[0] == '-' && a[1] == 'l') {
    356       const char* v = xxd_optval(a, argc, argv, &i);
    357       if (!v || driver_parse_u64(v, &o.limit) != 0) {
    358         driver_errf(XXD_TOOL, "-l requires a length");
    359         goto done;
    360       }
    361       o.have_limit = 1;
    362       continue;
    363     }
    364     if (driver_streq(a, "-o")) {
    365       if (i + 1 >= argc) {
    366         driver_errf(XXD_TOOL, "-o requires a path");
    367         goto done;
    368       }
    369       o.out = argv[++i];
    370       continue;
    371     }
    372     if (driver_streq(a, "-")) {
    373       if (npos == 0) o.in = NULL; /* stdin */
    374       ++npos;
    375       continue;
    376     }
    377     if (a[0] == '-' && a[1] != '\0') {
    378       driver_errf(XXD_TOOL, "unknown option: %s", a);
    379       goto done;
    380     }
    381     if (npos == 0)
    382       o.in = a;
    383     else if (npos == 1 && !o.out)
    384       o.out = a;
    385     else {
    386       driver_errf(XXD_TOOL, "too many operands: %s", a);
    387       goto done;
    388     }
    389     ++npos;
    390   }
    391 
    392   /* Resolve mode-dependent defaults. */
    393   cols = o.cols ? o.cols
    394                 : (o.mode == XXD_PLAIN     ? XXD_COLS_PLAIN
    395                    : o.mode == XXD_INCLUDE ? XXD_COLS_INCLUDE
    396                                            : XXD_COLS_DUMP);
    397   group = o.group ? o.group : 2u;
    398   hx = o.uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
    399 
    400   /* Load input. */
    401   if (o.in) {
    402     KitSlice in;
    403     if (driver_load_bytes(&env.file_io, XXD_TOOL, o.in, &ld, &in) != 0) {
    404       rc = 1;
    405       goto done;
    406     }
    407     loaded = 1;
    408     data = in.data;
    409     len = in.len;
    410   } else {
    411     if (!driver_read_stdin(&env, &sbuf, &sbuf_len)) {
    412       driver_errf(XXD_TOOL, "failed to read stdin");
    413       rc = 1;
    414       goto done;
    415     }
    416     data = sbuf;
    417     len = sbuf_len;
    418   }
    419 
    420   /* Apply seek/length (forward modes only; reverse consumes the whole text). */
    421   if (!o.reverse) {
    422     if (o.seek < len) {
    423       data += o.seek;
    424       len -= (size_t)o.seek;
    425     } else {
    426       data += len;
    427       len = 0;
    428     }
    429     if (o.have_limit && o.limit < (uint64_t)len) len = (size_t)o.limit;
    430   }
    431 
    432   /* Open output. */
    433   if (o.out) {
    434     if (ctx.file_io->open_writer(ctx.file_io->user, o.out, &w) != KIT_OK) {
    435       driver_errf(XXD_TOOL, "failed to open output: %s", o.out);
    436       rc = 1;
    437       goto done;
    438     }
    439     owned_writer = 1;
    440   } else {
    441     w = driver_stdout_writer(&env);
    442     owned_writer = 1;
    443   }
    444 
    445   memset(&b, 0, sizeof b);
    446   b.w = w;
    447 
    448   if (o.reverse) {
    449     xxd_reverse(&b, data, len, o.mode == XXD_PLAIN);
    450   } else if (o.mode == XXD_PLAIN) {
    451     xxd_emit_plain(&b, data, len, cols, hx);
    452   } else if (o.mode == XXD_INCLUDE) {
    453     ident[0] = '\0';
    454     if (o.in) xxd_ident(o.in, ident, sizeof ident);
    455     xxd_emit_include(&b, data, len, ident, cols, hx);
    456   } else {
    457     xxd_emit_dump(&b, data, len, o.seek, cols, group, hx);
    458   }
    459   xb_flush(&b);
    460   rc = b.err ? 1 : 0;
    461 
    462 done:
    463   if (owned_writer && w) kit_writer_close(w);
    464   if (sbuf) driver_free(&env, sbuf, sbuf_len);
    465   if (loaded) driver_release_bytes(&env.file_io, &ld);
    466   driver_env_fini(&env);
    467   return rc;
    468 }