File: si.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 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 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29 go build -ldflags "-s -w" -trimpath si.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "bytes"
  37     "encoding/base64"
  38     "errors"
  39     "io"
  40     "net"
  41     "os"
  42     "os/exec"
  43     "path/filepath"
  44     "runtime"
  45     "strconv"
  46     "strings"
  47 )
  48 
  49 const info = `
  50 si [options...] [filenames/URIs...]
  51 
  52 
  53 This app (Show It) shows data using your default web browser by auto-opening
  54 tabs. When reading from stdin, the content-type is auto-detected: data are
  55 then sent right away to the browser via localhost, using a random port among
  56 the available ones.
  57 
  58 The localhost connection is available only until all data are transferred:
  59 this means refreshing your browser tab will lose your content, replacing it
  60 with a server-not-found message page.
  61 
  62 When given filenames and/or URIs, the browser tabs will point to their paths,
  63 so accidentally reloading them doesn't make their content disappear, unless
  64 those files are actually deleted between reloads.
  65 
  66 Dozens of common data-formats are recognized when piped from stdin, such as
  67   - HTML (web pages)
  68   - PDF
  69   - pictures (PNG, JPEG, SVG, WEBP, GIF)
  70   - audio (AAC, MP3, FLAC, WAV, AU, MIDI)
  71   - video (MP4, MOV, WEBM, MKV, AVI)
  72   - JSON
  73   - generic UTF-8 plain-text
  74 
  75 Base64-encoded data URIs are auto-detected and decoded appropriately.
  76 
  77 The options are, available both in single and double-dash versions
  78 
  79     -h           show this help message
  80     -help        show this help message
  81 
  82     -from        declare MIME type, instead of auto-guessing it
  83     -mime        declare MIME type, instead of auto-guessing it
  84 
  85     -play        autoplay media; useful only for audio/video data
  86     -autoplay    autoplay media; useful only for audio/video data
  87 `
  88 
  89 func main() {
  90     var cfg config
  91     args := os.Args[1:]
  92 
  93     for len(args) > 0 {
  94         if hasAnyPrefix(args[0], `-from=`, `--from=`, `-mime=`, `--mime=`) {
  95             cfg.From = args[0][strings.IndexByte(args[0], '=')+1:]
  96             args = args[1:]
  97             continue
  98         }
  99 
 100         switch args[0] {
 101         case `-h`, `--h`, `-help`, `--help`:
 102             os.Stdout.WriteString(info[1:])
 103             return
 104 
 105         case `-autoplay`, `--autoplay`, `-play`, `--play`:
 106             cfg.Autoplay = true
 107             args = args[1:]
 108             continue
 109 
 110         case `-from`, `--from`, `-mime`, `--mime`:
 111             if len(args) == 0 {
 112                 os.Stderr.WriteString("missing MIME-type argument\n")
 113                 os.Exit(1)
 114                 return
 115             }
 116             cfg.From = args[1]
 117             args = args[2:]
 118             continue
 119         }
 120 
 121         break
 122     }
 123 
 124     if len(args) > 0 && args[0] == `--` {
 125         args = args[1:]
 126     }
 127 
 128     nerr := 0
 129 
 130     // show all filenames/URIs given by opening new browser tabs for each
 131     for _, s := range args {
 132         s = strings.TrimSpace(s)
 133         if err := handle(s, cfg); err != nil {
 134             os.Stderr.WriteString(err.Error())
 135             os.Stderr.WriteString("\n")
 136             nerr++
 137         }
 138     }
 139 
 140     // serve from stdin only if no filenames were given
 141     if len(args) == 0 {
 142         if err := handleInput(os.Stdin, cfg); err != nil {
 143             os.Stderr.WriteString(err.Error())
 144             os.Stderr.WriteString("\n")
 145             nerr++
 146         }
 147     }
 148 
 149     // quit in failure if any input clearly failed to show up
 150     if nerr > 0 {
 151         os.Exit(1)
 152         return
 153     }
 154 }
 155 
 156 func hasAnyPrefix(s string, prefixes ...string) bool {
 157     for _, p := range prefixes {
 158         if strings.HasPrefix(s, p) {
 159             return true
 160         }
 161     }
 162     return false
 163 }
 164 
 165 // handle shows a filename/URI by operning a new browser tab for it
 166 func handle(s string, cfg config) error {
 167     // open a new browser window for each URI
 168     if strings.HasPrefix(s, `https://`) || strings.HasPrefix(s, `http://`) {
 169         return showURI(s)
 170     }
 171 
 172     // handle data-URIs
 173     if strings.HasPrefix(s, `data:`) && strings.Contains(s, `;base64,`) {
 174         if err := showURI(s); err != nil {
 175             return err
 176         }
 177         return handleInput(strings.NewReader(s), cfg)
 178     }
 179 
 180     // the browser needs full paths when showing local files
 181     fpath, err := filepath.Abs(s)
 182     if err != nil {
 183         return err
 184     }
 185 
 186     // open a new browser tab for each full-path filename
 187     return showURI(`file:///` + fpath)
 188 }
 189 
 190 // showURI tries to open the file/url given using the host operating system's
 191 // defaults
 192 func showURI(what string) error {
 193     const fph = `url.dll,FileProtocolHandler`
 194 
 195     switch runtime.GOOS {
 196     case `windows`:
 197         return exec.Command(`rundll32`, fph, what).Run()
 198     case `darwin`:
 199         return exec.Command(`open`, what).Run()
 200     default:
 201         return exec.Command(`xdg-open`, what).Run()
 202     }
 203 }
 204 
 205 // handleInput specifically handles stdin and data-URIs
 206 func handleInput(r io.Reader, cfg config) error {
 207     if cfg.From != `` {
 208         return serveOnce(nil, r, serveConfig{
 209             ContentType:   cfg.From,
 210             ContentLength: -1,
 211             Autoplay:      cfg.Autoplay,
 212         })
 213     }
 214 
 215     // before starting the single-request server, try to detect the MIME type
 216     // by inspecting the first bytes of the stream and matching known filetype
 217     // starting patterns
 218     var buf [64]byte
 219     n, err := r.Read(buf[:])
 220     if err != nil && err != io.EOF {
 221         return err
 222     }
 223     start := buf[:n]
 224 
 225     // handle data-URI-like inputs
 226     if bytes.HasPrefix(start, []byte(`data:`)) {
 227         if bytes.Contains(start, []byte(`;base64,`)) {
 228             return handleDataURI(start, r, cfg)
 229         }
 230     }
 231 
 232     // handle regular data, trying to auto-detect its MIME type using
 233     // its first few bytes
 234     mime, ok := detectMIME(start)
 235     if !ok {
 236         mime = cfg.From
 237     }
 238     if mime == `` {
 239         mime = `text/plain`
 240     }
 241 
 242     // remember to precede the partly-used reader with the starting bytes;
 243     // give a negative/invalid filesize hint, since stream is single-use
 244     return serveOnce(start, r, serveConfig{
 245         ContentType:   mime,
 246         ContentLength: -1,
 247         Autoplay:      cfg.Autoplay,
 248     })
 249 }
 250 
 251 // handleDataURI handles data-URIs for func handleInput
 252 func handleDataURI(start []byte, r io.Reader, cfg config) error {
 253     if !bytes.HasPrefix(start, []byte(`data:`)) {
 254         return errors.New(`invalid data-URI`)
 255     }
 256 
 257     i := bytes.Index(start, []byte(`;base64,`))
 258     if i < 0 {
 259         return errors.New(`invalid data-URI`)
 260     }
 261 
 262     // force browser to play wave and aiff sounds, instead of
 263     // showing a useless download-file option
 264     switch mime := string(start[len(`data:`):i]); mime {
 265     case `audio/wav`, `audio/wave`, `audio/x-wav`, `audio/aiff`, `audio/x-aiff`:
 266         before := beforeAudio
 267         if cfg.Autoplay {
 268             before = beforeAutoplayAudio
 269         }
 270 
 271         // surround URI-encoded audio data with a web page only having
 272         // a media player in it: this is necessary for wave and aiff
 273         // sounds, since web browsers may insist on a useless download
 274         // option for those media types
 275         r = io.MultiReader(
 276             strings.NewReader(before),
 277             bytes.NewReader(start),
 278             r,
 279             strings.NewReader(afterAudio),
 280         )
 281 
 282         return serveOnce(nil, r, serveConfig{
 283             ContentType:   `text/html; charset=UTF-8`,
 284             ContentLength: -1,
 285             Autoplay:      cfg.Autoplay,
 286         })
 287 
 288     case `image/bmp`, `image/x-bmp`:
 289         // surround URI-encoded bitmap data with a web page only having
 290         // an image element in it: this is necessary for bitmap pictures,
 291         // since web browsers may insist on a useless download option for
 292         // that media type
 293         r = io.MultiReader(
 294             strings.NewReader(beforeBitmap),
 295             bytes.NewReader(start),
 296             r,
 297             strings.NewReader(afterBitmap),
 298         )
 299 
 300         return serveOnce(nil, r, serveConfig{
 301             ContentType:   `text/html; charset=UTF-8`,
 302             ContentLength: -1,
 303             Autoplay:      cfg.Autoplay,
 304         })
 305 
 306     default:
 307         start = start[i+len(`;base64,`):]
 308         r = io.MultiReader(bytes.NewReader(start), r)
 309         dec := base64.NewDecoder(base64.URLEncoding, r)
 310 
 311         // give a negative/invalid filesize hint, since stream is single-use
 312         return serveOnce(nil, dec, serveConfig{
 313             ContentType:   mime,
 314             ContentLength: -1,
 315             Autoplay:      cfg.Autoplay,
 316         })
 317     }
 318 }
 319 
 320 // config is the result of parsing all cmd-line arguments the app was given
 321 type config struct {
 322     // From is an optional hint for the source data format, and disables
 323     // type-autodetection when it's non-empty
 324     From string
 325 
 326     // Autoplay autoplays audio/video data from stdin
 327     Autoplay bool
 328 }
 329 
 330 const (
 331     fromUsage = `` +
 332         `declare MIME-type, disabling type-autodetection; ` +
 333         `use when MIME-type autodetection fails, or to use a ` +
 334         `charset different from UTF-8`
 335 
 336     mimeUsage     = `alias for option -from`
 337     playUsage     = `alias for option -autoplay`
 338     autoplayUsage = `autoplay; useful only when stdin has audio/video data`
 339 )
 340 
 341 // serveConfig has all details func serveOnce needs
 342 type serveConfig struct {
 343     // ContentType is the MIME type of what's being served
 344     ContentType string
 345 
 346     // ContentLength is the byte-count of what's being served; negative
 347     // values are ignored
 348     ContentLength int
 349 
 350     // Autoplay autoplays audio/video data from stdin
 351     Autoplay bool
 352 }
 353 
 354 // makeDotless is similar to filepath.Ext, except its results never start
 355 // with a dot
 356 func makeDotless(s string) string {
 357     i := strings.LastIndexByte(s, '.')
 358     if i >= 0 {
 359         return s[(i + 1):]
 360     }
 361     return s
 362 }
 363 
 364 // hasPrefixByte is a simpler, single-byte version of bytes.HasPrefix
 365 func hasPrefixByte(b []byte, prefix byte) bool {
 366     return len(b) > 0 && b[0] == prefix
 367 }
 368 
 369 // hasPrefixFold is a case-insensitive bytes.HasPrefix
 370 func hasPrefixFold(s []byte, prefix []byte) bool {
 371     n := len(prefix)
 372     return len(s) >= n && bytes.EqualFold(s[:n], prefix)
 373 }
 374 
 375 // trimLeadingWhitespace ignores leading space-like symbols: this is useful
 376 // to handle text-based data formats more flexibly
 377 func trimLeadingWhitespace(b []byte) []byte {
 378     for len(b) > 0 {
 379         switch b[0] {
 380         case ' ', '\t', '\n', '\r':
 381             b = b[1:]
 382         default:
 383             return b
 384         }
 385     }
 386 
 387     // an empty slice is all that's left, at this point
 388     return nil
 389 }
 390 
 391 const (
 392     // maxbufsize is the max capacity the HTTP-protocol line-scanners are
 393     // allowed to reach
 394     maxbufsize = 128 * 1024
 395 
 396     // beforeAudio starts HTML webpage with just an audio player
 397     beforeAudio = `<!DOCTYPE html>
 398 <html>
 399 <head>
 400   <meta charset="UTF-8">
 401   <link rel="icon" href="data:,">
 402   <title>wave sound</title>
 403   <style>
 404     body { margin: 2rem auto; width: 90vw; }
 405     audio { margin: auto; width: 100%; }
 406   </style>
 407 </head>
 408 <body>
 409   <audio controls autofocus src="`
 410 
 411     // beforeAutoplayAudio starts HTML webpage with just an audio player
 412     // in autoplay mode
 413     beforeAutoplayAudio = `<!DOCTYPE html>
 414     <html>
 415     <head>
 416       <meta charset="UTF-8">
 417       <link rel="icon" href="data:,">
 418       <title>wave sound</title>
 419       <style>
 420         body { margin: 2rem auto; width: 90vw; }
 421         audio { margin: auto; width: 100%; }
 422       </style>
 423     </head>
 424     <body>
 425       <audio controls autofocus autoplay src="`
 426 
 427     // afterAudio ends HTML webpage with just an audio player
 428     afterAudio = "\"></audio>\n</body>\n</html>\n"
 429 
 430     // beforeBitmap starts HTML webpage with just an image
 431     beforeBitmap = `<!DOCTYPE html>
 432 <html>
 433 <head>
 434   <meta charset="UTF-8">
 435   <link rel="icon" href="data:,">
 436   <title>bitmap image</title>
 437   <style>
 438     body { margin: 0.5rem auto; width: 90vw; }
 439     img { margin: auto; width: 100%; }
 440   </style>
 441 </head>
 442 <body>
 443   <img src="`
 444 
 445     // afterBitmap ends HTML webpage with just an image
 446     afterBitmap = "\"></img>\n</body>\n</html>\n"
 447 )
 448 
 449 // serveOnce literally serves a single web request and no more
 450 func serveOnce(start []byte, rest io.Reader, cfg serveConfig) error {
 451     // pick a random port from the currently-available ones
 452     srv, err := net.Listen(`tcp`, `127.0.0.1:0`)
 453     if err != nil {
 454         return err
 455     }
 456     defer srv.Close()
 457 
 458     // open a new browser tab for that localhost port
 459     err = showURI(`http://` + srv.Addr().String())
 460     if err != nil {
 461         return err
 462     }
 463 
 464     // accept first connection: no need for async as the server quits after
 465     // its first response
 466     conn, err := srv.Accept()
 467     if err != nil {
 468         return err
 469     }
 470     defer conn.Close()
 471 
 472     respond(conn, start, rest, cfg)
 473     return nil
 474 }
 475 
 476 // respond reads/ignores all request headers, and then replies with some
 477 // content given, quitting immediately after
 478 func respond(conn net.Conn, start []byte, rest io.Reader, cfg serveConfig) {
 479     sc := bufio.NewScanner(conn)
 480     sc.Buffer(nil, maxbufsize)
 481     for sc.Scan() && sc.Text() != `` {
 482         // ignore all request headers
 483     }
 484 
 485     switch cfg.ContentType {
 486     case `audio/wav`, `audio/wave`, `audio/x-wav`, `audio/aiff`, `audio/x-aiff`:
 487         // force browser to play wave and aiff sounds, instead of showing
 488         // a useless download-file option; encode audio bytes as data-URI
 489         // in an intermediate buffer
 490 
 491         writePreludeHTTP(conn, `text/html; charset=UTF-8`, -1)
 492         // emit opening HTML right until <audio controls src="
 493         if cfg.Autoplay {
 494             io.WriteString(conn, beforeAutoplayAudio)
 495         } else {
 496             io.WriteString(conn, beforeAudio)
 497         }
 498         // emit the data-URI
 499         writeBase64(conn, cfg.ContentType, start, rest)
 500         // emit closing HTML after data-URI audio
 501         io.WriteString(conn, afterAudio)
 502         return
 503 
 504     case `image/bmp`, `image/x-bmp`:
 505         // force browser to show bitmap pictures, instead of showing a
 506         // useless download-file option; encode picture bytes as data-URI
 507         // in an intermediate buffer
 508 
 509         writePreludeHTTP(conn, `text/html; charset=UTF-8`, -1)
 510         // emit opening HTML right until <img src="
 511         io.WriteString(conn, beforeBitmap)
 512         // emit the data-URI
 513         writeBase64(conn, cfg.ContentType, start, rest)
 514         // emit closing HTML after data-URI image
 515         io.WriteString(conn, afterBitmap)
 516         return
 517 
 518     default:
 519         writePreludeHTTP(conn, cfg.ContentType, cfg.ContentLength)
 520         // send the starting bytes used to auto-detect the content-type
 521         conn.Write(start)
 522         // send rest of payload at light-speed
 523         io.Copy(conn, rest)
 524     }
 525 }
 526 
 527 func writePreludeHTTP(conn net.Conn, contentType string, contentLength int) {
 528     // respond right after the first empty line, which always follows the
 529     // request's headers
 530     io.WriteString(conn, "HTTP/1.1 200 OK\r\n")
 531     io.WriteString(conn, `Content-Type: `)
 532     io.WriteString(conn, contentType)
 533     io.WriteString(conn, "\r\n")
 534 
 535     if contentLength > 0 {
 536         var buf [32]byte
 537         io.WriteString(conn, `Content-Length: `)
 538         conn.Write(strconv.AppendInt(buf[:0], int64(contentLength), 10))
 539         io.WriteString(conn, "\r\n")
 540     }
 541 
 542     // prevent download-dialog or auto-download from the browser's part
 543     io.WriteString(conn, "Content-Disposition: inline\r\n")
 544     // tell browser this is the last request
 545     io.WriteString(conn, "Connection: close\r\n")
 546     // payload starts right after an empty line
 547     io.WriteString(conn, "\r\n")
 548 }
 549 
 550 func writeBase64(conn net.Conn, mimeType string, start []byte, rest io.Reader) {
 551     // send the data-URI intro
 552     io.WriteString(conn, `data:`)
 553     io.WriteString(conn, mimeType)
 554     io.WriteString(conn, `;base64,`)
 555     enc := base64.NewEncoder(base64.StdEncoding, conn)
 556     // base64-encode the starting bytes used to auto-detect the input type
 557     enc.Write(start)
 558     // base64-encode the rest of the input
 559     io.Copy(enc, rest)
 560     enc.Close()
 561 }
 562 
 563 // nameToMIME tries to match a MIME type to a filename, dotted file extension,
 564 // or a dot-less filetype/extension given
 565 func nameToMIME(fname string) (mimeType string, ok bool) {
 566     // handle dotless file types and filenames alike
 567     kind, ok := type2mime[makeDotless(fname)]
 568     return kind, ok
 569 }
 570 
 571 // detectMIME guesses the first appropriate MIME type from the first few
 572 // data bytes given: 24 bytes are enough to detect all supported types
 573 func detectMIME(b []byte) (mimeType string, ok bool) {
 574     t, ok := detectType(b)
 575     if ok {
 576         return t, true
 577     }
 578     return ``, false
 579 }
 580 
 581 // detectType guesses the first appropriate file type for the data given:
 582 // here the type is a a filename extension without the leading dot
 583 func detectType(b []byte) (dotlessExt string, ok bool) {
 584     // empty data, so there's no way to detect anything
 585     if len(b) == 0 {
 586         return ``, false
 587     }
 588 
 589     // check for plain-text web-document formats case-insensitively
 590     kind, ok := checkDoc(b)
 591     if ok {
 592         return kind, true
 593     }
 594 
 595     // check data formats which allow any byte at the start
 596     kind, ok = checkSpecial(b)
 597     if ok {
 598         return kind, true
 599     }
 600 
 601     // check all other supported data formats
 602     headers := hdrDispatch[b[0]]
 603     for _, t := range headers {
 604         if hasPrefixPattern(b[1:], t.Header[1:], cba) {
 605             return t.Type, true
 606         }
 607     }
 608 
 609     // unrecognized data format
 610     return ``, false
 611 }
 612 
 613 // checkDoc tries to guess if the bytes given are the start of HTML, SVG,
 614 // XML, or JSON data
 615 func checkDoc(b []byte) (kind string, ok bool) {
 616     // ignore leading whitespaces
 617     b = trimLeadingWhitespace(b)
 618 
 619     // can't detect anything with empty data
 620     if len(b) == 0 {
 621         return ``, false
 622     }
 623 
 624     // handle XHTML documents which don't start with a doctype declaration
 625     if bytes.Contains(b, doctypeHTML) {
 626         return html, true
 627     }
 628 
 629     // handle HTML/SVG/XML documents
 630     if hasPrefixByte(b, '<') {
 631         if hasPrefixFold(b, []byte{'<', '?', 'x', 'm', 'l'}) {
 632             if bytes.Contains(b, []byte{'<', 's', 'v', 'g'}) {
 633                 return svg, true
 634             }
 635             return xml, true
 636         }
 637 
 638         headers := hdrDispatch['<']
 639         for _, v := range headers {
 640             if hasPrefixFold(b, v.Header) {
 641                 return v.Type, true
 642             }
 643         }
 644         return ``, false
 645     }
 646 
 647     // handle JSON with top-level arrays
 648     if hasPrefixByte(b, '[') {
 649         // match [", or [[, or [{, ignoring spaces between
 650         b = trimLeadingWhitespace(b[1:])
 651         if len(b) > 0 {
 652             switch b[0] {
 653             case '"', '[', '{':
 654                 return json, true
 655             }
 656         }
 657         return ``, false
 658     }
 659 
 660     // handle JSON with top-level objects
 661     if hasPrefixByte(b, '{') {
 662         // match {", ignoring spaces between: after {, the only valid syntax
 663         // which can follow is the opening quote for the expected object-key
 664         b = trimLeadingWhitespace(b[1:])
 665         if hasPrefixByte(b, '"') {
 666             return json, true
 667         }
 668         return ``, false
 669     }
 670 
 671     // checking for a quoted string, any of the JSON keywords, or even a
 672     // number seems too ambiguous to declare the data valid JSON
 673 
 674     // no web-document format detected
 675     return ``, false
 676 }
 677 
 678 // checkSpecial handles special file-format headers, which should be checked
 679 // before the normal file-type headers, since the first-byte dispatch algo
 680 // doesn't work for these
 681 func checkSpecial(b []byte) (kind string, ok bool) {
 682     if len(b) >= 8 && bytes.Index(b, []byte{'f', 't', 'y', 'p'}) == 4 {
 683         for _, t := range specialHeaders {
 684             if hasPrefixPattern(b[4:], t.Header[4:], cba) {
 685                 return t.Type, true
 686             }
 687         }
 688     }
 689     return ``, false
 690 }
 691 
 692 // hasPrefixPattern works like bytes.HasPrefix, except it allows for a special
 693 // value to signal any byte is allowed on specific spots
 694 func hasPrefixPattern(what []byte, pat []byte, wildcard byte) bool {
 695     // if the data are shorter than the pattern to match, there's no match
 696     if len(what) < len(pat) {
 697         return false
 698     }
 699 
 700     // use a slice which ensures the pattern length is never exceeded
 701     what = what[:len(pat)]
 702 
 703     for i, x := range what {
 704         y := pat[i]
 705         if x != y && y != wildcard {
 706             return false
 707         }
 708     }
 709     return true
 710 }
 711 
 712 // all the MIME types used/recognized in this package
 713 const (
 714     aiff    = `audio/aiff`
 715     au      = `audio/basic`
 716     avi     = `video/avi`
 717     avif    = `image/avif`
 718     bmp     = `image/x-bmp`
 719     caf     = `audio/x-caf`
 720     cur     = `image/vnd.microsoft.icon`
 721     css     = `text/css`
 722     csv     = `text/csv`
 723     djvu    = `image/x-djvu`
 724     elf     = `application/x-elf`
 725     exe     = `application/vnd.microsoft.portable-executable`
 726     flac    = `audio/x-flac`
 727     gif     = `image/gif`
 728     gz      = `application/gzip`
 729     heic    = `image/heic`
 730     htm     = `text/html`
 731     html    = `text/html`
 732     ico     = `image/x-icon`
 733     iso     = `application/octet-stream`
 734     jpg     = `image/jpeg`
 735     jpeg    = `image/jpeg`
 736     js      = `application/javascript`
 737     json    = `application/json`
 738     m4a     = `audio/aac`
 739     m4v     = `video/x-m4v`
 740     mid     = `audio/midi`
 741     mov     = `video/quicktime`
 742     mp4     = `video/mp4`
 743     mp3     = `audio/mpeg`
 744     mpg     = `video/mpeg`
 745     ogg     = `audio/ogg`
 746     opus    = `audio/opus`
 747     pdf     = `application/pdf`
 748     png     = `image/png`
 749     ps      = `application/postscript`
 750     psd     = `image/vnd.adobe.photoshop`
 751     rtf     = `application/rtf`
 752     sqlite3 = `application/x-sqlite3`
 753     svg     = `image/svg+xml`
 754     text    = `text/plain`
 755     tiff    = `image/tiff`
 756     tsv     = `text/tsv`
 757     wasm    = `application/wasm`
 758     wav     = `audio/x-wav`
 759     webp    = `image/webp`
 760     webm    = `video/webm`
 761     xml     = `application/xml`
 762     zip     = `application/zip`
 763     zst     = `application/zstd`
 764 )
 765 
 766 // type2mime turns dotless format-names into MIME types
 767 var type2mime = map[string]string{
 768     `aiff`:    aiff,
 769     `wav`:     wav,
 770     `avi`:     avi,
 771     `jpg`:     jpg,
 772     `jpeg`:    jpeg,
 773     `m4a`:     m4a,
 774     `mp4`:     mp4,
 775     `m4v`:     m4v,
 776     `mov`:     mov,
 777     `png`:     png,
 778     `avif`:    avif,
 779     `webp`:    webp,
 780     `gif`:     gif,
 781     `tiff`:    tiff,
 782     `psd`:     psd,
 783     `flac`:    flac,
 784     `webm`:    webm,
 785     `mpg`:     mpg,
 786     `zip`:     zip,
 787     `gz`:      gz,
 788     `zst`:     zst,
 789     `mp3`:     mp3,
 790     `opus`:    opus,
 791     `bmp`:     bmp,
 792     `mid`:     mid,
 793     `ogg`:     ogg,
 794     `html`:    html,
 795     `htm`:     htm,
 796     `svg`:     svg,
 797     `xml`:     xml,
 798     `rtf`:     rtf,
 799     `pdf`:     pdf,
 800     `ps`:      ps,
 801     `au`:      au,
 802     `ico`:     ico,
 803     `cur`:     cur,
 804     `caf`:     caf,
 805     `heic`:    heic,
 806     `sqlite3`: sqlite3,
 807     `elf`:     elf,
 808     `exe`:     exe,
 809     `wasm`:    wasm,
 810     `iso`:     iso,
 811     `txt`:     text,
 812     `css`:     css,
 813     `csv`:     csv,
 814     `tsv`:     tsv,
 815     `js`:      js,
 816     `json`:    json,
 817     `geojson`: json,
 818 }
 819 
 820 // formatDescriptor ties a file-header pattern to its data-format type
 821 type formatDescriptor struct {
 822     Header []byte
 823     Type   string
 824 }
 825 
 826 // can be anything: ensure this value differs from all other literal bytes
 827 // in the generic-headers table: failing that, its value could cause subtle
 828 // type-misdetection bugs
 829 const cba = 0xFD // 253, which is > 127, the highest-valued ascii symbol
 830 
 831 // dash-streamed m4a format
 832 var m4aDash = []byte{
 833     cba, cba, cba, cba, 'f', 't', 'y', 'p', 'd', 'a', 's', 'h',
 834     000, 000, 000, 000, 'i', 's', 'o', '6', 'm', 'p', '4', '1',
 835 }
 836 
 837 // format markers with leading wildcards, which should be checked before the
 838 // normal ones: this is to prevent mismatches with the latter types, even
 839 // though you can make probabilistic arguments which suggest these mismatches
 840 // should be very unlikely in practice
 841 var specialHeaders = []formatDescriptor{
 842     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', ' '}, m4a},
 843     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', '4', 'A', 000}, m4a},
 844     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'M', 'S', 'N', 'V'}, mp4},
 845     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, mp4},
 846     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'}, m4v},
 847     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'q', 't', ' ', ' '}, mov},
 848     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'h', 'e', 'i', 'c'}, heic},
 849     {[]byte{cba, cba, cba, cba, 'f', 't', 'y', 'p', 'a', 'v', 'i', 'f'}, avif},
 850     {m4aDash, m4a},
 851 }
 852 
 853 // sqlite3 database format
 854 var sqlite3db = []byte{
 855     'S', 'Q', 'L', 'i', 't', 'e', ' ',
 856     'f', 'o', 'r', 'm', 'a', 't', ' ', '3',
 857     000,
 858 }
 859 
 860 // windows-variant bitmap file-header, which is followed by a byte-counter for
 861 // the 40-byte infoheader which follows that
 862 var winbmp = []byte{
 863     'B', 'M', cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, cba, 40,
 864 }
 865 
 866 // deja-vu document format
 867 var djv = []byte{
 868     'A', 'T', '&', 'T', 'F', 'O', 'R', 'M', cba, cba, cba, cba, 'D', 'J', 'V',
 869 }
 870 
 871 var doctypeHTML = []byte{
 872     '<', '!', 'D', 'O', 'C', 'T', 'Y', 'P', 'E', ' ', 'h', 't', 'm', 'l',
 873 }
 874 
 875 // hdrDispatch groups format-description-groups by their first byte, thus
 876 // shortening total lookups for some data header: notice how the `ftyp` data
 877 // formats aren't handled here, since these can start with any byte, instead
 878 // of the literal value of the any-byte markers they use
 879 var hdrDispatch = [256][]formatDescriptor{
 880     {
 881         {[]byte{000, 000, 001, 0xBA}, mpg},
 882         {[]byte{000, 000, 001, 0xB3}, mpg},
 883         {[]byte{000, 000, 001, 000}, ico},
 884         {[]byte{000, 000, 002, 000}, cur},
 885         {[]byte{000, 'a', 's', 'm'}, wasm},
 886     }, // 0
 887     nil, // 1
 888     nil, // 2
 889     nil, // 3
 890     nil, // 4
 891     nil, // 5
 892     nil, // 6
 893     nil, // 7
 894     nil, // 8
 895     nil, // 9
 896     nil, // 10
 897     nil, // 11
 898     nil, // 12
 899     nil, // 13
 900     nil, // 14
 901     nil, // 15
 902     nil, // 16
 903     nil, // 17
 904     nil, // 18
 905     nil, // 19
 906     nil, // 20
 907     nil, // 21
 908     nil, // 22
 909     nil, // 23
 910     nil, // 24
 911     nil, // 25
 912     {
 913         {[]byte{0x1A, 0x45, 0xDF, 0xA3}, webm},
 914     }, // 26
 915     nil, // 27
 916     nil, // 28
 917     nil, // 29
 918     nil, // 30
 919     {
 920         // {[]byte{0x1F, 0x8B, 0x08, 0x08}, gz},
 921         {[]byte{0x1F, 0x8B, 0x08}, gz},
 922     }, // 31
 923     nil, // 32
 924     nil, // 33 !
 925     nil, // 34 "
 926     {
 927         {[]byte{'#', '!', ' '}, text},
 928         {[]byte{'#', '!', '/'}, text},
 929     }, // 35 #
 930     nil, // 36 $
 931     {
 932         {[]byte{'%', 'P', 'D', 'F'}, pdf},
 933         {[]byte{'%', '!', 'P', 'S'}, ps},
 934     }, // 37 %
 935     nil, // 38 &
 936     nil, // 39 '
 937     {
 938         {[]byte{0x28, 0xB5, 0x2F, 0xFD}, zst},
 939     }, // 40 (
 940     nil, // 41 )
 941     nil, // 42 *
 942     nil, // 43 +
 943     nil, // 44 ,
 944     nil, // 45 -
 945     {
 946         {[]byte{'.', 's', 'n', 'd'}, au},
 947     }, // 46 .
 948     nil, // 47 /
 949     nil, // 48 0
 950     nil, // 49 1
 951     nil, // 50 2
 952     nil, // 51 3
 953     nil, // 52 4
 954     nil, // 53 5
 955     nil, // 54 6
 956     nil, // 55 7
 957     {
 958         {[]byte{'8', 'B', 'P', 'S'}, psd},
 959     }, // 56 8
 960     nil, // 57 9
 961     nil, // 58 :
 962     nil, // 59 ;
 963     {
 964         // func checkDoc is better for these, since it's case-insensitive
 965         {doctypeHTML, html},
 966         {[]byte{'<', 's', 'v', 'g'}, svg},
 967         {[]byte{'<', 'h', 't', 'm', 'l', '>'}, html},
 968         {[]byte{'<', 'h', 'e', 'a', 'd', '>'}, html},
 969         {[]byte{'<', 'b', 'o', 'd', 'y', '>'}, html},
 970         {[]byte{'<', '?', 'x', 'm', 'l'}, xml},
 971     }, // 60 <
 972     nil, // 61 =
 973     nil, // 62 >
 974     nil, // 63 ?
 975     nil, // 64 @
 976     {
 977         {djv, djvu},
 978     }, // 65 A
 979     {
 980         {winbmp, bmp},
 981     }, // 66 B
 982     nil, // 67 C
 983     nil, // 68 D
 984     nil, // 69 E
 985     {
 986         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'F'}, aiff},
 987         {[]byte{'F', 'O', 'R', 'M', cba, cba, cba, cba, 'A', 'I', 'F', 'C'}, aiff},
 988     }, // 70 F
 989     {
 990         {[]byte{'G', 'I', 'F', '8', '7', 'a'}, gif},
 991         {[]byte{'G', 'I', 'F', '8', '9', 'a'}, gif},
 992     }, // 71 G
 993     nil, // 72 H
 994     {
 995         {[]byte{'I', 'D', '3', 2}, mp3}, // ID3-format metadata
 996         {[]byte{'I', 'D', '3', 3}, mp3}, // ID3-format metadata
 997         {[]byte{'I', 'D', '3', 4}, mp3}, // ID3-format metadata
 998         {[]byte{'I', 'I', '*', 000}, tiff},
 999     }, // 73 I
