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