Commit b155e79f authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

mime: style, perf, and test updates to case-insensitive lookups

Only grab the lock once, don't allocate, add more tests.

LGTM=ruiu
R=ruiu, josharian
CC=golang-codereviews
https://golang.org/cl/139780043
parent 558bd8e1
...@@ -13,12 +13,7 @@ import ( ...@@ -13,12 +13,7 @@ import (
var ( var (
mimeLock sync.RWMutex mimeLock sync.RWMutex
mimeTypes = map[string]string{} mimeTypesLower = map[string]string{
mimeTypesLower = map[string]string{}
)
func init() {
mimeTypes := map[string]string{
".css": "text/css; charset=utf-8", ".css": "text/css; charset=utf-8",
".gif": "image/gif", ".gif": "image/gif",
".htm": "text/html; charset=utf-8", ".htm": "text/html; charset=utf-8",
...@@ -29,12 +24,21 @@ func init() { ...@@ -29,12 +24,21 @@ func init() {
".png": "image/png", ".png": "image/png",
".xml": "text/xml; charset=utf-8", ".xml": "text/xml; charset=utf-8",
} }
for ext, typ := range mimeTypes { mimeTypes = clone(mimeTypesLower)
AddExtensionType(ext, typ) )
func clone(m map[string]string) map[string]string {
m2 := make(map[string]string, len(m))
for k, v := range m {
m2[k] = v
if strings.ToLower(k) != k {
panic("keys in mimeTypesLower must be lowercase")
}
} }
return m2
} }
var once sync.Once var once sync.Once // guards initMime
// TypeByExtension returns the MIME type associated with the file extension ext. // TypeByExtension returns the MIME type associated with the file extension ext.
// The extension ext should begin with a leading dot, as in ".html". // The extension ext should begin with a leading dot, as in ".html".
...@@ -50,21 +54,41 @@ var once sync.Once ...@@ -50,21 +54,41 @@ var once sync.Once
// /etc/apache2/mime.types // /etc/apache2/mime.types
// /etc/apache/mime.types // /etc/apache/mime.types
// //
// Windows system MIME types are extracted from registry. // On Windows, MIME types are extracted from the registry.
// //
// Text types have the charset parameter set to "utf-8" by default. // Text types have the charset parameter set to "utf-8" by default.
func TypeByExtension(ext string) string { func TypeByExtension(ext string) string {
once.Do(initMime) once.Do(initMime)
mimeLock.RLock() mimeLock.RLock()
typename := mimeTypes[ext] defer mimeLock.RUnlock()
mimeLock.RUnlock()
if typename == "" { // Case-sensitive lookup.
lower := strings.ToLower(ext) v := mimeTypes[ext]
mimeLock.RLock() if v != "" {
typename = mimeTypesLower[lower] return v
mimeLock.RUnlock() }
// Case-insensitive lookup.
// Optimistically assume a short ASCII extension and be
// allocation-free in that case.
var buf [10]byte
lower := buf[:0]
const utf8RuneSelf = 0x80 // from utf8 package, but not importing it.
for i := 0; i < len(ext); i++ {
c := ext[i]
if c >= utf8RuneSelf {
// Slow path.
return mimeTypesLower[strings.ToLower(ext)]
}
if 'A' <= c && c <= 'Z' {
lower = append(lower, c+('a'-'A'))
} else {
lower = append(lower, c)
}
} }
return typename // The conversion from []byte to string doesn't allocate in
// a map lookup.
return mimeTypesLower[string(lower)]
} }
// AddExtensionType sets the MIME type associated with // AddExtensionType sets the MIME type associated with
......
...@@ -19,17 +19,36 @@ func TestTypeByExtension(t *testing.T) { ...@@ -19,17 +19,36 @@ func TestTypeByExtension(t *testing.T) {
} }
} }
func TestCustomExtension(t *testing.T) { func TestTypeByExtensionCase(t *testing.T) {
custom := "test/test; charset=iso-8859-1" const custom = "test/test; charset=iso-8859-1"
if error := AddExtensionType(".tesT", custom); error != nil { const caps = "test/test; WAS=ALLCAPS"
t.Fatalf("error %s for AddExtension(%s)", error, custom) if err := AddExtensionType(".TEST", caps); err != nil {
} t.Fatalf("error %s for AddExtension(%s)", err, custom)
// test with same capitalization }
if registered := TypeByExtension(".tesT"); registered != custom { if err := AddExtensionType(".tesT", custom); err != nil {
t.Fatalf("registered %s instead of %s", registered, custom) t.Fatalf("error %s for AddExtension(%s)", err, custom)
} }
// test with different capitalization
if registered := TypeByExtension(".Test"); registered != custom { // case-sensitive lookup
t.Fatalf("registered %s instead of %s", registered, custom) if got := TypeByExtension(".tesT"); got != custom {
t.Fatalf("for .tesT, got %q; want %q", got, custom)
}
if got := TypeByExtension(".TEST"); got != caps {
t.Fatalf("for .TEST, got %q; want %s", got, caps)
}
// case-insensitive
if got := TypeByExtension(".TesT"); got != custom {
t.Fatalf("for .TesT, got %q; want %q", got, custom)
}
}
func TestLookupMallocs(t *testing.T) {
n := testing.AllocsPerRun(10000, func() {
TypeByExtension(".html")
TypeByExtension(".HtML")
})
if n > 0 {
t.Errorf("allocs = %v; want 0", n)
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment