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