1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package mime implements parts of the MIME spec. 6 package mime 7 8 import ( 9 "fmt" 10 "strings" 11 "sync" 12 ) 13 14 var ( 15 mimeLock sync.RWMutex // guards following 3 maps 16 mimeTypes map[string]string // ".Z" => "application/x-compress" 17 mimeTypesLower map[string]string // ".z" => "application/x-compress" 18 19 // extensions maps from MIME type to list of lowercase file 20 // extensions: "image/jpeg" => [".jpg", ".jpeg"] 21 extensions map[string][]string 22 ) 23 24 // setMimeTypes is used by initMime's non-test path, and by tests. 25 // The two maps must not be the same, or nil. 26 func setMimeTypes(lowerExt, mixExt map[string]string) { 27 if lowerExt == nil || mixExt == nil { 28 panic("nil map") 29 } 30 mimeTypesLower = lowerExt 31 mimeTypes = mixExt 32 extensions = invert(lowerExt) 33 } 34 35 var builtinTypesLower = map[string]string{ 36 ".css": "text/css; charset=utf-8", 37 ".gif": "image/gif", 38 ".htm": "text/html; charset=utf-8", 39 ".html": "text/html; charset=utf-8", 40 ".jpg": "image/jpeg", 41 ".js": "application/x-javascript", 42 ".pdf": "application/pdf", 43 ".png": "image/png", 44 ".svg": "image/svg+xml", 45 ".xml": "text/xml; charset=utf-8", 46 } 47 48 func clone(m map[string]string) map[string]string { 49 m2 := make(map[string]string, len(m)) 50 for k, v := range m { 51 m2[k] = v 52 if strings.ToLower(k) != k { 53 panic("keys in builtinTypesLower must be lowercase") 54 } 55 } 56 return m2 57 } 58 59 func invert(m map[string]string) map[string][]string { 60 m2 := make(map[string][]string, len(m)) 61 for k, v := range m { 62 justType, _, err := ParseMediaType(v) 63 if err != nil { 64 panic(err) 65 } 66 m2[justType] = append(m2[justType], k) 67 } 68 return m2 69 } 70 71 var once sync.Once // guards initMime 72 73 var testInitMime, osInitMime func() 74 75 func initMime() { 76 if fn := testInitMime; fn != nil { 77 fn() 78 } else { 79 setMimeTypes(builtinTypesLower, clone(builtinTypesLower)) 80 osInitMime() 81 } 82 } 83 84 // TypeByExtension returns the MIME type associated with the file extension ext. 85 // The extension ext should begin with a leading dot, as in ".html". 86 // When ext has no associated type, TypeByExtension returns "". 87 // 88 // Extensions are looked up first case-sensitively, then case-insensitively. 89 // 90 // The built-in table is small but on unix it is augmented by the local 91 // system's mime.types file(s) if available under one or more of these 92 // names: 93 // 94 // /etc/mime.types 95 // /etc/apache2/mime.types 96 // /etc/apache/mime.types 97 // 98 // On Windows, MIME types are extracted from the registry. 99 // 100 // Text types have the charset parameter set to "utf-8" by default. 101 func TypeByExtension(ext string) string { 102 once.Do(initMime) 103 mimeLock.RLock() 104 defer mimeLock.RUnlock() 105 106 // Case-sensitive lookup. 107 if v := mimeTypes[ext]; v != "" { 108 return v 109 } 110 111 // Case-insensitive lookup. 112 // Optimistically assume a short ASCII extension and be 113 // allocation-free in that case. 114 var buf [10]byte 115 lower := buf[:0] 116 const utf8RuneSelf = 0x80 // from utf8 package, but not importing it. 117 for i := 0; i < len(ext); i++ { 118 c := ext[i] 119 if c >= utf8RuneSelf { 120 // Slow path. 121 return mimeTypesLower[strings.ToLower(ext)] 122 } 123 if 'A' <= c && c <= 'Z' { 124 lower = append(lower, c+('a'-'A')) 125 } else { 126 lower = append(lower, c) 127 } 128 } 129 // The conversion from []byte to string doesn't allocate in 130 // a map lookup. 131 return mimeTypesLower[string(lower)] 132 } 133 134 // ExtensionsByType returns the extensions known to be associated with the MIME 135 // type typ. The returned extensions will each begin with a leading dot, as in 136 // ".html". When typ has no associated extensions, ExtensionsByType returns an 137 // nil slice. 138 func ExtensionsByType(typ string) ([]string, error) { 139 justType, _, err := ParseMediaType(typ) 140 if err != nil { 141 return nil, err 142 } 143 144 once.Do(initMime) 145 mimeLock.RLock() 146 defer mimeLock.RUnlock() 147 s, ok := extensions[justType] 148 if !ok { 149 return nil, nil 150 } 151 return append([]string{}, s...), nil 152 } 153 154 // AddExtensionType sets the MIME type associated with 155 // the extension ext to typ. The extension should begin with 156 // a leading dot, as in ".html". 157 func AddExtensionType(ext, typ string) error { 158 if !strings.HasPrefix(ext, ".") { 159 return fmt.Errorf("mime: extension %q missing leading dot", ext) 160 } 161 once.Do(initMime) 162 return setExtensionType(ext, typ) 163 } 164 165 func setExtensionType(extension, mimeType string) error { 166 justType, param, err := ParseMediaType(mimeType) 167 if err != nil { 168 return err 169 } 170 if strings.HasPrefix(mimeType, "text/") && param["charset"] == "" { 171 param["charset"] = "utf-8" 172 mimeType = FormatMediaType(mimeType, param) 173 } 174 extLower := strings.ToLower(extension) 175 176 mimeLock.Lock() 177 defer mimeLock.Unlock() 178 mimeTypes[extension] = mimeType 179 mimeTypesLower[extLower] = mimeType 180 for _, v := range extensions[justType] { 181 if v == extLower { 182 return nil 183 } 184 } 185 extensions[justType] = append(extensions[justType], extLower) 186 return nil 187 } 188