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.
16 // FormatMediaType serializes mediatype t and the parameters
17 // param as a media type conforming to RFC 2045 and RFC 2616.
18 // The type and parameter names are written in lower-case.
19 // When any of the arguments result in a standard violation then
20 // FormatMediaType returns the empty string.
21 func FormatMediaType(t
string, param
map[string]string) string {
23 if slash
:= strings
.Index(t
, "/"); slash
== -1 {
27 b
.WriteString(strings
.ToLower(t
))
29 major
, sub
:= t
[:slash
], t
[slash
+1:]
30 if !isToken(major
) ||
!isToken(sub
) {
33 b
.WriteString(strings
.ToLower(major
))
35 b
.WriteString(strings
.ToLower(sub
))
38 attrs
:= make([]string, 0, len(param
))
39 for a
:= range param
{
40 attrs
= append(attrs
, a
)
44 for _
, attribute
:= range attrs
{
45 value
:= param
[attribute
]
48 if !isToken(attribute
) {
51 b
.WriteString(strings
.ToLower(attribute
))
60 for index
, character
:= range value
{
61 if character
== '"' || character
== '\\' {
62 b
.WriteString(value
[offset
:index
])
66 if character
&0x80 != 0 {
70 b
.WriteString(value
[offset
:])
76 func checkMediaTypeDisposition(s
string) error
{
77 typ
, rest
:= consumeToken(s
)
79 return errors
.New("mime: no media type")
84 if !strings
.HasPrefix(rest
, "/") {
85 return errors
.New("mime: expected slash after first token")
87 subtype
, rest
:= consumeToken(rest
[1:])
89 return errors
.New("mime: expected token after slash")
92 return errors
.New("mime: unexpected content after media subtype")
97 // ErrInvalidMediaParameter is returned by ParseMediaType if
98 // the media type value was found but there was an error parsing
99 // the optional parameters
100 var ErrInvalidMediaParameter
= errors
.New("mime: invalid media parameter")
102 // ParseMediaType parses a media type value and any optional
103 // parameters, per RFC 1521. Media types are the values in
104 // Content-Type and Content-Disposition headers (RFC 2183).
105 // On success, ParseMediaType returns the media type converted
106 // to lowercase and trimmed of white space and a non-nil map.
107 // If there is an error parsing the optional parameter,
108 // the media type will be returned along with the error
109 // ErrInvalidMediaParameter.
110 // The returned map, params, maps from the lowercase
111 // attribute to the attribute value with its case preserved.
112 func ParseMediaType(v
string) (mediatype
string, params
map[string]string, err error
) {
113 i
:= strings
.Index(v
, ";")
117 mediatype
= strings
.TrimSpace(strings
.ToLower(v
[0:i
]))
119 err
= checkMediaTypeDisposition(mediatype
)
124 params
= make(map[string]string)
126 // Map of base parameter name -> parameter name -> value
127 // for parameters containing a '*' character.
128 // Lazily initialized.
129 var continuation
map[string]map[string]string
133 v
= strings
.TrimLeftFunc(v
, unicode
.IsSpace
)
137 key
, value
, rest
:= consumeMediaParam(v
)
139 if strings
.TrimSpace(rest
) == ";" {
140 // Ignore trailing semicolons.
145 return mediatype
, nil, ErrInvalidMediaParameter
149 if idx
:= strings
.Index(key
, "*"); idx
!= -1 {
150 baseName
:= key
[:idx
]
151 if continuation
== nil {
152 continuation
= make(map[string]map[string]string)
155 if pmap
, ok
= continuation
[baseName
]; !ok
{
156 continuation
[baseName
] = make(map[string]string)
157 pmap
= continuation
[baseName
]
160 if _
, exists
:= pmap
[key
]; exists
{
161 // Duplicate parameter name is bogus.
162 return "", nil, errors
.New("mime: duplicate parameter name")
168 // Stitch together any continuations or things with stars
169 // (i.e. RFC 2231 things with stars: "foo*0" or "foo*")
171 for key
, pieceMap
:= range continuation
{
172 singlePartKey
:= key
+ "*"
173 if v
, ok
:= pieceMap
[singlePartKey
]; ok
{
174 if decv
, ok
:= decode2231Enc(v
); ok
{
183 simplePart
:= fmt
.Sprintf("%s*%d", key
, n
)
184 if v
, ok
:= pieceMap
[simplePart
]; ok
{
189 encodedPart
:= simplePart
+ "*"
190 v
, ok
:= pieceMap
[encodedPart
]
196 if decv
, ok
:= decode2231Enc(v
); ok
{
197 buf
.WriteString(decv
)
200 decv
, _
:= percentHexUnescape(v
)
201 buf
.WriteString(decv
)
205 params
[key
] = buf
.String()
212 func decode2231Enc(v
string) (string, bool) {
213 sv
:= strings
.SplitN(v
, "'", 3)
217 // TODO: ignoring lang in sv[1] for now. If anybody needs it we'll
218 // need to decide how to expose it in the API. But I'm not sure
219 // anybody uses it in practice.
220 charset
:= strings
.ToLower(sv
[0])
221 if len(charset
) == 0 {
224 if charset
!= "us-ascii" && charset
!= "utf-8" {
225 // TODO: unsupported encoding
228 encv
, err
:= percentHexUnescape(sv
[2])
235 func isNotTokenChar(r rune
) bool {
236 return !isTokenChar(r
)
239 // consumeToken consumes a token from the beginning of provided
240 // string, per RFC 2045 section 5.1 (referenced from 2183), and return
241 // the token consumed and the rest of the string. Returns ("", v) on
242 // failure to consume at least one character.
243 func consumeToken(v
string) (token
, rest
string) {
244 notPos
:= strings
.IndexFunc(v
, isNotTokenChar
)
251 return v
[0:notPos
], v
[notPos
:]
254 // consumeValue consumes a "value" per RFC 2045, where a value is
255 // either a 'token' or a 'quoted-string'. On success, consumeValue
256 // returns the value consumed (and de-quoted/escaped, if a
257 // quoted-string) and the rest of the string. On failure, returns
259 func consumeValue(v
string) (value
, rest
string) {
264 return consumeToken(v
)
267 // parse a quoted-string
268 buffer
:= new(bytes
.Buffer
)
269 for i
:= 1; i
< len(v
); i
++ {
272 return buffer
.String(), v
[i
+1:]
274 // When MSIE sends a full file path (in "intranet mode"), it does not
275 // escape backslashes: "C:\dev\go\foo.txt", not "C:\\dev\\go\\foo.txt".
277 // No known MIME generators emit unnecessary backslash escapes
278 // for simple token characters like numbers and letters.
280 // If we see an unnecessary backslash escape, assume it is from MSIE
281 // and intended as a literal backslash. This makes Go servers deal better
282 // with MSIE without affecting the way they handle conforming MIME
284 if r
== '\\' && i
+1 < len(v
) && !isTokenChar(rune(v
[i
+1])) {
285 buffer
.WriteByte(v
[i
+1])
289 if r
== '\r' || r
== '\n' {
292 buffer
.WriteByte(v
[i
])
294 // Did not find end quote.
298 func consumeMediaParam(v
string) (param
, value
, rest
string) {
299 rest
= strings
.TrimLeftFunc(v
, unicode
.IsSpace
)
300 if !strings
.HasPrefix(rest
, ";") {
304 rest
= rest
[1:] // consume semicolon
305 rest
= strings
.TrimLeftFunc(rest
, unicode
.IsSpace
)
306 param
, rest
= consumeToken(rest
)
307 param
= strings
.ToLower(param
)
312 rest
= strings
.TrimLeftFunc(rest
, unicode
.IsSpace
)
313 if !strings
.HasPrefix(rest
, "=") {
316 rest
= rest
[1:] // consume equals sign
317 rest
= strings
.TrimLeftFunc(rest
, unicode
.IsSpace
)
318 value
, rest2
:= consumeValue(rest
)
319 if value
== "" && rest2
== rest
{
323 return param
, value
, rest
326 func percentHexUnescape(s
string) (string, error
) {
327 // Count %, check that they're well-formed.
329 for i
:= 0; i
< len(s
); {
335 if i
+2 >= len(s
) ||
!ishex(s
[i
+1]) ||
!ishex(s
[i
+2]) {
340 return "", fmt
.Errorf("mime: bogus characters after %%: %q", s
)
348 t
:= make([]byte, len(s
)-2*percents
)
350 for i
:= 0; i
< len(s
); {
353 t
[j
] = unhex(s
[i
+1])<<4 |
unhex(s
[i
+2])
362 return string(t
), nil
365 func ishex(c
byte) bool {
367 case '0' <= c
&& c
<= '9':
369 case 'a' <= c
&& c
<= 'f':
371 case 'A' <= c
&& c
<= 'F':
377 func unhex(c
byte) byte {
379 case '0' <= c
&& c
<= '9':
381 case 'a' <= c
&& c
<= 'f':
383 case 'A' <= c
&& c
<= 'F':