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