Source file
src/mime/mediatype.go
Documentation: mime
1
2
3
4
5 package mime
6
7 import (
8 "errors"
9 "fmt"
10 "sort"
11 "strings"
12 "unicode"
13 )
14
15
16
17
18
19
20 func FormatMediaType(t string, param map[string]string) string {
21 var b strings.Builder
22 if slash := strings.IndexByte(t, '/'); slash == -1 {
23 if !isToken(t) {
24 return ""
25 }
26 b.WriteString(strings.ToLower(t))
27 } else {
28 major, sub := t[:slash], t[slash+1:]
29 if !isToken(major) || !isToken(sub) {
30 return ""
31 }
32 b.WriteString(strings.ToLower(major))
33 b.WriteByte('/')
34 b.WriteString(strings.ToLower(sub))
35 }
36
37 attrs := make([]string, 0, len(param))
38 for a := range param {
39 attrs = append(attrs, a)
40 }
41 sort.Strings(attrs)
42
43 for _, attribute := range attrs {
44 value := param[attribute]
45 b.WriteByte(';')
46 b.WriteByte(' ')
47 if !isToken(attribute) {
48 return ""
49 }
50 b.WriteString(strings.ToLower(attribute))
51
52 needEnc := needsEncoding(value)
53 if needEnc {
54
55 b.WriteByte('*')
56 }
57 b.WriteByte('=')
58
59 if needEnc {
60 b.WriteString("utf-8''")
61
62 offset := 0
63 for index := 0; index < len(value); index++ {
64 ch := value[index]
65
66
67 if ch <= ' ' || ch >= 0x7F ||
68 ch == '*' || ch == '\'' || ch == '%' ||
69 isTSpecial(rune(ch)) {
70
71 b.WriteString(value[offset:index])
72 offset = index + 1
73
74 b.WriteByte('%')
75 b.WriteByte(upperhex[ch>>4])
76 b.WriteByte(upperhex[ch&0x0F])
77 }
78 }
79 b.WriteString(value[offset:])
80 continue
81 }
82
83 if isToken(value) {
84 b.WriteString(value)
85 continue
86 }
87
88 b.WriteByte('"')
89 offset := 0
90 for index := 0; index < len(value); index++ {
91 character := value[index]
92 if character == '"' || character == '\\' {
93 b.WriteString(value[offset:index])
94 offset = index
95 b.WriteByte('\\')
96 }
97 }
98 b.WriteString(value[offset:])
99 b.WriteByte('"')
100 }
101 return b.String()
102 }
103
104 func checkMediaTypeDisposition(s string) error {
105 typ, rest := consumeToken(s)
106 if typ == "" {
107 return errors.New("mime: no media type")
108 }
109 if rest == "" {
110 return nil
111 }
112 if !strings.HasPrefix(rest, "/") {
113 return errors.New("mime: expected slash after first token")
114 }
115 subtype, rest := consumeToken(rest[1:])
116 if subtype == "" {
117 return errors.New("mime: expected token after slash")
118 }
119 if rest != "" {
120 return errors.New("mime: unexpected content after media subtype")
121 }
122 return nil
123 }
124
125
126
127
128 var ErrInvalidMediaParameter = errors.New("mime: invalid media parameter")
129
130
131
132
133
134
135
136
137
138
139
140 func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
141 i := strings.Index(v, ";")
142 if i == -1 {
143 i = len(v)
144 }
145 mediatype = strings.TrimSpace(strings.ToLower(v[0:i]))
146
147 err = checkMediaTypeDisposition(mediatype)
148 if err != nil {
149 return "", nil, err
150 }
151
152 params = make(map[string]string)
153
154
155
156
157 var continuation map[string]map[string]string
158
159 v = v[i:]
160 for len(v) > 0 {
161 v = strings.TrimLeftFunc(v, unicode.IsSpace)
162 if len(v) == 0 {
163 break
164 }
165 key, value, rest := consumeMediaParam(v)
166 if key == "" {
167 if strings.TrimSpace(rest) == ";" {
168
169
170 return
171 }
172
173 return mediatype, nil, ErrInvalidMediaParameter
174 }
175
176 pmap := params
177 if idx := strings.Index(key, "*"); idx != -1 {
178 baseName := key[:idx]
179 if continuation == nil {
180 continuation = make(map[string]map[string]string)
181 }
182 var ok bool
183 if pmap, ok = continuation[baseName]; !ok {
184 continuation[baseName] = make(map[string]string)
185 pmap = continuation[baseName]
186 }
187 }
188 if _, exists := pmap[key]; exists {
189
190 return "", nil, errors.New("mime: duplicate parameter name")
191 }
192 pmap[key] = value
193 v = rest
194 }
195
196
197
198 var buf strings.Builder
199 for key, pieceMap := range continuation {
200 singlePartKey := key + "*"
201 if v, ok := pieceMap[singlePartKey]; ok {
202 if decv, ok := decode2231Enc(v); ok {
203 params[key] = decv
204 }
205 continue
206 }
207
208 buf.Reset()
209 valid := false
210 for n := 0; ; n++ {
211 simplePart := fmt.Sprintf("%s*%d", key, n)
212 if v, ok := pieceMap[simplePart]; ok {
213 valid = true
214 buf.WriteString(v)
215 continue
216 }
217 encodedPart := simplePart + "*"
218 v, ok := pieceMap[encodedPart]
219 if !ok {
220 break
221 }
222 valid = true
223 if n == 0 {
224 if decv, ok := decode2231Enc(v); ok {
225 buf.WriteString(decv)
226 }
227 } else {
228 decv, _ := percentHexUnescape(v)
229 buf.WriteString(decv)
230 }
231 }
232 if valid {
233 params[key] = buf.String()
234 }
235 }
236
237 return
238 }
239
240 func decode2231Enc(v string) (string, bool) {
241 sv := strings.SplitN(v, "'", 3)
242 if len(sv) != 3 {
243 return "", false
244 }
245
246
247
248 charset := strings.ToLower(sv[0])
249 if len(charset) == 0 {
250 return "", false
251 }
252 if charset != "us-ascii" && charset != "utf-8" {
253
254 return "", false
255 }
256 encv, err := percentHexUnescape(sv[2])
257 if err != nil {
258 return "", false
259 }
260 return encv, true
261 }
262
263 func isNotTokenChar(r rune) bool {
264 return !isTokenChar(r)
265 }
266
267
268
269
270
271 func consumeToken(v string) (token, rest string) {
272 notPos := strings.IndexFunc(v, isNotTokenChar)
273 if notPos == -1 {
274 return v, ""
275 }
276 if notPos == 0 {
277 return "", v
278 }
279 return v[0:notPos], v[notPos:]
280 }
281
282
283
284
285
286
287 func consumeValue(v string) (value, rest string) {
288 if v == "" {
289 return
290 }
291 if v[0] != '"' {
292 return consumeToken(v)
293 }
294
295
296 buffer := new(strings.Builder)
297 for i := 1; i < len(v); i++ {
298 r := v[i]
299 if r == '"' {
300 return buffer.String(), v[i+1:]
301 }
302
303
304
305
306
307
308
309
310
311
312 if r == '\\' && i+1 < len(v) && isTSpecial(rune(v[i+1])) {
313 buffer.WriteByte(v[i+1])
314 i++
315 continue
316 }
317 if r == '\r' || r == '\n' {
318 return "", v
319 }
320 buffer.WriteByte(v[i])
321 }
322
323 return "", v
324 }
325
326 func consumeMediaParam(v string) (param, value, rest string) {
327 rest = strings.TrimLeftFunc(v, unicode.IsSpace)
328 if !strings.HasPrefix(rest, ";") {
329 return "", "", v
330 }
331
332 rest = rest[1:]
333 rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
334 param, rest = consumeToken(rest)
335 param = strings.ToLower(param)
336 if param == "" {
337 return "", "", v
338 }
339
340 rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
341 if !strings.HasPrefix(rest, "=") {
342 return "", "", v
343 }
344 rest = rest[1:]
345 rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
346 value, rest2 := consumeValue(rest)
347 if value == "" && rest2 == rest {
348 return "", "", v
349 }
350 rest = rest2
351 return param, value, rest
352 }
353
354 func percentHexUnescape(s string) (string, error) {
355
356 percents := 0
357 for i := 0; i < len(s); {
358 if s[i] != '%' {
359 i++
360 continue
361 }
362 percents++
363 if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
364 s = s[i:]
365 if len(s) > 3 {
366 s = s[0:3]
367 }
368 return "", fmt.Errorf("mime: bogus characters after %%: %q", s)
369 }
370 i += 3
371 }
372 if percents == 0 {
373 return s, nil
374 }
375
376 t := make([]byte, len(s)-2*percents)
377 j := 0
378 for i := 0; i < len(s); {
379 switch s[i] {
380 case '%':
381 t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
382 j++
383 i += 3
384 default:
385 t[j] = s[i]
386 j++
387 i++
388 }
389 }
390 return string(t), nil
391 }
392
393 func ishex(c byte) bool {
394 switch {
395 case '0' <= c && c <= '9':
396 return true
397 case 'a' <= c && c <= 'f':
398 return true
399 case 'A' <= c && c <= 'F':
400 return true
401 }
402 return false
403 }
404
405 func unhex(c byte) byte {
406 switch {
407 case '0' <= c && c <= '9':
408 return c - '0'
409 case 'a' <= c && c <= 'f':
410 return c - 'a' + 10
411 case 'A' <= c && c <= 'F':
412 return c - 'A' + 10
413 }
414 return 0
415 }
416
View as plain text