File: si.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 ./si ./si.c
  29 */
  30 
  31 #include <arpa/inet.h>
  32 #include <fcntl.h>
  33 #include <netinet/tcp.h>
  34 #include <stdbool.h>
  35 #include <stdint.h>
  36 #include <stdio.h>
  37 #include <string.h>
  38 #include <sys/socket.h>
  39 #include <sys/wait.h>
  40 #include <unistd.h>
  41 
  42 #ifdef _WIN32
  43 #include <windows.h>
  44 #endif
  45 
  46 #ifdef _WIN32
  47 #define WSL
  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 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  62 
  63 #ifndef OPEN_COMMAND
  64 #define OPEN_COMMAND "xdg-open"
  65 #endif
  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) write(w, x, sizeof(x) - 1)
  73 
  74 const char* info = ""
  75 "si [filenames/URIs...]\n"
  76 "\n"
  77 "\n"
  78 "This app (Show It) shows data using your default web browser by auto-opening\n"
  79 "tabs. When reading from stdin, the content-type is auto-detected: data are\n"
  80 "then sent right away to the browser via localhost, using a random port among\n"
  81 "the available ones.\n"
  82 "\n"
  83 "The localhost connection is available only until all data are transferred:\n"
  84 "this means refreshing your browser tab will lose your content, replacing it\n"
  85 "with a server-not-found message page.\n"
  86 "\n"
  87 "When given filenames and/or URIs, the browser tabs will point to their paths,\n"
  88 "so accidentally reloading them doesn't make their content disappear, unless\n"
  89 "those files are actually deleted between reloads.\n"
  90 "\n"
  91 "Dozens of common data-formats are recognized when piped from stdin, such as\n"
  92 "  - HTML (web pages)\n"
  93 "  - PDF\n"
  94 "  - pictures (PNG, JPEG, SVG, WEBP, GIF)\n"
  95 "  - audio (AAC, MP3, FLAC, WAV, AU, MIDI)\n"
  96 "  - video (MP4, MOV, WEBM, MKV, AVI)\n"
  97 "  - JSON\n"
  98 "  - generic UTF-8 plain-text\n"
  99 "\n"
 100 "Base64-encoded data URIs are auto-detected and decoded appropriately.\n"
 101 "";
 102 
 103 #define default_mime_fallback "application/octet-stream"
 104 
 105 // can be anything: ensure this value differs from all other literal bytes
 106 // in the generic-headers table: failing that, its value could cause subtle
 107 // type-misdetection bugs; the value is chosen to be `obviously` findable
 108 // in the source, which also implies a constant beyond the ascii range, as
 109 // ascii char-constants are also used in the tables
 110 const unsigned char cba = 0xfd; // 253
 111 
 112 #define aiff "audio/aiff"
 113 #define au "audio/basic"
 114 #define avi "video/avi"
 115 #define avif "image/avif"
 116 #define bmp "image/x-bmp"
 117 #define caf "audio/x-caf"
 118 #define cur "image/vnd.microsoft.icon"
 119 #define css "text/css"
 120 #define csv "text/csv"
 121 #define djvu "image/x-djvu"
 122 #define elf "application/x-elf"
 123 #define exe "application/vnd.microsoft.portable-executable"
 124 #define flac "audio/x-flac"
 125 #define gif "image/gif"
 126 #define gz "application/gzip"
 127 #define heic "image/heic"
 128 #define htm "text/html"
 129 #define html "text/html"
 130 #define ico "image/x-icon"
 131 #define iso "application/octet-stream"
 132 #define jpeg "image/jpeg"
 133 #define js "application/javascript"
 134 #define json "application/json"
 135 #define m4a "audio/aac"
 136 #define m4v "video/x-m4v"
 137 #define midi "audio/midi"
 138 #define mov "video/quicktime"
 139 #define mp4 "video/mp4"
 140 #define mp3 "audio/mpeg"
 141 #define mpg "video/mpeg"
 142 #define octet "application/octet-stream"
 143 #define ogg "audio/ogg"
 144 #define opus "audio/opus"
 145 #define pdf "application/pdf"
 146 #define png "image/png"
 147 #define ps "application/postscript"
 148 #define psd "image/vnd.adobe.photoshop"
 149 #define rtf "application/rtf"
 150 #define sqlite3 "application/x-sqlite3"
 151 #define svg "image/svg+xml"
 152 #define text "text/plain"
 153 #define tiff "image/tiff"
 154 #define tsv "text/tsv"
 155 #define utf8 "text/plain; charset=UTF-8"
 156 #define wasm "application/wasm"
 157 #define wav "audio/x-wav"
 158 #define webp "image/webp"
 159 #define webm "video/webm"
 160 #define xml "application/xml"
 161 #define zip "application/zip"
 162 #define zst "application/zstd"
 163 
 164 // format_descriptor ties a file-header pattern to its data-format type;
 165 // the 15-byte header-limit nicely aligns with the 1-byte length before it
 166 typedef struct format_descriptor {
 167     unsigned char header_length;
 168     unsigned char header_bytes[15];
 169     const char* mime;
 170 } format_descriptor;
 171 
 172 // starts_as tries to match header data to the pattern given: this includes
 173 // allowing `any byte` when the pattern indicates so, using a value reserved
 174 // for that purpose
 175 bool starts_as(const uint8_t* x, size_t xlen, const uint8_t* y, size_t ylen) {
 176     // when header data aren't enough for a pattern, there's no match
 177     if (xlen < ylen) {
 178         return false;
 179     }
 180 
 181     for (size_t i = 0; i < ylen; i++) {
 182         if (y[i] == cba) {
 183             // `can be anything` value always matches
 184             continue;
 185         }
 186 
 187         if (x[i] != y[i]) {
 188             return false;
 189         }
 190     }
 191 
 192     return true;
 193 }
 194 
 195 // not confident enough to actually use this, and replace all table entries
 196 #define start_format_descriptor(...) \
 197     sizeof((unsigned char[]){ __VA_ARGS__ }) / sizeof(unsigned char), \
 198     { __VA_ARGS__ }
 199 
 200 // format markers with leading wildcards, which should be checked before the
 201 // normal ones: this is to prevent mismatches with the latter types, even
 202 // though you can make probabilistic arguments which suggest these mismatches
 203 // should be very unlikely in practice
 204 format_descriptor special_headers[] = {
 205     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 206     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 207     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 208     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 209     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 210     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 211     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 212     {12, {cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 213     // {
 214     //     24,
 215     //     {
 216     //         cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 217     //         000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 218     //     },
 219     //     m4a,
 220     // },
 221     {0},
 222 };
 223 
 224 format_descriptor hdr_dispatch_0[] = {
 225     {4, {000, 000, 001, 0xBA}, mpg},
 226     {4, {000, 000, 001, 0xB3}, mpg},
 227     {4, {000, 000, 001, 000}, ico},
 228     {4, {000, 000, 002, 000}, cur},
 229     {4, {000, 'a', 's', 'm'}, wasm},
 230     {0},
 231 };
 232 
 233 format_descriptor hdr_dispatch_26[] = {
 234     {4, {0x1A, 0x45, 0xDF, 0xA3}, webm},
 235     {0},
 236 };
 237 
 238 format_descriptor hdr_dispatch_31[] = {
 239     // {4, {0x1F, 0x8B, 0x08, 0x08}, gz},
 240     {3, {0x1F, 0x8B, 0x08}, gz},
 241     {0},
 242 };
 243 
 244 format_descriptor hdr_dispatch_35[] = {
 245     {3, "#! ", text},
 246     {3, "#!/", text},
 247     {0},
 248 };
 249 
 250 format_descriptor hdr_dispatch_37[] = {
 251     {4, "%PDF", pdf},
 252     {4, "%!PS", ps},
 253     {0},
 254 };
 255 
 256 format_descriptor hdr_dispatch_40[] = {
 257     {4, {0x28, 0xB5, 0x2F, 0xFD}, zst},
 258     {0},
 259 };
 260 
 261 format_descriptor hdr_dispatch_46[] = {
 262     {4, ".snd", au},
 263     {0},
 264 };
 265 
 266 format_descriptor hdr_dispatch_56[] = {
 267     {4, "8BPS", psd},
 268     {0},
 269 };
 270 
 271 format_descriptor hdr_dispatch_60[] = {
 272     {15, "<!DOCTYPE html>", html},
 273     {15, "<!DOCTYPE html ", html},
 274     {5, "<svg>", svg},
 275     {5, "<svg ", svg},
 276     {6, "<html>", html},
 277     {6, "<html ", html},
 278     {6, "<head>", html},
 279     {6, "<head ", html},
 280     {6, "<body>", html},
 281     {6, "<body ", html},
 282     {6, "<?xml>", xml},
 283     {6, "<?xml ", xml},
 284     {0},
 285 };
 286 
 287 format_descriptor hdr_dispatch_65[] = {
 288     {
 289         15,
 290         {
 291             'A', 'T', '&', 'T', 'F', 'O', 'R', 'M',
 292             cba, cba, cba, cba, 'D', 'J', 'V',
 293         },
 294         djvu,
 295     },
 296     {0},
 297 };
 298 
 299 format_descriptor hdr_dispatch_66[] = {
 300     {
 301         15,
 302         {
 303             'B', 'M', cba, cba, cba, cba, cba, cba,
 304             cba, cba, cba, cba, cba, cba, 0x28,
 305         },
 306         bmp,
 307     },
 308     {0},
 309 };
 310 
 311 format_descriptor hdr_dispatch_70[] = {
 312     {12, {'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 313     {12, {'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 314     {0},
 315 };
 316 
 317 format_descriptor hdr_dispatch_71[] = {
 318     {6, "GIF87a", gif},
 319     {6, "GIF89a", gif},
 320     {0},
 321 };
 322 
 323 format_descriptor hdr_dispatch_73[] = {
 324     {4, {'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 325     {4, {'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 326     {4, {'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 327     {4, {'I', 'I', '*', 000}, tiff},
 328     {0},
 329 };
 330 
 331 format_descriptor hdr_dispatch_77[] = {
 332     {4, {'M', 'M', 000, '*'}, tiff},
 333     {4, "MThd", midi},
 334     {6, {'M', 'Z', cba, 000, cba, 000}, exe},
 335     // {6, {'M', 'Z', 0x90, 000, 003, 000}, exe},
 336     // {6, {'M', 'Z', 0x78, 000, 001, 000}, exe},
 337     // {6, {'M', 'Z', 'P', 000, 002, 000}, exe},
 338     {0},
 339 };
 340 
 341 format_descriptor hdr_dispatch_79[] = {
 342     {4, "OggS", ogg},
 343     {0},
 344 };
 345 
 346 format_descriptor hdr_dispatch_80[] = {
 347     {4, {'P', 'K', 003, 004}, zip},
 348     {0},
 349 };
 350 
 351 format_descriptor hdr_dispatch_82[] = {
 352     {12, {'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
 353     {12, {'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
 354     {12, {'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
 355     {0},
 356 };
 357 
 358 // format_descriptor hdr_dispatch_83[] = {
 359 //     // {16, "SQLite format 3\x00", sqlite3},
 360 //     {0},
 361 // };
 362 
 363 format_descriptor hdr_dispatch_99[] = {
 364     {8, {'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
 365     {0},
 366 };
 367 
 368 format_descriptor hdr_dispatch_102[] = {
 369     {4, "fLaC", flac},
 370     {0},
 371 };
 372 
 373 format_descriptor hdr_dispatch_123[] = {
 374     {4, "{\\rtf", rtf},
 375     {0},
 376 };
 377 
 378 format_descriptor hdr_dispatch_127[] = {
 379     {4, {127, 'E', 'L', 'F'}, elf},
 380     {0},
 381 };
 382 
 383 format_descriptor hdr_dispatch_137[] = {
 384     {8, {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
 385     {0},
 386 };
 387 
 388 format_descriptor hdr_dispatch_255[] = {
 389     {3, {0xFF, 0xD8, 0xFF}, jpeg},
 390     {5, {0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
 391     {2, {0xFF, 0xFB}, mp3},
 392     {0},
 393 };
 394 
 395 // hdr_dispatch groups format-description-groups by their first byte, thus
 396 // shortening total lookups for some data header
 397 //
 398 // notice how the `ftyp` data formats aren't handled here, since these can
 399 // start with any byte, instead of the literal value of the any-byte markers
 400 // they use
 401 //
 402 // all entries are arrays which must always end with a special entry whose
 403 // pattern-length is declared to be 0, since there's no explicit way to know
 404 // the length of these arrays when looping on them
 405 //
 406 // all non-null entries are setup explicitly, later in the code
 407 format_descriptor* hdr_dispatch[256] = {
 408     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 409     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 410     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 411     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 412     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 413     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 414     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 415     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 416     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 417     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 418     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 419     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 420     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 421     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 422     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 423     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 424     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 425     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 426     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 427     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 428     NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
 429     NULL, NULL, NULL, NULL,
 430 };
 431 
 432 typedef struct guess_mime_args {
 433     const unsigned char* buf;
 434     size_t len;
 435     const char* fallback_mime_type;
 436 } guess_mime_args;
 437 
 438 // guess_mime tries to auto-detect a MIME-type from the header bytes given,
 439 // using the lookup-tables
 440 const char* guess_mime(guess_mime_args args) {
 441     const unsigned char* buf = args.buf;
 442     const size_t len = args.len;
 443     const char* fallback_mime_type = args.fallback_mime_type;
 444 
 445     if (len == 0) {
 446         return NULL;
 447     }
 448 
 449     // just in case, start with the patterns which allow any first byte
 450     for (size_t i = 0; special_headers[i].header_length > 0; i++) {
 451         const unsigned char* hb = special_headers[i].header_bytes;
 452         const size_t hl = special_headers[i].header_length;
 453         if (starts_as(buf, len, hb, hl)) {
 454             return special_headers[i].mime;
 455         }
 456     }
 457 
 458     // the m4a-dash header exceeds the 15-byte limit of the lookup tables
 459     const uint8_t header1[24] = {
 460         cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 461         000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 462     };
 463     if (starts_as(buf, len, header1, sizeof(header1))) {
 464         return m4a;
 465     }
 466 
 467     // the sqlite3 header exceeds the 15-byte limit of the lookup tables
 468     const uint8_t header2[16] = "SQLite format 3\x00";
 469     if (starts_as(buf, len, header2, sizeof(header2))) {
 470         return sqlite3;
 471     }
 472 
 473     format_descriptor* guesses = hdr_dispatch[buf[0]];
 474     if (guesses == NULL) {
 475         return fallback_mime_type;
 476     }
 477 
 478     for (size_t i = 0; guesses[i].header_length > 0; i++) {
 479         const unsigned char* hb = guesses[i].header_bytes;
 480         const size_t hl = guesses[i].header_length;
 481         if (starts_as(buf, len, hb, hl)) {
 482             return guesses[i].mime;
 483         }
 484     }
 485 
 486     return fallback_mime_type;
 487 }
 488 
 489 #ifdef WSL
 490 bool pop_up(const char* s) {
 491     return execlp("cmd.exe", "/b", "/c", "start", "", s, NULL) >= 0;
 492     // return execlp("rundll32", "url.dll,FileProtocolHandler", s, NULL) >= 0;
 493 }
 494 #else
 495 bool pop_up(const char* what) {
 496     return execlp(OPEN_COMMAND, what, NULL) >= 0;
 497 }
 498 #endif
 499 
 500 bool pop_up_wait(const char* what) {
 501     int code = 0;
 502     pid_t pid = fork();
 503     if (pid < 0) {
 504         return false;
 505     }
 506     if (pid == 0) {
 507         code = pop_up(what) ? 0 : 1;
 508     } else {
 509         code = 1;
 510         waitpid(pid, &code, 0);
 511     }
 512     return code == 0;
 513 }
 514 
 515 void ignore_request(const int conn) {
 516     char buf[4 * 1024];
 517 
 518     while (true) {
 519         const size_t n = recv(conn, buf, sizeof(buf), 0);
 520         if (n < 4) {
 521             return;
 522         }
 523 
 524         if (
 525             buf[n - 4] == '\r' && buf[n - 3] == '\n' &&
 526             buf[n - 2] == '\r' && buf[n - 1] == '\n'
 527         ) {
 528             return;
 529         }
 530     }
 531 }
 532 
 533 bool sync_send(int conn, const unsigned char* buf, size_t n) {
 534     struct tcp_info tcp_info;
 535     socklen_t size = sizeof(tcp_info);
 536     send(conn, buf, n, 0);
 537 
 538     for (size_t i = 0; i < 10; i++) {
 539         getsockopt(conn, SOL_TCP, TCP_INFO, &tcp_info, &size);
 540         if (tcp_info.tcpi_unacked <= 0) {
 541             return true;
 542         }
 543         usleep(1000);
 544     }
 545 
 546     return false;
 547 }
 548 
 549 size_t send_all(int conn, const unsigned char* buf, size_t n) {
 550     size_t sent = 0;
 551     const size_t min_delay = 50 * 1000;
 552     // const size_t max_delay = 50 * 1000;
 553     size_t delay = min_delay;
 554 
 555     for (size_t i = 0; i < 100 && n > 0; i++) {
 556         const size_t delta = send(conn, buf, n, 0);
 557         if (i > 0) {
 558             usleep(delay);
 559         }
 560 
 561         n -= delta;
 562         buf += delta;
 563         sent += delta;
 564 
 565         if (n > 0) {
 566             fprintf(stderr, "send_all: %zu bytes still pending\n", n);
 567         }
 568     }
 569 
 570     return sent;
 571 }
 572 
 573 bool handle_reader(FILE* src, const char* fallback_mime_type) {
 574     unsigned char buf[IBUF_SIZE];
 575 
 576     size_t n = fread(&buf, sizeof(buf[0]), sizeof(buf), src);
 577     guess_mime_args args;
 578     args.buf = buf;
 579     args.len = n;
 580     args.fallback_mime_type = fallback_mime_type;
 581     const char* mime = guess_mime(args);
 582     if (mime == NULL || mime[0] == 0) {
 583         return false;
 584     }
 585 
 586     const int fd = socket(AF_INET, SOCK_STREAM, 0);
 587     if (fd < 0) {
 588         fprintf(stderr, ERROR_LINE("can't open socket connection"));
 589         return false;
 590     }
 591     if (fcntl(fd, F_GETFD) == -1) {
 592         close(fd);
 593         fprintf(stderr, ERROR_LINE("can't open socket connection"));
 594         return false;
 595     }
 596 
 597     struct sockaddr_in srv;
 598     memset(&srv, 0, sizeof(srv));
 599     srv.sin_family = AF_INET;
 600     srv.sin_port = htons(0);
 601     srv.sin_addr.s_addr = htonl(INADDR_ANY);
 602     // srv.sin_addr.s_addr = inet_addr("0.0.0.0");
 603 
 604     if (bind(fd, (struct sockaddr*)&srv, sizeof(srv)) != 0) {
 605         close(fd);
 606         fprintf(stderr, ERROR_LINE("can't open socket connection"));
 607         return false;
 608     }
 609 
 610     if (listen(fd, 1) != 0) {
 611         close(fd);
 612         fprintf(stderr, ERROR_LINE("can't listen on open connection"));
 613         return false;
 614     }
 615 
 616     // get the randomly-picked port number
 617     socklen_t size = sizeof(srv);
 618     if (getsockname(fd, (struct sockaddr*)&srv, &size) < 0) {
 619         fprintf(stderr, ERROR_LINE("can't get socket name"));
 620         return false;
 621     }
 622 
 623     char uri[256];
 624     const int port = ntohs(srv.sin_port);
 625     sprintf(uri, "http://localhost:%d", port);
 626     // fprintf(stderr, "opening %s\n", uri);
 627 
 628     pop_up_wait(uri);
 629 
 630     const int conn = accept(fd, NULL, NULL);
 631     if (conn < 0) {
 632         close(fd);
 633         fprintf(stderr, ERROR_LINE("failed to accept connection"));
 634         return false;
 635     }
 636 
 637     ignore_request(conn);
 638 
 639     EMIT_CONST(conn, "HTTP/1.1 200 OK\r\n");
 640     EMIT_CONST(conn, "Content-Type: ");
 641     write(conn, mime, strlen(mime));
 642     EMIT_CONST(conn, "\r\n");
 643     EMIT_CONST(conn, "Content-Disposition: inline\r\n");
 644     EMIT_CONST(conn, "Connection: close\r\n");
 645     EMIT_CONST(conn, "\r\n");
 646 
 647     size_t got = n;
 648     // size_t sent = send_all(conn, buf, n);
 649     size_t sent = send(conn, buf, n, 0);
 650     const size_t delay = 100 * 1000;
 651 
 652     while (true) {
 653         // while (!feof(src)) {
 654         // n = fread(&buf, sizeof(buf[0]), sizeof(buf), src);
 655         // if (n < 1) {
 656         //     usleep(delay);
 657         //     continue;
 658         // }
 659         n = fread(&buf, sizeof(buf[0]), sizeof(buf) - 1, src);
 660         int last = fgetc(src);
 661         if (last != EOF) {
 662             buf[n] = (unsigned char)last;
 663             n++;
 664         }
 665         if (n < 1 && last != EOF) {
 666             usleep(delay);
 667             continue;
 668         }
 669 
 670         got += n;
 671         // const size_t delta = send_all(conn, buf, n);
 672         const size_t delta = send(conn, buf, n, 0);
 673         sent += delta;
 674         fprintf(stderr, "stdin eof: %d\n", feof(src));
 675         fprintf(stderr, "so far, got %zu bytes, sent %zu bytes\n", got, sent);
 676         if (delta == 0) {
 677             break;
 678         }
 679         if (last == EOF) {
 680             break;
 681         }
 682     }
 683 
 684     shutdown(conn, SHUT_WR);
 685     close(conn);
 686     close(fd);
 687     return true;
 688 }
 689 
 690 // is_help_option simplifies control-flow for func run
 691 bool is_help_option(const char* s) {
 692     return s[0] == '-' && (
 693         strcmp(s, "-h") == 0 ||
 694         strcmp(s, "-help") == 0 ||
 695         strcmp(s, "--h") == 0 ||
 696         strcmp(s, "--help") == 0
 697     );
 698 }
 699 
 700 // is_fallback_option simplifies control-flow for func run
 701 bool is_fallback_option(const char* s) {
 702     return s[0] == '-' && (
 703         strcmp(s, "-f") == 0 ||
 704         strcmp(s, "-fallback") == 0 ||
 705         strcmp(s, "--f") == 0 ||
 706         strcmp(s, "--fallback") == 0 ||
 707         strcmp(s, "-m") == 0 ||
 708         strcmp(s, "-mime") == 0 ||
 709         strcmp(s, "--m") == 0 ||
 710         strcmp(s, "--mime") == 0 ||
 711         strcmp(s, "-t") == 0 ||
 712         strcmp(s, "-type") == 0 ||
 713         strcmp(s, "--t") == 0 ||
 714         strcmp(s, "--type") == 0
 715     );
 716 }
 717 
 718 const char* fallback_aliases[196] = {
 719     // tiny shortcuts
 720     "b", octet,
 721     "j", json,
 722     "t", text,
 723     "u", utf8,
 724 
 725     // failure fallbacks
 726     "e", "",
 727     "err", "",
 728     "error", "",
 729     "f", "",
 730     "fail", "",
 731 
 732     // common mistakes
 733     "text/json", json,
 734 
 735     // other
 736     "", default_mime_fallback,
 737     "default", default_mime_fallback,
 738 
 739     "aif", aiff,
 740     "aiff", aiff,
 741     "au", au,
 742     "avi", avi,
 743     "avif", avif,
 744     "bmp", bmp,
 745     "caf", caf,
 746     "cur", cur,
 747     "css", css,
 748     "csv", csv,
 749     "djvu", djvu,
 750     "elf", elf,
 751     "exe", exe,
 752     "flac", flac,
 753     "gif", gif,
 754     "gz", gz,
 755     "heic", heic,
 756     "html", html,
 757     "ico", ico,
 758     "iso", iso,
 759     "jpg", jpeg,
 760     "jpeg", jpeg,
 761     "js", js,
 762     "json", json,
 763     "m4a", m4a,
 764     "m4v", m4v,
 765     "midi", midi,
 766     "mov", mov,
 767     "mp4", mp4,
 768     "mp3", mp3,
 769     "mpeg", mpg,
 770     "ogg", ogg,
 771     "opus", opus,
 772     "pdf", pdf,
 773     "png", png,
 774     "ps", ps,
 775     "psd", psd,
 776     "rtf", rtf,
 777     "sqlite3", sqlite3,
 778     "svg", svg,
 779     "text", text,
 780     "tiff", tiff,
 781     "tsv", tsv,
 782     "wasm", wasm,
 783     "wav", wav,
 784     "webp", webp,
 785     "webm", webm,
 786     "xml", xml,
 787     "zip", zip,
 788     "zst", zst,
 789 
 790     // longer shortcuts
 791     "aac", m4a,
 792     "aif", aiff,
 793     "bin", octet,
 794     "binary", octet,
 795     "bits", octet,
 796     "gzip", gz,
 797     "htm", htm,
 798     "mid", midi,
 799     "mpg", mpg,
 800     "octet", octet,
 801     "octets", octet,
 802     "octetstream", octet,
 803     "octet-stream", octet,
 804     "plain", text,
 805     "sqlite", sqlite3,
 806     "svg+xml", svg,
 807     "tif", tiff,
 808     "utf8", utf8,
 809     "utf-8", utf8,
 810     "xbmp", bmp,
 811     "xcaf", caf,
 812     "xflac", flac,
 813     "xicon", ico,
 814     "xm4v", m4v,
 815     "xsqlite3", sqlite3,
 816     "xwav", wav,
 817     "xwave", wav,
 818     "x-bmp", bmp,
 819     "x-caf", caf,
 820     "x-flac", flac,
 821     "x-icon", ico,
 822     "x-m4v", m4v,
 823     "x-sqlite3", sqlite3,
 824     "x-wav", wav,
 825     "wave", wav,
 826     "zstd", zst,
 827 };
 828 
 829 const char* resolve_alias(const char* name) {
 830     const size_t n = sizeof(fallback_aliases) / sizeof(fallback_aliases[0]);
 831     for (size_t i = 0; i < n; i += 2) {
 832         if (strcmp(name, fallback_aliases[i]) == 0) {
 833             return fallback_aliases[i + 1];
 834         }
 835     }
 836     return name;
 837 }
 838 
 839 // run returns the number of errors
 840 int run(int argc, char** argv) {
 841     const char* fallback_mime_type = default_mime_fallback;
 842 
 843     size_t files = 0;
 844     size_t dashes = 0;
 845     size_t errors = 0;
 846     bool change_fallback = false;
 847     bool options = true;
 848 
 849     // handle all filenames given
 850     for (size_t i = 1; i < argc; i++) {
 851         if (strcmp(argv[i], "-") == 0) {
 852             dashes++;
 853             if (dashes < 2) {
 854                 if (!handle_reader(stdin, fallback_mime_type)) {
 855                     errors++;
 856                 }
 857                 files++;
 858             }
 859             continue;
 860         }
 861 
 862         if (strcmp(argv[i], "--") == 0) {
 863             options = false;
 864             continue;
 865         }
 866 
 867         if (change_fallback) {
 868             fallback_mime_type = resolve_alias(argv[i]);
 869             change_fallback = false;
 870             continue;
 871         }
 872 
 873         if (options && is_fallback_option(argv[i])) {
 874             change_fallback = true;
 875             continue;
 876         }
 877 
 878         if (!pop_up_wait(argv[i])) {
 879             errors++;
 880         }
 881         files++;
 882     }
 883 
 884     if (change_fallback) {
 885         fprintf(stderr, ERROR_LINE("forgot new fallback MIME-type"));
 886         errors++;
 887         return errors;
 888     }
 889 
 890     // no filenames means use stdin as the only input
 891     if (files == 0) {
 892         if (!handle_reader(stdin, fallback_mime_type)) {
 893             errors++;
 894         }
 895     }
 896 
 897     return errors;
 898 }
 899 
 900 int main(int argc, char** argv) {
 901 #ifdef _WIN32
 902     setmode(fileno(stdin), O_BINARY);
 903     // ensure output lines end in LF instead of CRLF on windows
 904     setmode(fileno(stdout), O_BINARY);
 905     setmode(fileno(stderr), O_BINARY);
 906 #endif
 907 
 908     if (argc > 1 && is_help_option(argv[1])) {
 909         printf("%s", info);
 910         return 0;
 911     }
 912 
 913     // fill entries in the type-detection dispatch table
 914     memset(hdr_dispatch, 0, sizeof(hdr_dispatch));
 915     hdr_dispatch[0] = hdr_dispatch_0; // 0
 916     hdr_dispatch[26] = hdr_dispatch_26; // 26
 917     hdr_dispatch[31] = hdr_dispatch_31; // 31
 918     hdr_dispatch[35] = hdr_dispatch_35; // 35 #
 919     hdr_dispatch[37] = hdr_dispatch_37; // 37 %
 920     hdr_dispatch[40] = hdr_dispatch_40; // 40 (
 921     hdr_dispatch[46] = hdr_dispatch_46; // 46 .
 922     hdr_dispatch[56] = hdr_dispatch_56; // 56 8
 923     hdr_dispatch[60] = hdr_dispatch_60; // 60 <
 924     hdr_dispatch[65] = hdr_dispatch_65; // 65 A
 925     hdr_dispatch[66] = hdr_dispatch_66; // 66 B
 926     hdr_dispatch[70] = hdr_dispatch_70; // 70 F
 927     hdr_dispatch[71] = hdr_dispatch_71; // 71 G
 928     hdr_dispatch[73] = hdr_dispatch_73; // 73 I
 929     hdr_dispatch[77] = hdr_dispatch_77; // 77 M
 930     hdr_dispatch[79] = hdr_dispatch_79; // 79 O
 931     hdr_dispatch[80] = hdr_dispatch_80; // 80 P
 932     hdr_dispatch[82] = hdr_dispatch_82; // 82 R
 933     // hdr_dispatch[83] = hdr_dispatch_83; // 83 S
 934     hdr_dispatch[99] = hdr_dispatch_99; // 99 c
 935     hdr_dispatch[102] = hdr_dispatch_102; // 102 f
 936     hdr_dispatch[123] = hdr_dispatch_123; // 123 {
 937     hdr_dispatch[127] = hdr_dispatch_127; // 127
 938     hdr_dispatch[137] = hdr_dispatch_137; // 137
 939     hdr_dispatch[255] = hdr_dispatch_255; // 255
 940 
 941     // setvbuf(stdin, NULL, _IOFBF, 0);
 942     // setvbuf(stdin, NULL, _IONBF, 0);
 943     return run(argc, argv) == 0 ? 0 : 1;
 944 }