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