From 56ba9d089cb9441af65caf4cd534f1aec1726cbc Mon Sep 17 00:00:00 2001 From: Andrew Stucki Date: Tue, 5 May 2020 15:26:48 -0400 Subject: [PATCH] [Auditbeat] File Integrity ECS update (#18012) * Add extension, mime_type and drive_letter * run go mod vendor * Add ECS categorization fields * mage fmt * Update hash and NOTICE * Update test * Run mage vendor * Add changelog entry * Extract isASCIILetter * switch over to godoc style comment --- CHANGELOG.next.asciidoc | 2 + NOTICE.txt | 31 ++ auditbeat/module/file_integrity/action.go | 39 +++ auditbeat/module/file_integrity/event.go | 39 ++- auditbeat/module/file_integrity/event_test.go | 48 ++- auditbeat/module/file_integrity/mime.go | 53 ++++ auditbeat/module/file_integrity/mime_test.go | 68 ++++ go.mod | 1 + go.sum | 3 + .../github.com/h2non/filetype/.editorconfig | 12 + vendor/github.com/h2non/filetype/.gitignore | 2 + vendor/github.com/h2non/filetype/.travis.yml | 17 + vendor/github.com/h2non/filetype/History.md | 128 ++++++++ vendor/github.com/h2non/filetype/LICENSE | 24 ++ vendor/github.com/h2non/filetype/README.md | 290 ++++++++++++++++++ vendor/github.com/h2non/filetype/filetype.go | 87 ++++++ vendor/github.com/h2non/filetype/go.mod | 3 + vendor/github.com/h2non/filetype/kind.go | 80 +++++ vendor/github.com/h2non/filetype/match.go | 90 ++++++ .../h2non/filetype/matchers/application.go | 20 ++ .../h2non/filetype/matchers/archive.go | 234 ++++++++++++++ .../h2non/filetype/matchers/audio.go | 75 +++++ .../h2non/filetype/matchers/document.go | 184 +++++++++++ .../h2non/filetype/matchers/font.go | 45 +++ .../h2non/filetype/matchers/image.go | 143 +++++++++ .../filetype/matchers/isobmff/isobmff.go | 37 +++ .../h2non/filetype/matchers/matchers.go | 51 +++ .../h2non/filetype/matchers/video.go | 145 +++++++++ .../h2non/filetype/types/defaults.go | 4 + .../github.com/h2non/filetype/types/mime.go | 14 + .../github.com/h2non/filetype/types/split.go | 11 + .../github.com/h2non/filetype/types/type.go | 16 + .../github.com/h2non/filetype/types/types.go | 18 ++ vendor/github.com/h2non/filetype/version.go | 4 + vendor/modules.txt | 5 + 35 files changed, 2020 insertions(+), 3 deletions(-) create mode 100644 auditbeat/module/file_integrity/mime.go create mode 100644 auditbeat/module/file_integrity/mime_test.go create mode 100644 vendor/github.com/h2non/filetype/.editorconfig create mode 100644 vendor/github.com/h2non/filetype/.gitignore create mode 100644 vendor/github.com/h2non/filetype/.travis.yml create mode 100644 vendor/github.com/h2non/filetype/History.md create mode 100644 vendor/github.com/h2non/filetype/LICENSE create mode 100644 vendor/github.com/h2non/filetype/README.md create mode 100644 vendor/github.com/h2non/filetype/filetype.go create mode 100644 vendor/github.com/h2non/filetype/go.mod create mode 100644 vendor/github.com/h2non/filetype/kind.go create mode 100644 vendor/github.com/h2non/filetype/match.go create mode 100644 vendor/github.com/h2non/filetype/matchers/application.go create mode 100644 vendor/github.com/h2non/filetype/matchers/archive.go create mode 100644 vendor/github.com/h2non/filetype/matchers/audio.go create mode 100644 vendor/github.com/h2non/filetype/matchers/document.go create mode 100644 vendor/github.com/h2non/filetype/matchers/font.go create mode 100644 vendor/github.com/h2non/filetype/matchers/image.go create mode 100644 vendor/github.com/h2non/filetype/matchers/isobmff/isobmff.go create mode 100644 vendor/github.com/h2non/filetype/matchers/matchers.go create mode 100644 vendor/github.com/h2non/filetype/matchers/video.go create mode 100644 vendor/github.com/h2non/filetype/types/defaults.go create mode 100644 vendor/github.com/h2non/filetype/types/mime.go create mode 100644 vendor/github.com/h2non/filetype/types/split.go create mode 100644 vendor/github.com/h2non/filetype/types/type.go create mode 100644 vendor/github.com/h2non/filetype/types/types.go create mode 100644 vendor/github.com/h2non/filetype/version.go diff --git a/CHANGELOG.next.asciidoc b/CHANGELOG.next.asciidoc index d4458bd881b..27b1701f875 100644 --- a/CHANGELOG.next.asciidoc +++ b/CHANGELOG.next.asciidoc @@ -233,6 +233,8 @@ https://github.com/elastic/beats/compare/v7.0.0-alpha2...master[Check the HEAD d - Add system module package dataset ECS categorization fields. {pull}18033[18033] - Add system module login dataset ECS categorization fields. {pull}18034[18034] - Add system module user dataset ECS categorization fields. {pull}18035[18035] +- Add file integrity module ECS categorization fields. {pull}18012[18012] +- Add `file.mime_type`, `file.extension`, and `file.drive_letter` for file integrity module. {pull}18012[18012] *Filebeat* diff --git a/NOTICE.txt b/NOTICE.txt index 841ae02c45f..41b3e588a89 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -3261,6 +3261,37 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------- +Dependency: github.com/h2non/filetype +Version: v1.0.12 +License type (autodetected): MIT +./vendor/github.com/h2non/filetype/LICENSE: +-------------------------------------------------------------------- +The MIT License + +Copyright (c) Tomas Aparicio + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + -------------------------------------------------------------------- Dependency: github.com/hashicorp/errwrap Version: v1.0.0 diff --git a/auditbeat/module/file_integrity/action.go b/auditbeat/module/file_integrity/action.go index 8f32b2884c5..dbdd531b7ec 100644 --- a/auditbeat/module/file_integrity/action.go +++ b/auditbeat/module/file_integrity/action.go @@ -20,6 +20,8 @@ package file_integrity import ( "math/bits" "strings" + + "github.com/elastic/beats/v7/libbeat/common" ) // Action is a description of the changes described by an event. @@ -51,6 +53,17 @@ var actionNames = map[Action]string{ InitialScan: "initial_scan", } +var ecsActionNames = map[Action]string{ + None: "info", + AttributesModified: "change", + Created: "creation", + Deleted: "deletion", + Updated: "change", + Moved: "change", + ConfigChange: "change", + InitialScan: "info", +} + type actionOrderKey struct { ExistsBefore, ExistsNow bool Action Action @@ -102,6 +115,22 @@ func (action Action) String() string { return strings.Join(list, "|") } +// ECSTypes returns the ECS categorization types associated with the +// particular action. +func (action Action) ECSTypes() []string { + if name, found := ecsActionNames[action]; found { + return []string{name} + } + var list []string + for flag, name := range ecsActionNames { + if action&flag != 0 { + action ^= flag + list = append(list, name) + } + } + return common.MakeStringSet(list...).ToSlice() +} + // MarshalText marshals the Action to a textual representation of itself. func (action Action) MarshalText() ([]byte, error) { return []byte(action.String()), nil } @@ -174,3 +203,13 @@ func (actions ActionArray) StringArray() []string { } return result } + +// ECSTypes returns the array of ECS categorization types for +// the set of actions. +func (actions ActionArray) ECSTypes() []string { + var list []string + for _, action := range actions { + list = append(list, action.ECSTypes()...) + } + return common.MakeStringSet(list...).ToSlice() +} diff --git a/auditbeat/module/file_integrity/event.go b/auditbeat/module/file_integrity/event.go index 61e9b214819..41e4d5a3795 100644 --- a/auditbeat/module/file_integrity/event.go +++ b/auditbeat/module/file_integrity/event.go @@ -31,6 +31,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "time" "github.com/cespare/xxhash/v2" @@ -214,6 +215,24 @@ func NewEvent( return NewEventFromFileInfo(path, info, err, action, source, maxFileSize, hashTypes) } +func isASCIILetter(letter byte) bool { + // It appears that Windows only allows ascii characters for drive letters + // and that's what go checks for: https://golang.org/src/path/filepath/path_windows.go#L63 + // **If** Windows/go ever return multibyte utf16 characters we'll need to change + // the drive letter mapping logic. + return (letter >= 'a' && letter <= 'z') || (letter >= 'A' && letter <= 'Z') +} + +func getDriveLetter(path string) string { + volume := filepath.VolumeName(path) + if len(volume) == 2 && volume[1] == ':' { + if isASCIILetter(volume[0]) { + return strings.ToUpper(volume[:1]) + } + } + return "" +} + func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event { file := common.MapStr{ "path": e.Path, @@ -237,6 +256,12 @@ func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event { file["ctime"] = info.CTime if e.Info.Type == FileType { + if extension := filepath.Ext(e.Path); extension != "" { + file["extension"] = extension + } + if mimeType := getMimeType(e.Path); mimeType != "" { + file["mime_type"] = mimeType + } file["size"] = info.Size } @@ -245,6 +270,9 @@ func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event { } if runtime.GOOS == "windows" { + if drive := getDriveLetter(e.Path); drive != "" { + file["drive_letter"] = drive + } if info.SID != "" { file["uid"] = info.SID } @@ -276,12 +304,19 @@ func buildMetricbeatEvent(e *Event, existedBefore bool) mb.Event { for hashType, digest := range e.Hashes { hashes[string(hashType)] = digest } + file["hash"] = hashes + // Remove this for 8.x out.MetricSetFields.Put("hash", hashes) } + out.MetricSetFields.Put("event.kind", "event") + out.MetricSetFields.Put("event.category", []string{"file"}) if e.Action > 0 { - actions := e.Action.InOrder(existedBefore, e.Info != nil).StringArray() - out.MetricSetFields.Put("event.action", actions) + actions := e.Action.InOrder(existedBefore, e.Info != nil) + out.MetricSetFields.Put("event.type", actions.ECSTypes()) + out.MetricSetFields.Put("event.action", actions.StringArray()) + } else { + out.MetricSetFields.Put("event.type", None.ECSTypes()) } return out diff --git a/auditbeat/module/file_integrity/event_test.go b/auditbeat/module/file_integrity/event_test.go index 1d20f1b3965..79d1309903d 100644 --- a/auditbeat/module/file_integrity/event_test.go +++ b/auditbeat/module/file_integrity/event_test.go @@ -37,7 +37,7 @@ var testEventTime = time.Now().UTC() func testEvent() *Event { return &Event{ Timestamp: testEventTime, - Path: "/home/user", + Path: "/home/user/file.txt", Source: SourceScan, Action: ConfigChange, Info: &Metadata{ @@ -290,8 +290,12 @@ func TestBuildEvent(t *testing.T) { assert.Equal(t, testEventTime, e.Timestamp) assertHasKey(t, fields, "event.action") + assertHasKey(t, fields, "event.kind") + assertHasKey(t, fields, "event.category") + assertHasKey(t, fields, "event.type") assertHasKey(t, fields, "file.path") + assertHasKey(t, fields, "file.extension") assertHasKey(t, fields, "file.target_path") assertHasKey(t, fields, "file.inode") assertHasKey(t, fields, "file.uid") @@ -309,9 +313,51 @@ func TestBuildEvent(t *testing.T) { assertHasKey(t, fields, "file.mode") } + assertHasKey(t, fields, "file.hash.sha1") + assertHasKey(t, fields, "file.hash.sha256") + // Remove in 8.x assertHasKey(t, fields, "hash.sha1") assertHasKey(t, fields, "hash.sha256") }) + if runtime.GOOS == "windows" { + t.Run("drive letter", func(t *testing.T) { + e := testEvent() + e.Path = "c:\\Documents" + fields := buildMetricbeatEvent(e, false).MetricSetFields + value, err := fields.GetValue("file.drive_letter") + assert.NoError(t, err) + assert.Equal(t, "C", value) + }) + t.Run("no drive letter", func(t *testing.T) { + e := testEvent() + e.Path = "\\\\remote\\Documents" + fields := buildMetricbeatEvent(e, false).MetricSetFields + _, err := fields.GetValue("file.drive_letter") + assert.Error(t, err) + }) + } + t.Run("ecs categorization", func(t *testing.T) { + e := testEvent() + e.Action = ConfigChange + fields := buildMetricbeatEvent(e, false).MetricSetFields + types, err := fields.GetValue("event.type") + if err != nil { + t.Fatal(err) + } + ecsTypes, ok := types.([]string) + assert.True(t, ok) + assert.Equal(t, []string{"change"}, ecsTypes) + + e.Action = Action(Created | Updated | Deleted) + fields = buildMetricbeatEvent(e, false).MetricSetFields + types, err = fields.GetValue("event.type") + if err != nil { + t.Fatal(err) + } + ecsTypes, ok = types.([]string) + assert.True(t, ok) + assert.Equal(t, []string{"change", "creation", "deletion"}, ecsTypes) + }) t.Run("no setuid/setgid", func(t *testing.T) { e := testEvent() e.Info.SetGID = false diff --git a/auditbeat/module/file_integrity/mime.go b/auditbeat/module/file_integrity/mime.go new file mode 100644 index 00000000000..94dcdc60de5 --- /dev/null +++ b/auditbeat/module/file_integrity/mime.go @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package file_integrity + +import ( + "github.com/h2non/filetype" + + "github.com/elastic/beats/v7/libbeat/common/file" +) + +const ( + // Size for mime detection, office file + // detection requires ~8kb to detect properly + headerSize = 8192 +) + +// getMimeType does a best-effort to get the file type, if no +// filetype can be determined, it just returns an empty +// string +func getMimeType(path string) string { + f, err := file.ReadOpen(path) + if err != nil { + return "" + } + defer f.Close() + + head := make([]byte, headerSize) + n, err := f.Read(head) + if err != nil { + return "" + } + + kind, err := filetype.Match(head[:n]) + if err != nil { + return "" + } + return kind.MIME.Value +} diff --git a/auditbeat/module/file_integrity/mime_test.go b/auditbeat/module/file_integrity/mime_test.go new file mode 100644 index 00000000000..a4779c96fe8 --- /dev/null +++ b/auditbeat/module/file_integrity/mime_test.go @@ -0,0 +1,68 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package file_integrity + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +var mimeSamples = map[string][]byte{ + "docx": []byte("PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00word/numbering.xml\xa5\x93MN\xc30\x10\x85O\xc0\x1d\"\xef\xdb$\x15 \x145\xed\x82\n6\xec\x80\x03\xb8\x8e\x93X\xb5=\xd6\xd8I\xe8\xedq\x9b\xbfR$\x94\x86U\xe4\x8c\xdf\xf7\xc6\xe3\xe7\xf5\xf6K\u0260\xe6h\x05\xe8\x94\xc4\u02c8\x04\\3\u0204.R\xf2\xf9\xf1\xb2x\"\x81uTgT\x82\xe6)9rK\xb6\x9b\xbbu\x93\xe8J\xed9\xfa}\x81Gh\x9b(\x96\x92\xd29\x93\x84\xa1e%W\xd4.\xc1p\xed\x8b9\xa0\xa2\xce/\xb1\b\x15\xc5Ce\x16\f\x94\xa1N\xec\x85\x14\xee\x18\xae\xa2\xe8\x91t\x18HI\x85:\xe9\x10\v%\x18\x82\x85\u071d$\t\xe4\xb9`\xbc\xfb\xf4\n\x9c\xe2\xdbJv\xc0*\u0175;;\x86\u0225\xef\x01\xb4-\x85\xb1=M\u0365\xf9b\xd9C\xea\xbf\x0eQ+\xd9\xefk\xcc\x14\xb7\fi\xe3\xe7\xacdk\xd4\x00f\x06\x81qk\xfd\xdf][\x1c\x88q4a\x80'\u0120\x98\xd2\xc2O\u03fe\x13E\x85\x1e0\xa7t\\\x81\x06\xef\xa5\xf7\xee\x86vF\x8d\a\x19ga\xe5\x94F\xda\u049b\xd8#\xc5\xe3\xef.\xe8\x8cy^\ua358\x94\xe2+\x82W\xb9\n\x87@\xceA\xb0\x92\xa2\xeb\x01r\x0eA\x02;\xf0\xec\x99\xea\x9a\x0ea\u038aIq\xbe\"e\x82\x16H\xd5\x18R{\xd3\xcd\xc6\xd1U\\\xdeKj\xf8H+\xfeG{E\xa8\xcc\x18\xf7\xfb9\xb4\x8b\x17\x18?\xdc\x06X\xf5\x80p\xf3\rPK\a\bI\x13C\u007fh\x01\x00\x00=\x05\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00word/settings.xml\xa5\x95\xcdn\xdb0\f\u01df`\xef\x10\xe8\x9e\xf8\xa3I6\x18uzX\xb1\xed\xb0\x9e\xd2=\x00#\u0276\x10}A\x92\xe3\xe5\xed'\u01d6\u0564@\xe1f\xa7H\u007f\x92?\xd2\fM?>\xfd\x15|q\xa2\xc62%K\x94\xadR\xb4\xa0\x12+\xc2d]\xa2?\xaf?\x96\xdf\xd0\xc2:\x90\x04\xb8\x92\xb4Dgj\xd1\xd3\xee\xcbcWX\xea\x9c\xf7\xb2\vO\x90\xb6\x10\xb8D\x8ds\xbaH\x12\x8b\x1b*\xc0\xae\x94\xa6\xd2\x1b+e\x048\u007f5u\"\xc0\x1c[\xbd\xc4Jhp\xec\xc08s\xe7$O\xd3-\x1a1\xaaD\xad\x91\u0148X\n\x86\x8d\xb2\xaar}H\xa1\xaa\x8aa:\xfe\x84\b3'\xef\x10\xf2\xacp+\xa8t\x97\x8c\x89\xa1\xdc\u05e0\xa4m\x98\xb6\x81&\xee\xa5yc\x13 \xa7\x8f\x1e\xe2$x\xf0\xeb\xf4\x9cl\xc4@\xe7\x1b-\xf8\x90\xa8S\x86h\xa30\xb5\u05ab\u03c3q\"f\xe9\x8c\x06\xf6\x88)bN\t\xd79C%\x02\x98\x9c0\xfdp\u0700\xa6\xdc+\x9f{l\xda\x05\x15\x1f$\xf6\xc2\xf29\x85\f\xa6\xdf\xec`\xc0\x9c\xdfW\x01w\xf4\xf3m\xbcf\xb3\xa6\xf8\x86\xe0\xa3\\k\xa6\x81\xbc\a\x81\x1b0.\x00\xf8=\x04\xae\xf0\x91\x92\xef O0\r3\xa9g\x8d\xf3\r\x890\xa8\r\x888\xa4\xf6S\xffl\x96\u078c\u02fe\x01M#\xad\xfe?\xdaO\xa3Z\x1d\xc7}}\x0f\xed\xcd\x1b\x98m>\a\xc8\x03`\xe7W \xa1\x15\xb4\u073d\xc2a\xef\x94^t\xc5\t\xfc\x14\u007f\xcdS\x94\xf4\xe6a\xcb\xc5\xd3~\u0618\xc1/\xdb \u007f\x94 \xfc\x9bs\xb5\x10_\x14\xa1\xbd\xa95l~q}\xca\xe4*'7\xfb>\x88\xbe\x80\xd6C\xdaC\x9d\x95\x88\xb3\xbaqY\xcfw\xfeF\xfcB\xbe\\\x0eu>\xda\xf2\x8b-\x1fl\x97\v`\xec\xf7\x9c\xf7\x1e\x0fQ\u02c3\xf6\xc6\xef!h\x0fQ[\am\x1d\xb5M\xd06Q\xdb\x06m\xdbk\xcdYS\u00d9<\xfa6\x84c\xafW\x8as\xd5Q\xf2+\xda\xdfIc?\xc2Wj\xf7\x0fPK\a\b\x8e\xb3\u00e4\x05\x02\x00\x00\xea\x06\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00word/fontTable.xml\xa5\x94MN\xc30\x10\x85O\xc0\x1d\"\xef\u06e4\b\x10\x8a\x9aT\b\x04\x1bv\xc0\x01\x06\xc7I\xac\xda\x1ek\xec4\xf4\xf6\xb84?P$\x94\x86U\x94\x8c\xdf\xf7\xc6\xe3\x17\xaf7\x1fZE;AN\xa2\xc9\xd8j\x99\xb0H\x18\x8e\x854U\xc6\xde^\x1f\x17\xb7,r\x1eL\x01\n\x8d\xc8\xd8^8\xb6\xc9/\xd6mZ\xa2\xf1.\nr\xe3R\xcd3V{o\xd38v\xbc\x16\x1a\xdc\x12\xad0\xa1X\"i\xf0\u156aX\x03m\x1b\xbb\xe0\xa8-x\xf9.\x95\xf4\xfb\xf82InX\x87\xc1\x8c5d\xd2\x0e\xb1\u0412\x13:,\xfdA\x92bYJ.\xbaG\xaf\xa0)\xbeG\xc9\x03\xf2F\v\xe3\xbf\x1cc\x12*\xf4\x80\xc6\xd5\u04ba\x9e\xa6\xe7\xd2B\xb1\xee!\xbb\xbf6\xb1\u04ea_\xd7\xda)n\x05A\x1b\xceB\xab\xa3Q\x8bTXB.\x9c\v_\x1f\x8e\u0141\xb8J&\f\xf0\x80\x18\x14SZ\xf8\xe9\xd9w\xa2A\x9a\x01sH\xc6\th\xf0^\x06\xefnh_\xa8q#\xe3,\x9c\x9a\xd2\u0231\xf4,\xdf\th\xff\xbb\v\x981\xcf\xefz+'\xa5\xf8\x84\x10T\xbe\xa1!\x90s\x10\xbc\x06\xf2=@\xcd!(\xe4[Q\u0703\xd9\xc1\x10\u689a\x14\xe7\x13R!\xa1\"\xd0cH\xddY'\xbbJN\xe2\xf2R\x83\x15#\xad\xfa\x1f\ud270\xb1c\u072f\xe6\u043e\xfd\x81\xab\xeb\xf3\x00\x97= \xef\uefe8M\r\xe8\x10\xfe;\x92\xa0X\x9c\xaf\xe3\xeeb\xcc?\x01PK\a\b\xad\x87m\x00y\x01\x00\x00Z\x05\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0f\x00\x00\x00word/styles.xml\u0556\xedn\xda0\x14\x86\xaf`\xf7\x80\xf2\xbfMH\x02CQiU\xb5\xea6\xa9\ua9b5\xbb\x80\x83c\x88U\u01f6l\a\u02ae~\xce7$\xa1J\x03\x12\x1d\xfc\x00\x1f\xfb\xbc\xc7~\xfc:\xce\xd5\xcd[LGk,\x15\xe1ln\x8d/\x1dk\x84\x19\xe2!a\xab\xb9\xf5\xe7\xe5\xe1bf\x8d\x94\x06\x16\x02\xe5\f\u03ed-V\xd6\xcd\xf5\x97\xabM\xa0\xf4\x96b52\xf9L\x051\x9a[\x91\xd6\"\xb0m\x85\"\x1c\x83\xba\xe4\x023\u04f9\xe42\x06m\x9are\xc7 _\x13q\x81x,@\x93\x05\xa1Dom\xd7q\xa6V!\xc3\xe7V\"YPH\\\xc4\x04I\xae\xf8R\xa7)\x01_.\t\xc2\xc5O\x99!\xfb\xd4\xcdS\xee9Jb\xcctV\u0456\x98\x9a9p\xa6\"\"T\xa9\x16\x0fU3\x9dQ)\xb2~o\x11\ub616\xe36\xa2O\xb5P\xc2\xc6lFL\xf3B\x1b.C!9\xc2J\x99\xe8}\xdeY)\x8e\x9d\x1e\x00S\x89*\xa3\xcf\x14\xf6k\x963\x89\x81\xb0J&\xb5FC\xa8\xaa}ij\x17\xd02\xa9z!5\vE\xfbL$\xefz$\v\tr\u06de\x05\f\u0e5b/H/\x177\x14L\x96Nde\xc8!\x12(\x02\xa9K\x01:D\x81r\xf4\x8a\xc3;`k\xa8\xcc\x1c\xaez\u0679\xa1\x14\x12XI\x88k\x93\xaa\x0f\xed\xec\xd8i\xd8\xe59\x02\x81k\xb5\xd5qj\xdf$ODmw\u007f\x88\xda\xce\t\x1cO>&\xe0\x96\x02\xd7\xe6\x01\x18rt\x8f\x97\x90P\xad\u04a6\xfc%\x8bf\xd1\xca~\x1e8\xd3j\xb4\t@!B\xe6\u05ad$`\xcao\x02\xa4v\x1a\x18\x94\xbeU\x04vB\xd1-S\xd5x;\x95R\u007fMx\r\u6838n\x19\xb9S\xcd\x18\x05\xb6*c\x98\xa51\xbb\x98\x8c\u075c\xa2h\xb62M\x01\x88d\x12\x94\xa4\x87\xda\xfd:\xb5\x8a\xc6\uf11a\x00$\x9a\x17\xb2\xa2\x90\xdd\x15\xb2[\\\xb2{\xc2H\xe8\xad0\xe9\x02d\xea/\x11\xa5\xaaY\u05cfpn=\xa5~\xcc\xd6\x1d\xe6\x99\xe6*\xca\x183\x88q\xb9\x1c\x96\x0f\xcakg\xa9my\r\v\x8a\xf7\xa4_\xd2H/\xfdl\xe4\xe8\xa9G\x95\xeeE|\u01d0^\x9bm\xe1(\xef\x18\x8d\xf3-Z\x80\xc2\xe1OV\xf6\xd6\x05M\x16~\xd3]\xf1bs^1\x16O;C\n\xc14\xfch6H5\xe2\xf5^\xc2RcsS\x8e]'\x9d\xf1\x02\x9b\xf3o\x96\xe1;\xce\xfb{[\u0678\xf6\x9e\ufd3d\x97\xc7v|6\x04\x9b{\x10\x9b\xfb\u0270y\u04fe\xd8\x16\xa5\xb2\xd3<\xc2^\xc7\x11\xcecGb\xf4\x0eb\xf4\u038dq\xb6O\xd1\x1dJ\x11q\xcae\xe5=/\xfd\xb6\x9e\x90\xb3\x8e'\xe4\xec\x04x\xfd\x83x\xfd\u03c5\u05dd\xf5\u017b\x87s\x9a}Z8\xfd\x0e\x9c\xfe\tpN\x0e\xe2\x9c|2\x9c\xfe)q\x1e\xbc\xbf\x8f\xc49=\x88s\xfa\xbf\xe2$\r\xe1\xb3\xe0}!\u06bcU\xb4\xde\x17\xb2\u8679N\xf7\xb8~\xfc>\x9ft\xc0\x9a\x1c\x05\xeb9Y\xe8N^U\u01d9\x91y\xee f'|\x95\xafL\xddu\xa3u\x9b\xda\xebx\xef\xf2\x0e\xbcw\x95\xff\xd4\xf5?PK\a\b\x88\xceE\f\x1d\x03\x00\x00\xdf\x11\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00word/document.xml\xa5\x95Yn\xdb0\x10\x86O\xd0;\x18|\xb7%\x19N\x1b\b\x96\xf3P\xa3E\x81\xb60\xb2\x1c\x80&)\x89\b7\f)\xab\xee\xe9Kj\xf5\x12\x04\x8a\xeb\x17b\xb6o~\x91cr\xfd\xf0G\x8a\u0641\x81\xe5Ze(Y\xc4h\xc6\x14\u0454\xab\"C/\xcf\xdf\xe6\xf7hf\x1dV\x14\v\xadX\x86\x8e\u0322\x87\u0367u\x9dRM*\u0254\x9by\x82\xb2\xa9$\x19*\x9d3i\x14YR2\x89\xedB\x1b\xa6|0\xd7 \xb1\xf3&\x14\x91\xc4\xf0Z\x999\xd1\xd2`\xc7\xf7\\pw\x8c\x96q\xfc\x19u\x18\x9d\xa1\nT\xda!\xe6\x92\x13\xd0V\xe7.\x94\xa4:\xcf9a\xdd\xd2W\xc0\x94\xbem\u0276\x93\xdct\x8c\x80\t\xafA+[rc{\x9a\xbc\x95\xe6\x83e\x0f9\xbc\xf7\x11\a)\xfa\xbc\xdaL\xe9F\x01\xd7\xfe8\xa4h\x1b\xd5\x1a\xa8\x01M\x98\xb5\u07bbm\x83\x031\x89'l`@\f\x15S$\x9c\xf7\xec\x95H\xcc\u0540\t\xc3q\x01\x1az/|\xefn\xd3\x1a\xd4\xf8!\xe3^X1EH\x1b\xfa\xc9\xf7\x80\xe1x\xad\x02\u07f0\x9f\xa7\xf5\x86O\x9a\xe2\v\x82\xafr\x15\f\x03y\v\x82\x94\x18\\\x0f\x10\xb7\x10\x84&\xaf\x8c~\xc5\ua007a\xa6\u0164q\xbe Q\x8e\v\xc0r\x1cR\xfb\xa1\x93M\xe2\x8bqy*\xb1a#\xad\xf8?\xdaw\u0415\x19\xc7}u\v\xed\xe4\x1f\x98\xdc}\f\xb0\xec\x01\x1b\u007f\x05\xee5=\x86\xd5\xcc\xea\xd4\u07e0\xf41Cq\xf7C\x9dk\xcb\u0135sw\xedz\u0732\x1cW\u00bd\x11\xd9\xc1\x993Y\xa5\x06\x03\xfeA\ao\u04881;\b\v\xec \u06ac\xa3\xd1~O\xc8\x1b\x82\xcf\xdbu\xc4fq\u00a7\x1cp\xc0\xa0\xb6E\x13\tk\xdb0dYF\\\x9bo\x8a\xa7\xbf\xbe\xa0\xf4\xaf\xca\xdd\xfd\xaa\xe1\xfb\xbb&Y.WMyH\xf8\x85\x83\xba\xbdvN\xfbAMVm\x96\xd3f4\x04\xcb\xddh\x01/\xca\x13\xb3d\x982/\xf7\u02f21s\xad]ov\x1d~W\xf2\xf9h\x98\x0f\xfaG\fBi'\xbd\xd7\x19\xf5\xa7\x18\x8d/\xda\xe6\x1fPK\a\b@Z0\xeb\x17\x02\x00\x00\x16\a\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00word/_rels/document.xml.rels\xad\x92Mj\xc30\x10\x85O\xd0;\x88\xd9\u05f2\xd3\x1fJ\x89\x9cM\bd[\xdc\x03(\xf2\xf8\x87Z#!MJ}\xfb\x8a\x94$\x0e\x04\u04c5\x97\xef\x89y\xf3\u034c\u059b\x1f;\x88o\f\xb1w\xa4\xa0\xc8r\x10H\xc6\xd5=\xb5\n>\xab\xdd\xe3\x1b\x88\u021aj=8B\x05#F\u0614\x0f\xeb\x0f\x1c4\xa7\x9a\xd8\xf5>\x8a\x14BQA\xc7\xec\u07e5\x8c\xa6C\xabc\xe6\f\x93U\\\xac9\x88\xe7%!\xe8h\x0f\x18\xd2\xdcW\x88\x8b5\a\xf1\xb2\xe81x\x1cpz\x8a\x93>\xb7\x977\x9f\xbc\xfc\x05PK\a\b\x90\x00\xab\xeb\xf1\x00\x00\x00,\x03\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\v\x00\x00\x00_rels/.rels\x8d\xcf;\x0e\xc20\f\x06\xe0\x13p\x87\xc8;M\u02c0\x10j\xd2\x05!uE\xe5\x00Q\xe2\xa6\x11\xcdCIx\xf4\xf6d`\x00\xc4\xc0h\xfb\xf7g\xb9\xed\x1ev&7\x8c\xc9x\u01e0\xa9j \xe8\xa4W\xc6i\x06\xe7\xe1\xb8\xde\x01IY8%f\xef\x90\xc1\x82\t:\xbejO8\x8b\\v\xd2dB\"\x05q\x89\xc1\x94s\xd8S\x9a\xe4\x84V\xa4\xca\ate2\xfahE.e\xd44\by\x11\x1a\u99ae\xb74\xbe\x1b\xc0?L\xd2+\x06\xb1W\r\x90a\t\xf8\x8f\xed\xc7\xd1HV\xcbr-\xc2\xf7\xb8\xe8\x03 \v.V4Fj\x92\x90\x11\xf6\x00\xd7\u014c\x0e\x05M\x05\xe05\x82\xb5'\xf9\x92'\x17\x96RYHz\x82&\xaam}\x9c`\xa8\x88\x19\xe4\xe5\xb3\x1f_>{\x82\x8e\xee?=\xba\xff\xcb\u0443\aG\xf7\u007f6P]\xc3q\xa0S\xbd\xf8\xfe\x8b\xbf\x1f}\x8a\xfez\xf2\u074b\x87_\x99\xf1R\xc7\xff\xfe\xd3g\xbf\xfd\xfa\xa5\x19\xa8t\xe0\xf3\xaf\x1f\xff\xf1\xf4\xf1\xf3o>\xff\xf3\x87\x87\x06\xf8\x86\xc0C\x1d>\xa0\x11\x91\xe8&9@;<\x02\xc3\f\x02\xc8P\x9c\x8db\x10b\xaaSl\u0101\xc41Ni\f\xe8\x9e\n+\xe8\x9b\x13\u0330\x01\xd7!U\x0f\xde\x11\xd0\x02L\xc0\xab\xe3{\x15\x85wC1V\xd4\x00\xbc\x1eF\x15\xe0\x16\xe7\xac\u00c5\u0466\xeb\xa9,\xdd\v\xe380\v\x17c\x1d\xb7\x83\xf1\xbeIvw.\xbe\xbdq\x02\xb9LM,\xbb!\xa9\xa8\xb9\xcd \xe48 1Q(}\xc6\xf7\b1\x90\u0765\xb4\xe2\xd7-\xea\t.\xf9H\xa1\xbb\x14u05\xbad@\x87\xcaLt\x8dF\x10\x97\x89IA\x88w\xc57[wP\x873\x13\xfbM\xb2_EBU`fbIX\u014dW\xf1X\xe1\u02281\x8e\x98\x8e\xbc\x81UhRrw\"\xbc\x8a\u00e5\x82H\a\x84q\xd4\xf3\x89\x94&\x9a[bRQ\xf7:\xb4\x0es\u0637\xd8$\xaa\"\x85\xa2{&\xe4\r\u0339\x8e\xdc\xe4{\xdd\x10G\x89Qg\x1a\x87:\xf6#\xb9\a)\x8a\xd16WF%x\xb5B\xd29\xc4\x01\xc7K\xc3}\x87\x12u\xb6\u06beM\x83\u041c \u94f10\x95\x04\xe1\xd5z\x9c\xb0\x11&q\xd1\xe1+\xbd:\xa2\xf1q\x8d;\x82\xbe\x8d\u03fbqC\xab|\xfe\xed\xa3\xffQ\xcb\xde\x00'\x98jf\xbeQ/\xc3\u0377\xe7.\x17>}\xfb\xbb\xf3&\x1e\xc7\xdb\x04\n\xe2}s~\u07dc\xdf\xc5\u6f2c\x9e\u03ff%\u03fa\xb0\xad\x1f\xb436\xd1\xd2S\xf7\x882\xb6\xab&\x8c\u0710Y\xff\x96`\x9e\u07c7\xc5l\x92\x11\x95\x87\xfc$\x84a!\xae\x82\v\x04\xce\xc6Hp\xf5\tU\xe1n\x88\x13\x10\xe3d\x12\x02Y\xb0\x0e$J\xb8\x84\xab\x85\xb5\x94wv?\xa5`s\xb6\xe6N/\x95\x80\xc6j\x8b\xfb\xf9rC\xbfl\x96l\xb2Y uA\x8d\x94\xc1i\x855.\xbd\x9e0'\a\x9eR\x9a\u36a5\xb9\xc7J\xb35oB\xdd \x9c\xbeJpV\xea\xb9hH\x14\u0308\x9f\xfa=g0\r\xcb\x1b\f\x91S\xd3b\x14b\x9f\x18\x965\xfb\x9c\xc6\x1b\xf1\xa6{&%\xce\xc7\u0275\x05'\u06cb\xd5\xc4\xe2\xea\f\x1d\xb4\xadU\xb7\xeeZ\xc8\xc3I\xdb\x1a\xc1i\t\x86Q\x02\xfcd\xdai0\v\xe2\xb6\xe5\xa9\xdc\xc0\x93kq\xce\xe2UsV95w\x99\xc1\x15\x11\x89\x90j\x13\xcb0\xa7\xca\x1eM_\xa5\xc43\xfd\xebn3\xf5\xc3\xf9\x18`h&\xa7\u04e2\xd1r\xfeC-\xec\xf9\u0412\u0448xj\xc9\xcalZ<\xe3cE\xc4n\xe8\x1f\xa0!\x1b\x8b\x1d\fz7\xf3\xec\xf2\xa9\x84N_\x9fN\x04\xe4v\xb3H\xbcj\xe1\x16\xb51\xff\u02a6\xa8\x19\u0312\x10\x17\xd9\xde\xd2b\x9f\u00f3q\xa9C6\xd3\u0533\x97\xe8\xfe\x8a\xa64\xce\xd1\x14\xf7\xdd5%\xcd\\8\x9f6\xfc\xec\xd2\x04\xbb\xb8\xc0(\xcd\u0476\u0145\n9t\xa1$\xa4^_\xc0\xbe\x9f\xc9\x02\xbd\x10\x94E\xaa\x12b\xe9\v\xe8TW\xb2?\xeb[9\x8f\xbc\xc9\x05\xa1\u06a1\x01\x12\x14:\x9d\n\x05!\u06ea\xb0\xf3\x04fN]\xdf\x1e\xa7\x8c\x8a>S\xaa+\x93\xfcwH\xf6\t\x1b\xa4\u057b\x92\xdao\xa1p\xdaM\nGd\xb8\xf9\xa0\u0666\xea\x1a\x06\xfd\xb7\xf8\xe0\xd2|\xa5\x8dg&\xa8y\x96\u036f\xa95}m+X}=\x15N\xb3\x01k\xe2\xeaf\x8b\xeb\xee\u049dg~\xabM\xe0\x96\x81\xd2/h\xdcTxlv<\x1d\xf0\x1d\x88>*\xf7y\x04\x89x\xa1U\x94_\xb98\x04\x9d[\x9aq)\xab\u007f\xeb\x14\xd4Z\x12\xef\xf3<;j\xcen,q\xf6\xf1\xe2^\xdd\u066e\xc1\xd7\xee\xf1\xae\xb6\x17K\xd4\xd6\xee!\xd9l\xe1\x8f(>\xbc\a\xb27\xe1z3f\xf9\x8aL`\x96\x0f\xb6Ef\xf0\x90\xfb\x93b\xc8d\xde\x12rGL[:\x8bw\xc8\bQ\xffp\x1a\xd69\x8f\x16\xff\xf4\x94\x9b\xf9N. \xb5\xbd$l\x9cLX\xe0g\x9bHI\\?\x99\xb8\xa4\x98\xde\xf1J\xe2\xec\x16gb\xc0f\x92s|\x1e\xe5\xb2E\x96\x9eb\xf1\xeb\xb8\xec\x14\u029b]f\xcc\xde\u04fa\xec\x14\x81z\x05\x97\xa9\xc3\xe3]Vx\xca6%\x1e9T\x02w\xa7\u007f]A\xfe\u06b3\x94]\xff\aPK\a\b!Z\xa2\x84,\x06\x00\x00\xdb\x1d\x00\x00PK\x03\x04\x14\x00\b\b\b\x00\x13b\x96P\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x13\x00\x00\x00[Content_Types].xml\xb5\x93Mn\xc20\x10\x85O\xd0;D\xdeV\xc4\xd0EUU\x04\x16\xfdY\xb6]\xd0\x03\f\xce\x04\xac\xfaO\x9e\x81\xc2\xed;\t\x90\x05\x02\xa9\x95\x9a\x8de\xfb\u037c\xf7y$O\xe7;\xef\x8a-f\xb21TjR\x8eU\x81\xc1\xc4\u0686U\xa5>\x17\xaf\xa3\aU\x10C\xa8\xc1\u0140\x95\xda#\xa9\xf9\xecf\xba\xd8'\xa4B\x9a\x03Uj\u035c\x1e\xb5&\xb3F\x0fT\u0184A\x94&f\x0f,\u01fc\xd2\t\xcc\x17\xacP\u07cd\xc7\xf7\xda\xc4\xc0\x18x\u012d\x87\x9aM\x9f\xb1\x81\x8d\xe3\xe2\xe9p\xdfZW\nRr\xd6\x00\v\x97\x163U\xbc\xecD<`\xb6g\xfd\x8b\xbem\xa8\xcf`FG\x902\xa3\xebjhm\x13\u075e\a\x88Jm\u00bbL&\xdb\x1a\xff\x14\x11\x9b\xc6\x1a\xac\xa3\xd9xi)\xbfc\xaeS\x8e\x06\x89d\xa8\u0795\x84\u0332;\xa6~@\xe67\xf0b\xab\xdbJ}R\xcb\xe3#\x87A\xe0\xbd\xc3k\x00\x9d6h|#^\vX:\xbcL\xd0\u02c3B\x84\x8d_b\x96\xfde\x88^\x1e\x14\xa2W<\xd8p\x19\xa4/\xf9G\x0e\x96\x8fze\xf8\x9dtX'\xa7H\xdd\xfd\xf6\xd9\x0fPK\a\b3\xaf\x0f\xb7,\x01\x00\x00-\x04\x00\x00PK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96PI\x13C\u007fh\x01\x00\x00=\x05\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00word/numbering.xmlPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P\x8e\xb3\u00e4\x05\x02\x00\x00\xea\x06\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa8\x01\x00\x00word/settings.xmlPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P\xad\x87m\x00y\x01\x00\x00Z\x05\x00\x00\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xec\x03\x00\x00word/fontTable.xmlPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P\x88\xceE\f\x1d\x03\x00\x00\xdf\x11\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa5\x05\x00\x00word/styles.xmlPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P@Z0\xeb\x17\x02\x00\x00\x16\a\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\b\x00\x00word/document.xmlPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P\x90\x00\xab\xeb\xf1\x00\x00\x00,\x03\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00U\v\x00\x00word/_rels/document.xml.relsPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P-h\xcf\"\xb1\x00\x00\x00*\x01\x00\x00\v\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x90\f\x00\x00_rels/.relsPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P!Z\xa2\x84,\x06\x00\x00\xdb\x1d\x00\x00\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00z\r\x00\x00word/theme/theme1.xmlPK\x01\x02\x14\x00\x14\x00\b\b\b\x00\x13b\x96P3\xaf\x0f\xb7,\x01\x00\x00-\x04\x00\x00\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x13\x00\x00[Content_Types].xmlPK\x05\x06\x00\x00\x00\x00\t\x00\t\x00B\x02\x00\x00V\x15\x00\x00\x00\x00"), + "elf": []byte("\u007fELF\x01\x01\x01\x00\x00\xb3*1\xc0@\u0340\x02\x00\x03\x00\x01\x00\x00\x00\t\x00 \x00 \x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00\x01\x00\x00\x00@\x00\x00\x00@\x00\x00\x00\x05\x00\x00\x00\x00\x10\x00\x00"), + "jpg": []byte("\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00\xff\xdb\x00C\x00\b\x06\x06\a\x06\x05\b\a\a\a\t\t\b\n\f\x14\r\f\v\v\f\x19\x12\x13\x0f\x14\x1d\x1a\x1f\x1e\x1d\x1a\x1c\x1c $.' \",#\x1c\x1c(7),01444\x1f'9=82<.342\xff\xdb\x00C\x01\t\t\t\f\v\f\x18\r\r\x182!\x1c!22222222222222222222222222222222222222222222222222\xff\xc0\x00\x11\b\x00\x01\x00\x01\x03\x01\"\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x15\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\a\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\f\x03\x01\x00\x02\x11\x03\x11\x00?\x00\xbf\x80\x0f\xff\xd9"), + "gif": []byte("GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\xff\xff\xff!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01D\x00;"), + "png": []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\b\x04\x00\x00\x00\xb5\x1c\f\x02\x00\x00\x00\vIDATx\xdacd`\x00\x00\x00\x06\x00\x020\x81\xd0/\x00\x00\x00\x00IEND\xaeB`\x82"), +} + +func TestGetMimeType(t *testing.T) { + tests := []struct { + extension string + expected string + }{ + {"docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {"elf", "application/x-executable"}, + {"jpg", "image/jpeg"}, + {"gif", "image/gif"}, + {"png", "image/png"}, + } + + dir, err := ioutil.TempDir("", "mime-samples") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir) + + for extension, sample := range mimeSamples { + samplePath := filepath.Join(dir, "sample."+extension) + if err := ioutil.WriteFile(samplePath, sample, 0700); err != nil { + t.Fatal(err) + } + } + + for _, test := range tests { + t.Run(test.extension, func(t *testing.T) { + samplePath := filepath.Join(dir, "sample."+test.extension) + assert.Equal(t, test.expected, getMimeType(samplePath)) + }) + } +} diff --git a/go.mod b/go.mod index 290a0e31209..9af250226dd 100644 --- a/go.mod +++ b/go.mod @@ -93,6 +93,7 @@ require ( github.com/gorilla/mux v1.7.2 // indirect github.com/gorilla/websocket v1.4.1 // indirect github.com/grpc-ecosystem/grpc-gateway v1.13.0 // indirect + github.com/h2non/filetype v1.0.12 github.com/hashicorp/go-multierror v1.0.0 github.com/hashicorp/golang-lru v0.5.2-0.20190520140433-59383c442f7d // indirect github.com/insomniacslk/dhcp v0.0.0-20180716145214-633285ba52b2 diff --git a/go.sum b/go.sum index fa8f016742b..c669cc0a92e 100644 --- a/go.sum +++ b/go.sum @@ -389,6 +389,9 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/grpc-gateway v1.13.0 h1:sBDQoHXrOlfPobnKw69FIKa1wg9qsLLvvQ/Y19WtFgI= github.com/grpc-ecosystem/grpc-gateway v1.13.0/go.mod h1:8XEsbTttt/W+VvjtQhLACqCisSPWTxCZ7sBRjU6iH9c= +github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao= +github.com/h2non/filetype v1.0.12/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce h1:prjrVgOk2Yg6w+PflHoszQNLTUh4kaByUcEWM/9uin4= github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/vendor/github.com/h2non/filetype/.editorconfig b/vendor/github.com/h2non/filetype/.editorconfig new file mode 100644 index 00000000000..000dc0a7aa4 --- /dev/null +++ b/vendor/github.com/h2non/filetype/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tabs +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/vendor/github.com/h2non/filetype/.gitignore b/vendor/github.com/h2non/filetype/.gitignore new file mode 100644 index 00000000000..6fefe6cce24 --- /dev/null +++ b/vendor/github.com/h2non/filetype/.gitignore @@ -0,0 +1,2 @@ +bin +.DS_Store diff --git a/vendor/github.com/h2non/filetype/.travis.yml b/vendor/github.com/h2non/filetype/.travis.yml new file mode 100644 index 00000000000..ed996a2aa92 --- /dev/null +++ b/vendor/github.com/h2non/filetype/.travis.yml @@ -0,0 +1,17 @@ +language: go + +go: + - "1.10" + - "1.11" + - "1.12" + - "1.13" + - "tip" + +before_install: + - go get -u -v golang.org/x/lint/golint + +script: + - diff -u <(echo -n) <(gofmt -s -d ./) + - diff -u <(echo -n) <(go vet ./...) + - diff -u <(echo -n) <(golint) + - go test -v -race ./... diff --git a/vendor/github.com/h2non/filetype/History.md b/vendor/github.com/h2non/filetype/History.md new file mode 100644 index 00000000000..ccb4014f785 --- /dev/null +++ b/vendor/github.com/h2non/filetype/History.md @@ -0,0 +1,128 @@ + +v1.0.10 / 2019-08-06 +==================== + + * Merge pull request #76 from lex-r/fix-matroska-detection + * fix: mkv and webm types detection + +v1.0.9 / 2019-07-25 +=================== + + * Merge pull request #75 from Trane9991/master + * add video/3gpp support + * fix: use proper iso file mime type + * feat: add iso image format + * Merge pull request #65 from Fentonz/master + * Merge pull request #70 from fanpei91/master + * add image/vnd.dwg to README + * add image/vnd.dwg support + * Added support for .iso files + +v1.0.8 / 2019-02-10 +=================== + + * refactor(images): heic -> heif + * feat(docs): add heif format + * Merge pull request #60 from rikonor/master + * add heif/heic support + * fix(docs): dicom -> dcm + * feat: add dicom type + * Merge pull request #58 from Fentonz/master + * Merge pull request #59 from kmanley/master + * fix example; related to h2non/filetype#43 + * Added DICOM type to archive + + +v1.0.7 / 2019-02-09 +=================== + + * Merge pull request #56 from akupila/wasm + * add wasm to readme + * detect wasm file type + +v1.0.6 / 2019-01-22 +=================== + + * Merge pull request #55 from ivanlemeshev/master + * Added ftypmp4v to MP4 matcher + * Merge pull request #54 from aofei/master + * chore: add support for Go modules + * feat: add support for AAC (audio/aac) + * Merge pull request #53 from lynxbyorion/check-for-docoments + * Added checks for documents. + * Merge pull request #51 from eriken/master + * fixed bad mime and import paths + * Merge pull request #50 from eriken/jpeg2000_support + * fix import paths + * jpeg2000 support + * Merge pull request #47 from Ma124/master + * Merge pull request #49 from amoore614/master + * more robust check for .mov files + * bugfix: reverse order of matcher key list so user registered matchers appear first + * bugfix: store ptr to MatcherKeys in case user registered matchers are used. + * update comment + * Bump buffer size to 8K to allow for more custom file matching + * refactor(readme): update package import path + * Merge pull request #48 from kumakichi/support_msooxml + * do not use v1 + * ok, master already changed travis + * add fixtures, but MatchReader may not work for some msooxml files, 4096 bytes maybe not enough + * support ms ooxml, #40 + * Fixed misspells + * fix(travis): use string notation for matrix items + * Merge pull request #42 from bruth/patch-2 + * refactor(travis): remove Go 1.6, add Go 1.10 + * Change maximum bytes required for detection + * Merge pull request #36 from yiiTT/patch-1 + * Add MP4 dash and additional ISO formats + * Merge pull request #34 from RangelReale/fix-mp4-case + * Merge pull request #32 from yiiTT/fix-m4v + * Fixed mp4 detection case-sensitivity according to http://www.ftyps.com/ + * Fix M4v matcher + +v1.0.5 / 2017-12-12 +=================== + + * Merge pull request #30 from RangelReale/fix_mp4 + * Fix duplicated item in mp4 fix + * Fix MP4 matcher, with information from http://www.file-recovery.com/mp4-signature-format.htm + * Merge pull request #28 from ikovic/master + * Updated file header example. + +v1.0.4 / 2017-11-29 +=================== + + * fix: tests and document types matchers + * refactor(docs): remove codesponsor + * Merge pull request #26 from bienkma/master + * Add support check file type: .doc, .docx, .pptx, .ppt, .xls, .xlsx + * feat(docs): add code sponsor banner + * feat(travis): add go 1.9 + * Merge pull request #24 from strazzere/patch-1 + * Fix typo in unknown + +v1.0.3 / 2017-08-03 +=================== + + * Merge pull request #21 from elemeta/master + * Add Elf file as supported matcher archive type + +v1.0.2 / 2017-07-26 +=================== + + * Merge pull request #20 from marshyski/master + * Added RedHat RPM as supported matcher archive type + * Merge pull request #19 from nlamirault/patch-1 + * Fix typo in documentation + +v1.0.1 / 2017-02-24 +=================== + + * Merge pull request #18 from Impyy/enable-webm + * Enable the webm matcher + * feat(docs): add Go version badge + +1.0.0 / 2016-12-11 +================== + +- Initial stable version (v1.0.0). diff --git a/vendor/github.com/h2non/filetype/LICENSE b/vendor/github.com/h2non/filetype/LICENSE new file mode 100644 index 00000000000..30ede59b653 --- /dev/null +++ b/vendor/github.com/h2non/filetype/LICENSE @@ -0,0 +1,24 @@ +The MIT License + +Copyright (c) Tomas Aparicio + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/h2non/filetype/README.md b/vendor/github.com/h2non/filetype/README.md new file mode 100644 index 00000000000..7f1e8e73537 --- /dev/null +++ b/vendor/github.com/h2non/filetype/README.md @@ -0,0 +1,290 @@ +# 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) [![Go Report Card](http://goreportcard.com/badge/h2non/filetype)](http://goreportcard.com/report/h2non/filetype) [![Go Version](https://img.shields.io/badge/go-v1.0+-green.svg?style=flat)](https://github.com/h2non/gentleman) + +Small and dependency free [Go](https://golang.org) package to infer file and MIME type checking the [magic numbers](https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files) signature. + +For SVG file type checking, see [go-is-svg](https://github.com/h2non/go-is-svg) package. + +## 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...) +- Provides a bunch of helpers and file matching shortcuts +- [Pluggable](#add-additional-file-type-matchers): add custom new types and matchers +- Simple and semantic API +- [Blazing fast](#benchmarks), even processing large files +- Only first 262 bytes representing the max file header is required, so you can just [pass a slice](#file-header) +- Dependency free (just Go code, no C compilation needed) +- Cross-platform file recognition + +## Installation + +```bash +go get github.com/h2non/filetype +``` + +## API + +See [Godoc](https://godoc.org/github.com/h2non/filetype) reference. + +### Subpackages + +- [`github.com/h2non/filetype/types`](https://godoc.org/github.com/h2non/filetype/types) +- [`github.com/h2non/filetype/matchers`](https://godoc.org/github.com/h2non/filetype/matchers) + +## Examples + +#### Simple file type checking + +```go +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/h2non/filetype" +) + +func main() { + buf, _ := ioutil.ReadFile("sample.jpg") + + kind, _ := filetype.Match(buf) + if kind == filetype.Unknown { + fmt.Println("Unknown file type") + return + } + + fmt.Printf("File type: %s. MIME: %s\n", kind.Extension, kind.MIME.Value) +} +``` + +#### Check type class + +```go +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/h2non/filetype" +) + +func main() { + buf, _ := ioutil.ReadFile("sample.jpg") + + if filetype.IsImage(buf) { + fmt.Println("File is an image") + } else { + fmt.Println("Not an image") + } +} +``` + +#### Supported type + +```go +package main + +import ( + "fmt" + + "github.com/h2non/filetype" +) + +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") + } +} +``` + +#### File header + +```go +package main + +import ( + "fmt" + "io/ioutil" + + "github.com/h2non/filetype" +) + +func main() { + // Open a file descriptor + file, _ := os.Open("movie.mp4") + + // We only have to pass the file header = first 261 bytes + head := make([]byte, 261) + file.Read(head) + + if filetype.IsImage(head) { + fmt.Println("File is an image") + } else { + fmt.Println("Not an image") + } +} +``` + +#### Add additional file type matchers + +```go +package main + +import ( + "fmt" + + "github.com/h2non/filetype" +) + +var fooType = filetype.NewType("foo", "foo/foo") + +func fooMatcher(buf []byte) bool { + return len(buf) > 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("New supported type: foo") + } + + // Check if the new type is supported by MIME + if filetype.IsMIMESupported("foo/foo") { + fmt.Println("New supported MIME 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` +- **heif** - `image/heif` +- **jxr** - `image/vnd.ms-photo` +- **psd** - `image/vnd.adobe.photoshop` +- **ico** - `image/x-icon` +- **dwg** - `image/vnd.dwg` + +#### 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` +- **3gp** - `video/3gpp` + +#### Audio + +- **mid** - `audio/midi` +- **mp3** - `audio/mpeg` +- **m4a** - `audio/m4a` +- **ogg** - `audio/ogg` +- **flac** - `audio/x-flac` +- **wav** - `audio/x-wav` +- **amr** - `audio/amr` +- **aac** - `audio/aac` + +#### 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` +- **iso** - `application/x-iso9660-image` +- **eot** - `application/octet-stream` +- **ps** - `application/postscript` +- **sqlite** - `application/x-sqlite3` +- **nes** - `application/x-nintendo-nes-rom` +- **crx** - `application/x-google-chrome-extension` +- **cab** - `application/vnd.ms-cab-compressed` +- **deb** - `application/x-deb` +- **ar** - `application/x-unix-archive` +- **Z** - `application/x-compress` +- **lz** - `application/x-lzip` +- **rpm** - `application/x-rpm` +- **elf** - `application/x-executable` +- **dcm** - `application/dicom` + +#### Documents + +- **doc** - `application/msword` +- **docx** - `application/vnd.openxmlformats-officedocument.wordprocessingml.document` +- **xls** - `application/vnd.ms-excel` +- **xlsx** - `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` +- **ppt** - `application/vnd.ms-powerpoint` +- **pptx** - `application/vnd.openxmlformats-officedocument.presentationml.presentation` + +#### Font + +- **woff** - `application/font-woff` +- **woff2** - `application/font-woff` +- **ttf** - `application/font-sfnt` +- **otf** - `application/font-sfnt` + +#### Application + +- **wasm** - `application/wasm` + +## Benchmarks + +Measured using [real files](https://github.com/h2non/filetype/tree/master/fixtures). + +Environment: OSX x64 i7 2.7 Ghz + +```bash +BenchmarkMatchTar-8 1000000 1083 ns/op +BenchmarkMatchZip-8 1000000 1162 ns/op +BenchmarkMatchJpeg-8 1000000 1280 ns/op +BenchmarkMatchGif-8 1000000 1315 ns/op +BenchmarkMatchPng-8 1000000 1121 ns/op +``` + +## License + +MIT - Tomas Aparicio diff --git a/vendor/github.com/h2non/filetype/filetype.go b/vendor/github.com/h2non/filetype/filetype.go new file mode 100644 index 00000000000..933058c8532 --- /dev/null +++ b/vendor/github.com/h2non/filetype/filetype.go @@ -0,0 +1,87 @@ +package filetype + +import ( + "errors" + + "github.com/h2non/filetype/matchers" + "github.com/h2non/filetype/types" +) + +// Types stores a map of supported types +var Types = types.Types + +// NewType creates and registers a new type +var NewType = types.NewType + +// Unknown represents an unknown file type +var Unknown = types.Unknown + +// ErrEmptyBuffer represents an empty buffer error +var ErrEmptyBuffer = errors.New("Empty buffer") + +// ErrUnknownBuffer represents a unknown buffer error +var ErrUnknownBuffer = errors.New("Unknown buffer type") + +// AddType registers a new file type +func AddType(ext, mime string) types.Type { + return types.NewType(ext, mime) +} + +// Is 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 { + return IsType(buf, kind) + } + return false +} + +// IsExtension semantic alias to Is() +func IsExtension(buf []byte, ext string) bool { + return Is(buf, ext) +} + +// IsType 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 + } + return matcher(buf) != types.Unknown +} + +// IsMIME 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) != types.Unknown + } + } + return false +} + +// IsSupported checks if a given file extension is supported +func IsSupported(ext string) bool { + for name := range Types { + if name == ext { + return true + } + } + return false +} + +// IsMIMESupported checks if a given MIME type is supported +func IsMIMESupported(mime string) bool { + for _, m := range Types { + if m.MIME.Value == mime { + return true + } + } + return false +} + +// GetType retrieves a Type by file extension +func GetType(ext string) types.Type { + return types.Get(ext) +} diff --git a/vendor/github.com/h2non/filetype/go.mod b/vendor/github.com/h2non/filetype/go.mod new file mode 100644 index 00000000000..071ea73201e --- /dev/null +++ b/vendor/github.com/h2non/filetype/go.mod @@ -0,0 +1,3 @@ +module github.com/h2non/filetype + +go 1.13 diff --git a/vendor/github.com/h2non/filetype/kind.go b/vendor/github.com/h2non/filetype/kind.go new file mode 100644 index 00000000000..de8473507dc --- /dev/null +++ b/vendor/github.com/h2non/filetype/kind.go @@ -0,0 +1,80 @@ +package filetype + +import ( + "github.com/h2non/filetype/matchers" + "github.com/h2non/filetype/types" +) + +// Image tries to match a file as image type +func Image(buf []byte) (types.Type, error) { + return doMatchMap(buf, matchers.Image) +} + +// IsImage checks if the given buffer is an image type +func IsImage(buf []byte) bool { + kind, _ := Image(buf) + return kind != types.Unknown +} + +// Audio tries to match a file as audio type +func Audio(buf []byte) (types.Type, error) { + return doMatchMap(buf, matchers.Audio) +} + +// IsAudio checks if the given buffer is an audio type +func IsAudio(buf []byte) bool { + kind, _ := Audio(buf) + return kind != types.Unknown +} + +// Video tries to match a file as video type +func Video(buf []byte) (types.Type, error) { + return doMatchMap(buf, matchers.Video) +} + +// IsVideo checks if the given buffer is a video type +func IsVideo(buf []byte) bool { + kind, _ := Video(buf) + return kind != types.Unknown +} + +// Font tries to match a file as text font type +func Font(buf []byte) (types.Type, error) { + return doMatchMap(buf, matchers.Font) +} + +// IsFont checks if the given buffer is a font type +func IsFont(buf []byte) bool { + kind, _ := Font(buf) + return kind != types.Unknown +} + +// Archive tries to match a file as generic archive type +func Archive(buf []byte) (types.Type, error) { + return doMatchMap(buf, matchers.Archive) +} + +// IsArchive checks if the given buffer is an archive type +func IsArchive(buf []byte) bool { + kind, _ := Archive(buf) + return kind != types.Unknown +} + +// Document tries to match a file as document type +func Document(buf []byte) (types.Type, error) { + return doMatchMap(buf, matchers.Document) +} + +// IsDocument checks if the given buffer is an document type +func IsDocument(buf []byte) bool { + kind, _ := Document(buf) + return kind != types.Unknown +} + +func doMatchMap(buf []byte, machers matchers.Map) (types.Type, error) { + kind := MatchMap(buf, machers) + if kind != types.Unknown { + return kind, nil + } + return kind, ErrUnknownBuffer +} diff --git a/vendor/github.com/h2non/filetype/match.go b/vendor/github.com/h2non/filetype/match.go new file mode 100644 index 00000000000..82cf8046845 --- /dev/null +++ b/vendor/github.com/h2non/filetype/match.go @@ -0,0 +1,90 @@ +package filetype + +import ( + "io" + "os" + + "github.com/h2non/filetype/matchers" + "github.com/h2non/filetype/types" +) + +// Matchers is an alias to matchers.Matchers +var Matchers = matchers.Matchers + +// MatcherKeys is an alias to matchers.MatcherKeys +var MatcherKeys = &matchers.MatcherKeys + +// NewMatcher is an alias to matchers.NewMatcher +var NewMatcher = matchers.NewMatcher + +// Match infers the file type of a given buffer inspecting its magic numbers signature +func Match(buf []byte) (types.Type, error) { + length := len(buf) + if length == 0 { + return types.Unknown, ErrEmptyBuffer + } + + for _, kind := range *MatcherKeys { + checker := Matchers[kind] + match := checker(buf) + if match != types.Unknown && match.Extension != "" { + return match, nil + } + } + + return types.Unknown, nil +} + +// Get is an alias to Match() +func Get(buf []byte) (types.Type, error) { + return Match(buf) +} + +// MatchFile infers a file type for a file +func MatchFile(filepath string) (types.Type, error) { + file, err := os.Open(filepath) + if err != nil { + return types.Unknown, err + } + defer file.Close() + + return MatchReader(file) +} + +// MatchReader is convenient wrapper to Match() any Reader +func MatchReader(reader io.Reader) (types.Type, error) { + buffer := make([]byte, 8192) // 8K makes msooxml tests happy and allows for expanded custom file checks + + _, err := reader.Read(buffer) + if err != nil && err != io.EOF { + return types.Unknown, err + } + + return Match(buffer) +} + +// AddMatcher registers a new matcher type +func AddMatcher(fileType types.Type, matcher matchers.Matcher) matchers.TypeMatcher { + return matchers.NewMatcher(fileType, matcher) +} + +// Matches checks if the given buffer matches with some supported file type +func Matches(buf []byte) bool { + kind, _ := Match(buf) + return kind != types.Unknown +} + +// MatchMap performs a file matching against a map of match functions +func MatchMap(buf []byte, matchers matchers.Map) types.Type { + for kind, matcher := range matchers { + if matcher(buf) { + return kind + } + } + return types.Unknown +} + +// MatchesMap is an alias to Matches() but using matching against a map of match functions +func MatchesMap(buf []byte, matchers matchers.Map) bool { + return MatchMap(buf, matchers) != types.Unknown +} diff --git a/vendor/github.com/h2non/filetype/matchers/application.go b/vendor/github.com/h2non/filetype/matchers/application.go new file mode 100644 index 00000000000..f482062d6c0 --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/application.go @@ -0,0 +1,20 @@ +package matchers + +var ( + TypeWasm = newType("wasm", "application/wasm") +) + +var Application = Map{ + TypeWasm: Wasm, +} + +// Wasm detects a Web Assembly 1.0 filetype. +func Wasm(buf []byte) bool { + // WASM has starts with `\0asm`, followed by the version. + // http://webassembly.github.io/spec/core/binary/modules.html#binary-magic + return len(buf) >= 8 && + buf[0] == 0x00 && buf[1] == 0x61 && + buf[2] == 0x73 && buf[3] == 0x6D && + buf[4] == 0x01 && buf[5] == 0x00 && + buf[6] == 0x00 && buf[7] == 0x00 +} diff --git a/vendor/github.com/h2non/filetype/matchers/archive.go b/vendor/github.com/h2non/filetype/matchers/archive.go new file mode 100644 index 00000000000..a2201378328 --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/archive.go @@ -0,0 +1,234 @@ +package matchers + +var ( + TypeEpub = newType("epub", "application/epub+zip") + TypeZip = newType("zip", "application/zip") + TypeTar = newType("tar", "application/x-tar") + TypeRar = newType("rar", "application/x-rar-compressed") + TypeGz = newType("gz", "application/gzip") + TypeBz2 = newType("bz2", "application/x-bzip2") + Type7z = newType("7z", "application/x-7z-compressed") + TypeXz = newType("xz", "application/x-xz") + TypePdf = newType("pdf", "application/pdf") + TypeExe = newType("exe", "application/x-msdownload") + TypeSwf = newType("swf", "application/x-shockwave-flash") + TypeRtf = newType("rtf", "application/rtf") + TypeEot = newType("eot", "application/octet-stream") + TypePs = newType("ps", "application/postscript") + TypeSqlite = newType("sqlite", "application/x-sqlite3") + TypeNes = newType("nes", "application/x-nintendo-nes-rom") + TypeCrx = newType("crx", "application/x-google-chrome-extension") + TypeCab = newType("cab", "application/vnd.ms-cab-compressed") + TypeDeb = newType("deb", "application/x-deb") + TypeAr = newType("ar", "application/x-unix-archive") + TypeZ = newType("Z", "application/x-compress") + TypeLz = newType("lz", "application/x-lzip") + TypeRpm = newType("rpm", "application/x-rpm") + TypeElf = newType("elf", "application/x-executable") + TypeDcm = newType("dcm", "application/dicom") + TypeIso = newType("iso", "application/x-iso9660-image") +) + +var Archive = Map{ + TypeEpub: Epub, + TypeZip: Zip, + TypeTar: Tar, + TypeRar: Rar, + TypeGz: Gz, + TypeBz2: Bz2, + Type7z: SevenZ, + TypeXz: Xz, + TypePdf: Pdf, + TypeExe: Exe, + TypeSwf: Swf, + TypeRtf: Rtf, + TypeEot: Eot, + TypePs: Ps, + TypeSqlite: Sqlite, + TypeNes: Nes, + TypeCrx: Crx, + TypeCab: Cab, + TypeDeb: Deb, + TypeAr: Ar, + TypeZ: Z, + TypeLz: Lz, + TypeRpm: Rpm, + TypeElf: Elf, + TypeDcm: Dcm, + TypeIso: Iso, +} + +func Epub(buf []byte) bool { + return len(buf) > 57 && + buf[0] == 0x50 && buf[1] == 0x4B && buf[2] == 0x3 && buf[3] == 0x4 && + buf[30] == 0x6D && buf[31] == 0x69 && buf[32] == 0x6D && buf[33] == 0x65 && + buf[34] == 0x74 && buf[35] == 0x79 && buf[36] == 0x70 && buf[37] == 0x65 && + buf[38] == 0x61 && buf[39] == 0x70 && buf[40] == 0x70 && buf[41] == 0x6C && + buf[42] == 0x69 && buf[43] == 0x63 && buf[44] == 0x61 && buf[45] == 0x74 && + buf[46] == 0x69 && buf[47] == 0x6F && buf[48] == 0x6E && buf[49] == 0x2F && + buf[50] == 0x65 && buf[51] == 0x70 && buf[52] == 0x75 && buf[53] == 0x62 && + buf[54] == 0x2B && buf[55] == 0x7A && buf[56] == 0x69 && buf[57] == 0x70 +} + +func Zip(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x50 && buf[1] == 0x4B && + (buf[2] == 0x3 || buf[2] == 0x5 || buf[2] == 0x7) && + (buf[3] == 0x4 || buf[3] == 0x6 || buf[3] == 0x8) +} + +func Tar(buf []byte) bool { + return len(buf) > 261 && + buf[257] == 0x75 && buf[258] == 0x73 && + buf[259] == 0x74 && buf[260] == 0x61 && + buf[261] == 0x72 +} + +func Rar(buf []byte) bool { + return len(buf) > 6 && + buf[0] == 0x52 && buf[1] == 0x61 && buf[2] == 0x72 && + buf[3] == 0x21 && buf[4] == 0x1A && buf[5] == 0x7 && + (buf[6] == 0x0 || buf[6] == 0x1) +} + +func Gz(buf []byte) bool { + return len(buf) > 2 && + buf[0] == 0x1F && buf[1] == 0x8B && buf[2] == 0x8 +} + +func Bz2(buf []byte) bool { + return len(buf) > 2 && + buf[0] == 0x42 && buf[1] == 0x5A && buf[2] == 0x68 +} + +func SevenZ(buf []byte) bool { + return len(buf) > 5 && + buf[0] == 0x37 && buf[1] == 0x7A && buf[2] == 0xBC && + buf[3] == 0xAF && buf[4] == 0x27 && buf[5] == 0x1C +} + +func Pdf(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x25 && buf[1] == 0x50 && + buf[2] == 0x44 && buf[3] == 0x46 +} + +func Exe(buf []byte) bool { + return len(buf) > 1 && + buf[0] == 0x4D && buf[1] == 0x5A +} + +func Swf(buf []byte) bool { + return len(buf) > 2 && + (buf[0] == 0x43 || buf[0] == 0x46) && + buf[1] == 0x57 && buf[2] == 0x53 +} + +func Rtf(buf []byte) bool { + return len(buf) > 4 && + buf[0] == 0x7B && buf[1] == 0x5C && + buf[2] == 0x72 && buf[3] == 0x74 && + buf[4] == 0x66 +} + +func Nes(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x4E && buf[1] == 0x45 && + buf[2] == 0x53 && buf[3] == 0x1A +} + +func Crx(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x43 && buf[1] == 0x72 && + buf[2] == 0x32 && buf[3] == 0x34 +} + +func Cab(buf []byte) bool { + return len(buf) > 3 && + ((buf[0] == 0x4D && buf[1] == 0x53 && buf[2] == 0x43 && buf[3] == 0x46) || + (buf[0] == 0x49 && buf[1] == 0x53 && buf[2] == 0x63 && buf[3] == 0x28)) +} + +func Eot(buf []byte) bool { + return len(buf) > 35 && + buf[34] == 0x4C && buf[35] == 0x50 && + ((buf[8] == 0x02 && buf[9] == 0x00 && + buf[10] == 0x01) || (buf[8] == 0x01 && + buf[9] == 0x00 && buf[10] == 0x00) || + (buf[8] == 0x02 && buf[9] == 0x00 && + buf[10] == 0x02)) +} + +func Ps(buf []byte) bool { + return len(buf) > 1 && + buf[0] == 0x25 && buf[1] == 0x21 +} + +func Xz(buf []byte) bool { + return len(buf) > 5 && + buf[0] == 0xFD && buf[1] == 0x37 && + buf[2] == 0x7A && buf[3] == 0x58 && + buf[4] == 0x5A && buf[5] == 0x00 +} + +func Sqlite(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x53 && buf[1] == 0x51 && + buf[2] == 0x4C && buf[3] == 0x69 +} + +func Deb(buf []byte) bool { + return len(buf) > 20 && + buf[0] == 0x21 && buf[1] == 0x3C && buf[2] == 0x61 && + buf[3] == 0x72 && buf[4] == 0x63 && buf[5] == 0x68 && + buf[6] == 0x3E && buf[7] == 0x0A && buf[8] == 0x64 && + buf[9] == 0x65 && buf[10] == 0x62 && buf[11] == 0x69 && + buf[12] == 0x61 && buf[13] == 0x6E && buf[14] == 0x2D && + buf[15] == 0x62 && buf[16] == 0x69 && buf[17] == 0x6E && + buf[18] == 0x61 && buf[19] == 0x72 && buf[20] == 0x79 +} + +func Ar(buf []byte) bool { + return len(buf) > 6 && + buf[0] == 0x21 && buf[1] == 0x3C && + buf[2] == 0x61 && buf[3] == 0x72 && + buf[4] == 0x63 && buf[5] == 0x68 && + buf[6] == 0x3E +} + +func Z(buf []byte) bool { + return len(buf) > 1 && + ((buf[0] == 0x1F && buf[1] == 0xA0) || + (buf[0] == 0x1F && buf[1] == 0x9D)) +} + +func Lz(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x4C && buf[1] == 0x5A && + buf[2] == 0x49 && buf[3] == 0x50 +} + +func Rpm(buf []byte) bool { + return len(buf) > 96 && + buf[0] == 0xED && buf[1] == 0xAB && + buf[2] == 0xEE && buf[3] == 0xDB +} + +func Elf(buf []byte) bool { + return len(buf) > 52 && + buf[0] == 0x7F && buf[1] == 0x45 && + buf[2] == 0x4C && buf[3] == 0x46 +} + +func Dcm(buf []byte) bool { + return len(buf) > 131 && + buf[128] == 0x44 && buf[129] == 0x49 && + buf[130] == 0x43 && buf[131] == 0x4D +} + +func Iso(buf []byte) bool { + return len(buf) > 32773 && + buf[32769] == 0x43 && buf[32770] == 0x44 && + buf[32771] == 0x30 && buf[32772] == 0x30 && + buf[32773] == 0x31 +} diff --git a/vendor/github.com/h2non/filetype/matchers/audio.go b/vendor/github.com/h2non/filetype/matchers/audio.go new file mode 100644 index 00000000000..6d532630af0 --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/audio.go @@ -0,0 +1,75 @@ +package matchers + +var ( + TypeMidi = newType("mid", "audio/midi") + TypeMp3 = newType("mp3", "audio/mpeg") + TypeM4a = newType("m4a", "audio/m4a") + TypeOgg = newType("ogg", "audio/ogg") + TypeFlac = newType("flac", "audio/x-flac") + TypeWav = newType("wav", "audio/x-wav") + TypeAmr = newType("amr", "audio/amr") + TypeAac = newType("aac", "audio/aac") +) + +var Audio = Map{ + TypeMidi: Midi, + TypeMp3: Mp3, + TypeM4a: M4a, + TypeOgg: Ogg, + TypeFlac: Flac, + TypeWav: Wav, + TypeAmr: Amr, + TypeAac: Aac, +} + +func Midi(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x4D && buf[1] == 0x54 && + buf[2] == 0x68 && buf[3] == 0x64 +} + +func Mp3(buf []byte) bool { + return len(buf) > 2 && + ((buf[0] == 0x49 && buf[1] == 0x44 && buf[2] == 0x33) || + (buf[0] == 0xFF && buf[1] == 0xfb)) +} + +func M4a(buf []byte) bool { + return len(buf) > 10 && + ((buf[4] == 0x66 && buf[5] == 0x74 && buf[6] == 0x79 && + buf[7] == 0x70 && buf[8] == 0x4D && buf[9] == 0x34 && buf[10] == 0x41) || + (buf[0] == 0x4D && buf[1] == 0x34 && buf[2] == 0x41 && buf[3] == 0x20)) +} + +func Ogg(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x4F && buf[1] == 0x67 && + buf[2] == 0x67 && buf[3] == 0x53 +} + +func Flac(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x66 && buf[1] == 0x4C && + buf[2] == 0x61 && buf[3] == 0x43 +} + +func Wav(buf []byte) bool { + return len(buf) > 11 && + buf[0] == 0x52 && buf[1] == 0x49 && + buf[2] == 0x46 && buf[3] == 0x46 && + buf[8] == 0x57 && buf[9] == 0x41 && + buf[10] == 0x56 && buf[11] == 0x45 +} + +func Amr(buf []byte) bool { + return len(buf) > 11 && + buf[0] == 0x23 && buf[1] == 0x21 && + buf[2] == 0x41 && buf[3] == 0x4D && + buf[4] == 0x52 && buf[5] == 0x0A +} + +func Aac(buf []byte) bool { + return len(buf) > 1 && + ((buf[0] == 0xFF && buf[1] == 0xF1) || + (buf[0] == 0xFF && buf[1] == 0xF9)) +} diff --git a/vendor/github.com/h2non/filetype/matchers/document.go b/vendor/github.com/h2non/filetype/matchers/document.go new file mode 100644 index 00000000000..0a27217cf5d --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/document.go @@ -0,0 +1,184 @@ +package matchers + +import ( + "bytes" + "encoding/binary" +) + +var ( + TypeDoc = newType("doc", "application/msword") + TypeDocx = newType("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document") + TypeXls = newType("xls", "application/vnd.ms-excel") + TypeXlsx = newType("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + TypePpt = newType("ppt", "application/vnd.ms-powerpoint") + TypePptx = newType("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation") +) + +var Document = Map{ + TypeDoc: Doc, + TypeDocx: Docx, + TypeXls: Xls, + TypeXlsx: Xlsx, + TypePpt: Ppt, + TypePptx: Pptx, +} + +type docType int + +const ( + TYPE_DOC docType = iota + TYPE_DOCX + TYPE_XLS + TYPE_XLSX + TYPE_PPT + TYPE_PPTX + TYPE_OOXML +) + +func Doc(buf []byte) bool { + return len(buf) > 7 && + buf[0] == 0xD0 && buf[1] == 0xCF && + buf[2] == 0x11 && buf[3] == 0xE0 && + buf[4] == 0xA1 && buf[5] == 0xB1 && + buf[6] == 0x1A && buf[7] == 0xE1 +} + +func Docx(buf []byte) bool { + typ, ok := msooxml(buf) + return ok && typ == TYPE_DOCX +} + +func Xls(buf []byte) bool { + return len(buf) > 7 && + buf[0] == 0xD0 && buf[1] == 0xCF && + buf[2] == 0x11 && buf[3] == 0xE0 && + buf[4] == 0xA1 && buf[5] == 0xB1 && + buf[6] == 0x1A && buf[7] == 0xE1 +} + +func Xlsx(buf []byte) bool { + typ, ok := msooxml(buf) + return ok && typ == TYPE_XLSX +} + +func Ppt(buf []byte) bool { + return len(buf) > 7 && + buf[0] == 0xD0 && buf[1] == 0xCF && + buf[2] == 0x11 && buf[3] == 0xE0 && + buf[4] == 0xA1 && buf[5] == 0xB1 && + buf[6] == 0x1A && buf[7] == 0xE1 +} + +func Pptx(buf []byte) bool { + typ, ok := msooxml(buf) + return ok && typ == TYPE_PPTX +} + +func msooxml(buf []byte) (typ docType, found bool) { + signature := []byte{'P', 'K', 0x03, 0x04} + + // start by checking for ZIP local file header signature + if ok := compareBytes(buf, signature, 0); !ok { + return + } + + // make sure the first file is correct + if v, ok := checkMSOoml(buf, 0x1E); ok { + return v, ok + } + + if !compareBytes(buf, []byte("[Content_Types].xml"), 0x1E) && + !compareBytes(buf, []byte("_rels/.rels"), 0x1E) && + !compareBytes(buf, []byte("docProps"), 0x1E) { + return + } + + // skip to the second local file header + // since some documents include a 520-byte extra field following the file + // header, we need to scan for the next header + startOffset := int(binary.LittleEndian.Uint32(buf[18:22]) + 49) + idx := search(buf, startOffset, 6000) + if idx == -1 { + return + } + + // now skip to the *third* local file header; again, we need to scan due to a + // 520-byte extra field following the file header + startOffset += idx + 4 + 26 + idx = search(buf, startOffset, 6000) + if idx == -1 { + return + } + + // and check the subdirectory name to determine which type of OOXML + // file we have. Correct the mimetype with the registered ones: + // http://technet.microsoft.com/en-us/library/cc179224.aspx + startOffset += idx + 4 + 26 + if typ, ok := checkMSOoml(buf, startOffset); ok { + return typ, ok + } + + // OpenOffice/Libreoffice orders ZIP entry differently, so check the 4th file + startOffset += 26 + idx = search(buf, startOffset, 6000) + if idx == -1 { + return TYPE_OOXML, true + } + + startOffset += idx + 4 + 26 + if typ, ok := checkMSOoml(buf, startOffset); ok { + return typ, ok + } else { + return TYPE_OOXML, true + } +} + +func compareBytes(slice, subSlice []byte, startOffset int) bool { + sl := len(subSlice) + + if startOffset+sl > len(slice) { + return false + } + + s := slice[startOffset : startOffset+sl] + for i := range s { + if subSlice[i] != s[i] { + return false + } + } + + return true +} + +func checkMSOoml(buf []byte, offset int) (typ docType, ok bool) { + ok = true + + switch { + case compareBytes(buf, []byte("word/"), offset): + typ = TYPE_DOCX + case compareBytes(buf, []byte("ppt/"), offset): + typ = TYPE_PPTX + case compareBytes(buf, []byte("xl/"), offset): + typ = TYPE_XLSX + default: + ok = false + } + + return +} + +func search(buf []byte, start, rangeNum int) int { + length := len(buf) + end := start + rangeNum + signature := []byte{'P', 'K', 0x03, 0x04} + + if end > length { + end = length + } + + if start >= end { + return -1 + } + + return bytes.Index(buf[start:end], signature) +} diff --git a/vendor/github.com/h2non/filetype/matchers/font.go b/vendor/github.com/h2non/filetype/matchers/font.go new file mode 100644 index 00000000000..f39171675e9 --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/font.go @@ -0,0 +1,45 @@ +package matchers + +var ( + TypeWoff = newType("woff", "application/font-woff") + TypeWoff2 = newType("woff2", "application/font-woff") + TypeTtf = newType("ttf", "application/font-sfnt") + TypeOtf = newType("otf", "application/font-sfnt") +) + +var Font = Map{ + TypeWoff: Woff, + TypeWoff2: Woff2, + TypeTtf: Ttf, + TypeOtf: Otf, +} + +func Woff(buf []byte) bool { + return len(buf) > 7 && + buf[0] == 0x77 && buf[1] == 0x4F && + buf[2] == 0x46 && buf[3] == 0x46 && + buf[4] == 0x00 && buf[5] == 0x01 && + buf[6] == 0x00 && buf[7] == 0x00 +} + +func Woff2(buf []byte) bool { + return len(buf) > 7 && + buf[0] == 0x77 && buf[1] == 0x4F && + buf[2] == 0x46 && buf[3] == 0x32 && + buf[4] == 0x00 && buf[5] == 0x01 && + buf[6] == 0x00 && buf[7] == 0x00 +} + +func Ttf(buf []byte) bool { + return len(buf) > 4 && + buf[0] == 0x00 && buf[1] == 0x01 && + buf[2] == 0x00 && buf[3] == 0x00 && + buf[4] == 0x00 +} + +func Otf(buf []byte) bool { + return len(buf) > 4 && + buf[0] == 0x4F && buf[1] == 0x54 && + buf[2] == 0x54 && buf[3] == 0x4F && + buf[4] == 0x00 +} diff --git a/vendor/github.com/h2non/filetype/matchers/image.go b/vendor/github.com/h2non/filetype/matchers/image.go new file mode 100644 index 00000000000..6a5eefce3eb --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/image.go @@ -0,0 +1,143 @@ +package matchers + +import "github.com/h2non/filetype/matchers/isobmff" + +var ( + TypeJpeg = newType("jpg", "image/jpeg") + TypeJpeg2000 = newType("jp2", "image/jp2") + TypePng = newType("png", "image/png") + TypeGif = newType("gif", "image/gif") + TypeWebp = newType("webp", "image/webp") + TypeCR2 = newType("cr2", "image/x-canon-cr2") + TypeTiff = newType("tif", "image/tiff") + TypeBmp = newType("bmp", "image/bmp") + TypeJxr = newType("jxr", "image/vnd.ms-photo") + TypePsd = newType("psd", "image/vnd.adobe.photoshop") + TypeIco = newType("ico", "image/x-icon") + TypeHeif = newType("heif", "image/heif") + TypeDwg = newType("dwg", "image/vnd.dwg") +) + +var Image = Map{ + TypeJpeg: Jpeg, + TypeJpeg2000: Jpeg2000, + TypePng: Png, + TypeGif: Gif, + TypeWebp: Webp, + TypeCR2: CR2, + TypeTiff: Tiff, + TypeBmp: Bmp, + TypeJxr: Jxr, + TypePsd: Psd, + TypeIco: Ico, + TypeHeif: Heif, + TypeDwg: Dwg, +} + +func Jpeg(buf []byte) bool { + return len(buf) > 2 && + buf[0] == 0xFF && + buf[1] == 0xD8 && + buf[2] == 0xFF +} + +func Jpeg2000(buf []byte) bool { + return len(buf) > 12 && + buf[0] == 0x0 && + buf[1] == 0x0 && + buf[2] == 0x0 && + buf[3] == 0xC && + buf[4] == 0x6A && + buf[5] == 0x50 && + buf[6] == 0x20 && + buf[7] == 0x20 && + buf[8] == 0xD && + buf[9] == 0xA && + buf[10] == 0x87 && + buf[11] == 0xA && + buf[12] == 0x0 +} + +func Png(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x89 && buf[1] == 0x50 && + buf[2] == 0x4E && buf[3] == 0x47 +} + +func Gif(buf []byte) bool { + return len(buf) > 2 && + buf[0] == 0x47 && buf[1] == 0x49 && buf[2] == 0x46 +} + +func Webp(buf []byte) bool { + return len(buf) > 11 && + buf[8] == 0x57 && buf[9] == 0x45 && + buf[10] == 0x42 && buf[11] == 0x50 +} + +func CR2(buf []byte) bool { + return len(buf) > 9 && + ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || + (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) && + buf[8] == 0x43 && buf[9] == 0x52 +} + +func Tiff(buf []byte) bool { + return len(buf) > 9 && + ((buf[0] == 0x49 && buf[1] == 0x49 && buf[2] == 0x2A && buf[3] == 0x0) || + (buf[0] == 0x4D && buf[1] == 0x4D && buf[2] == 0x0 && buf[3] == 0x2A)) && + // Differentiate Tiff from CR2 + buf[8] != 0x43 && buf[9] != 0x52 +} + +func Bmp(buf []byte) bool { + return len(buf) > 1 && + buf[0] == 0x42 && + buf[1] == 0x4D +} + +func Jxr(buf []byte) bool { + return len(buf) > 2 && + buf[0] == 0x49 && + buf[1] == 0x49 && + buf[2] == 0xBC +} + +func Psd(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x38 && buf[1] == 0x42 && + buf[2] == 0x50 && buf[3] == 0x53 +} + +func Ico(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x00 && buf[1] == 0x00 && + buf[2] == 0x01 && buf[3] == 0x00 +} + +func Heif(buf []byte) bool { + if !isobmff.IsISOBMFF(buf) { + return false + } + + majorBrand, _, compatibleBrands := isobmff.GetFtyp(buf) + if majorBrand == "heic" { + return true + } + + if majorBrand == "mif1" || majorBrand == "msf1" { + for _, compatibleBrand := range compatibleBrands { + if compatibleBrand == "heic" { + return true + } + } + } + + return false +} + +func Dwg(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x41 && buf[1] == 0x43 && + buf[2] == 0x31 && buf[3] == 0x30 +} diff --git a/vendor/github.com/h2non/filetype/matchers/isobmff/isobmff.go b/vendor/github.com/h2non/filetype/matchers/isobmff/isobmff.go new file mode 100644 index 00000000000..b3e39bf59a6 --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/isobmff/isobmff.go @@ -0,0 +1,37 @@ +package isobmff + +import "encoding/binary" + +// IsISOBMFF checks whether the given buffer represents ISO Base Media File Format data +func IsISOBMFF(buf []byte) bool { + if len(buf) < 16 || string(buf[4:8]) != "ftyp" { + return false + } + + if ftypLength := binary.BigEndian.Uint32(buf[0:4]); len(buf) < int(ftypLength) { + return false + } + + return true +} + +// GetFtyp returns the major brand, minor version and compatible brands of the ISO-BMFF data +func GetFtyp(buf []byte) (string, string, []string) { + if len(buf) < 17 { + return "", "", []string{""} + } + + ftypLength := binary.BigEndian.Uint32(buf[0:4]) + + majorBrand := string(buf[8:12]) + minorVersion := string(buf[12:16]) + + compatibleBrands := []string{} + for i := 16; i < int(ftypLength); i += 4 { + if len(buf) >= (i + 4) { + compatibleBrands = append(compatibleBrands, string(buf[i:i+4])) + } + } + + return majorBrand, minorVersion, compatibleBrands +} diff --git a/vendor/github.com/h2non/filetype/matchers/matchers.go b/vendor/github.com/h2non/filetype/matchers/matchers.go new file mode 100644 index 00000000000..20d74d080d8 --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/matchers.go @@ -0,0 +1,51 @@ +package matchers + +import ( + "github.com/h2non/filetype/types" +) + +// Internal shortcut to NewType +var newType = types.NewType + +// Matcher function interface as type alias +type Matcher func([]byte) bool + +// Type interface to store pairs of type with its matcher function +type Map map[types.Type]Matcher + +// Type specific matcher function interface +type TypeMatcher func([]byte) types.Type + +// Store registered file type matchers +var Matchers = make(map[types.Type]TypeMatcher) +var MatcherKeys []types.Type + +// Create and register a new type matcher function +func NewMatcher(kind types.Type, fn Matcher) TypeMatcher { + matcher := func(buf []byte) types.Type { + if fn(buf) { + return kind + } + return types.Unknown + } + + Matchers[kind] = matcher + // prepend here so any user defined matchers get added first + MatcherKeys = append([]types.Type{kind}, MatcherKeys...) + return matcher +} + +func register(matchers ...Map) { + MatcherKeys = MatcherKeys[:0] + for _, m := range matchers { + for kind, matcher := range m { + NewMatcher(kind, matcher) + } + } +} + +func init() { + // Arguments order is intentional + // Archive files will be checked last due to prepend above in func NewMatcher + register(Archive, Document, Font, Audio, Video, Image, Application) +} diff --git a/vendor/github.com/h2non/filetype/matchers/video.go b/vendor/github.com/h2non/filetype/matchers/video.go new file mode 100644 index 00000000000..e97cf28a11d --- /dev/null +++ b/vendor/github.com/h2non/filetype/matchers/video.go @@ -0,0 +1,145 @@ +package matchers + +import "bytes" + +var ( + TypeMp4 = newType("mp4", "video/mp4") + TypeM4v = newType("m4v", "video/x-m4v") + TypeMkv = newType("mkv", "video/x-matroska") + TypeWebm = newType("webm", "video/webm") + TypeMov = newType("mov", "video/quicktime") + TypeAvi = newType("avi", "video/x-msvideo") + TypeWmv = newType("wmv", "video/x-ms-wmv") + TypeMpeg = newType("mpg", "video/mpeg") + TypeFlv = newType("flv", "video/x-flv") + Type3gp = newType("3gp", "video/3gpp") +) + +var Video = Map{ + TypeMp4: Mp4, + TypeM4v: M4v, + TypeMkv: Mkv, + TypeWebm: Webm, + TypeMov: Mov, + TypeAvi: Avi, + TypeWmv: Wmv, + TypeMpeg: Mpeg, + TypeFlv: Flv, + Type3gp: Match3gp, +} + +func M4v(buf []byte) bool { + return len(buf) > 10 && + buf[4] == 0x66 && buf[5] == 0x74 && + buf[6] == 0x79 && buf[7] == 0x70 && + buf[8] == 0x4D && buf[9] == 0x34 && + buf[10] == 0x56 +} + +func Mkv(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x1A && buf[1] == 0x45 && + buf[2] == 0xDF && buf[3] == 0xA3 && + containsMatroskaSignature(buf, []byte{'m', 'a', 't', 'r', 'o', 's', 'k', 'a'}) +} + +func Webm(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x1A && buf[1] == 0x45 && + buf[2] == 0xDF && buf[3] == 0xA3 && + containsMatroskaSignature(buf, []byte{'w', 'e', 'b', 'm'}) +} + +func Mov(buf []byte) bool { + return len(buf) > 15 && ((buf[0] == 0x0 && buf[1] == 0x0 && + buf[2] == 0x0 && buf[3] == 0x14 && + buf[4] == 0x66 && buf[5] == 0x74 && + buf[6] == 0x79 && buf[7] == 0x70) || + (buf[4] == 0x6d && buf[5] == 0x6f && buf[6] == 0x6f && buf[7] == 0x76) || + (buf[4] == 0x6d && buf[5] == 0x64 && buf[6] == 0x61 && buf[7] == 0x74) || + (buf[12] == 0x6d && buf[13] == 0x64 && buf[14] == 0x61 && buf[15] == 0x74)) +} + +func Avi(buf []byte) bool { + return len(buf) > 10 && + buf[0] == 0x52 && buf[1] == 0x49 && + buf[2] == 0x46 && buf[3] == 0x46 && + buf[8] == 0x41 && buf[9] == 0x56 && + buf[10] == 0x49 +} + +func Wmv(buf []byte) bool { + return len(buf) > 9 && + buf[0] == 0x30 && buf[1] == 0x26 && + buf[2] == 0xB2 && buf[3] == 0x75 && + buf[4] == 0x8E && buf[5] == 0x66 && + buf[6] == 0xCF && buf[7] == 0x11 && + buf[8] == 0xA6 && buf[9] == 0xD9 +} + +func Mpeg(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x0 && buf[1] == 0x0 && + buf[2] == 0x1 && buf[3] >= 0xb0 && + buf[3] <= 0xbf +} + +func Flv(buf []byte) bool { + return len(buf) > 3 && + buf[0] == 0x46 && buf[1] == 0x4C && + buf[2] == 0x56 && buf[3] == 0x01 +} + +func Mp4(buf []byte) bool { + return len(buf) > 11 && + (buf[4] == 'f' && buf[5] == 't' && buf[6] == 'y' && buf[7] == 'p') && + ((buf[8] == 'a' && buf[9] == 'v' && buf[10] == 'c' && buf[11] == '1') || + (buf[8] == 'd' && buf[9] == 'a' && buf[10] == 's' && buf[11] == 'h') || + (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '2') || + (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '3') || + (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '4') || + (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '5') || + (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == '6') || + (buf[8] == 'i' && buf[9] == 's' && buf[10] == 'o' && buf[11] == 'm') || + (buf[8] == 'm' && buf[9] == 'm' && buf[10] == 'p' && buf[11] == '4') || + (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '4' && buf[11] == '1') || + (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '4' && buf[11] == '2') || + (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '4' && buf[11] == 'v') || + (buf[8] == 'm' && buf[9] == 'p' && buf[10] == '7' && buf[11] == '1') || + (buf[8] == 'M' && buf[9] == 'S' && buf[10] == 'N' && buf[11] == 'V') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'A' && buf[11] == 'S') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'C') || + (buf[8] == 'N' && buf[9] == 'S' && buf[10] == 'D' && buf[11] == 'C') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'H') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'M') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'P') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'S' && buf[11] == 'S') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'C') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'H') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'M') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'P') || + (buf[8] == 'N' && buf[9] == 'D' && buf[10] == 'X' && buf[11] == 'S') || + (buf[8] == 'F' && buf[9] == '4' && buf[10] == 'V' && buf[11] == ' ') || + (buf[8] == 'F' && buf[9] == '4' && buf[10] == 'P' && buf[11] == ' ')) +} + +func Match3gp(buf []byte) bool { + return len(buf) > 10 && + buf[4] == 0x66 && buf[5] == 0x74 && buf[6] == 0x79 && + buf[7] == 0x70 && buf[8] == 0x33 && buf[9] == 0x67 && + buf[10] == 0x70 +} + +func containsMatroskaSignature(buf, subType []byte) bool { + limit := 4096 + if len(buf) < limit { + limit = len(buf) + } + + index := bytes.Index(buf[:limit], subType) + if index < 3 { + return false + } + + return buf[index-3] == 0x42 && buf[index-2] == 0x82 +} diff --git a/vendor/github.com/h2non/filetype/types/defaults.go b/vendor/github.com/h2non/filetype/types/defaults.go new file mode 100644 index 00000000000..0d985a05d68 --- /dev/null +++ b/vendor/github.com/h2non/filetype/types/defaults.go @@ -0,0 +1,4 @@ +package types + +// Unknown default type +var Unknown = NewType("unknown", "") diff --git a/vendor/github.com/h2non/filetype/types/mime.go b/vendor/github.com/h2non/filetype/types/mime.go new file mode 100644 index 00000000000..fe8ea822e55 --- /dev/null +++ b/vendor/github.com/h2non/filetype/types/mime.go @@ -0,0 +1,14 @@ +package types + +// MIME stores the file MIME type values +type MIME struct { + Type string + Subtype string + 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/vendor/github.com/h2non/filetype/types/split.go b/vendor/github.com/h2non/filetype/types/split.go new file mode 100644 index 00000000000..68a5a8b3b52 --- /dev/null +++ b/vendor/github.com/h2non/filetype/types/split.go @@ -0,0 +1,11 @@ +package types + +import "strings" + +func splitMime(s string) (string, string) { + x := strings.Split(s, "/") + if len(x) > 1 { + return x[0], x[1] + } + return x[0], "" +} diff --git a/vendor/github.com/h2non/filetype/types/type.go b/vendor/github.com/h2non/filetype/types/type.go new file mode 100644 index 00000000000..5cf7dfc4bbc --- /dev/null +++ b/vendor/github.com/h2non/filetype/types/type.go @@ -0,0 +1,16 @@ +package types + +// Type represents a file MIME type and its extension +type Type struct { + MIME MIME + Extension string +} + +// NewType creates a new Type +func NewType(ext, mime string) Type { + t := Type{ + MIME: NewMIME(mime), + Extension: ext, + } + return Add(t) +} diff --git a/vendor/github.com/h2non/filetype/types/types.go b/vendor/github.com/h2non/filetype/types/types.go new file mode 100644 index 00000000000..27d433eec31 --- /dev/null +++ b/vendor/github.com/h2non/filetype/types/types.go @@ -0,0 +1,18 @@ +package types + +var Types = make(map[string]Type) + +// Add registers a new type in the package +func Add(t Type) Type { + Types[t.Extension] = t + return t +} + +// Get retrieves a Type by extension +func Get(ext string) Type { + kind := Types[ext] + if kind.Extension != "" { + return kind + } + return Unknown +} diff --git a/vendor/github.com/h2non/filetype/version.go b/vendor/github.com/h2non/filetype/version.go new file mode 100644 index 00000000000..fd46bf3ffc0 --- /dev/null +++ b/vendor/github.com/h2non/filetype/version.go @@ -0,0 +1,4 @@ +package filetype + +// Version exposes the current package version. +const Version = "1.0.12" diff --git a/vendor/modules.txt b/vendor/modules.txt index 8105121f6f1..ba039152a6a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -583,6 +583,11 @@ github.com/gorilla/websocket github.com/grpc-ecosystem/grpc-gateway/internal github.com/grpc-ecosystem/grpc-gateway/runtime github.com/grpc-ecosystem/grpc-gateway/utilities +# github.com/h2non/filetype v1.0.12 +github.com/h2non/filetype +github.com/h2non/filetype/matchers +github.com/h2non/filetype/matchers/isobmff +github.com/h2non/filetype/types # github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/errwrap # github.com/hashicorp/go-multierror v1.0.0