package http
+import (
+ "bytes"
+)
+
// Content-type sniffing algorithm.
-// http://tools.ietf.org/html/draft-ietf-websec-mime-sniff-03
+// References in this file refer to this draft specification:
+// http://tools.ietf.org/html/draft-ietf-websec-mime-sniff-03
// The algorithm prefers to use sniffLen bytes to make its decision.
-const sniffLen = 1024
+const sniffLen = 512
+
+// DetectContentType returns the sniffed Content-Type string
+// for the given data. This function always returns a valid MIME type.
+func DetectContentType(data []byte) string {
+ if len(data) > sniffLen {
+ data = data[:sniffLen]
+ }
+
+ // Index of the first non-whitespace byte in data.
+ firstNonWS := 0
+ for ; firstNonWS < len(data) && isWS(data[firstNonWS]); firstNonWS++ {
+ }
+
+ for _, sig := range sniffSignatures {
+ if ct := sig.match(data, firstNonWS); ct != "" {
+ return ct
+ }
+ }
+
+ return "application/octet-stream" // fallback
+}
+
+func isWS(b byte) bool {
+ return bytes.IndexByte([]byte("\t\n\x0C\n "), b) != -1
+}
+
+type sniffSig interface {
+ // match returns the MIME type of the data, or "" if unknown.
+ match(data []byte, firstNonWS int) string
+}
+
+// Data matching the table in section 6.
+var sniffSignatures = []sniffSig{
+ htmlSig([]byte("<!DOCTYPE HTML")),
+ htmlSig([]byte("<HTML")),
+ htmlSig([]byte("<HEAD")),
+ htmlSig([]byte("<SCRIPT")),
+ htmlSig([]byte("<IFRAME")),
+ htmlSig([]byte("<H1")),
+ htmlSig([]byte("<DIV")),
+ htmlSig([]byte("<FONT")),
+ htmlSig([]byte("<TABLE")),
+ htmlSig([]byte("<A")),
+ htmlSig([]byte("<STYLE")),
+ htmlSig([]byte("<TITLE")),
+ htmlSig([]byte("<B")),
+ htmlSig([]byte("<BODY")),
+ htmlSig([]byte("<BR")),
+ htmlSig([]byte("<P")),
+ htmlSig([]byte("<!--")),
+
+ &maskedSig{mask: []byte("\xFF\xFF\xFF\xFF\xFF"), pat: []byte("<?xml"), skipWS: true, ct: "text/xml; charset=utf-8"},
+
+ &exactSig{[]byte("%PDF-"), "application/pdf"},
+ &exactSig{[]byte("%!PS-Adobe-"), "application/postscript"},
+
+ // UTF BOMs.
+ &maskedSig{mask: []byte("\xFF\xFF\x00\x00"), pat: []byte("\xFE\xFF\x00\x00"), ct: "text/plain; charset=utf-16be"},
+ &maskedSig{mask: []byte("\xFF\xFF\x00\x00"), pat: []byte("\xFF\xFE\x00\x00"), ct: "text/plain; charset=utf-16le"},
+ &maskedSig{mask: []byte("\xFF\xFF\xFF\x00"), pat: []byte("\xEF\xBB\xBF\x00"), ct: "text/plain; charset=utf-8"},
+
+ &exactSig{[]byte("GIF87a"), "image/gif"},
+ &exactSig{[]byte("GIF89a"), "image/gif"},
+ &exactSig{[]byte("\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"), "image/png"},
+ &exactSig{[]byte("\xFF\xD8\xFF"), "image/jpeg"},
+ &exactSig{[]byte("BM"), "image/bmp"},
+ &maskedSig{
+ mask: []byte("\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF"),
+ pat: []byte("RIFF\x00\x00\x00\x00WEBPVP"),
+ ct: "image/webp",
+ },
+ &exactSig{[]byte("\x00\x00\x01\x00"), "image/vnd.microsoft.icon"},
+ &exactSig{[]byte("\x4F\x67\x67\x53\x00"), "application/ogg"},
+ &maskedSig{
+ mask: []byte("\xFF\xFF\xFF\xFF\x00\x00\x00\x00\xFF\xFF\xFF\xFF"),
+ pat: []byte("RIFF\x00\x00\x00\x00WAVE"),
+ ct: "audio/wave",
+ },
+ &exactSig{[]byte("\x1A\x45\xDF\xA3"), "video/webm"},
+ &exactSig{[]byte("\x52\x61\x72\x20\x1A\x07\x00"), "application/x-rar-compressed"},
+ &exactSig{[]byte("\x50\x4B\x03\x04"), "application/zip"},
+ &exactSig{[]byte("\x1F\x8B\x08"), "application/x-gzip"},
-// detectContentType returns the sniffed Content-Type string
-// for the given data.
-func detectContentType(data []byte) string {
- // TODO(dsymonds,rsc): Implement algorithm from draft.
+ // TODO(dsymonds): MP4.
+
+ textSig(0), // should be last
+}
+
+type exactSig struct {
+ sig []byte
+ ct string
+}
+
+func (e *exactSig) match(data []byte, firstNonWS int) string {
+ if bytes.HasPrefix(data, e.sig) {
+ return e.ct
+ }
+ return ""
+}
+
+type maskedSig struct {
+ mask, pat []byte
+ skipWS bool
+ ct string
+}
+
+func (m *maskedSig) match(data []byte, firstNonWS int) string {
+ if m.skipWS {
+ data = data[firstNonWS:]
+ }
+ if len(data) < len(m.mask) {
+ return ""
+ }
+ for i, mask := range m.mask {
+ db := data[i] & mask
+ if db != m.pat[i] {
+ return ""
+ }
+ }
+ return m.ct
+}
+
+type htmlSig []byte
+
+func (h htmlSig) match(data []byte, firstNonWS int) string {
+ data = data[firstNonWS:]
+ if len(data) < len(h)+1 {
+ return ""
+ }
+ for i, b := range h {
+ db := data[i]
+ if 'A' <= b && b <= 'Z' {
+ db &= 0xDF
+ }
+ if b != db {
+ return ""
+ }
+ }
+ // Next byte must be space or right angle bracket.
+ if db := data[len(h)]; db != ' ' && db != '>' {
+ return ""
+ }
return "text/html; charset=utf-8"
}
+
+type textSig int
+
+func (textSig) match(data []byte, firstNonWS int) string {
+ // c.f. section 5, step 4.
+ for _, b := range data[firstNonWS:] {
+ switch {
+ case 0x00 <= b && b <= 0x08,
+ b == 0x0B,
+ 0x0E <= b && b <= 0x1A,
+ 0x1C <= b && b <= 0x1F:
+ return ""
+ }
+ }
+ return "text/plain; charset=utf-8"
+}
--- /dev/null
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package http
+
+import (
+ "testing"
+)
+
+var sniffTests = []struct {
+ desc string
+ data []byte
+ exp string
+}{
+ // Some nonsense.
+ {"Empty", []byte{}, "text/plain; charset=utf-8"},
+ {"Binary", []byte{1, 2, 3}, "application/octet-stream"},
+
+ {"HTML document #1", []byte(`<HtMl><bOdY>blah blah blah</body></html>`), "text/html; charset=utf-8"},
+ {"HTML document #2", []byte(`<HTML></HTML>`), "text/html; charset=utf-8"},
+ {"HTML document #3 (leading whitespace)", []byte(` <!DOCTYPE HTML>...`), "text/html; charset=utf-8"},
+
+ {"Plain text", []byte(`This is not HTML. It has ☃ though.`), "text/plain; charset=utf-8"},
+
+ {"XML", []byte("\n<?xml!"), "text/xml; charset=utf-8"},
+
+ // Image types.
+ {"GIF 87a", []byte(`GIF87a`), "image/gif"},
+ {"GIF 89a", []byte(`GIF89a...`), "image/gif"},
+}
+
+func TestSniffing(t *testing.T) {
+ for _, st := range sniffTests {
+ got := DetectContentType(st.data)
+ if got != st.exp {
+ t.Errorf("%v: sniffed as %v, want %v", st.desc, got, st.exp)
+ }
+ }
+}