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 }