1000     nil, // 74 J
1001     nil, // 75 K
1002     nil, // 76 L
1003     {
1004         {[]byte{'M', 'M', 000, '*'}, tiff},
1005         {[]byte{'M', 'T', 'h', 'd'}, mid},
1006         {[]byte{'M', 'Z', cba, 000, cba, 000}, exe},
1007         // {[]byte{'M', 'Z', 0x90, 000, 003, 000}, exe},
1008         // {[]byte{'M', 'Z', 0x78, 000, 001, 000}, exe},
1009         // {[]byte{'M', 'Z', 'P', 000, 002, 000}, exe},
1010     }, // 77 M
1011     nil, // 78 N
1012     {
1013         {[]byte{'O', 'g', 'g', 'S'}, ogg},
1014     }, // 79 O
1015     {
1016         {[]byte{'P', 'K', 003, 004}, zip},
1017     }, // 80 P
1018     nil, // 81 Q
1019     {
1020         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'E', 'B', 'P'}, webp},
1021         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'W', 'A', 'V', 'E'}, wav},
1022         {[]byte{'R', 'I', 'F', 'F', cba, cba, cba, cba, 'A', 'V', 'I', ' '}, avi},
1023     }, // 82 R
1024     {
1025         {sqlite3db, sqlite3},
1026     }, // 83 S
1027     nil, // 84 T
1028     nil, // 85 U
1029     nil, // 86 V
1030     nil, // 87 W
1031     nil, // 88 X
1032     nil, // 89 Y
1033     nil, // 90 Z
1034     nil, // 91 [
1035     nil, // 92 \
1036     nil, // 93 ]
1037     nil, // 94 ^
1038     nil, // 95 _
1039     nil, // 96 `
1040     nil, // 97 a
1041     nil, // 98 b
1042     {
1043         {[]byte{'c', 'a', 'f', 'f', 000, 001, 000, 000}, caf},
1044     }, // 99 c
1045     nil, // 100 d
1046     nil, // 101 e
1047     {
1048         {[]byte{'f', 'L', 'a', 'C'}, flac},
1049     }, // 102 f
1050     nil, // 103 g
1051     nil, // 104 h
1052     nil, // 105 i
1053     nil, // 106 j
1054     nil, // 107 k
1055     nil, // 108 l
1056     nil, // 109 m
1057     nil, // 110 n
1058     nil, // 111 o
1059     nil, // 112 p
1060     nil, // 113 q
1061     nil, // 114 r
1062     nil, // 115 s
1063     nil, // 116 t
1064     nil, // 117 u
1065     nil, // 118 v
1066     nil, // 119 w
1067     nil, // 120 x
1068     nil, // 121 y
1069     nil, // 122 z
1070     {
1071         {[]byte{'{', '\\', 'r', 't', 'f'}, rtf},
1072     }, // 123 {
1073     nil, // 124 |
1074     nil, // 125 }
1075     nil, // 126
1076     {
1077         {[]byte{127, 'E', 'L', 'F'}, elf},
1078     }, // 127
1079     nil, // 128
1080     nil, // 129
1081     nil, // 130
1082     nil, // 131
1083     nil, // 132
1084     nil, // 133
1085     nil, // 134
1086     nil, // 135
1087     nil, // 136
1088     {
1089         {[]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, png},
1090     }, // 137
1091     nil, // 138
1092     nil, // 139
1093     nil, // 140
1094     nil, // 141
1095     nil, // 142
1096     nil, // 143
1097     nil, // 144
1098     nil, // 145
1099     nil, // 146
1100     nil, // 147
1101     nil, // 148
1102     nil, // 149
1103     nil, // 150
1104     nil, // 151
1105     nil, // 152
1106     nil, // 153
1107     nil, // 154
1108     nil, // 155
1109     nil, // 156
1110     nil, // 157
1111     nil, // 158
1112     nil, // 159
1113     nil, // 160
1114     nil, // 161
1115     nil, // 162
1116     nil, // 163
1117     nil, // 164
1118     nil, // 165
1119     nil, // 166
1120     nil, // 167
1121     nil, // 168
1122     nil, // 169
1123     nil, // 170
1124     nil, // 171
1125     nil, // 172
1126     nil, // 173
1127     nil, // 174
1128     nil, // 175
1129     nil, // 176
1130     nil, // 177
1131     nil, // 178
1132     nil, // 179
1133     nil, // 180
1134     nil, // 181
1135     nil, // 182
1136     nil, // 183
1137     nil, // 184
1138     nil, // 185
1139     nil, // 186
1140     nil, // 187
1141     nil, // 188
1142     nil, // 189
1143     nil, // 190
1144     nil, // 191
1145     nil, // 192
1146     nil, // 193
1147     nil, // 194
1148     nil, // 195
1149     nil, // 196
1150     nil, // 197
1151     nil, // 198
1152     nil, // 199
1153     nil, // 200
1154     nil, // 201
1155     nil, // 202
1156     nil, // 203
1157     nil, // 204
1158     nil, // 205
1159     nil, // 206
1160     nil, // 207
1161     nil, // 208
1162     nil, // 209
1163     nil, // 210
1164     nil, // 211
1165     nil, // 212
1166     nil, // 213
1167     nil, // 214
1168     nil, // 215
1169     nil, // 216
1170     nil, // 217
1171     nil, // 218
1172     nil, // 219
1173     nil, // 220
1174     nil, // 221
1175     nil, // 222
1176     nil, // 223
1177     nil, // 224
1178     nil, // 225
1179     nil, // 226
1180     nil, // 227
1181     nil, // 228
1182     nil, // 229
1183     nil, // 230
1184     nil, // 231
1185     nil, // 232
1186     nil, // 233
1187     nil, // 234
1188     nil, // 235
1189     nil, // 236
1190     nil, // 237
1191     nil, // 238
1192     nil, // 239
1193     nil, // 240
1194     nil, // 241
1195     nil, // 242
1196     nil, // 243
1197     nil, // 244
1198     nil, // 245
1199     nil, // 246
1200     nil, // 247
1201     nil, // 248
1202     nil, // 249
1203     nil, // 250
1204     nil, // 251
1205     nil, // 252
1206     nil, // 253
1207     nil, // 254
1208     {
1209         {[]byte{0xFF, 0xD8, 0xFF}, jpg},
1210         {[]byte{0xFF, 0xF3, 0x48, 0xC4, 0x00}, mp3},
1211         {[]byte{0xFF, 0xFB}, mp3},
1212     }, // 255
1213 }