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 }