File: nhex.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 You can build this command-line app by running
  27 
  28 cc -Wall -s -O2 -march=native -mtune=native -flto -o ./nhex ./nhex.c
  29 
  30 Building with COMPACT_OUTPUT defined makes `nhex` output many fewer bytes, at
  31 the cost of using arguably worse colors. You can do that by running
  32 
  33 cc -s -O2 -march=native -mtune=native -flto -D COMPACT_OUTPUT -o ./nhex ./nhex.c
  34 
  35 Building for macos always uses COMPACT_OUTPUT, as the default terminal app
  36 there still doesn't support rgb colors.
  37 */
  38 
  39 #include <stdbool.h>
  40 #include <stdio.h>
  41 #include <stdlib.h>
  42 #include <string.h>
  43 #include <sys/stat.h>
  44 
  45 #ifdef _WIN32
  46 #include <fcntl.h>
  47 #include <windows.h>
  48 #endif
  49 
  50 #ifdef RED_ERRORS
  51 #define ERROR_STYLE "\x1b[38;2;204;0;0m"
  52 #ifdef __APPLE__
  53 #define ERROR_STYLE "\x1b[31m"
  54 #endif
  55 #define RESET_STYLE "\x1b[0m"
  56 #else
  57 #define ERROR_STYLE
  58 #define RESET_STYLE
  59 #endif
  60 
  61 #ifdef __APPLE__
  62 #define COMPACT_OUTPUT
  63 #endif
  64 
  65 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  66 
  67 #ifndef IBUF_SIZE
  68 #define IBUF_SIZE (32 * 1024)
  69 #endif
  70 
  71 #ifndef OBUF_SIZE
  72 #define OBUF_SIZE (32 * 1024)
  73 #endif
  74 
  75 // EMIT_CONST emits string constants without their final null byte
  76 #define EMIT_CONST(w, x) fwrite(x, 1, sizeof(x) - 1, w)
  77 
  78 const char* info = ""
  79 "nhex [options...] [filenames...]\n"
  80 "\n"
  81 "Nice HEXadecimal is a simple hexadecimal (base-16) viewer to inspect bytes\n"
  82 "from files or standard input.\n"
  83 "\n"
  84 "Each line shows the starting offset for the bytes shown, 16 of the bytes\n"
  85 "themselves in base-16 notation, and any ASCII codes when the byte values\n"
  86 "are in the typical ASCII range.\n"
  87 "\n"
  88 "The base-16 codes are color-coded, with most bytes shown in gray, while\n"
  89 "all-1 and all-0 bytes are shown in orange and blue respectively.\n"
  90 "\n"
  91 "All-0 bytes are the commonest kind in most binary file types and, along\n"
  92 "with all-1 bytes are also a special case worth noticing when exploring\n"
  93 "binary data, so it makes sense for them to stand out right away.\n"
  94 "\n"
  95 "\n"
  96 "Options\n"
  97 "\n"
  98 "    -h, --h            show this help message\n"
  99 "    -help, --help      aliases for option -h\n"
 100 "\n"
 101 "    -p, --p            plain-text output, without ANSI styles\n"
 102 "    -plain, --plain    aliases for option -p\n"
 103 "\n"
 104 "    -do, --do          show decimal (base-10) offsets, instead of hex ones\n"
 105 "    -dec, --dec        aliases for option -do\n"
 106 "    -ho, --ho          show hex (base-16) offsets, instead of base-10 ones\n"
 107 "    -hex, --hex        aliases for option -ho\n"
 108 "";
 109 
 110 #ifdef COMPACT_OUTPUT
 111 #define OUTPUT_FOR_00 "\x1b[36m00 "
 112 #define OUTPUT_FOR_FF "\x1b[35mff "
 113 // #define NORMAL_HEX_STYLE "\x1b[37m"
 114 #define NORMAL_HEX_STYLE "\x1b[38;5;250m"
 115 #define ASCII_HEX_STYLE "\x1b[32m"
 116 #define ASCII_BYTE_STYLE "\x1b[0m"
 117 #define ASCII_WS_STYLE "\x1b[33m"
 118 #else
 119 #define OUTPUT_FOR_00 "\x1b[38;2;135;175;255m00 "
 120 #define OUTPUT_FOR_FF "\x1b[38;2;255;135;95mff "
 121 #define NORMAL_HEX_STYLE "\x1b[38;2;192;192;192m"
 122 // #define ASCII_HEX_STYLE "\x1b[38;2;102;175;135m"
 123 // #define ASCII_HEX_STYLE "\x1b[38;2;42;217;71m"
 124 #define ASCII_HEX_STYLE "\x1b[38;2;42;207;1m"
 125 // #define ASCII_BYTE_STYLE "\x1b[38;2;78;78;78m"
 126 #define ASCII_BYTE_STYLE "\x1b[0m"
 127 #define ASCII_WS_STYLE "\x1b[38;2;6;152;154m"
 128 #endif
 129 
 130 // write_hex is faster than calling fprintf(w, "%02x", b): this matters
 131 // because it's called for every input byte
 132 static inline void write_hex(FILE* w, unsigned char b) {
 133     const char* hex_digits = "0123456789abcdef";
 134     fputc(hex_digits[b >> 4], w);
 135     fputc(hex_digits[b & 0x0f], w);
 136 }
 137 
 138 // write_styled_hex emits an ANSI color-coded hexadecimal representation of
 139 // the byte given; the int given/returned is to keep track of the type of
 140 // color-coding used in the previous call, to save some output bytes when
 141 // emitting multiple same-styled bytes in sequence, in case the specific
 142 // same-styled run allows it
 143 static inline int write_styled_hex(FILE* w, unsigned char b, int prev) {
 144     // regular ASCII display symbols
 145     if (33 <= b && b <= 126) {
 146         EMIT_CONST(w, ASCII_HEX_STYLE);
 147         write_hex(w, b);
 148         EMIT_CONST(w, ASCII_BYTE_STYLE);
 149         fputc(b, w);
 150         return -1;
 151     }
 152 
 153     // all-bits-off is almost always noteworthy
 154     if (b == 0) {
 155         if (prev != 0) {
 156             EMIT_CONST(w, OUTPUT_FOR_00);
 157         } else {
 158             EMIT_CONST(w, "00 ");
 159         }
 160         return 0;
 161     }
 162     // all-bits-on is often noteworthy
 163     if (b == 0xff) {
 164         if (prev != 255) {
 165             EMIT_CONST(w, OUTPUT_FOR_FF);
 166         } else {
 167             EMIT_CONST(w, "ff ");
 168         }
 169         return 255;
 170     }
 171 
 172     // ASCII whitespace
 173     if (b == ' ' || b == '\n' || b == '\t' || b == '\r') {
 174         EMIT_CONST(w, ASCII_WS_STYLE);
 175         write_hex(w, b);
 176         EMIT_CONST(w, ASCII_BYTE_STYLE " ");
 177         return -1;
 178     }
 179 
 180     // ASCII control values, and other bytes beyond displayable ASCII
 181     if (prev != 10) {
 182         EMIT_CONST(w, NORMAL_HEX_STYLE);
 183     }
 184     write_hex(w, b);
 185     fputc(' ', w);
 186     return 10;
 187 }
 188 
 189 // ruler emits a ruler-like string of spaced-out symbols
 190 static inline void ruler(FILE* w, size_t bytes_per_line) {
 191     const size_t gap = 4;
 192     if (bytes_per_line < gap) {
 193         return;
 194     }
 195 
 196     EMIT_CONST(w, "             ·");
 197     for (size_t n = bytes_per_line - gap; n >= gap; n -= gap) {
 198         EMIT_CONST(w, "           ·");
 199     }
 200 }
 201 
 202 // write_commas_uint shows a number by separating 3-digits groups with commas
 203 void write_commas_uint(FILE* w, size_t n) {
 204     if (n == 0) {
 205         fputc('0', w);
 206         return;
 207     }
 208 
 209     size_t digits;
 210     // 20 is the most digits unsigned 64-bit ints can ever need
 211     unsigned char buf[24];
 212     for (digits = 0; n > 0; digits++, n /= 10) {
 213         buf[sizeof(buf) - 1 - digits] = (n % 10) + '0';
 214     }
 215 
 216     // now emit the leading digits, which may not come in 3
 217     size_t leading = digits % 3;
 218     if (leading == 0) {
 219         // avoid having a comma before the first digit
 220         leading = digits < 3 ? digits : 3;
 221     }
 222     unsigned char* start = buf + sizeof(buf) - digits;
 223     fwrite(start, 1, leading, w);
 224     start += leading;
 225     digits -= leading;
 226 
 227     // now emit all remaining digits in groups of 3, alternating styles
 228     for (; digits > 0; start += 3, digits -= 3) {
 229         fputc(',', w);
 230         fwrite(start, 1, 3, w);
 231     }
 232 }
 233 
 234 // output_state ties all values representing the current state shared across
 235 // all functions involved in interpreting the input-buffer and showing its
 236 // bytes and ASCII values
 237 typedef struct output_state {
 238     // the whole input-buffer and its currently-used length in bytes
 239     unsigned char* buf;
 240     size_t buflen;
 241 
 242     // the ASCII-text buffer and its currently-used length in bytes
 243     unsigned char* txt;
 244     size_t txtlen;
 245 
 246     // offset is the byte counter, shown at the start of each line
 247     size_t offset;
 248 
 249     // line_width is how many bytes each line can show at most
 250     size_t line_width;
 251 
 252     // lines is the line counter, which is used to provide periodic
 253     // breather lines, to make eye-scanning big output blobs easier
 254     size_t lines;
 255 
 256     // emit_offset is chosen to emit the offset at the start of each line
 257     void (*emit_offset)(FILE* w, size_t offset);
 258 
 259     // showtxt is a hint on whether it's sensible to show the ASCII-text
 260     // buffer for the current line
 261     bool showtxt;
 262 } output_state;
 263 
 264 // peek_ascii looks 2 lines ahead in the buffer to get all ASCII-like runs
 265 // of bytes, which are later meant to show on the side panel
 266 void peek_ascii(size_t i, size_t end, output_state* os) {
 267     os->txtlen = 0;
 268     os->showtxt = false;
 269 
 270     for (size_t j = i; j < end; j++) {
 271         const unsigned char b = os->buf[j];
 272         const bool is_vis_ascii = ' ' <= b && b <= '~';
 273         os->showtxt = os->showtxt | is_vis_ascii;
 274         os->txt[os->txtlen] = is_vis_ascii ? b : ' ';
 275         os->txtlen++;
 276     }
 277 
 278     // keep trailing spaces in the ASCII viewer, since real ASCII streaks
 279     // can have them: an example is the `WAVEfmt ` header in .wav files
 280 
 281     // while (os->txtlen > 0 && os->txt[os->txtlen - 1] == ' ') {
 282     //     os->txtlen--;
 283     // }
 284 }
 285 
 286 // write_plain_uint is the unstyled counterpart of func write_styled_uint
 287 void write_plain_uint(FILE* w, size_t n) {
 288     if (n < 1) {
 289         EMIT_CONST(w, "       0");
 290         return;
 291     }
 292 
 293     size_t digits;
 294     // 20 is the most digits unsigned 64-bit ints can ever need
 295     unsigned char buf[24];
 296     for (digits = 0; n > 0; digits++, n /= 10) {
 297         buf[sizeof(buf) - 1 - digits] = (n % 10) + '0';
 298     }
 299 
 300     // left-pad the coming digits up to 8 chars
 301     if (digits < 8) {
 302         fwrite((unsigned char*)"        ", 1, 8 - digits, w);
 303     }
 304 
 305     // emit all digits
 306     const unsigned char* start = buf + sizeof(buf) - digits;
 307     fwrite(start, 1, digits, w);
 308 }
 309 
 310 void write_hex_uint(FILE* w, size_t n) {
 311     if (n < 1) {
 312         EMIT_CONST(w, "00000000");
 313         return;
 314     }
 315 
 316     size_t digits;
 317     // 20 is the most digits unsigned 64-bit ints can ever need
 318     unsigned char buf[24];
 319     for (digits = 0; n > 0; digits += 2, n /= 256) {
 320         unsigned char b = n % 256;
 321         const char* hex_digits = "0123456789abcdef";
 322         buf[sizeof(buf) - 1 - digits - 1] = hex_digits[b >> 4];
 323         buf[sizeof(buf) - 1 - digits - 0] = hex_digits[b & 0x0f];
 324     }
 325 
 326     // left-pad the coming digits up to 8 chars
 327     if (digits < 8) {
 328         fwrite((unsigned char*)"00000000", 1, 8 - digits, w);
 329     }
 330 
 331     // emit all digits
 332     const unsigned char* start = buf + sizeof(buf) - digits;
 333     fwrite(start, 1, digits, w);
 334 }
 335 
 336 // write_styled_uint is a quick way to emit the offset-counter showing at the
 337 // start of each line; it assumes 8-item left-padding of values, unless the
 338 // numbers are too big for that
 339 void write_styled_uint(FILE* w, size_t n) {
 340     if (n < 1) {
 341         EMIT_CONST(w, "       0");
 342         return;
 343     }
 344 
 345     size_t digits;
 346     // 20 is the most digits unsigned 64-bit ints can ever need
 347     unsigned char buf[24];
 348     for (digits = 0; n > 0; digits++, n /= 10) {
 349         buf[sizeof(buf) - 1 - digits] = (n % 10) + '0';
 350     }
 351 
 352     // left-pad the coming digits up to 8 chars
 353     if (digits < 8) {
 354         fwrite((unsigned char*)"        ", 1, 8 - digits, w);
 355     }
 356 
 357     // now emit the leading digits, which may be fewer than 3
 358     size_t leading = digits % 3;
 359     unsigned char* start = buf + sizeof(buf) - digits;
 360     fwrite(start, 1, leading, w);
 361     start += leading;
 362     digits -= leading;
 363 
 364     // now emit all remaining digits in groups of 3, alternating styles
 365     bool styled = leading != 0;
 366     for (; digits > 0; start += 3, digits -= 3, styled = !styled) {
 367         if (styled) {
 368 #ifdef COMPACT_OUTPUT
 369             EMIT_CONST(w, "\x1b[38;5;248m");
 370 #else
 371             EMIT_CONST(w, "\x1b[38;2;168;168;168m");
 372 #endif
 373             fwrite(start, 1, 3, w);
 374             EMIT_CONST(w, "\x1b[0m");
 375         } else {
 376             fwrite(start, 1, 3, w);
 377         }
 378     }
 379 }
 380 
 381 // emit_styled_file_info emits an ANSI-styled line showing a filename and the
 382 // file's size in bytes
 383 void emit_styled_file_info(FILE* w, const char* path, size_t nbytes) {
 384     EMIT_CONST(w, "");
 385     fwrite((unsigned char*)path, 1, strlen(path), w);
 386 #ifdef COMPACT_OUTPUT
 387     EMIT_CONST(w, "  \x1b[38;5;245m(");
 388 #else
 389     EMIT_CONST(w, "  \x1b[38;2;138;138;138m(");
 390 #endif
 391     write_commas_uint(w, nbytes);
 392     EMIT_CONST(w, " bytes)\x1b[0m\n");
 393 }
 394 
 395 // emit_plain_file_info is the unstyled counterpart of func emit_styled_file_info
 396 void emit_plain_file_info(FILE* w, const char* path, size_t nbytes) {
 397     EMIT_CONST(w, "");
 398     fwrite((unsigned char*)path, 1, strlen(path), w);
 399     EMIT_CONST(w, "  (");
 400     write_commas_uint(w, nbytes);
 401     EMIT_CONST(w, " bytes)\n");
 402 }
 403 
 404 // emit_styled_line handles the details of showing a styled line out of the
 405 // current input-buffer chunk
 406 void emit_styled_line(FILE* w, size_t i, size_t end, output_state* os) {
 407     int prev_type = -1;
 408 
 409     for (size_t j = i; j < end; j++, os->offset++) {
 410         const unsigned char b = os->buf[j];
 411 
 412         if (j % os->line_width == 0) {
 413             // show a ruler every few lines to make eye-scanning easier
 414             if (os->lines % 5 == 0 && os->lines > 0) {
 415 #ifdef COMPACT_OUTPUT
 416                 EMIT_CONST(w, "        \x1b[38;5;245m");
 417 #else
 418                 EMIT_CONST(w, "        \x1b[38;2;138;138;138m");
 419 #endif
 420                 ruler(w, os->line_width);
 421                 EMIT_CONST(w, "\x1b[0m\n");
 422             }
 423             os->lines++;
 424             prev_type = -1; // reset previous-style, since it's a new line
 425 
 426             // start next line with offset of its 1st item, also changing the
 427             // background color for the colored hex code which will follow
 428             // fprintf(stdout, "%8d", os->offset);
 429 
 430             os->emit_offset(w, os->offset);
 431             EMIT_CONST(w, "  ");
 432         }
 433 
 434         // show the current byte `with style`
 435         prev_type = write_styled_hex(w, b, prev_type);
 436     }
 437 
 438     if (os->showtxt) {
 439         EMIT_CONST(w, "\x1b[0m  ");
 440         for (size_t j = end - i; j < os->line_width; j++) {
 441             EMIT_CONST(w, "   ");
 442         }
 443 
 444         fwrite(os->txt, 1, os->txtlen, w);
 445         fputc('\n', w);
 446         return;
 447     }
 448 
 449     EMIT_CONST(w, "\x1b[0m\n");
 450 }
 451 
 452 // emit_plain_line handles the details of showing a plain (unstyled) line out
 453 // of the current input-buffer chunk
 454 void emit_plain_line(FILE* w, size_t i, size_t end, output_state* os) {
 455     for (size_t j = i; j < end; j++, os->offset++) {
 456         const unsigned char b = os->buf[j];
 457 
 458         if (j % os->line_width == 0) {
 459             // show a ruler every few lines to make eye-scanning easier
 460             if (os->lines % 5 == 0 && os->lines > 0) {
 461                 fputc('\n', w);
 462             }
 463             os->lines++;
 464 
 465             os->emit_offset(w, os->offset);
 466 
 467             // start next line with offset of its 1st item, also
 468             // changing the background color for the colored hex
 469             // code which will follow
 470             // fprintf(stdout, "%8d", os->offset);
 471             EMIT_CONST(w, "  ");
 472         }
 473 
 474         // show the current byte unstyled
 475         write_hex(w, b);
 476         fputc(' ', w);
 477     }
 478 
 479     if (os->showtxt) {
 480         EMIT_CONST(w, "  ");
 481         for (size_t j = end - i; j < os->line_width; j++) {
 482             EMIT_CONST(w, "   ");
 483         }
 484 
 485         fwrite(os->txt, 1, os->txtlen, w);
 486         fputc('\n', w);
 487         return;
 488     }
 489     fputc('\n', w);
 490 }
 491 
 492 // config has all the settings used to emit output
 493 typedef struct config {
 494     // bytes_per_line determines the `width` of output lines
 495     size_t bytes_per_line;
 496 
 497     // emit_file_info is chosen to emit file-info with colors or plainly
 498     void (*emit_file_info)(FILE* w, const char* path, size_t nbytes);
 499 
 500     // emit_line is chosen to emit hex bytes with colors or plainly
 501     void (*emit_line)(FILE* w, size_t i, size_t end, output_state* os);
 502 
 503     // emit_offset is chosen to emit the offset at the start of each line
 504     void (*emit_offset)(FILE* w, size_t offset);
 505 } config;
 506 
 507 // handle_reader shows all bytes read from the source given as colored hex
 508 // values, showing offsets and ASCII symbols on the sides of each output line
 509 void handle_reader(FILE* w, FILE* src, config cfg) {
 510     // limit line-width to the buffer's capacity
 511     if (cfg.bytes_per_line > IBUF_SIZE) {
 512         cfg.bytes_per_line = IBUF_SIZE;
 513     }
 514 
 515     const size_t two_lines = 2 * cfg.bytes_per_line;
 516     unsigned char txt[two_lines];
 517 
 518     unsigned char buf[IBUF_SIZE];
 519     // ensure the effective buffer-size is a multiple of the line-width
 520     size_t max = sizeof(buf) - sizeof(buf) % cfg.bytes_per_line;
 521 
 522     output_state os;
 523     os.buf = buf;
 524     os.line_width = cfg.bytes_per_line;
 525     os.lines = 0;
 526     os.offset = 0;
 527     os.txt = txt;
 528     os.emit_offset = cfg.emit_offset;
 529     os.showtxt = true;
 530 
 531     const size_t one_line = cfg.bytes_per_line;
 532 
 533     while (!feof(w)) {
 534         os.buflen = fread(&buf, sizeof(buf[0]), max, src);
 535         if (os.buflen < 1) {
 536             // assume input is over when no bytes were read
 537             break;
 538         }
 539 
 540         for (size_t i = 0; i < os.buflen; i += one_line) {
 541             size_t end;
 542 
 543             // remember all ASCII symbols in current pair of output lines
 544             end = i + two_lines < os.buflen ? i + two_lines : os.buflen;
 545             peek_ascii(i, end, &os);
 546 
 547             // show current output line
 548             end = i + one_line < os.buflen ? i + one_line : os.buflen;
 549             cfg.emit_line(w, i, end, &os);
 550         }
 551     }
 552 
 553     fflush(w);
 554 }
 555 
 556 // handle_file handles data from the filename given; returns false only when
 557 // the file can't be opened
 558 bool handle_file(FILE* w, const char* path, config cfg) {
 559     FILE* f = fopen(path, "rb");
 560     if (f == NULL) {
 561         fputc('\n', w);
 562         fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path);
 563         return false;
 564     }
 565 
 566     // get the file size
 567     struct stat st;
 568     fstat(fileno(f), &st);
 569 
 570     // show output
 571     cfg.emit_file_info(w, path, st.st_size);
 572     fputc('\n', w);
 573     handle_reader(w, f, cfg);
 574 
 575     fclose(f);
 576     return true;
 577 }
 578 
 579 // is_help_option simplifies control-flow for func run
 580 bool is_help_option(const char* s) {
 581     return (s[0] == '-') && (
 582         strcmp(s, "-h") == 0 ||
 583         strcmp(s, "-help") == 0 ||
 584         strcmp(s, "--h") == 0 ||
 585         strcmp(s, "--help") == 0
 586     );
 587 }
 588 
 589 // is_plain_option simplifies control-flow for func run
 590 bool is_plain_option(const char* s) {
 591     return (s[0] == '-') && (
 592         strcmp(s, "-p") == 0 ||
 593         strcmp(s, "--p") == 0 ||
 594         strcmp(s, "-plain") == 0 ||
 595         strcmp(s, "--plain") == 0
 596     );
 597 }
 598 
 599 // is_hex_offsets simplifies control-flow for func run
 600 bool is_hex_offsets_option(const char* s) {
 601     return (s[0] == '-') && (
 602         strcmp(s, "-ho") == 0 ||
 603         strcmp(s, "--ho") == 0 ||
 604         strcmp(s, "-hex") == 0 ||
 605         strcmp(s, "--hex") == 0 ||
 606         strcmp(s, "-hexoffsets") == 0 ||
 607         strcmp(s, "--hexoffsets") == 0 ||
 608         strcmp(s, "-hex-offsets") == 0 ||
 609         strcmp(s, "--hex-offsets") == 0
 610     );
 611 }
 612 
 613 // is_dec_offsets simplifies control-flow for func run
 614 bool is_dec_offsets_option(const char* s) {
 615     return (s[0] == '-') && (
 616         strcmp(s, "-do") == 0 ||
 617         strcmp(s, "--do") == 0 ||
 618         strcmp(s, "-dec") == 0 ||
 619         strcmp(s, "--dec") == 0 ||
 620         strcmp(s, "-decoffsets") == 0 ||
 621         strcmp(s, "--decoffsets") == 0 ||
 622         strcmp(s, "-dec-offsets") == 0 ||
 623         strcmp(s, "--dec-offsets") == 0
 624     );
 625 }
 626 
 627 // run returns the number of errors
 628 int run(int argc, char** argv, FILE* w) {
 629     size_t dashes = 0;
 630     for (int i = 1; i < argc; i++) {
 631         if (strcmp(argv[i], "-") == 0) {
 632             dashes++;
 633         }
 634     }
 635 
 636     if (dashes > 1) {
 637         const char* m = "can't use the standard input (dash) more than once";
 638         fprintf(stderr, ERROR_LINE("%s"), m);
 639         return 1;
 640     }
 641 
 642     config cfg;
 643     cfg.bytes_per_line = 16;
 644     cfg.emit_line = &emit_styled_line;
 645     cfg.emit_file_info = &emit_styled_file_info;
 646     cfg.emit_offset = &write_styled_uint;
 647 
 648     size_t files = 0;
 649     size_t errors = 0;
 650 
 651     // handle all filenames/options given
 652     for (size_t i = 1; i < argc && !feof(w); i++) {
 653         // a `-` filename stands for the standard input
 654         if (strcmp(argv[i], "-") == 0) {
 655             EMIT_CONST(w, "• <stdin>\n");
 656             fputc('\n', w);
 657             handle_reader(w, stdin, cfg);
 658             continue;
 659         }
 660 
 661         if (is_plain_option(argv[i])) {
 662             cfg.emit_line = &emit_plain_line;
 663             cfg.emit_file_info = &emit_plain_file_info;
 664             // hex offsets are already plain
 665             if (cfg.emit_offset != &write_hex_uint) {
 666                 cfg.emit_offset = &write_plain_uint;
 667             }
 668             continue;
 669         }
 670 
 671         if (is_hex_offsets_option(argv[i])) {
 672             cfg.emit_offset = &write_hex_uint;
 673             continue;
 674         }
 675 
 676         if (is_dec_offsets_option(argv[i])) {
 677             // keep plain decimal offsets that way
 678             if (cfg.emit_offset != &write_plain_uint) {
 679                 cfg.emit_offset = &write_styled_uint;
 680             }
 681             continue;
 682         }
 683 
 684         if (files > 0) {
 685             // put an empty line between adjacent hex outputs
 686             fputc('\n', w);
 687         }
 688 
 689         if (!handle_file(w, argv[i], cfg)) {
 690             errors++;
 691         }
 692         files++;
 693     }
 694 
 695     // no filenames means use stdin as the only input
 696     if (files == 0 && !feof(w)) {
 697         EMIT_CONST(w, "• <stdin>\n");
 698         fputc('\n', w);
 699         handle_reader(w, stdin, cfg);
 700     }
 701 
 702     return errors;
 703 }
 704 
 705 int main(int argc, char** argv) {
 706 #ifdef _WIN32
 707     setmode(fileno(stdin), O_BINARY);
 708     // ensure output lines end in LF instead of CRLF on windows
 709     setmode(fileno(stdout), O_BINARY);
 710     setmode(fileno(stderr), O_BINARY);
 711 #endif
 712 
 713     if (argc > 1 && is_help_option(argv[1])) {
 714         fprintf(stderr, "%s", info);
 715         return 0;
 716     }
 717 
 718     // enable full buffering for stdout
 719     char outbuf[OBUF_SIZE];
 720     setvbuf(stdout, outbuf, _IOFBF, sizeof(outbuf));
 721 
 722     return run(argc, argv, stdout) == 0 ? 0 : 1;
 723 }