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.
15 // FormatMediaType serializes mediatype t and the parameters
16 // param as a media type conforming to RFC 2045 and RFC 2616.
17 // The type and parameter names are written in lower-case.
18 // When any of the arguments result in a standard violation then
19 // FormatMediaType returns the empty string.
20 func FormatMediaType(t string, param map[string]string) string {
22 if major, sub, ok := strings.Cut(t, "/"); !ok {
26 b.WriteString(strings.ToLower(t))
28 if !isToken(major) || !isToken(sub) {
31 b.WriteString(strings.ToLower(major))
33 b.WriteString(strings.ToLower(sub))
36 attrs := make([]string, 0, len(param))
37 for a := range param {
38 attrs = append(attrs, a)
42 for _, attribute := range attrs {
43 value := param[attribute]
46 if !isToken(attribute) {
49 b.WriteString(strings.ToLower(attribute))
51 needEnc := needsEncoding(value)
59 b.WriteString("utf-8''")
62 for index := 0; index < len(value); index++ {
64 // {RFC 2231 section 7}
65 // attribute-char := <any (US-ASCII) CHAR except SPACE, CTLs, "*", "'", "%", or tspecials>
66 if ch <= ' ' || ch >= 0x7F ||
67 ch == '*' || ch == '\'' || ch == '%' ||
68 isTSpecial(rune(ch)) {
70 b.WriteString(value[offset:index])
74 b.WriteByte(upperhex[ch>>4])
75 b.WriteByte(upperhex[ch&0x0F])
78 b.WriteString(value[offset:])
89 for index := 0; index < len(value); index++ {
90 character := value[index]
91 if character == '"' || character == '\\' {
92 b.WriteString(value[offset:index])
97 b.WriteString(value[offset:])
103 func checkMediaTypeDisposition(s string) error {
104 typ, rest := consumeToken(s)
106 return errors.New("mime: no media type")
111 if !strings.HasPrefix(rest, "/") {
112 return errors.New("mime: expected slash after first token")
114 subtype, rest := consumeToken(rest[1:])
116 return errors.New("mime: expected token after slash")
119 return errors.New("mime: unexpected content after media subtype")
124 // ErrInvalidMediaParameter is returned by ParseMediaType if
125 // the media type value was found but there was an error parsing
126 // the optional parameters
127 var ErrInvalidMediaParameter = errors.New("mime: invalid media parameter")
129 // ParseMediaType parses a media type value and any optional
130 // parameters, per RFC 1521. Media types are the values in
131 // Content-Type and Content-Disposition headers (RFC 2183).
132 // On success, ParseMediaType returns the media type converted
133 // to lowercase and trimmed of white space and a non-nil map.
134 // If there is an error parsing the optional parameter,
135 // the media type will be returned along with the error
136 // ErrInvalidMediaParameter.
137 // The returned map, params, maps from the lowercase
138 // attribute to the attribute value with its case preserved.
139 func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
140 base, _, _ := strings.Cut(v, ";")
141 mediatype = strings.TrimSpace(strings.ToLower(base))
143 err = checkMediaTypeDisposition(mediatype)
148 params = make(map[string]string)
150 // Map of base parameter name -> parameter name -> value
151 // for parameters containing a '*' character.
152 // Lazily initialized.
153 var continuation map[string]map[string]string
157 v = strings.TrimLeftFunc(v, unicode.IsSpace)
161 key, value, rest := consumeMediaParam(v)
163 if strings.TrimSpace(rest) == ";" {
164 // Ignore trailing semicolons.
169 return mediatype, nil, ErrInvalidMediaParameter
173 if baseName, _, ok := strings.Cut(key, "*"); ok {
174 if continuation == nil {
175 continuation = make(map[string]map[string]string)
178 if pmap, ok = continuation[baseName]; !ok {
179 continuation[baseName] = make(map[string]string)
180 pmap = continuation[baseName]
183 if v, exists := pmap[key]; exists && v != value {
184 // Duplicate parameter names are incorrect, but we allow them if they are equal.
185 return "", nil, errors.New("mime: duplicate parameter name")
191 // Stitch together any continuations or things with stars
192 // (i.e. RFC 2231 things with stars: "foo*0" or "foo*")
193 var buf strings.Builder
194 for key, pieceMap := range continuation {
195 singlePartKey := key + "*"
196 if v, ok := pieceMap[singlePartKey]; ok {
197 if decv, ok := decode2231Enc(v); ok {
206 simplePart := fmt.Sprintf("%s*%d", key, n)
207 if v, ok := pieceMap[simplePart]; ok {
212 encodedPart := simplePart + "*"
213 v, ok := pieceMap[encodedPart]
219 if decv, ok := decode2231Enc(v); ok {
220 buf.WriteString(decv)
223 decv, _ := percentHexUnescape(v)
224 buf.WriteString(decv)
228 params[key] = buf.String()
235 func decode2231Enc(v string) (string, bool) {
236 sv := strings.SplitN(v, "'", 3)
240 // TODO: ignoring lang in sv[1] for now. If anybody needs it we'll
241 // need to decide how to expose it in the API. But I'm not sure
242 // anybody uses it in practice.
243 charset := strings.ToLower(sv[0])
244 if len(charset) == 0 {
247 if charset != "us-ascii" && charset != "utf-8" {
248 // TODO: unsupported encoding
251 encv, err := percentHexUnescape(sv[2])
258 func isNotTokenChar(r rune) bool {
259 return !isTokenChar(r)
262 // consumeToken consumes a token from the beginning of provided
263 // string, per RFC 2045 section 5.1 (referenced from 2183), and return
264 // the token consumed and the rest of the string. Returns ("", v) on
265 // failure to consume at least one character.
266 func consumeToken(v string) (token, rest string) {
267 notPos := strings.IndexFunc(v, isNotTokenChar)
274 return v[0:notPos], v[notPos:]
277 // consumeValue consumes a "value" per RFC 2045, where a value is
278 // either a 'token' or a 'quoted-string'. On success, consumeValue
279 // returns the value consumed (and de-quoted/escaped, if a
280 // quoted-string) and the rest of the string. On failure, returns
282 func consumeValue(v string) (value, rest string) {
287 return consumeToken(v)
290 // parse a quoted-string
291 buffer := new(strings.Builder)
292 for i := 1; i < len(v); i++ {
295 return buffer.String(), v[i+1:]
297 // When MSIE sends a full file path (in "intranet mode"), it does not
298 // escape backslashes: "C:\dev\go\foo.txt", not "C:\\dev\\go\\foo.txt".
300 // No known MIME generators emit unnecessary backslash escapes
301 // for simple token characters like numbers and letters.
303 // If we see an unnecessary backslash escape, assume it is from MSIE
304 // and intended as a literal backslash. This makes Go servers deal better
305 // with MSIE without affecting the way they handle conforming MIME
307 if r == '\\' && i+1 < len(v) && isTSpecial(rune(v[i+1])) {
308 buffer.WriteByte(v[i+1])
312 if r == '\r' || r == '\n' {
315 buffer.WriteByte(v[i])
317 // Did not find end quote.
321 func consumeMediaParam(v string) (param, value, rest string) {
322 rest = strings.TrimLeftFunc(v, unicode.IsSpace)
323 if !strings.HasPrefix(rest, ";") {
327 rest = rest[1:] // consume semicolon
328 rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
329 param, rest = consumeToken(rest)
330 param = strings.ToLower(param)
335 rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
336 if !strings.HasPrefix(rest, "=") {
339 rest = rest[1:] // consume equals sign
340 rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
341 value, rest2 := consumeValue(rest)
342 if value == "" && rest2 == rest {
346 return param, value, rest
349 func percentHexUnescape(s string) (string, error) {
350 // Count %, check that they're well-formed.
352 for i := 0; i < len(s); {
358 if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
363 return "", fmt.Errorf("mime: bogus characters after %%: %q", s)
371 t := make([]byte, len(s)-2*percents)
373 for i := 0; i < len(s); {
376 t[j] = unhex(s[i+1])<<4 | unhex(s[i+2])
385 return string(t), nil
388 func ishex(c byte) bool {
390 case '0' <= c && c <= '9':
392 case 'a' <= c && c <= 'f':
394 case 'A' <= c && c <= 'F':
400 func unhex(c byte) byte {
402 case '0' <= c && c <= '9':
404 case 'a' <= c && c <= 'f':
406 case 'A' <= c && c <= 'F':