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