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 }