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