├── .gitignore ├── go.mod ├── fixtures ├── sample.dwg ├── sample.exr ├── sample.gif ├── sample.jpg ├── sample.mkv ├── sample.mov ├── sample.mp4 ├── sample.odp ├── sample.ods ├── sample.odt ├── sample.png ├── sample.tar ├── sample.tif ├── sample.zip ├── sample.zst ├── sample.avif ├── sample.docx ├── sample.pptx ├── sample.webm ├── sample.xlsx ├── sample.parquet ├── sample.wasm ├── sample_skippable.zst └── sources.txt ├── types ├── defaults.go ├── split.go ├── mime.go ├── type.go ├── types.go └── split_test.go ├── version.go ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── kind_test.go ├── matchers ├── isobmff │ └── isobmff.go ├── font.go ├── application.go ├── matchers.go ├── audio.go ├── image.go ├── video.go ├── document.go └── archive.go ├── LICENSE ├── match.go ├── kind.go ├── filetype.go ├── filetype_test.go ├── History.md ├── match_test.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/h2non/filetype 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /fixtures/sample.dwg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.dwg -------------------------------------------------------------------------------- /fixtures/sample.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.exr -------------------------------------------------------------------------------- /fixtures/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.gif -------------------------------------------------------------------------------- /fixtures/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.jpg -------------------------------------------------------------------------------- /fixtures/sample.mkv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.mkv -------------------------------------------------------------------------------- /fixtures/sample.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.mov -------------------------------------------------------------------------------- /fixtures/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.mp4 -------------------------------------------------------------------------------- /fixtures/sample.odp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.odp -------------------------------------------------------------------------------- /fixtures/sample.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.ods -------------------------------------------------------------------------------- /fixtures/sample.odt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.odt -------------------------------------------------------------------------------- /fixtures/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.png -------------------------------------------------------------------------------- /fixtures/sample.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.tar -------------------------------------------------------------------------------- /fixtures/sample.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.tif -------------------------------------------------------------------------------- /fixtures/sample.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.zip -------------------------------------------------------------------------------- /fixtures/sample.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.zst -------------------------------------------------------------------------------- /fixtures/sample.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.avif -------------------------------------------------------------------------------- /fixtures/sample.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.docx -------------------------------------------------------------------------------- /fixtures/sample.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.pptx -------------------------------------------------------------------------------- /fixtures/sample.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.webm -------------------------------------------------------------------------------- /fixtures/sample.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.xlsx -------------------------------------------------------------------------------- /fixtures/sample.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample.parquet -------------------------------------------------------------------------------- /fixtures/sample.wasm: -------------------------------------------------------------------------------- 1 | asm``imports imported_func exported_func 2 | A* -------------------------------------------------------------------------------- /fixtures/sample_skippable.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h2non/filetype/HEAD/fixtures/sample_skippable.zst -------------------------------------------------------------------------------- /types/defaults.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Unknown default type 4 | var Unknown = NewType("unknown", "") 5 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | // Version exposes the current package version. 4 | const Version = "1.1.3" 5 | -------------------------------------------------------------------------------- /types/split.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "strings" 4 | 5 | func splitMime(s string) (string, string) { 6 | x := strings.Split(s, "/") 7 | if len(x) > 1 { 8 | return x[0], x[1] 9 | } 10 | return x[0], "" 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tabs 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /types/mime.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // MIME stores the file MIME type values 4 | type MIME struct { 5 | Type string 6 | Subtype string 7 | Value string 8 | } 9 | 10 | // Creates a new MIME type 11 | func NewMIME(mime string) MIME { 12 | kind, subtype := splitMime(mime) 13 | return MIME{Type: kind, Subtype: subtype, Value: mime} 14 | } 15 | -------------------------------------------------------------------------------- /types/type.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Type represents a file MIME type and its extension 4 | type Type struct { 5 | MIME MIME 6 | Extension string 7 | } 8 | 9 | // NewType creates a new Type 10 | func NewType(ext, mime string) Type { 11 | t := Type{ 12 | MIME: NewMIME(mime), 13 | Extension: ext, 14 | } 15 | return Add(t) 16 | } 17 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "sync" 4 | 5 | // Types Support concurrent map writes 6 | var Types sync.Map 7 | 8 | // Add registers a new type in the package 9 | func Add(t Type) Type { 10 | Types.Store(t.Extension, t) 11 | return t 12 | } 13 | 14 | // Get retrieves a Type by extension 15 | func Get(ext string) Type { 16 | if tmp, ok := Types.Load(ext); ok { 17 | kind := tmp.(Type) 18 | if kind.Extension != "" { 19 | return kind 20 | } 21 | } 22 | return Unknown 23 | } 24 | -------------------------------------------------------------------------------- /types/split_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "testing" 4 | 5 | func TestSplit(t *testing.T) { 6 | cases := []struct { 7 | mime string 8 | kind string 9 | subtype string 10 | }{ 11 | {"image/jpeg", "image", "jpeg"}, 12 | {"/jpeg", "", "jpeg"}, 13 | {"image/", "image", ""}, 14 | {"/", "", ""}, 15 | {"image", "image", ""}, 16 | } 17 | 18 | for _, test := range cases { 19 | kind, subtype := splitMime(test.mime) 20 | if test.kind != kind { 21 | t.Fatalf("Invalid kind: %s", test.kind) 22 | } 23 | if test.subtype != subtype { 24 | t.Fatalf("Invalid subtype: %s", test.subtype) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: ["1.19", "1.20", "1.21"] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-go@v4 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - run: go test ./... 21 | 22 | lint: 23 | strategy: 24 | matrix: 25 | go-version: ["1.21"] 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-go@v4 30 | with: 31 | go-version: ${{ matrix.go-version }} 32 | - run: diff -u <(echo -n) <(gofmt -s -d ./) 33 | - run: diff -u <(echo -n) <(go vet ./...) -------------------------------------------------------------------------------- /kind_test.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestKind(t *testing.T) { 8 | var cases = []struct { 9 | buf []byte 10 | ext string 11 | }{ 12 | {[]byte{0xFF, 0xD8, 0xFF}, "jpg"}, 13 | {[]byte{0x89, 0x50, 0x4E, 0x47}, "png"}, 14 | {[]byte{0x89, 0x0, 0x0}, "unknown"}, 15 | } 16 | 17 | for _, test := range cases { 18 | kind, _ := Image(test.buf) 19 | if kind.Extension != test.ext { 20 | t.Fatalf("Invalid match: %s != %s", kind.Extension, test.ext) 21 | } 22 | } 23 | } 24 | 25 | func TestIsKind(t *testing.T) { 26 | var cases = []struct { 27 | buf []byte 28 | match bool 29 | }{ 30 | {[]byte{0xFF, 0xD8, 0xFF}, true}, 31 | {[]byte{0x89, 0x50, 0x4E, 0x47}, true}, 32 | {[]byte{0x89, 0x0, 0x0}, false}, 33 | } 34 | 35 | for _, test := range cases { 36 | if IsImage(test.buf) != test.match { 37 | t.Fatalf("Invalid match: %t", test.match) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /fixtures/sources.txt: -------------------------------------------------------------------------------- 1 | 2 | sample.gif 3 | ========== 4 | * Original filename: 5 | Universal_joint.gif 6 | * Source: 7 | https://commons.wikimedia.org/wiki/File:Universal_joint.gif 8 | * Attribution: 9 | Van helsing via Wikimedia Commons 10 | * License: 11 | CC BY-SA (http://creativecommons.org/licenses/by-sa/3.0/) 12 | 13 | sample.tif 14 | ========== 15 | * Original filename: 16 | STSCI-H-p1918a-f-2000x2000.tif 17 | * Source: 18 | https://hubblesite.org/contents/media/images/2019/18/4505-Image 19 | * Attribution: 20 | NASA, ESA, N. Smith (University of Arizona), and J. Morse (BoldlyGo Institute) 21 | * License: 22 | Public Domain 23 | 24 | sample.webm 25 | =========== 26 | * Original filename: 27 | Artefact_comparison_ToS_f15509.webm 28 | * Source: 29 | https://commons.wikimedia.org/wiki/File:Artefact_comparison_ToS_f15509.webm 30 | * Attribution: 31 | Mango Blender Open Movie Project, Blender Foundation via Wikimedia Commons 32 | * License: 33 | CC BY (https://creativecommons.org/licenses/by/3.0) 34 | 35 | -------------------------------------------------------------------------------- /matchers/isobmff/isobmff.go: -------------------------------------------------------------------------------- 1 | package isobmff 2 | 3 | import "encoding/binary" 4 | 5 | // IsISOBMFF checks whether the given buffer represents ISO Base Media File Format data 6 | func IsISOBMFF(buf []byte) bool { 7 | if len(buf) < 16 || string(buf[4:8]) != "ftyp" { 8 | return false 9 | } 10 | 11 | if ftypLength := binary.BigEndian.Uint32(buf[0:4]); len(buf) < int(ftypLength) { 12 | return false 13 | } 14 | 15 | return true 16 | } 17 | 18 | // GetFtyp returns the major brand, minor version and compatible brands of the ISO-BMFF data 19 | func GetFtyp(buf []byte) (string, string, []string) { 20 | if len(buf) < 17 { 21 | return "", "", []string{""} 22 | } 23 | 24 | ftypLength := binary.BigEndian.Uint32(buf[0:4]) 25 | 26 | majorBrand := string(buf[8:12]) 27 | minorVersion := string(buf[12:16]) 28 | 29 | compatibleBrands := []string{} 30 | for i := 16; i < int(ftypLength); i += 4 { 31 | if len(buf) >= (i + 4) { 32 | compatibleBrands = append(compatibleBrands, string(buf[i:i+4])) 33 | } 34 | } 35 | 36 | return majorBrand, minorVersion, compatibleBrands 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Tomas Aparicio 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /matchers/font.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | var ( 4 | TypeWoff = newType("woff", "application/font-woff") 5 | TypeWoff2 = newType("woff2", "application/font-woff") 6 | TypeTtf = newType("ttf", "application/font-sfnt") 7 | TypeOtf = newType("otf", "application/font-sfnt") 8 | ) 9 | 10 | var Font = Map{ 11 | TypeWoff: Woff, 12 | TypeWoff2: Woff2, 13 | TypeTtf: Ttf, 14 | TypeOtf: Otf, 15 | } 16 | 17 | func Woff(buf []byte) bool { 18 | return len(buf) > 7 && 19 | buf[0] == 0x77 && buf[1] == 0x4F && 20 | buf[2] == 0x46 && buf[3] == 0x46 && 21 | buf[4] == 0x00 && buf[5] == 0x01 && 22 | buf[6] == 0x00 && buf[7] == 0x00 23 | } 24 | 25 | func Woff2(buf []byte) bool { 26 | return len(buf) > 7 && 27 | buf[0] == 0x77 && buf[1] == 0x4F && 28 | buf[2] == 0x46 && buf[3] == 0x32 && 29 | buf[4] == 0x00 && buf[5] == 0x01 && 30 | buf[6] == 0x00 && buf[7] == 0x00 31 | } 32 | 33 | func Ttf(buf []byte) bool { 34 | return len(buf) > 4 && 35 | buf[0] == 0x00 && buf[1] == 0x01 && 36 | buf[2] == 0x00 && buf[3] == 0x00 && 37 | buf[4] == 0x00 38 | } 39 | 40 | func Otf(buf []byte) bool { 41 | return len(buf) > 4 && 42 | buf[0] == 0x4F && buf[1] == 0x54 && 43 | buf[2] == 0x54 && buf[3] == 0x4F && 44 | buf[4] == 0x00 45 | } 46 | -------------------------------------------------------------------------------- /matchers/application.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | var ( 4 | TypeWasm = newType("wasm", "application/wasm") 5 | TypeDex = newType("dex", "application/vnd.android.dex") 6 | TypeDey = newType("dey", "application/vnd.android.dey") 7 | ) 8 | 9 | var Application = Map{ 10 | TypeWasm: Wasm, 11 | TypeDex: Dex, 12 | TypeDey: Dey, 13 | } 14 | 15 | // Wasm detects a Web Assembly 1.0 filetype. 16 | func Wasm(buf []byte) bool { 17 | // WASM has starts with `\0asm`, followed by the version. 18 | // http://webassembly.github.io/spec/core/binary/modules.html#binary-magic 19 | return len(buf) >= 8 && 20 | buf[0] == 0x00 && buf[1] == 0x61 && 21 | buf[2] == 0x73 && buf[3] == 0x6D && 22 | buf[4] == 0x01 && buf[5] == 0x00 && 23 | buf[6] == 0x00 && buf[7] == 0x00 24 | } 25 | 26 | // Dex detects dalvik executable(DEX) 27 | func Dex(buf []byte) bool { 28 | // https://source.android.com/devices/tech/dalvik/dex-format#dex-file-magic 29 | return len(buf) > 36 && 30 | // magic 31 | buf[0] == 0x64 && buf[1] == 0x65 && buf[2] == 0x78 && buf[3] == 0x0A && 32 | // file sise 33 | buf[36] == 0x70 34 | } 35 | 36 | // Dey Optimized Dalvik Executable(ODEX) 37 | func Dey(buf []byte) bool { 38 | return len(buf) > 100 && 39 | // dey magic 40 | buf[0] == 0x64 && buf[1] == 0x65 && buf[2] == 0x79 && buf[3] == 0x0A && 41 | // dex 42 | Dex(buf[40:100]) 43 | } 44 | -------------------------------------------------------------------------------- /matchers/matchers.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "github.com/h2non/filetype/types" 5 | ) 6 | 7 | // Internal shortcut to NewType 8 | var newType = types.NewType 9 | 10 | // Matcher function interface as type alias 11 | type Matcher func([]byte) bool 12 | 13 | // Type interface to store pairs of type with its matcher function 14 | type Map map[types.Type]Matcher 15 | 16 | // Type specific matcher function interface 17 | type TypeMatcher func([]byte) types.Type 18 | 19 | // Store registered file type matchers 20 | var Matchers = make(map[types.Type]TypeMatcher) 21 | var MatcherKeys []types.Type 22 | 23 | // Create and register a new type matcher function 24 | func NewMatcher(kind types.Type, fn Matcher) TypeMatcher { 25 | matcher := func(buf []byte) types.Type { 26 | if fn(buf) { 27 | return kind 28 | } 29 | return types.Unknown 30 | } 31 | 32 | Matchers[kind] = matcher 33 | // prepend here so any user defined matchers get added first 34 | MatcherKeys = append([]types.Type{kind}, MatcherKeys...) 35 | return matcher 36 | } 37 | 38 | func register(matchers ...Map) { 39 | MatcherKeys = MatcherKeys[:0] 40 | for _, m := range matchers { 41 | for kind, matcher := range m { 42 | NewMatcher(kind, matcher) 43 | } 44 | } 45 | } 46 | 47 | func init() { 48 | // Arguments order is intentional 49 | // Archive files will be checked last due to prepend above in func NewMatcher 50 | register(Archive, Document, Font, Audio, Video, Image, Application) 51 | } 52 | -------------------------------------------------------------------------------- /matchers/audio.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | var ( 4 | TypeMidi = newType("mid", "audio/midi") 5 | TypeMp3 = newType("mp3", "audio/mpeg") 6 | TypeM4a = newType("m4a", "audio/mp4") 7 | TypeOgg = newType("ogg", "audio/ogg") 8 | TypeFlac = newType("flac", "audio/x-flac") 9 | TypeWav = newType("wav", "audio/x-wav") 10 | TypeAmr = newType("amr", "audio/amr") 11 | TypeAac = newType("aac", "audio/aac") 12 | TypeAiff = newType("aiff", "audio/x-aiff") 13 | ) 14 | 15 | var Audio = Map{ 16 | TypeMidi: Midi, 17 | TypeMp3: Mp3, 18 | TypeM4a: M4a, 19 | TypeOgg: Ogg, 20 | TypeFlac: Flac, 21 | TypeWav: Wav, 22 | TypeAmr: Amr, 23 | TypeAac: Aac, 24 | TypeAiff: Aiff, 25 | } 26 | 27 | func Midi(buf []byte) bool { 28 | return len(buf) > 3 && 29 | buf[0] == 0x4D && buf[1] == 0x54 && 30 | buf[2] == 0x68 && buf[3] == 0x64 31 | } 32 | 33 | func Mp3(buf []byte) bool { 34 | return len(buf) > 2 && 35 | ((buf[0] == 0x49 && buf[1] == 0x44 && buf[2] == 0x33) || 36 | (buf[0] == 0xFF && buf[1] == 0xfb)) 37 | } 38 | 39 | func M4a(buf []byte) bool { 40 | return len(buf) > 10 && 41 | ((buf[4] == 0x66 && buf[5] == 0x74 && buf[6] == 0x79 && 42 | buf[7] == 0x70 && buf[8] == 0x4D && buf[9] == 0x34 && buf[10] == 0x41) || 43 | (buf[0] == 0x4D && buf[1] == 0x34 && buf[2] == 0x41 && buf[3] == 0x20)) 44 | } 45 | 46 | func Ogg(buf []byte) bool { 47 | return len(buf) > 3 && 48 | buf[0] == 0x4F && buf[1] == 0x67 && 49 | buf[2] == 0x67 && buf[3] == 0x53 50 | } 51 | 52 | func Flac(buf []byte) bool { 53 | return len(buf) > 3 && 54 | buf[0] == 0x66 && buf[1] == 0x4C && 55 | buf[2] == 0x61 && buf[3] == 0x43 56 | } 57 | 58 | func Wav(buf []byte) bool { 59 | return len(buf) > 11 && 60 | buf[0] == 0x52 && buf[1] == 0x49 && 61 | buf[2] == 0x46 && buf[3] == 0x46 && 62 | buf[8] == 0x57 && buf[9] == 0x41 && 63 | buf[10] == 0x56 && buf[11] == 0x45 64 | } 65 | 66 | func Amr(buf []byte) bool { 67 | return len(buf) > 11 && 68 | buf[0] == 0x23 && buf[1] == 0x21 && 69 | buf[2] == 0x41 && buf[3] == 0x4D && 70 | buf[4] == 0x52 && buf[5] == 0x0A 71 | } 72 | 73 | func Aac(buf []byte) bool { 74 | return len(buf) > 1 && 75 | ((buf[0] == 0xFF && buf[1] == 0xF1) || 76 | (buf[0] == 0xFF && buf[1] == 0xF9)) 77 | } 78 | 79 | func Aiff(buf []byte) bool { 80 | return len(buf) > 11 && 81 | buf[0] == 0x46 && buf[1] == 0x4F && 82 | buf[2] == 0x52 && buf[3] == 0x4D && 83 | buf[8] == 0x41 && buf[9] == 0x49 && 84 | buf[10] == 0x46 && buf[11] == 0x46 85 | } 86 | -------------------------------------------------------------------------------- /match.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/h2non/filetype/matchers" 8 | "github.com/h2non/filetype/types" 9 | ) 10 | 11 | // Matchers is an alias to matchers.Matchers 12 | var Matchers = matchers.Matchers 13 | 14 | // MatcherKeys is an alias to matchers.MatcherKeys 15 | var MatcherKeys = &matchers.MatcherKeys 16 | 17 | // NewMatcher is an alias to matchers.NewMatcher 18 | var NewMatcher = matchers.NewMatcher 19 | 20 | // Match infers the file type of a given buffer inspecting its magic numbers signature 21 | func Match(buf []byte) (types.Type, error) { 22 | length := len(buf) 23 | if length == 0 { 24 | return types.Unknown, ErrEmptyBuffer 25 | } 26 | 27 | for _, kind := range *MatcherKeys { 28 | checker := Matchers[kind] 29 | match := checker(buf) 30 | if match != types.Unknown && match.Extension != "" { 31 | return match, nil 32 | } 33 | } 34 | 35 | return types.Unknown, nil 36 | } 37 | 38 | // Get is an alias to Match() 39 | func Get(buf []byte) (types.Type, error) { 40 | return Match(buf) 41 | } 42 | 43 | // MatchFile infers a file type for a file 44 | func MatchFile(filepath string) (types.Type, error) { 45 | file, err := os.Open(filepath) 46 | if err != nil { 47 | return types.Unknown, err 48 | } 49 | defer file.Close() 50 | 51 | return MatchReader(file) 52 | } 53 | 54 | // MatchReader is convenient wrapper to Match() any Reader 55 | func MatchReader(reader io.Reader) (types.Type, error) { 56 | buffer := make([]byte, 8192) // 8K makes msooxml tests happy and allows for expanded custom file checks 57 | 58 | _, err := reader.Read(buffer) 59 | if err != nil && err != io.EOF { 60 | return types.Unknown, err 61 | } 62 | 63 | return Match(buffer) 64 | } 65 | 66 | // AddMatcher registers a new matcher type 67 | func AddMatcher(fileType types.Type, matcher matchers.Matcher) matchers.TypeMatcher { 68 | return matchers.NewMatcher(fileType, matcher) 69 | } 70 | 71 | // Matches checks if the given buffer matches with some supported file type 72 | func Matches(buf []byte) bool { 73 | kind, _ := Match(buf) 74 | return kind != types.Unknown 75 | } 76 | 77 | // MatchMap performs a file matching against a map of match functions 78 | func MatchMap(buf []byte, matchers matchers.Map) types.Type { 79 | for kind, matcher := range matchers { 80 | if matcher(buf) { 81 | return kind 82 | } 83 | } 84 | return types.Unknown 85 | } 86 | 87 | // MatchesMap is an alias to Matches() but using matching against a map of match functions 88 | func MatchesMap(buf []byte, matchers matchers.Map) bool { 89 | return MatchMap(buf, matchers) != types.Unknown 90 | } 91 | -------------------------------------------------------------------------------- /kind.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "github.com/h2non/filetype/matchers" 5 | "github.com/h2non/filetype/types" 6 | ) 7 | 8 | // Image tries to match a file as image type 9 | func Image(buf []byte) (types.Type, error) { 10 | return doMatchMap(buf, matchers.Image) 11 | } 12 | 13 | // IsImage checks if the given buffer is an image type 14 | func IsImage(buf []byte) bool { 15 | kind, _ := Image(buf) 16 | return kind != types.Unknown 17 | } 18 | 19 | // Audio tries to match a file as audio type 20 | func Audio(buf []byte) (types.Type, error) { 21 | return doMatchMap(buf, matchers.Audio) 22 | } 23 | 24 | // IsAudio checks if the given buffer is an audio type 25 | func IsAudio(buf []byte) bool { 26 | kind, _ := Audio(buf) 27 | return kind != types.Unknown 28 | } 29 | 30 | // Video tries to match a file as video type 31 | func Video(buf []byte) (types.Type, error) { 32 | return doMatchMap(buf, matchers.Video) 33 | } 34 | 35 | // IsVideo checks if the given buffer is a video type 36 | func IsVideo(buf []byte) bool { 37 | kind, _ := Video(buf) 38 | return kind != types.Unknown 39 | } 40 | 41 | // Font tries to match a file as text font type 42 | func Font(buf []byte) (types.Type, error) { 43 | return doMatchMap(buf, matchers.Font) 44 | } 45 | 46 | // IsFont checks if the given buffer is a font type 47 | func IsFont(buf []byte) bool { 48 | kind, _ := Font(buf) 49 | return kind != types.Unknown 50 | } 51 | 52 | // Archive tries to match a file as generic archive type 53 | func Archive(buf []byte) (types.Type, error) { 54 | return doMatchMap(buf, matchers.Archive) 55 | } 56 | 57 | // IsArchive checks if the given buffer is an archive type 58 | func IsArchive(buf []byte) bool { 59 | kind, _ := Archive(buf) 60 | return kind != types.Unknown 61 | } 62 | 63 | // Document tries to match a file as document type 64 | func Document(buf []byte) (types.Type, error) { 65 | return doMatchMap(buf, matchers.Document) 66 | } 67 | 68 | // IsDocument checks if the given buffer is an document type 69 | func IsDocument(buf []byte) bool { 70 | kind, _ := Document(buf) 71 | return kind != types.Unknown 72 | } 73 | 74 | // Application tries to match a file as an application type 75 | func Application(buf []byte) (types.Type, error) { 76 | return doMatchMap(buf, matchers.Application) 77 | } 78 | 79 | // IsApplication checks if the given buffer is an application type 80 | func IsApplication(buf []byte) bool { 81 | kind, _ := Application(buf) 82 | return kind != types.Unknown 83 | } 84 | 85 | func doMatchMap(buf []byte, machers matchers.Map) (types.Type, error) { 86 | kind := MatchMap(buf, machers) 87 | if kind != types.Unknown { 88 | return kind, nil 89 | } 90 | return kind, ErrUnknownBuffer 91 | } 92 | -------------------------------------------------------------------------------- /filetype.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/h2non/filetype/matchers" 7 | "github.com/h2non/filetype/types" 8 | ) 9 | 10 | // Types stores a map of supported types 11 | var Types = types.Types 12 | 13 | // NewType creates and registers a new type 14 | var NewType = types.NewType 15 | 16 | // Unknown represents an unknown file type 17 | var Unknown = types.Unknown 18 | 19 | // ErrEmptyBuffer represents an empty buffer error 20 | var ErrEmptyBuffer = errors.New("Empty buffer") 21 | 22 | // ErrUnknownBuffer represents a unknown buffer error 23 | var ErrUnknownBuffer = errors.New("Unknown buffer type") 24 | 25 | // AddType registers a new file type 26 | func AddType(ext, mime string) types.Type { 27 | return types.NewType(ext, mime) 28 | } 29 | 30 | // Is checks if a given buffer matches with the given file type extension 31 | func Is(buf []byte, ext string) bool { 32 | kind := types.Get(ext) 33 | if kind != types.Unknown { 34 | return IsType(buf, kind) 35 | } 36 | return false 37 | } 38 | 39 | // IsExtension semantic alias to Is() 40 | func IsExtension(buf []byte, ext string) bool { 41 | return Is(buf, ext) 42 | } 43 | 44 | // IsType checks if a given buffer matches with the given file type 45 | func IsType(buf []byte, kind types.Type) bool { 46 | matcher := matchers.Matchers[kind] 47 | if matcher == nil { 48 | return false 49 | } 50 | return matcher(buf) != types.Unknown 51 | } 52 | 53 | // IsMIME checks if a given buffer matches with the given MIME type 54 | func IsMIME(buf []byte, mime string) bool { 55 | result := false 56 | types.Types.Range(func(k, v interface{}) bool { 57 | kind := v.(types.Type) 58 | if kind.MIME.Value == mime { 59 | matcher := matchers.Matchers[kind] 60 | result = matcher(buf) != types.Unknown 61 | return false 62 | } 63 | return true 64 | }) 65 | 66 | return result 67 | } 68 | 69 | // IsSupported checks if a given file extension is supported 70 | func IsSupported(ext string) bool { 71 | result := false 72 | types.Types.Range(func(k, v interface{}) bool { 73 | key := k.(string) 74 | if key == ext { 75 | result = true 76 | return false 77 | } 78 | return true 79 | }) 80 | 81 | return result 82 | } 83 | 84 | // IsMIMESupported checks if a given MIME type is supported 85 | func IsMIMESupported(mime string) bool { 86 | result := false 87 | types.Types.Range(func(k, v interface{}) bool { 88 | kind := v.(types.Type) 89 | if kind.MIME.Value == mime { 90 | result = true 91 | return false 92 | } 93 | return true 94 | }) 95 | 96 | return result 97 | } 98 | 99 | // GetType retrieves a Type by file extension 100 | func GetType(ext string) types.Type { 101 | return types.Get(ext) 102 | } 103 | -------------------------------------------------------------------------------- /filetype_test.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/h2non/filetype/types" 8 | ) 9 | 10 | func TestConcurrent(t *testing.T) { 11 | go func() { 12 | for i := 0; i < 10000; i++ { 13 | types.NewType("xml", "text/xml") 14 | } 15 | }() 16 | go func() { 17 | for i := 0; i < 10000; i++ { 18 | types.NewType("xml", "text/xml") 19 | } 20 | }() 21 | 22 | time.Sleep(time.Second * 2) 23 | } 24 | 25 | func TestIs(t *testing.T) { 26 | cases := []struct { 27 | buf []byte 28 | ext string 29 | match bool 30 | }{ 31 | {[]byte{0xFF, 0xD8, 0xFF}, "jpg", true}, 32 | {[]byte{0xFF, 0xD8, 0x00}, "jpg", false}, 33 | {[]byte{0x89, 0x50, 0x4E, 0x47}, "png", true}, 34 | } 35 | 36 | for _, test := range cases { 37 | if Is(test.buf, test.ext) != test.match { 38 | t.Fatalf("Invalid match: %s", test.ext) 39 | } 40 | } 41 | 42 | } 43 | 44 | func TestIsType(t *testing.T) { 45 | cases := []struct { 46 | buf []byte 47 | kind types.Type 48 | match bool 49 | }{ 50 | {[]byte{0xFF, 0xD8, 0xFF}, types.Get("jpg"), true}, 51 | {[]byte{0xFF, 0xD8, 0x00}, types.Get("jpg"), false}, 52 | {[]byte{0x89, 0x50, 0x4E, 0x47}, types.Get("png"), true}, 53 | } 54 | 55 | for _, test := range cases { 56 | if IsType(test.buf, test.kind) != test.match { 57 | t.Fatalf("Invalid match: %s", test.kind.Extension) 58 | } 59 | } 60 | } 61 | 62 | func TestIsMIME(t *testing.T) { 63 | cases := []struct { 64 | buf []byte 65 | mime string 66 | match bool 67 | }{ 68 | {[]byte{0xFF, 0xD8, 0xFF}, "image/jpeg", true}, 69 | {[]byte{0xFF, 0xD8, 0x00}, "image/jpeg", false}, 70 | {[]byte{0x89, 0x50, 0x4E, 0x47}, "image/png", true}, 71 | } 72 | 73 | for _, test := range cases { 74 | if IsMIME(test.buf, test.mime) != test.match { 75 | t.Fatalf("Invalid match: %s", test.mime) 76 | } 77 | } 78 | } 79 | 80 | func TestIsSupported(t *testing.T) { 81 | cases := []struct { 82 | ext string 83 | match bool 84 | }{ 85 | {"jpg", true}, 86 | {"jpeg", false}, 87 | {"abc", false}, 88 | {"png", true}, 89 | {"mp4", true}, 90 | {"", false}, 91 | } 92 | 93 | for _, test := range cases { 94 | if IsSupported(test.ext) != test.match { 95 | t.Fatalf("Invalid match: %s", test.ext) 96 | } 97 | } 98 | } 99 | 100 | func TestIsMIMESupported(t *testing.T) { 101 | cases := []struct { 102 | mime string 103 | match bool 104 | }{ 105 | {"image/jpeg", true}, 106 | {"foo/bar", false}, 107 | {"image/png", true}, 108 | {"video/mpeg", true}, 109 | } 110 | 111 | for _, test := range cases { 112 | if IsMIMESupported(test.mime) != test.match { 113 | t.Fatalf("Invalid match: %s", test.mime) 114 | } 115 | } 116 | } 117 | 118 | func TestAddType(t *testing.T) { 119 | AddType("foo", "foo/foo") 120 | 121 | if !IsSupported("foo") { 122 | t.Fatalf("Not supported extension") 123 | } 124 | 125 | if !IsMIMESupported("foo/foo") { 126 | t.Fatalf("Not supported MIME type") 127 | } 128 | } 129 | 130 | func TestGetType(t *testing.T) { 131 | jpg := GetType("jpg") 132 | if jpg == types.Unknown { 133 | t.Fatalf("Type should be supported") 134 | } 135 | 136 | invalid := GetType("invalid") 137 | if invalid != Unknown { 138 | t.Fatalf("Type should not be supported") 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /matchers/image.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import "github.com/h2non/filetype/matchers/isobmff" 4 | 5 | var ( 6 | TypeJpeg = newType("jpg", "image/jpeg") 7 | TypeJpeg2000 = newType("jp2", "image/jp2") 8 | TypePng = newType("png", "image/png") 9 | TypeGif = newType("gif", "image/gif") 10 | TypeWebp = newType("webp", "image/webp") 11 | TypeCR2 = newType("cr2", "image/x-canon-cr2") 12 | TypeTiff = newType("tif", "image/tiff") 13 | TypeBmp = newType("bmp", "image/bmp") 14 | TypeJxr = newType("jxr", "image/vnd.ms-photo") 15 | TypePsd = newType("psd", "image/vnd.adobe.photoshop") 16 | TypeIco = newType("ico", "image/vnd.microsoft.icon") 17 | TypeHeif = newType("heif", "image/heif") 18 | TypeDwg = newType("dwg", "image/vnd.dwg") 19 | TypeExr = newType("exr", "image/x-exr") 20 | TypeAvif = newType("avif", "image/avif") 21 | ) 22 | 23 | var Image = Map{ 24 | TypeJpeg: Jpeg, 25 | TypeJpeg2000: Jpeg2000, 26 | TypePng: Png, 27 | TypeGif: Gif, 28 | TypeWebp: Webp, 29 | TypeCR2: CR2, 30 | TypeTiff: Tiff, 31 | TypeBmp: Bmp, 32 | TypeJxr: Jxr, 33 | TypePsd: Psd, 34 | TypeIco: Ico, 35 | TypeHeif: Heif, 36 | TypeDwg: Dwg, 37 | TypeExr: Exr, 38 | TypeAvif: Avif, 39 | } 40 | 41 | func Jpeg(buf []byte) bool { 42 | return len(buf) > 2 && 43 | buf[0] == 0xFF && 44 | buf[1] == 0xD8 && 45 | buf[2] == 0xFF 46 | } 47 | 48 | func Jpeg2000(buf []byte) bool { 49 | return len(buf) > 12 && 50 | buf[0] == 0x0 && 51 | buf[1] == 0x0 && 52 | buf[2] == 0x0 && 53 | buf[3] == 0xC && 54 | buf[4] == 0x6A && 55 | buf[5] == 0x50 && 56 | buf[6] == 0x20 && 57 | buf[7] == 0x20 && 58 | buf[8] == 0xD && 59 | buf[9] == 0xA && 60 | buf[10] == 0x87 && 61 | buf[11] == 0xA && 62 | buf[12] == 0x0 63 | } 64 | 65 | func Png(buf []byte) bool { 66 | return len(buf) > 3 && 67 | buf[0] == 0x89 && buf[1] == 0x50 && 68 | buf[2] == 0x4E && buf[3] == 0x47 69 | } 70 | 71 | func Gif(buf []byte) bool { 72 | return len(buf) > 2 && 73 | buf[0] == 0x47 && buf[1] == 0x49 && buf[2] == 0x46 74 | } 75 | 76 | func Webp(buf []byte) bool { 77 | return len(buf) > 11 && 78 | buf[8] == 0x57 && buf[9] == 0x45 && 79 | buf[10] == 0x42 && buf[11] == 0x50 80 | } 81 | 82 | func CR2(buf []byte) bool { 83 | return len(buf) > 10 && 84 | ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || // Little Endian 85 | (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) && // Big Endian 86 | buf[8] == 0x43 && buf[9] == 0x52 && // CR2 magic word 87 | buf[10] == 0x02 // CR2 major version 88 | } 89 | 90 | func Tiff(buf []byte) bool { 91 | return len(buf) > 10 && 92 | ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || // Little Endian 93 | (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) && // Big Endian 94 | !CR2(buf) // To avoid conflicts differentiate Tiff from CR2 95 | } 96 | 97 | func Bmp(buf []byte) bool { 98 | return len(buf) > 1 && 99 | buf[0] == 0x42 && 100 | buf[1] == 0x4D 101 | } 102 | 103 | func Jxr(buf []byte) bool { 104 | return len(buf) > 2 && 105 | buf[0] == 0x49 && 106 | buf[1] == 0x49 && 107 | buf[2] == 0xBC 108 | } 109 | 110 | func Psd(buf []byte) bool { 111 | return len(buf) > 3 && 112 | buf[0] == 0x38 && buf[1] == 0x42 && 113 | buf[2] == 0x50 && buf[3] == 0x53 114 | } 115 | 116 | func Ico(buf []byte) bool { 117 | return len(buf) > 3 && 118 | buf[0] == 0x00 && buf[1] == 0x00 && 119 | buf[2] == 0x01 && buf[3] == 0x00 120 | } 121 | 122 | func Heif(buf []byte) bool { 123 | if !isobmff.IsISOBMFF(buf) { 124 | return false 125 | } 126 | 127 | majorBrand, _, compatibleBrands := isobmff.GetFtyp(buf) 128 | if majorBrand == "heic" { 129 | return true 130 | } 131 | 132 | if majorBrand == "mif1" || majorBrand == "msf1" { 133 | for _, compatibleBrand := range compatibleBrands { 134 | if compatibleBrand == "heic" { 135 | return true 136 | } 137 | } 138 | } 139 | 140 | return false 141 | } 142 | 143 | func Dwg(buf []byte) bool { 144 | return len(buf) > 3 && 145 | buf[0] == 0x41 && buf[1] == 0x43 && 146 | buf[2] == 0x31 && buf[3] == 0x30 147 | } 148 | 149 | func Exr(buf []byte) bool { 150 | return len(buf) > 3 && 151 | buf[0] == 0x76 && buf[1] == 0x2f && 152 | buf[2] == 0x31 && buf[3] == 0x01 153 | } 154 | 155 | func Avif(buf []byte) bool { 156 | if !isobmff.IsISOBMFF(buf) { 157 | return false 158 | } 159 | 160 | majorBrand, _, compatibleBrands := isobmff.GetFtyp(buf) 161 | if majorBrand == "avif" { 162 | return true 163 | } 164 | 165 | if majorBrand == "mif1" || majorBrand == "msf1" { 166 | for _, compatibleBrand := range compatibleBrands { 167 | if compatibleBrand == "avif" { 168 | return true 169 | } 170 | } 171 | } 172 | 173 | return false 174 | } 175 | -------------------------------------------------------------------------------- /matchers/video.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import "bytes" 4 | 5 | var ( 6 | TypeMp4 = newType("mp4", "video/mp4") 7 | TypeM4v = newType("m4v", "video/x-m4v") 8 | TypeMkv = newType("mkv", "video/x-matroska") 9 | TypeWebm = newType("webm", "video/webm") 10 | TypeMov = newType("mov", "video/quicktime") 11 | TypeAvi = newType("avi", "video/x-msvideo") 12 | TypeWmv = newType("wmv", "video/x-ms-wmv") 13 | TypeMpeg = newType("mpg", "video/mpeg") 14 | TypeFlv = newType("flv", "video/x-flv") 15 | Type3gp = newType("3gp", "video/3gpp") 16 | ) 17 | 18 | var Video = Map{ 19 | TypeMp4: Mp4, 20 | TypeM4v: M4v, 21 | TypeMkv: Mkv, 22 | TypeWebm: Webm, 23 | TypeMov: Mov, 24 | TypeAvi: Avi, 25 | TypeWmv: Wmv, 26 | TypeMpeg: Mpeg, 27 | TypeFlv: Flv, 28 | Type3gp: Match3gp, 29 | } 30 | 31 | func M4v(buf []byte) bool { 32 | return len(buf) > 10 && 33 | buf[4] == 0x66 && buf[5] == 0x74 && 34 | buf[6] == 0x79 && buf[7] == 0x70 && 35 | buf[8] == 0x4D && buf[9] == 0x34 && 36 | buf[10] == 0x56 37 | } 38 | 39 | func Mkv(buf []byte) bool { 40 | return len(buf) > 3 && 41 | buf[0] == 0x1A && buf[1] == 0x45 && 42 | buf[2] == 0xDF && buf[3] == 0xA3 && 43 | containsMatroskaSignature(buf, []byte{'m', 'a', 't', 'r', 'o', 's', 'k', 'a'}) 44 | } 45 | 46 | func Webm(buf []byte) bool { 47 | return len(buf) > 3 && 48 | buf[0] == 0x1A && buf[1] == 0x45 && 49 | buf[2] == 0xDF && buf[3] == 0xA3 && 50 | containsMatroskaSignature(buf, []byte{'w', 'e', 'b', 'm'}) 51 | } 52 | 53 | func Mov(buf []byte) bool { 54 | return len(buf) > 15 && ((buf[0] == 0x0 && buf[1] == 0x0 && 55 | buf[2] == 0x0 && (buf[3] == 0x14 || buf[3] == 0x20) && 56 | buf[4] == 0x66 && buf[5] == 0x74 && // 'f' 't' 57 | buf[6] == 0x79 && buf[7] == 0x70 && // 'y' 'p' 58 | buf[8] == 0x71 && buf[9] == 0x74) || // 'q' 't' 59 | (buf[4] == 0x6d && buf[5] == 0x6f && buf[6] == 0x6f && buf[7] == 0x76) || 60 | (buf[4] == 0x6d && buf[5] == 0x64 && buf[6] == 0x61 && buf[7] == 0x74) || 61 | (buf[12] == 0x6d && buf[13] == 0x64 && buf[14] == 0x61 && buf[15] == 0x74)) 62 | } 63 | 64 | func Avi(buf []byte) bool { 65 | return len(buf) > 10 && 66 | buf[0] == 0x52 && buf[1] == 0x49 && 67 | buf[2] == 0x46 && buf[3] == 0x46 && 68 | buf[8] == 0x41 && buf[9] == 0x56 && 69 | buf[10] == 0x49 70 | } 71 | 72 | func Wmv(buf []byte) bool { 73 | return len(buf) > 9 && 74 | buf[0] == 0x30 && buf[1] == 0x26 && 75 | buf[2] == 0xB2 && buf[3] == 0x75 && 76 | buf[4] == 0x8E && buf[5] == 0x66 && 77 | buf[6] == 0xCF && buf[7] == 0x11 && 78 | buf[8] == 0xA6 && buf[9] == 0xD9 79 | } 80 | 81 | func Mpeg(buf []byte) bool { 82 | return len(buf) > 3 && 83 | buf[0] == 0x0 && buf[1] == 0x0 && 84 | buf[2] == 0x1 && buf[3] >= 0xb0 && 85 | buf[3] <= 0xbf 86 | } 87 | 88 | func Flv(buf []byte) bool { 89 | return len(buf) > 3 && 90 | buf[0] == 0x46 && buf[1] == 0x4C && 91 | buf[2] == 0x56 && buf[3] == 0x01 92 | } 93 | 94 | func Mp4(buf []byte) bool { 95 | return len(buf) > 11 && 96 | (buf[4] == 'f' && buf[5] == 't' && buf[6] == 'y' && buf[7] == 'p') && 97 | ((buf[8] == 'a' && buf[9] == 'v' && buf[10] == 'c' && buf[11] == '1') || 98 | (buf[8] == 'd' && buf[9] == 'a' && buf[10] == 's' && buf[11] == 'h') || 99 | (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '2') || 100 | (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '3') || 101 | (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '4') || 102 | (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '5') || 103 | (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '6') || 104 | (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == 'm') || 105 | (buf[8] == 'm' && buf[9] == 'm' && buf[10] == 'p' && buf[11] == '4') || 106 | (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '4' && buf[11] == '1') || 107 | (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '4' && buf[11] == '2') || 108 | (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '4' && buf[11] == 'v') || 109 | (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '7' && buf[11] == '1') || 110 | (buf[8] == 'M' && buf[9] == 'S' && buf[10] == 'N' && buf[11] == 'V') || 111 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'A' && buf[11] == 'S') || 112 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'C') || 113 | (buf[8] == 'N' && buf[9] == 'S' && buf[10] == 'D' && buf[11] == 'C') || 114 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'H') || 115 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'M') || 116 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'P') || 117 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'S') || 118 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'C') || 119 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'H') || 120 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'M') || 121 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'P') || 122 | (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'S') || 123 | (buf[8] == 'F' && buf[9] == '4' && buf[10] == 'V' && buf[11] == ' ') || 124 | (buf[8] == 'F' && buf[9] == '4' && buf[10] == 'P' && buf[11] == ' ')) 125 | } 126 | 127 | func Match3gp(buf []byte) bool { 128 | return len(buf) > 10 && 129 | buf[4] == 0x66 && buf[5] == 0x74 && buf[6] == 0x79 && 130 | buf[7] == 0x70 && buf[8] == 0x33 && buf[9] == 0x67 && 131 | buf[10] == 0x70 132 | } 133 | 134 | func containsMatroskaSignature(buf, subType []byte) bool { 135 | limit := 4096 136 | if len(buf) < limit { 137 | limit = len(buf) 138 | } 139 | 140 | index := bytes.Index(buf[:limit], subType) 141 | if index < 3 { 142 | return false 143 | } 144 | 145 | return buf[index-3] == 0x42 && buf[index-2] == 0x82 146 | } 147 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v1.0.3 / 2021-11-21 3 | =================== 4 | 5 | * fix(#108): add application file matchers 6 | * Merge pull request #106 from hannesbraun/aiff-support 7 | * Add AIFF support 8 | * fix(archive): format issue indentation 9 | * feat(version): bump patch 10 | * Merge pull request #100 from da2018/master 11 | * Enhance Zstd support 12 | * Merge pull request #98 from cfergeau/zstd 13 | * Add zstd support 14 | * Merge pull request #99 from cfergeau/byteprefixmatcher 15 | * Introduce bytePrefixMatcher helper 16 | 17 | v1.1.0 / 2020-06-06 18 | =================== 19 | 20 | * feat: version bump v1.10 21 | * feat(ci): add go 1.14 22 | * Merge pull request #82 from andrewstucki/sqlite-update 23 | * Merge pull request #84 from evanoberholster/master 24 | * Better differentiation: between image/x-canon-cr2 and image/tiff 25 | * Merge pull request #1 from h2non/master 26 | * Update ico filetype per https://www.iana.org/assignments/media-types/image/vnd.microsoft.icon 27 | * Update rar filetype per https://www.iana.org/assignments/media-types/application/vnd.rar 28 | * Update exe filetype per https://www.iana.org/assignments/media-types/application/vnd.microsoft.portable-executable 29 | * Update deb filetype per https://www.iana.org/assignments/media-types/application/vnd.debian.binary-package 30 | * Update sqlite filetype per https://www.iana.org/assignments/media-types/application/vnd.sqlite3 31 | * Merge pull request #72 from turn88/master 32 | * Update document.go 33 | * Update document.go 34 | * Update document.go 35 | * add matchers for office 2003 36 | 37 | v1.0.10 / 2019-08-06 38 | ==================== 39 | 40 | * Merge pull request #76 from lex-r/fix-matroska-detection 41 | * fix: mkv and webm types detection 42 | 43 | v1.0.9 / 2019-07-25 44 | =================== 45 | 46 | * Merge pull request #75 from Trane9991/master 47 | * add video/3gpp support 48 | * fix: use proper iso file mime type 49 | * feat: add iso image format 50 | * Merge pull request #65 from Fentonz/master 51 | * Merge pull request #70 from fanpei91/master 52 | * add image/vnd.dwg to README 53 | * add image/vnd.dwg support 54 | * Added support for .iso files 55 | 56 | v1.0.8 / 2019-02-10 57 | =================== 58 | 59 | * refactor(images): heic -> heif 60 | * feat(docs): add heif format 61 | * Merge pull request #60 from rikonor/master 62 | * add heif/heic support 63 | * fix(docs): dicom -> dcm 64 | * feat: add dicom type 65 | * Merge pull request #58 from Fentonz/master 66 | * Merge pull request #59 from kmanley/master 67 | * fix example; related to h2non/filetype#43 68 | * Added DICOM type to archive 69 | 70 | 71 | v1.0.7 / 2019-02-09 72 | =================== 73 | 74 | * Merge pull request #56 from akupila/wasm 75 | * add wasm to readme 76 | * detect wasm file type 77 | 78 | v1.0.6 / 2019-01-22 79 | =================== 80 | 81 | * Merge pull request #55 from ivanlemeshev/master 82 | * Added ftypmp4v to MP4 matcher 83 | * Merge pull request #54 from aofei/master 84 | * chore: add support for Go modules 85 | * feat: add support for AAC (audio/aac) 86 | * Merge pull request #53 from lynxbyorion/check-for-docoments 87 | * Added checks for documents. 88 | * Merge pull request #51 from eriken/master 89 | * fixed bad mime and import paths 90 | * Merge pull request #50 from eriken/jpeg2000_support 91 | * fix import paths 92 | * jpeg2000 support 93 | * Merge pull request #47 from Ma124/master 94 | * Merge pull request #49 from amoore614/master 95 | * more robust check for .mov files 96 | * bugfix: reverse order of matcher key list so user registered matchers appear first 97 | * bugfix: store ptr to MatcherKeys in case user registered matchers are used. 98 | * update comment 99 | * Bump buffer size to 8K to allow for more custom file matching 100 | * refactor(readme): update package import path 101 | * Merge pull request #48 from kumakichi/support_msooxml 102 | * do not use v1 103 | * ok, master already changed travis 104 | * add fixtures, but MatchReader may not work for some msooxml files, 4096 bytes maybe not enough 105 | * support ms ooxml, #40 106 | * Fixed misspells 107 | * fix(travis): use string notation for matrix items 108 | * Merge pull request #42 from bruth/patch-2 109 | * refactor(travis): remove Go 1.6, add Go 1.10 110 | * Change maximum bytes required for detection 111 | * Merge pull request #36 from yiiTT/patch-1 112 | * Add MP4 dash and additional ISO formats 113 | * Merge pull request #34 from RangelReale/fix-mp4-case 114 | * Merge pull request #32 from yiiTT/fix-m4v 115 | * Fixed mp4 detection case-sensitivity according to http://www.ftyps.com/ 116 | * Fix M4v matcher 117 | 118 | v1.0.5 / 2017-12-12 119 | =================== 120 | 121 | * Merge pull request #30 from RangelReale/fix_mp4 122 | * Fix duplicated item in mp4 fix 123 | * Fix MP4 matcher, with information from http://www.file-recovery.com/mp4-signature-format.htm 124 | * Merge pull request #28 from ikovic/master 125 | * Updated file header example. 126 | 127 | v1.0.4 / 2017-11-29 128 | =================== 129 | 130 | * fix: tests and document types matchers 131 | * refactor(docs): remove codesponsor 132 | * Merge pull request #26 from bienkma/master 133 | * Add support check file type: .doc, .docx, .pptx, .ppt, .xls, .xlsx 134 | * feat(docs): add code sponsor banner 135 | * feat(travis): add go 1.9 136 | * Merge pull request #24 from strazzere/patch-1 137 | * Fix typo in unknown 138 | 139 | v1.0.3 / 2017-08-03 140 | =================== 141 | 142 | * Merge pull request #21 from elemeta/master 143 | * Add Elf file as supported matcher archive type 144 | 145 | v1.0.2 / 2017-07-26 146 | =================== 147 | 148 | * Merge pull request #20 from marshyski/master 149 | * Added RedHat RPM as supported matcher archive type 150 | * Merge pull request #19 from nlamirault/patch-1 151 | * Fix typo in documentation 152 | 153 | v1.0.1 / 2017-02-24 154 | =================== 155 | 156 | * Merge pull request #18 from Impyy/enable-webm 157 | * Enable the webm matcher 158 | * feat(docs): add Go version badge 159 | 160 | 1.0.0 / 2016-12-11 161 | ================== 162 | 163 | - Initial stable version (v1.0.0). 164 | -------------------------------------------------------------------------------- /match_test.go: -------------------------------------------------------------------------------- 1 | package filetype 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "testing" 8 | 9 | "github.com/h2non/filetype/matchers" 10 | "github.com/h2non/filetype/types" 11 | ) 12 | 13 | func TestMatch(t *testing.T) { 14 | cases := []struct { 15 | buf []byte 16 | ext string 17 | }{ 18 | {[]byte{0xFF, 0xD8, 0xFF}, "jpg"}, 19 | {[]byte{0xFF, 0xD8, 0x00}, "unknown"}, 20 | {[]byte{0x89, 0x50, 0x4E, 0x47}, "png"}, 21 | } 22 | 23 | for _, test := range cases { 24 | match, err := Match(test.buf) 25 | if err != nil { 26 | t.Fatalf("Error: %s", err) 27 | } 28 | 29 | if match.Extension != test.ext { 30 | t.Fatalf("Invalid image type: %s != %s", match.Extension, test.ext) 31 | } 32 | } 33 | } 34 | 35 | func TestMatchFile(t *testing.T) { 36 | cases := []struct { 37 | ext string 38 | }{ 39 | {"gif"}, 40 | {"jpg"}, 41 | {"png"}, 42 | {"zip"}, 43 | {"tar"}, 44 | {"tif"}, 45 | {"mp4"}, 46 | {"mkv"}, 47 | {"webm"}, 48 | {"docx"}, 49 | {"pptx"}, 50 | {"xlsx"}, 51 | {"mov"}, 52 | {"wasm"}, 53 | {"dwg"}, 54 | {"zst"}, 55 | {"exr"}, 56 | {"avif"}, 57 | {"parquet"}, 58 | } 59 | 60 | for _, test := range cases { 61 | kind, _ := MatchFile("./fixtures/sample." + test.ext) 62 | if kind.Extension != test.ext { 63 | t.Fatalf("Invalid type: %s != %s", kind.Extension, test.ext) 64 | } 65 | } 66 | // test zstd with skippable frame 67 | kind, _ := MatchFile("./fixtures/sample_skippable.zst") 68 | if kind.Extension != "zst" { 69 | t.Fatalf("Invalid type: %s != %s", kind.Extension, "zst") 70 | } 71 | } 72 | 73 | func TestMatchReader(t *testing.T) { 74 | cases := []struct { 75 | buf io.Reader 76 | ext string 77 | }{ 78 | {bytes.NewBuffer([]byte{0xFF, 0xD8, 0xFF}), "jpg"}, 79 | {bytes.NewBuffer([]byte{0xFF, 0xD8, 0x00}), "unknown"}, 80 | {bytes.NewBuffer([]byte{0x89, 0x50, 0x4E, 0x47}), "png"}, 81 | } 82 | 83 | for _, test := range cases { 84 | match, err := MatchReader(test.buf) 85 | if err != nil { 86 | t.Fatalf("Error: %s", err) 87 | } 88 | 89 | if match.Extension != test.ext { 90 | t.Fatalf("Invalid image type: %s", match.Extension) 91 | } 92 | } 93 | } 94 | 95 | func TestMatches(t *testing.T) { 96 | cases := []struct { 97 | buf []byte 98 | match bool 99 | }{ 100 | {[]byte{0xFF, 0xD8, 0xFF}, true}, 101 | {[]byte{0xFF, 0x0, 0x0}, false}, 102 | {[]byte{0x89, 0x50, 0x4E, 0x47}, true}, 103 | } 104 | 105 | for _, test := range cases { 106 | if Matches(test.buf) != test.match { 107 | t.Fatalf("Do not matches: %#v", test.buf) 108 | } 109 | } 110 | } 111 | 112 | func TestAddMatcher(t *testing.T) { 113 | fileType := AddType("foo", "foo/foo") 114 | 115 | AddMatcher(fileType, func(buf []byte) bool { 116 | return len(buf) == 2 && buf[0] == 0x00 && buf[1] == 0x00 117 | }) 118 | 119 | if !Is([]byte{0x00, 0x00}, "foo") { 120 | t.Fatalf("Type cannot match") 121 | } 122 | 123 | if !IsSupported("foo") { 124 | t.Fatalf("Not supported extension") 125 | } 126 | 127 | if !IsMIMESupported("foo/foo") { 128 | t.Fatalf("Not supported MIME type") 129 | } 130 | } 131 | 132 | func TestMatchMap(t *testing.T) { 133 | cases := []struct { 134 | buf []byte 135 | kind types.Type 136 | }{ 137 | {[]byte{0xFF, 0xD8, 0xFF}, types.Get("jpg")}, 138 | {[]byte{0x89, 0x50, 0x4E, 0x47}, types.Get("png")}, 139 | {[]byte{0xFF, 0x0, 0x0}, Unknown}, 140 | } 141 | 142 | for _, test := range cases { 143 | if kind := MatchMap(test.buf, matchers.Image); kind != test.kind { 144 | t.Fatalf("Do not matches: %#v", test.buf) 145 | } 146 | } 147 | } 148 | 149 | func TestMatchesMap(t *testing.T) { 150 | cases := []struct { 151 | buf []byte 152 | match bool 153 | }{ 154 | {[]byte{0xFF, 0xD8, 0xFF}, true}, 155 | {[]byte{0x89, 0x50, 0x4E, 0x47}, true}, 156 | {[]byte{0xFF, 0x0, 0x0}, false}, 157 | } 158 | 159 | for _, test := range cases { 160 | if match := MatchesMap(test.buf, matchers.Image); match != test.match { 161 | t.Fatalf("Do not matches: %#v", test.buf) 162 | } 163 | } 164 | } 165 | 166 | // 167 | // Benchmarks 168 | // 169 | 170 | var tarBuffer, _ = ioutil.ReadFile("./fixtures/sample.tar") 171 | var zipBuffer, _ = ioutil.ReadFile("./fixtures/sample.zip") 172 | var jpgBuffer, _ = ioutil.ReadFile("./fixtures/sample.jpg") 173 | var gifBuffer, _ = ioutil.ReadFile("./fixtures/sample.gif") 174 | var pngBuffer, _ = ioutil.ReadFile("./fixtures/sample.png") 175 | var xlsxBuffer, _ = ioutil.ReadFile("./fixtures/sample.xlsx") 176 | var pptxBuffer, _ = ioutil.ReadFile("./fixtures/sample.pptx") 177 | var docxBuffer, _ = ioutil.ReadFile("./fixtures/sample.docx") 178 | var dwgBuffer, _ = ioutil.ReadFile("./fixtures/sample.dwg") 179 | var mkvBuffer, _ = ioutil.ReadFile("./fixtures/sample.mkv") 180 | var webmBuffer, _ = ioutil.ReadFile("./fixtures/sample.webm") 181 | var exrBuffer, _ = ioutil.ReadFile("./fixtures/sample.exr") 182 | 183 | func BenchmarkMatchTar(b *testing.B) { 184 | for n := 0; n < b.N; n++ { 185 | Match(tarBuffer) 186 | } 187 | } 188 | 189 | func BenchmarkMatchZip(b *testing.B) { 190 | for n := 0; n < b.N; n++ { 191 | Match(zipBuffer) 192 | } 193 | } 194 | 195 | func BenchmarkMatchJpeg(b *testing.B) { 196 | for n := 0; n < b.N; n++ { 197 | Match(jpgBuffer) 198 | } 199 | } 200 | 201 | func BenchmarkMatchGif(b *testing.B) { 202 | for n := 0; n < b.N; n++ { 203 | Match(gifBuffer) 204 | } 205 | } 206 | 207 | func BenchmarkMatchPng(b *testing.B) { 208 | for n := 0; n < b.N; n++ { 209 | Match(pngBuffer) 210 | } 211 | } 212 | 213 | func BenchmarkMatchXlsx(b *testing.B) { 214 | for n := 0; n < b.N; n++ { 215 | Match(xlsxBuffer) 216 | } 217 | } 218 | 219 | func BenchmarkMatchPptx(b *testing.B) { 220 | for n := 0; n < b.N; n++ { 221 | Match(pptxBuffer) 222 | } 223 | } 224 | 225 | func BenchmarkMatchDocx(b *testing.B) { 226 | for n := 0; n < b.N; n++ { 227 | Match(docxBuffer) 228 | } 229 | } 230 | 231 | func BenchmarkMatchDwg(b *testing.B) { 232 | for n := 0; n < b.N; n++ { 233 | Match(dwgBuffer) 234 | } 235 | } 236 | 237 | func BenchmarkMatchMkv(b *testing.B) { 238 | for n := 0; n < b.N; n++ { 239 | Match(mkvBuffer) 240 | } 241 | } 242 | 243 | func BenchmarkMatchWebm(b *testing.B) { 244 | for n := 0; n < b.N; n++ { 245 | Match(webmBuffer) 246 | } 247 | } 248 | 249 | func BenchmarkMatchExr(b *testing.B) { 250 | for n := 0; n < b.N; n++ { 251 | Match(exrBuffer) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /matchers/document.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | var ( 9 | TypeDoc = newType("doc", "application/msword") 10 | TypeDocx = newType("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document") 11 | TypeXls = newType("xls", "application/vnd.ms-excel") 12 | TypeXlsx = newType("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") 13 | TypePpt = newType("ppt", "application/vnd.ms-powerpoint") 14 | TypePptx = newType("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation") 15 | TypeOdp = newType("odp", "application/vnd.oasis.opendocument.presentation") 16 | TypeOds = newType("ods", "application/vnd.oasis.opendocument.spreadsheet") 17 | TypeOdt = newType("odt", "application/vnd.oasis.opendocument.text") 18 | ) 19 | 20 | var Document = Map{ 21 | TypeDoc: Doc, 22 | TypeDocx: Docx, 23 | TypeXls: Xls, 24 | TypeXlsx: Xlsx, 25 | TypePpt: Ppt, 26 | TypePptx: Pptx, 27 | TypeOdp: Odp, 28 | TypeOds: Ods, 29 | TypeOdt: Odt, 30 | } 31 | 32 | type docType int 33 | 34 | const ( 35 | TYPE_DOC docType = iota 36 | TYPE_DOCX 37 | TYPE_XLS 38 | TYPE_XLSX 39 | TYPE_PPT 40 | TYPE_PPTX 41 | TYPE_OOXML 42 | TYPE_ODP 43 | TYPE_ODS 44 | TYPE_ODT 45 | ) 46 | 47 | // reference: https://bz.apache.org/ooo/show_bug.cgi?id=111457 48 | func Doc(buf []byte) bool { 49 | if len(buf) > 513 { 50 | return buf[0] == 0xD0 && buf[1] == 0xCF && 51 | buf[2] == 0x11 && buf[3] == 0xE0 && 52 | buf[512] == 0xEC && buf[513] == 0xA5 53 | } else { 54 | return len(buf) > 3 && 55 | buf[0] == 0xD0 && buf[1] == 0xCF && 56 | buf[2] == 0x11 && buf[3] == 0xE0 57 | } 58 | } 59 | 60 | func Docx(buf []byte) bool { 61 | typ, ok := msooxml(buf) 62 | return ok && typ == TYPE_DOCX 63 | } 64 | 65 | func Xls(buf []byte) bool { 66 | if len(buf) > 513 { 67 | isMSOfficeBFF := buf[0] == 0xD0 && buf[1] == 0xCF && buf[2] == 0x11 && buf[3] == 0xE0 68 | 69 | switch { 70 | case isMSOfficeBFF && buf[512] == 0x09 && buf[513] == 0x08: // BIFF5 && BIFF12(12) 71 | return true 72 | case isMSOfficeBFF && buf[512] == 0xFD && buf[513] == 0xFF: // BIFF12(11) 73 | return true 74 | } 75 | } 76 | 77 | return false 78 | } 79 | 80 | func Xlsx(buf []byte) bool { 81 | typ, ok := msooxml(buf) 82 | return ok && typ == TYPE_XLSX 83 | } 84 | 85 | func Ppt(buf []byte) bool { 86 | if len(buf) > 513 { 87 | return buf[0] == 0xD0 && buf[1] == 0xCF && 88 | buf[2] == 0x11 && buf[3] == 0xE0 && 89 | buf[512] == 0xA0 && buf[513] == 0x46 90 | } else { 91 | return len(buf) > 3 && 92 | buf[0] == 0xD0 && buf[1] == 0xCF && 93 | buf[2] == 0x11 && buf[3] == 0xE0 94 | } 95 | } 96 | 97 | func Pptx(buf []byte) bool { 98 | typ, ok := msooxml(buf) 99 | return ok && typ == TYPE_PPTX 100 | } 101 | 102 | func msooxml(buf []byte) (typ docType, found bool) { 103 | signature := []byte{'P', 'K', 0x03, 0x04} 104 | 105 | // start by checking for ZIP local file header signature 106 | if ok := compareBytes(buf, signature, 0); !ok { 107 | return 108 | } 109 | 110 | // make sure the first file is correct 111 | if v, ok := checkMSOoml(buf, 0x1E); ok { 112 | return v, ok 113 | } 114 | 115 | if !compareBytes(buf, []byte("[Content_Types].xml"), 0x1E) && 116 | !compareBytes(buf, []byte("_rels/.rels"), 0x1E) && 117 | !compareBytes(buf, []byte("docProps"), 0x1E) && 118 | !compareBytes(buf, []byte("_rels"), 0x1E) { 119 | return 120 | } 121 | 122 | // skip to the second local file header 123 | // since some documents include a 520-byte extra field following the file 124 | // header, we need to scan for the next header 125 | startOffset := int(binary.LittleEndian.Uint32(buf[18:22]) + 49) 126 | idx := search(buf, startOffset, 6000) 127 | if idx == -1 { 128 | return 129 | } 130 | 131 | // now skip to the *third* local file header; again, we need to scan due to a 132 | // 520-byte extra field following the file header 133 | startOffset += idx + 4 + 26 134 | idx = search(buf, startOffset, 6000) 135 | if idx == -1 { 136 | return 137 | } 138 | 139 | // and check the subdirectory name to determine which type of OOXML 140 | // file we have. Correct the mimetype with the registered ones: 141 | // http://technet.microsoft.com/en-us/library/cc179224.aspx 142 | startOffset += idx + 4 + 26 143 | if typ, ok := checkMSOoml(buf, startOffset); ok { 144 | return typ, ok 145 | } 146 | 147 | // OpenOffice/Libreoffice orders ZIP entry differently, so check the 4th file 148 | startOffset += 26 149 | idx = search(buf, startOffset, 6000) 150 | if idx == -1 { 151 | return TYPE_OOXML, true 152 | } 153 | 154 | startOffset += idx + 4 + 26 155 | if typ, ok := checkMSOoml(buf, startOffset); ok { 156 | return typ, ok 157 | } else { 158 | return TYPE_OOXML, true 159 | } 160 | } 161 | 162 | func compareBytes(slice, subSlice []byte, startOffset int) bool { 163 | sl := len(subSlice) 164 | 165 | if startOffset+sl > len(slice) { 166 | return false 167 | } 168 | 169 | s := slice[startOffset : startOffset+sl] 170 | for i := range s { 171 | if subSlice[i] != s[i] { 172 | return false 173 | } 174 | } 175 | 176 | return true 177 | } 178 | 179 | func checkMSOoml(buf []byte, offset int) (typ docType, ok bool) { 180 | ok = true 181 | 182 | switch { 183 | case compareBytes(buf, []byte("word/"), offset): 184 | typ = TYPE_DOCX 185 | case compareBytes(buf, []byte("ppt/"), offset): 186 | typ = TYPE_PPTX 187 | case compareBytes(buf, []byte("xl/"), offset): 188 | typ = TYPE_XLSX 189 | default: 190 | ok = false 191 | } 192 | 193 | return 194 | } 195 | 196 | func search(buf []byte, start, rangeNum int) int { 197 | length := len(buf) 198 | end := start + rangeNum 199 | signature := []byte{'P', 'K', 0x03, 0x04} 200 | 201 | if end > length { 202 | end = length 203 | } 204 | 205 | if start >= end { 206 | return -1 207 | } 208 | 209 | return bytes.Index(buf[start:end], signature) 210 | } 211 | 212 | func Odp(buf []byte) bool { 213 | return checkOdf(buf, TypeOdp.MIME.Value) 214 | } 215 | 216 | func Ods(buf []byte) bool { 217 | return checkOdf(buf, TypeOds.MIME.Value) 218 | } 219 | 220 | func Odt(buf []byte) bool { 221 | return checkOdf(buf, TypeOdt.MIME.Value) 222 | } 223 | 224 | // https://en.wikipedia.org/wiki/OpenDocument_technical_specification 225 | // https://en.wikipedia.org/wiki/ZIP_(file_format) 226 | func checkOdf(buf []byte, mimetype string) bool { 227 | if 38+len(mimetype) >= len(buf) { 228 | return false 229 | } 230 | // Perform all byte checks first for better performance 231 | // Check ZIP start 232 | if buf[0] != 'P' || buf[1] != 'K' || buf[2] != 3 || buf[3] != 4 { 233 | return false 234 | } 235 | // Now check the first file data 236 | // Compression method: not compressed 237 | if buf[8] != 0 || buf[9] != 0 { 238 | return false 239 | } 240 | // Filename length must be 8 for "mimetype" 241 | if buf[26] != 8 || buf[27] != 0 { 242 | return false 243 | } 244 | // Check the file contents sizes 245 | if int(buf[18]) != len(mimetype) || 246 | buf[19] != 0 || buf[20] != 0 || buf[21] != 0 || 247 | int(buf[22]) != len(mimetype) || 248 | buf[23] != 0 || buf[24] != 0 || buf[25] != 0 { 249 | return false 250 | } 251 | // No extra field (for data offset below) 252 | if buf[28] != 0 || buf[29] != 0 { 253 | return false 254 | } 255 | // Finally check the file name and contents 256 | return string(buf[30:38]) == "mimetype" && 257 | string(buf[38:38+len(mimetype)]) == mimetype 258 | } 259 | -------------------------------------------------------------------------------- /matchers/archive.go: -------------------------------------------------------------------------------- 1 | package matchers 2 | 3 | import "encoding/binary" 4 | 5 | const ( 6 | ZstdMagicSkippableStart = 0x184D2A50 7 | ZstdMagicSkippableMask = 0xFFFFFFF0 8 | ) 9 | 10 | var ( 11 | TypeEpub = newType("epub", "application/epub+zip") 12 | TypeZip = newType("zip", "application/zip") 13 | TypeTar = newType("tar", "application/x-tar") 14 | TypeRar = newType("rar", "application/vnd.rar") 15 | TypeGz = newType("gz", "application/gzip") 16 | TypeBz2 = newType("bz2", "application/x-bzip2") 17 | Type7z = newType("7z", "application/x-7z-compressed") 18 | TypeXz = newType("xz", "application/x-xz") 19 | TypeZstd = newType("zst", "application/zstd") 20 | TypePdf = newType("pdf", "application/pdf") 21 | TypeExe = newType("exe", "application/vnd.microsoft.portable-executable") 22 | TypeSwf = newType("swf", "application/x-shockwave-flash") 23 | TypeRtf = newType("rtf", "application/rtf") 24 | TypeEot = newType("eot", "application/octet-stream") 25 | TypePs = newType("ps", "application/postscript") 26 | TypeSqlite = newType("sqlite", "application/vnd.sqlite3") 27 | TypeNes = newType("nes", "application/x-nintendo-nes-rom") 28 | TypeCrx = newType("crx", "application/x-google-chrome-extension") 29 | TypeCab = newType("cab", "application/vnd.ms-cab-compressed") 30 | TypeDeb = newType("deb", "application/vnd.debian.binary-package") 31 | TypeAr = newType("ar", "application/x-unix-archive") 32 | TypeZ = newType("Z", "application/x-compress") 33 | TypeLz = newType("lz", "application/x-lzip") 34 | TypeRpm = newType("rpm", "application/x-rpm") 35 | TypeElf = newType("elf", "application/x-executable") 36 | TypeDcm = newType("dcm", "application/dicom") 37 | TypeIso = newType("iso", "application/x-iso9660-image") 38 | TypeMachO = newType("macho", "application/x-mach-binary") // Mach-O binaries have no common extension. 39 | TypeParquet = newType("parquet", "application/vnd.apache.parquet") 40 | ) 41 | 42 | var Archive = Map{ 43 | TypeEpub: bytePrefixMatcher(epubMagic), 44 | TypeZip: Zip, 45 | TypeTar: Tar, 46 | TypeRar: Rar, 47 | TypeGz: bytePrefixMatcher(gzMagic), 48 | TypeBz2: bytePrefixMatcher(bz2Magic), 49 | Type7z: bytePrefixMatcher(sevenzMagic), 50 | TypeXz: bytePrefixMatcher(xzMagic), 51 | TypeZstd: Zst, 52 | TypePdf: bytePrefixMatcher(pdfMagic), 53 | TypeExe: bytePrefixMatcher(exeMagic), 54 | TypeSwf: Swf, 55 | TypeRtf: bytePrefixMatcher(rtfMagic), 56 | TypeEot: Eot, 57 | TypePs: bytePrefixMatcher(psMagic), 58 | TypeSqlite: bytePrefixMatcher(sqliteMagic), 59 | TypeNes: bytePrefixMatcher(nesMagic), 60 | TypeCrx: bytePrefixMatcher(crxMagic), 61 | TypeCab: Cab, 62 | TypeDeb: bytePrefixMatcher(debMagic), 63 | TypeAr: bytePrefixMatcher(arMagic), 64 | TypeZ: Z, 65 | TypeLz: bytePrefixMatcher(lzMagic), 66 | TypeRpm: Rpm, 67 | TypeElf: Elf, 68 | TypeDcm: Dcm, 69 | TypeIso: Iso, 70 | TypeMachO: MachO, 71 | TypeParquet: bytePrefixMatcher(parquetMagic), 72 | } 73 | 74 | var ( 75 | epubMagic = []byte{ 76 | 0x50, 0x4B, 0x03, 0x04, 0x6D, 0x69, 0x6D, 0x65, 77 | 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C, 78 | 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 79 | 0x65, 0x70, 0x75, 0x62, 0x2B, 0x7A, 0x69, 0x70, 80 | } 81 | gzMagic = []byte{0x1F, 0x8B, 0x08} 82 | bz2Magic = []byte{0x42, 0x5A, 0x68} 83 | sevenzMagic = []byte{0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C} 84 | pdfMagic = []byte{0x25, 0x50, 0x44, 0x46} 85 | exeMagic = []byte{0x4D, 0x5A} 86 | rtfMagic = []byte{0x7B, 0x5C, 0x72, 0x74, 0x66} 87 | nesMagic = []byte{0x4E, 0x45, 0x53, 0x1A} 88 | crxMagic = []byte{0x43, 0x72, 0x32, 0x34} 89 | psMagic = []byte{0x25, 0x21} 90 | xzMagic = []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00} 91 | sqliteMagic = []byte{0x53, 0x51, 0x4C, 0x69} 92 | debMagic = []byte{ 93 | 0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A, 94 | 0x64, 0x65, 0x62, 0x69, 0x61, 0x6E, 0x2D, 0x62, 95 | 0x69, 0x6E, 0x61, 0x72, 0x79, 96 | } 97 | arMagic = []byte{0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E} 98 | zstdMagic = []byte{0x28, 0xB5, 0x2F, 0xFD} 99 | lzMagic = []byte{0x4C, 0x5A, 0x49, 0x50} 100 | parquetMagic = []byte{0x50, 0x41, 0x52, 0x31} 101 | ) 102 | 103 | func bytePrefixMatcher(magicPattern []byte) Matcher { 104 | return func(data []byte) bool { 105 | return compareBytes(data, magicPattern, 0) 106 | } 107 | } 108 | 109 | func Zip(buf []byte) bool { 110 | return len(buf) > 3 && 111 | buf[0] == 0x50 && buf[1] == 0x4B && 112 | (buf[2] == 0x3 || buf[2] == 0x5 || buf[2] == 0x7) && 113 | (buf[3] == 0x4 || buf[3] == 0x6 || buf[3] == 0x8) 114 | } 115 | 116 | func Tar(buf []byte) bool { 117 | return len(buf) > 261 && 118 | buf[257] == 0x75 && buf[258] == 0x73 && 119 | buf[259] == 0x74 && buf[260] == 0x61 && 120 | buf[261] == 0x72 121 | } 122 | 123 | func Rar(buf []byte) bool { 124 | return len(buf) > 6 && 125 | buf[0] == 0x52 && buf[1] == 0x61 && buf[2] == 0x72 && 126 | buf[3] == 0x21 && buf[4] == 0x1A && buf[5] == 0x7 && 127 | (buf[6] == 0x0 || buf[6] == 0x1) 128 | } 129 | 130 | func Swf(buf []byte) bool { 131 | return len(buf) > 2 && 132 | (buf[0] == 0x43 || buf[0] == 0x46) && 133 | buf[1] == 0x57 && buf[2] == 0x53 134 | } 135 | 136 | func Cab(buf []byte) bool { 137 | return len(buf) > 3 && 138 | ((buf[0] == 0x4D && buf[1] == 0x53 && buf[2] == 0x43 && buf[3] == 0x46) || 139 | (buf[0] == 0x49 && buf[1] == 0x53 && buf[2] == 0x63 && buf[3] == 0x28)) 140 | } 141 | 142 | func Eot(buf []byte) bool { 143 | return len(buf) > 35 && 144 | buf[34] == 0x4C && buf[35] == 0x50 && 145 | ((buf[8] == 0x02 && buf[9] == 0x00 && 146 | buf[10] == 0x01) || (buf[8] == 0x01 && 147 | buf[9] == 0x00 && buf[10] == 0x00) || 148 | (buf[8] == 0x02 && buf[9] == 0x00 && 149 | buf[10] == 0x02)) 150 | } 151 | 152 | func Z(buf []byte) bool { 153 | return len(buf) > 1 && 154 | ((buf[0] == 0x1F && buf[1] == 0xA0) || 155 | (buf[0] == 0x1F && buf[1] == 0x9D)) 156 | } 157 | 158 | func Rpm(buf []byte) bool { 159 | return len(buf) > 96 && 160 | buf[0] == 0xED && buf[1] == 0xAB && 161 | buf[2] == 0xEE && buf[3] == 0xDB 162 | } 163 | 164 | func Elf(buf []byte) bool { 165 | return len(buf) > 52 && 166 | buf[0] == 0x7F && buf[1] == 0x45 && 167 | buf[2] == 0x4C && buf[3] == 0x46 168 | } 169 | 170 | func Dcm(buf []byte) bool { 171 | return len(buf) > 131 && 172 | buf[128] == 0x44 && buf[129] == 0x49 && 173 | buf[130] == 0x43 && buf[131] == 0x4D 174 | } 175 | 176 | func Iso(buf []byte) bool { 177 | return len(buf) > 32773 && 178 | buf[32769] == 0x43 && buf[32770] == 0x44 && 179 | buf[32771] == 0x30 && buf[32772] == 0x30 && 180 | buf[32773] == 0x31 181 | } 182 | 183 | func MachO(buf []byte) bool { 184 | return len(buf) > 3 && ((buf[0] == 0xFE && buf[1] == 0xED && buf[2] == 0xFA && buf[3] == 0xCF) || 185 | (buf[0] == 0xFE && buf[1] == 0xED && buf[2] == 0xFA && buf[3] == 0xCE) || 186 | (buf[0] == 0xBE && buf[1] == 0xBA && buf[2] == 0xFE && buf[3] == 0xCA) || 187 | // Big endian versions below here... 188 | (buf[0] == 0xCF && buf[1] == 0xFA && buf[2] == 0xED && buf[3] == 0xFE) || 189 | (buf[0] == 0xCE && buf[1] == 0xFA && buf[2] == 0xED && buf[3] == 0xFE) || 190 | (buf[0] == 0xCA && buf[1] == 0xFE && buf[2] == 0xBA && buf[3] == 0xBE)) 191 | } 192 | 193 | // Zstandard compressed data is made of one or more frames. 194 | // There are two frame formats defined by Zstandard: Zstandard frames and Skippable frames. 195 | // See more details from https://tools.ietf.org/id/draft-kucherawy-dispatch-zstd-00.html#rfc.section.2 196 | func Zst(buf []byte) bool { 197 | if compareBytes(buf, zstdMagic, 0) { 198 | return true 199 | } else { 200 | // skippable frames 201 | if len(buf) < 8 { 202 | return false 203 | } 204 | if binary.LittleEndian.Uint32(buf[:4])&ZstdMagicSkippableMask == ZstdMagicSkippableStart { 205 | userDataLength := binary.LittleEndian.Uint32(buf[4:8]) 206 | if len(buf) < 8+int(userDataLength) { 207 | return false 208 | } 209 | nextFrame := buf[8+userDataLength:] 210 | return Zst(nextFrame) 211 | } 212 | return false 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # filetype [![GoDoc](https://godoc.org/github.com/h2non/filetype?status.svg)](https://godoc.org/github.com/h2non/filetype) [![Go Version](https://img.shields.io/badge/go-v1.0+-green.svg?style=flat)](https://github.com/h2non/gentleman) 2 | 3 | Small and dependency free [Go](https://golang.org) package to infer file and MIME type checking the [magic numbers]() signature. 4 | 5 | For SVG file type checking, see [go-is-svg](https://github.com/h2non/go-is-svg) package. Python port: [filetype.py](https://github.com/h2non/filetype.py). 6 | 7 | ## Features 8 | 9 | - Supports a [wide range](#supported-types) of file types 10 | - Provides file extension and proper MIME type 11 | - File discovery by extension or MIME type 12 | - File discovery by class (image, video, audio...) 13 | - Provides a bunch of helpers and file matching shortcuts 14 | - [Pluggable](#add-additional-file-type-matchers): add custom new types and matchers 15 | - Simple and semantic API 16 | - [Blazing fast](#benchmarks), even processing large files 17 | - Only first 262 bytes representing the max file header is required, so you can just [pass a slice](#file-header) 18 | - Dependency free (just Go code, no C compilation needed) 19 | - Cross-platform file recognition 20 | 21 | ## Installation 22 | 23 | ```bash 24 | go get github.com/h2non/filetype 25 | ``` 26 | 27 | ## API 28 | 29 | See [Godoc](https://godoc.org/github.com/h2non/filetype) reference. 30 | 31 | ### Subpackages 32 | 33 | - [`github.com/h2non/filetype/types`](https://godoc.org/github.com/h2non/filetype/types) 34 | - [`github.com/h2non/filetype/matchers`](https://godoc.org/github.com/h2non/filetype/matchers) 35 | 36 | ## Examples 37 | 38 | #### Simple file type checking 39 | 40 | ```go 41 | package main 42 | 43 | import ( 44 | "fmt" 45 | "io/ioutil" 46 | 47 | "github.com/h2non/filetype" 48 | ) 49 | 50 | func main() { 51 | buf, _ := ioutil.ReadFile("sample.jpg") 52 | 53 | kind, _ := filetype.Match(buf) 54 | if kind == filetype.Unknown { 55 | fmt.Println("Unknown file type") 56 | return 57 | } 58 | 59 | fmt.Printf("File type: %s. MIME: %s\n", kind.Extension, kind.MIME.Value) 60 | } 61 | ``` 62 | 63 | #### Check type class 64 | 65 | ```go 66 | package main 67 | 68 | import ( 69 | "fmt" 70 | "io/ioutil" 71 | 72 | "github.com/h2non/filetype" 73 | ) 74 | 75 | func main() { 76 | buf, _ := ioutil.ReadFile("sample.jpg") 77 | 78 | if filetype.IsImage(buf) { 79 | fmt.Println("File is an image") 80 | } else { 81 | fmt.Println("Not an image") 82 | } 83 | } 84 | ``` 85 | 86 | #### Supported type 87 | 88 | ```go 89 | package main 90 | 91 | import ( 92 | "fmt" 93 | 94 | "github.com/h2non/filetype" 95 | ) 96 | 97 | func main() { 98 | // Check if file is supported by extension 99 | if filetype.IsSupported("jpg") { 100 | fmt.Println("Extension supported") 101 | } else { 102 | fmt.Println("Extension not supported") 103 | } 104 | 105 | // Check if file is supported by mime type 106 | if filetype.IsMIMESupported("image/jpeg") { 107 | fmt.Println("MIME type supported") 108 | } else { 109 | fmt.Println("MIME type not supported") 110 | } 111 | } 112 | ``` 113 | 114 | #### File header 115 | 116 | ```go 117 | package main 118 | 119 | import ( 120 | "fmt" 121 | "os" 122 | 123 | "github.com/h2non/filetype" 124 | ) 125 | 126 | func main() { 127 | // Open a file descriptor 128 | file, _ := os.Open("movie.mp4") 129 | 130 | // We only have to pass the file header = first 261 bytes 131 | head := make([]byte, 261) 132 | file.Read(head) 133 | 134 | if filetype.IsImage(head) { 135 | fmt.Println("File is an image") 136 | } else { 137 | fmt.Println("Not an image") 138 | } 139 | } 140 | ``` 141 | 142 | #### Add additional file type matchers 143 | 144 | ```go 145 | package main 146 | 147 | import ( 148 | "fmt" 149 | 150 | "github.com/h2non/filetype" 151 | ) 152 | 153 | var fooType = filetype.NewType("foo", "foo/foo") 154 | 155 | func fooMatcher(buf []byte) bool { 156 | return len(buf) > 1 && buf[0] == 0x01 && buf[1] == 0x02 157 | } 158 | 159 | func main() { 160 | // Register the new matcher and its type 161 | filetype.AddMatcher(fooType, fooMatcher) 162 | 163 | // Check if the new type is supported by extension 164 | if filetype.IsSupported("foo") { 165 | fmt.Println("New supported type: foo") 166 | } 167 | 168 | // Check if the new type is supported by MIME 169 | if filetype.IsMIMESupported("foo/foo") { 170 | fmt.Println("New supported MIME type: foo/foo") 171 | } 172 | 173 | // Try to match the file 174 | fooFile := []byte{0x01, 0x02} 175 | kind, _ := filetype.Match(fooFile) 176 | if kind == filetype.Unknown { 177 | fmt.Println("Unknown file type") 178 | } else { 179 | fmt.Printf("File type matched: %s\n", kind.Extension) 180 | } 181 | } 182 | ``` 183 | 184 | ## Supported types 185 | 186 | #### Image 187 | 188 | - **jpg** - `image/jpeg` 189 | - **png** - `image/png` 190 | - **gif** - `image/gif` 191 | - **webp** - `image/webp` 192 | - **cr2** - `image/x-canon-cr2` 193 | - **tif** - `image/tiff` 194 | - **bmp** - `image/bmp` 195 | - **heif** - `image/heif` 196 | - **jxr** - `image/vnd.ms-photo` 197 | - **psd** - `image/vnd.adobe.photoshop` 198 | - **ico** - `image/vnd.microsoft.icon` 199 | - **dwg** - `image/vnd.dwg` 200 | - **avif** - `image/avif` 201 | 202 | #### Video 203 | 204 | - **mp4** - `video/mp4` 205 | - **m4v** - `video/x-m4v` 206 | - **mkv** - `video/x-matroska` 207 | - **webm** - `video/webm` 208 | - **mov** - `video/quicktime` 209 | - **avi** - `video/x-msvideo` 210 | - **wmv** - `video/x-ms-wmv` 211 | - **mpg** - `video/mpeg` 212 | - **flv** - `video/x-flv` 213 | - **3gp** - `video/3gpp` 214 | 215 | #### Audio 216 | 217 | - **mid** - `audio/midi` 218 | - **mp3** - `audio/mpeg` 219 | - **m4a** - `audio/mp4` 220 | - **ogg** - `audio/ogg` 221 | - **flac** - `audio/x-flac` 222 | - **wav** - `audio/x-wav` 223 | - **amr** - `audio/amr` 224 | - **aac** - `audio/aac` 225 | - **aiff** - `audio/x-aiff` 226 | 227 | #### Archive 228 | 229 | - **epub** - `application/epub+zip` 230 | - **zip** - `application/zip` 231 | - **tar** - `application/x-tar` 232 | - **rar** - `application/vnd.rar` 233 | - **gz** - `application/gzip` 234 | - **bz2** - `application/x-bzip2` 235 | - **7z** - `application/x-7z-compressed` 236 | - **xz** - `application/x-xz` 237 | - **zstd** - `application/zstd` 238 | - **pdf** - `application/pdf` 239 | - **exe** - `application/vnd.microsoft.portable-executable` 240 | - **swf** - `application/x-shockwave-flash` 241 | - **rtf** - `application/rtf` 242 | - **iso** - `application/x-iso9660-image` 243 | - **eot** - `application/octet-stream` 244 | - **ps** - `application/postscript` 245 | - **sqlite** - `application/vnd.sqlite3` 246 | - **nes** - `application/x-nintendo-nes-rom` 247 | - **crx** - `application/x-google-chrome-extension` 248 | - **cab** - `application/vnd.ms-cab-compressed` 249 | - **deb** - `application/vnd.debian.binary-package` 250 | - **ar** - `application/x-unix-archive` 251 | - **Z** - `application/x-compress` 252 | - **lz** - `application/x-lzip` 253 | - **rpm** - `application/x-rpm` 254 | - **elf** - `application/x-executable` 255 | - **dcm** - `application/dicom` 256 | 257 | #### Documents 258 | 259 | - **doc** - `application/msword` 260 | - **docx** - `application/vnd.openxmlformats-officedocument.wordprocessingml.document` 261 | - **xls** - `application/vnd.ms-excel` 262 | - **xlsx** - `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` 263 | - **ppt** - `application/vnd.ms-powerpoint` 264 | - **pptx** - `application/vnd.openxmlformats-officedocument.presentationml.presentation` 265 | 266 | #### Font 267 | 268 | - **woff** - `application/font-woff` 269 | - **woff2** - `application/font-woff` 270 | - **ttf** - `application/font-sfnt` 271 | - **otf** - `application/font-sfnt` 272 | 273 | #### Application 274 | 275 | - **wasm** - `application/wasm` 276 | - **dex** - `application/vnd.android.dex` 277 | - **dey** - `application/vnd.android.dey` 278 | - **parquet** - `application/vnd.apache.parquet` 279 | 280 | ## Benchmarks 281 | 282 | Measured using [real files](https://github.com/h2non/filetype/tree/master/fixtures). 283 | 284 | Environment: OSX x64 i7 2.7 Ghz 285 | 286 | ```bash 287 | BenchmarkMatchTar-8 1000000 1083 ns/op 288 | BenchmarkMatchZip-8 1000000 1162 ns/op 289 | BenchmarkMatchJpeg-8 1000000 1280 ns/op 290 | BenchmarkMatchGif-8 1000000 1315 ns/op 291 | BenchmarkMatchPng-8 1000000 1121 ns/op 292 | ``` 293 | 294 | ## License 295 | 296 | MIT - Tomas Aparicio 297 | --------------------------------------------------------------------------------