diff --git a/.travis.yml b/.travis.yml index 4444bd4..5957bf5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go +script: go test ./... go: - 1.5 - 1.4 - 1.3 - - release - tip diff --git a/README.md b/README.md index 6f86951..3c3353e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ # filetype [![Build Status](https://travis-ci.org/h2non/filetype.png)](https://travis-ci.org/h2non/filetype) [![GoDoc](https://godoc.org/github.com/h2non/filetype?status.svg)](https://godoc.org/github.com/h2non/filetype) -Small [Go](https://golang.org) package to infer the file type checking the [magic number](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) of a given binary buffer. +Small [Go](https://golang.org) package to infer the file and MIME type checking the [magic numbers](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) signature. -Supports a wide range of file types, including images formats, fonts, videos, audio and other common application files, and provides the proper file extension and convenient MIME code. +## Features + +- Supports a [wide range](#supported-types) of file types +- Provides file extension and proper MIME type +- File discovery by extension or MIME type +- File discovery by class (image, video, audio...) +- Bunch of helpers and shortcuts for easy file checking +- Pluggable by default: plug in new types and file matchers +- Simple and semantic API ## Installation @@ -10,31 +18,192 @@ Supports a wide range of file types, including images formats, fonts, videos, au go get gopkg.in/h2non/filetype.v0 ``` -## Usage +## Usage ```go +import "gopkg.in/h2non/filetype.v0" +``` + +## API + +See [Godoc](https://godoc.org/github.com/h2non/filetype) reference. + +## Examples + +#### Simple file type checking + +```go +package main + import ( "fmt" - "io/ioutil" "gopkg.in/h2non/filetype.v0" + "io/ioutil" ) func main() { buf, _ := ioutil.ReadFile("sample.jpg") - kind, unkwown := filetype.Type(buf) + kind, unkwown := filetype.Match(buf) if unkwown != nil { - fmt.Printf("Unkwown file type") + fmt.Printf("Unkwown: %s", unkwown) return } - fmt.Printf("File type found: %s. MIME: %s", kind.Extension, kind.MIME.Value) + fmt.Printf("File type: %s. MIME: %s\n", kind.Extension, kind.MIME.Value) } ``` -## API +#### Check type class + +```go +package main + +import ( + "fmt" + "gopkg.in/h2non/filetype.v0" + "io/ioutil" +) + +func main() { + buf, _ := ioutil.ReadFile("sample.jpg") + + if filetype.IsImage(buf) { + fmt.Println("Image file") + } else { + fmt.Println("Not an image") + } +} +``` + +#### Supported type + +```go +package main + +import ( + "fmt" + "gopkg.in/h2non/filetype.v0" +) + +func main() { + // Check if file is supported by extension + if filetype.IsSupported("jpg") { + fmt.Println("Extension supported") + } else { + fmt.Println("Extension not supported") + } + + // Check if file is supported by extension + if filetype.IsMIMESupported("image/jpeg") { + fmt.Println("MIME type supported") + } else { + fmt.Println("MIME type not supported") + } +} +``` + +#### Add additional file type matchers + +```go +package main + +import ( + "fmt" + "gopkg.in/h2non/filetype.v0" +) + +var fooType = filetype.NewType("foo", "foo/foo") + +func fooMatcher(buf []byte, length int) bool { + return length > 1 && buf[0] == 0x01 && buf[1] == 0x02 +} + +func main() { + // Register the new matcher and its type + filetype.AddMatcher(fooType, fooMatcher) + + // Check if the new type is supported by extension + if filetype.IsSupported("foo") { + fmt.Println("Suppored type: foo") + } + + // Check if the new type is supported by MIME + if filetype.IsMIMESupported("foo/foo") { + fmt.Println("Suppored type: foo/foo") + } + + // Try to match the file + fooFile := []byte{0x01, 0x02} + kind, _ := filetype.Match(fooFile) + if kind == filetype.Unknown { + fmt.Println("Unknown file type") + } else { + fmt.Printf("File type matched: %s\n", kind.Extension) + } +} +``` + +## Supported types + +#### Image + +- **jpg** - `image/jpeg` +- **png** - `image/png` +- **gif** - `image/gif` +- **webp** - `image/webp` +- **cr2** - `image/x-canon-cr2` +- **tif** - `image/tiff` +- **bmp** - `image/bmp` +- **jxr** - `image/vnd.ms-photo` +- **psd** - `image/vnd.adobe.photoshop` +- **ico** - `image/x-icon` + +#### Video + +- **mp4** - `video/mp4` +- **m4v** - `video/x-m4v` +- **mkv** - `video/x-matroska` +- **webm** - `video/webm` +- **mov** - `video/quicktime` +- **avi** - `video/x-msvideo` +- **wmv** - `video/x-ms-wmv` +- **mpg** - `video/mpeg` +- **flv** - `video/x-flv` + +#### Audio + +- **mid** - `audio/midi` +- **mp3** - `audio/mpeg` +- **m4a** - `audio/m4a` +- **ogg** - `audio/ogg` +- **flac** - `audio/x-flac` +- **wav** - `audio/x-wav` + +#### Archive + +- **epub** - `application/epub+zip` +- **zip** - `application/zip` +- **tar** - `application/x-tar` +- **rar** - `application/x-rar-compressed` +- **gz** - `application/gzip` +- **bz2** - `application/x-bzip2` +- **7z** - `application/x-7z-compressed` +- **xz** - `application/x-xz` +- **pdf** - `application/pdf` +- **exe** - `application/x-msdownload` +- **swf** - `application/x-shockwave-flash` +- **rtf** - `application/rtf` +- **eot** - `application/octet-stream` +- **ps** - `application/postscript` +- **sqlite** - `application/x-sqlite3` +#### Font +- **woff** - `application/font-woff` +- **woff2** - `application/font-woff` +- **ttf** - `application/font-sfnt` +- **otf** - `application/font-sfnt` ## License diff --git a/filetype.go b/filetype.go index 41ea0d1..1cd4158 100644 --- a/filetype.go +++ b/filetype.go @@ -12,14 +12,19 @@ var Types = types.Types // Create and register a new type var NewType = types.NewType -// Default types -var Empty = types.Empty +// Default unknown file type var Unknown = types.Unknown // Predefined errors var EmptyBufferErr = errors.New("Empty buffer") var UnknownBufferErr = errors.New("Unknown buffer type") +// Register a new file type +func AddType(ext, mime string) types.Type { + return types.NewType(ext, mime) +} + +// Checks if a given buffer matches with the given file type extension func Is(buf []byte, ext string) bool { kind, ok := types.Types[ext] if ok { @@ -28,24 +33,29 @@ func Is(buf []byte, ext string) bool { return false } +// Semantic alias to Is() +func IsExtension(buf []byte, ext string) bool { + return Is(buf, ext) +} + +// Checks if a given buffer matches with the given file type func IsType(buf []byte, kind types.Type) bool { matcher := matchers.Matchers[kind] if matcher == nil { return false } - - length := len(buf) - return matcher(buf, length) != types.Unknown -} - -// Register a new matcher type -func AddMatcher(fileType types.Type, matcher matchers.Matcher) matchers.TypeMatcher { - return matchers.NewMatcher(fileType, matcher) + return matcher(buf, len(buf)) != types.Unknown } -// Register a new file type -func AddType(ext, mime string) types.Type { - return types.NewType(ext, mime) +// Checks if a given buffer matches with the given MIME type +func IsMIME(buf []byte, mime string) bool { + for _, kind := range types.Types { + if kind.MIME.Value == mime { + matcher := matchers.Matchers[kind] + return matcher(buf, len(buf)) != types.Unknown + } + } + return false } // Check if a given file extension is supported @@ -58,7 +68,7 @@ func IsSupported(ext string) bool { return false } -// Check if a given MIME expression is supported +// Check if a given MIME type is supported func IsMIMESupported(mime string) bool { for _, m := range Types { if m.MIME.Value == mime { @@ -67,3 +77,8 @@ func IsMIMESupported(mime string) bool { } return false } + +// Retrieve a Type by file extension +func GetType(ext string) types.Type { + return types.Get(ext) +} diff --git a/filetype_test.go b/filetype_test.go index 24265be..2446313 100644 --- a/filetype_test.go +++ b/filetype_test.go @@ -1,25 +1,122 @@ package filetype import ( + "gopkg.in/h2non/filetype.v0/types" "testing" ) -func TestMatches(t *testing.T) { +func TestIs(t *testing.T) { cases := []struct { - buf []byte - ext string + buf []byte + ext string + match bool }{ - {[]byte{0xFF, 0xD8, 0xFF}, "jpg"}, + {[]byte{0xFF, 0xD8, 0xFF}, "jpg", true}, + {[]byte{0xFF, 0xD8, 0x00}, "jpg", false}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, "png", true}, } for _, test := range cases { - match, err := Match(test.buf) - if err != nil { - t.Fatalf("Error: %s", err) + if Is(test.buf, test.ext) != test.match { + t.Fatalf("Invalid match: %s", test.ext) } + } +} + +func TestIsType(t *testing.T) { + cases := []struct { + buf []byte + kind types.Type + match bool + }{ + {[]byte{0xFF, 0xD8, 0xFF}, types.Get("jpg"), true}, + {[]byte{0xFF, 0xD8, 0x00}, types.Get("jpg"), false}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, types.Get("png"), true}, + } - if match.Extension != test.ext { - t.Fatalf("Invalid image type: %s", match.Extension) + for _, test := range cases { + if IsType(test.buf, test.kind) != test.match { + t.Fatalf("Invalid match: %s", test.kind.Extension) } } } + +func TestIsMIME(t *testing.T) { + cases := []struct { + buf []byte + mime string + match bool + }{ + {[]byte{0xFF, 0xD8, 0xFF}, "image/jpeg", true}, + {[]byte{0xFF, 0xD8, 0x00}, "image/jpeg", false}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, "image/png", true}, + } + + for _, test := range cases { + if IsMIME(test.buf, test.mime) != test.match { + t.Fatalf("Invalid match: %s", test.mime) + } + } +} + +func TestIsSupported(t *testing.T) { + cases := []struct { + ext string + match bool + }{ + {"jpg", true}, + {"jpeg", false}, + {"abc", false}, + {"png", true}, + {"mp4", true}, + {"", false}, + } + + for _, test := range cases { + if IsSupported(test.ext) != test.match { + t.Fatalf("Invalid match: %s", test.ext) + } + } +} + +func TestIsMIMESupported(t *testing.T) { + cases := []struct { + mime string + match bool + }{ + {"image/jpeg", true}, + {"foo/bar", false}, + {"image/png", true}, + {"video/mpeg", true}, + } + + for _, test := range cases { + if IsMIMESupported(test.mime) != test.match { + t.Fatalf("Invalid match: %s", test.mime) + } + } +} + +func TestAddType(t *testing.T) { + AddType("foo", "foo/foo") + + if !IsSupported("foo") { + t.Fatalf("Not supported extension") + } + + if !IsMIMESupported("foo/foo") { + t.Fatalf("Not supported MIME type") + } +} + +func TestGetType(t *testing.T) { + jpg := GetType("jpg") + if jpg == types.Unknown { + t.Fatalf("Type should be supported") + } + + invalid := GetType("invalid") + if invalid != Unknown { + t.Fatalf("Type should not be supported") + } +} diff --git a/kind.go b/kind.go index 558045a..4daa45e 100644 --- a/kind.go +++ b/kind.go @@ -5,7 +5,7 @@ import ( "gopkg.in/h2non/filetype.v0/types" ) -// Match file as image type +// Try to match a file as image type func Image(buf []byte) (types.Type, error) { return doMatchMap(buf, matchers.Image) } @@ -16,7 +16,7 @@ func IsImage(buf []byte) bool { return kind != types.Unknown } -// Match file as audio type +// Try to match a file as audio type func Audio(buf []byte) (types.Type, error) { return doMatchMap(buf, matchers.Audio) } @@ -27,7 +27,7 @@ func IsAudio(buf []byte) bool { return kind != types.Unknown } -// Match file as video type +// Try to match a file as video type func Video(buf []byte) (types.Type, error) { return doMatchMap(buf, matchers.Video) } @@ -38,7 +38,7 @@ func IsVideo(buf []byte) bool { return kind != types.Unknown } -// Match file as text font type +// Try to match a file as text font type func Font(buf []byte) (types.Type, error) { return doMatchMap(buf, matchers.Font) } @@ -49,7 +49,7 @@ func IsFont(buf []byte) bool { return kind != types.Unknown } -// Match file as generic archive type +// Try to match a file as generic archive type func Archive(buf []byte) (types.Type, error) { return doMatchMap(buf, matchers.Archive) } diff --git a/kind_test.go b/kind_test.go index 676e066..ec3d903 100644 --- a/kind_test.go +++ b/kind_test.go @@ -1 +1,40 @@ package filetype + +import ( + "testing" +) + +func TestKind(t *testing.T) { + var cases = []struct { + buf []byte + ext string + }{ + {[]byte{0xFF, 0xD8, 0xFF}, "jpg"}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, "png"}, + {[]byte{0x89, 0x0, 0x0}, "unknown"}, + } + + for _, test := range cases { + kind, _ := Image(test.buf) + if kind.Extension != test.ext { + t.Fatalf("Invalid match: %s != %s", kind.Extension, test.ext) + } + } +} + +func TestIsKind(t *testing.T) { + var cases = []struct { + buf []byte + match bool + }{ + {[]byte{0xFF, 0xD8, 0xFF}, true}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, true}, + {[]byte{0x89, 0x0, 0x0}, false}, + } + + for _, test := range cases { + if IsImage(test.buf) != test.match { + t.Fatalf("Invalid match: %s", test.match) + } + } +} diff --git a/match.go b/match.go index a913010..4d2f09a 100644 --- a/match.go +++ b/match.go @@ -15,7 +15,7 @@ var NewMatcher = matchers.NewMatcher func Match(buf []byte) (types.Type, error) { length := len(buf) if length == 0 { - return types.Empty, nil + return types.Unknown, EmptyBufferErr } for _, checker := range Matchers { @@ -33,6 +33,11 @@ func Get(buf []byte) (types.Type, error) { return Match(buf) } +// Register a new matcher type +func AddMatcher(fileType types.Type, matcher matchers.Matcher) matchers.TypeMatcher { + return matchers.NewMatcher(fileType, matcher) +} + // Checks if the given buffer matches with some supported file type func Matches(buf []byte) bool { kind, _ := Match(buf) diff --git a/match_test.go b/match_test.go index 676e066..7a26e92 100644 --- a/match_test.go +++ b/match_test.go @@ -1 +1,100 @@ package filetype + +import ( + "gopkg.in/h2non/filetype.v0/matchers" + "gopkg.in/h2non/filetype.v0/types" + "testing" +) + +func TestMatch(t *testing.T) { + cases := []struct { + buf []byte + ext string + }{ + {[]byte{0xFF, 0xD8, 0xFF}, "jpg"}, + {[]byte{0xFF, 0xD8, 0x00}, "unknown"}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, "png"}, + } + + for _, test := range cases { + match, err := Match(test.buf) + if err != nil { + t.Fatalf("Error: %s", err) + } + + if match.Extension != test.ext { + t.Fatalf("Invalid image type: %s", match.Extension) + } + } +} + +func TestMatches(t *testing.T) { + cases := []struct { + buf []byte + match bool + }{ + {[]byte{0xFF, 0xD8, 0xFF}, true}, + {[]byte{0xFF, 0x0, 0x0}, false}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, true}, + } + + for _, test := range cases { + if Matches(test.buf) != test.match { + t.Fatalf("Do not matches: %#v", test.buf) + } + } +} + +func TestAddMatcher(t *testing.T) { + fileType := AddType("foo", "foo/foo") + + AddMatcher(fileType, func(buf []byte, l int) bool { + return l == 2 && buf[0] == 0x00 && buf[1] == 0x00 + }) + + if !Is([]byte{0x00, 0x00}, "foo") { + t.Fatalf("Type cannot match") + } + + if !IsSupported("foo") { + t.Fatalf("Not supported extension") + } + + if !IsMIMESupported("foo/foo") { + t.Fatalf("Not supported MIME type") + } +} + +func TestMatchMap(t *testing.T) { + cases := []struct { + buf []byte + kind types.Type + }{ + {[]byte{0xFF, 0xD8, 0xFF}, types.Get("jpg")}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, types.Get("png")}, + {[]byte{0xFF, 0x0, 0x0}, Unknown}, + } + + for _, test := range cases { + if kind := MatchMap(test.buf, matchers.Image); kind != test.kind { + t.Fatalf("Do not matches: %#v", test.buf) + } + } +} + +func TestMatchesMap(t *testing.T) { + cases := []struct { + buf []byte + match bool + }{ + {[]byte{0xFF, 0xD8, 0xFF}, true}, + {[]byte{0x89, 0x50, 0x4E, 0x47}, true}, + {[]byte{0xFF, 0x0, 0x0}, false}, + } + + for _, test := range cases { + if match := MatchesMap(test.buf, matchers.Image); match != test.match { + t.Fatalf("Do not matches: %#v", test.buf) + } + } +} diff --git a/matchers/matchers.go b/matchers/matchers.go index 384d6cb..d12e3b7 100644 --- a/matchers/matchers.go +++ b/matchers/matchers.go @@ -39,5 +39,6 @@ func register(matchers ...Map) { } func init() { + // Arguments order is intentional register(Image, Video, Audio, Font, Archive) } diff --git a/matchers/matchers_test.go b/matchers/matchers_test.go deleted file mode 100644 index c103992..0000000 --- a/matchers/matchers_test.go +++ /dev/null @@ -1 +0,0 @@ -package matchers diff --git a/types/defaults.go b/types/defaults.go index b3dc36c..efced4e 100644 --- a/types/defaults.go +++ b/types/defaults.go @@ -1,4 +1,3 @@ package types -var Empty = NewType("", "") var Unknown = NewType("unknown", "") diff --git a/types/mime.go b/types/mime.go index ca95110..511899a 100644 --- a/types/mime.go +++ b/types/mime.go @@ -6,6 +6,7 @@ type MIME struct { Value string } +// Creates a new MIME type func NewMIME(mime string) MIME { kind, subtype := splitMime(mime) return MIME{Type: kind, Subtype: subtype, Value: mime} diff --git a/types/split_test.go b/types/split_test.go index ab1254f..b051457 100644 --- a/types/split_test.go +++ b/types/split_test.go @@ -1 +1,27 @@ package types + +import "testing" + +func TestSplit(t *testing.T) { + cases := []struct { + mime string + kind string + subtype string + }{ + {"image/jpeg", "image", "jpeg"}, + {"/jpeg", "", "jpeg"}, + {"image/", "image", ""}, + {"/", "", ""}, + {"image", "image", ""}, + } + + for _, test := range cases { + kind, subtype := splitMime(test.mime) + if test.kind != kind { + t.Fatalf("Invalid kind: %s", test.kind) + } + if test.subtype != subtype { + t.Fatalf("Invalid subtype: %s", test.subtype) + } + } +} diff --git a/types/types.go b/types/types.go index 97e347e..8d5a317 100644 --- a/types/types.go +++ b/types/types.go @@ -2,7 +2,17 @@ package types var Types = make(map[string]Type) +// Register a new type func Add(t Type) Type { Types[t.Extension] = t return t } + +// Retrieve a Type by extension +func Get(ext string) Type { + kind := Types[ext] + if kind.Extension != "" { + return kind + } + return Unknown +}