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