diff --git a/src/pkg/mime/mediatype.go b/src/pkg/mime/mediatype.go index eb629aa6f7f..e9e649f9507 100644 --- a/src/pkg/mime/mediatype.go +++ b/src/pkg/mime/mediatype.go @@ -10,6 +10,24 @@ import ( "unicode" ) +func validMediaTypeOrDisposition(s string) bool { + typ, rest := consumeToken(s) + if typ == "" { + return false + } + if rest == "" { + return true + } + if !strings.HasPrefix(rest, "/") { + return false + } + subtype, rest := consumeToken(rest[1:]) + if subtype == "" { + return false + } + return rest == "" +} + // ParseMediaType parses a media type value and any optional // parameters, per RFC 1531. Media types are the values in // Content-Type and Content-Disposition headers (RFC 2183). On @@ -22,6 +40,10 @@ func ParseMediaType(v string) (mediatype string, params map[string]string) { i = len(v) } mediatype = strings.TrimSpace(strings.ToLower(v[0:i])) + if !validMediaTypeOrDisposition(mediatype) { + return "", nil + } + params = make(map[string]string) v = v[i:] @@ -32,6 +54,11 @@ func ParseMediaType(v string) (mediatype string, params map[string]string) { } key, value, rest := consumeMediaParam(v) if key == "" { + if strings.TrimSpace(rest) == ";" { + // Ignore trailing semicolons. + // Not an error. + return + } // Parse error. return "", nil } @@ -66,10 +93,12 @@ func consumeToken(v string) (token, rest string) { // quoted-string) and the rest of the string. On failure, returns // ("", v). func consumeValue(v string) (value, rest string) { - if !strings.HasPrefix(v, `"`) { + if !strings.HasPrefix(v, `"`) && !strings.HasPrefix(v, `'`) { return consumeToken(v) } + leadQuote := int(v[0]) + // parse a quoted-string rest = v[1:] // consume the leading quote buffer := new(bytes.Buffer) @@ -83,7 +112,7 @@ func consumeValue(v string) (value, rest string) { } buffer.WriteRune(rune) nextIsLiteral = false - case rune == '"': + case rune == leadQuote: return buffer.String(), rest[idx+1:] case IsQText(rune): buffer.WriteRune(rune) @@ -108,10 +137,12 @@ func consumeMediaParam(v string) (param, value, rest string) { if param == "" { return "", "", v } + rest = strings.TrimLeftFunc(rest, unicode.IsSpace) if !strings.HasPrefix(rest, "=") { return "", "", v } rest = rest[1:] // consume equals sign + rest = strings.TrimLeftFunc(rest, unicode.IsSpace) value, rest = consumeValue(rest) if value == "" { return "", "", v diff --git a/src/pkg/mime/mediatype_test.go b/src/pkg/mime/mediatype_test.go index 4891e899d4c..f9603159575 100644 --- a/src/pkg/mime/mediatype_test.go +++ b/src/pkg/mime/mediatype_test.go @@ -5,6 +5,7 @@ package mime import ( + "reflect" "testing" ) @@ -85,23 +86,97 @@ func TestConsumeMediaParam(t *testing.T) { } } +type mediaTypeTest struct { + in string + t string + p map[string]string +} + func TestParseMediaType(t *testing.T) { - tests := [...]string{ - `form-data; name="foo"`, - ` form-data ; name=foo`, - `FORM-DATA;name="foo"`, - ` FORM-DATA ; name="foo"`, - ` FORM-DATA ; name="foo"`, - `form-data; key=value; blah="value";name="foo" `, + // Convenience map initializer + m := func(s ...string) map[string]string { + sm := make(map[string]string) + for i := 0; i < len(s); i += 2 { + sm[s[i]] = s[i+1] + } + return sm + } + + nameFoo := map[string]string{"name": "foo"} + tests := []mediaTypeTest{ + {`form-data; name="foo"`, "form-data", nameFoo}, + {` form-data ; name=foo`, "form-data", nameFoo}, + {`FORM-DATA;name="foo"`, "form-data", nameFoo}, + {` FORM-DATA ; name="foo"`, "form-data", nameFoo}, + {` FORM-DATA ; name="foo"`, "form-data", nameFoo}, + + {`form-data; key=value; blah="value";name="foo" `, + "form-data", + m("key", "value", "blah", "value", "name", "foo")}, + + // Tests from http://greenbytes.de/tech/tc2231/ + // TODO(bradfitz): add the rest of the tests from that site. + {`attachment; filename="f\oo.html"`, + "attachment", + m("filename", "foo.html")}, + {`attachment; filename="\"quoting\" tested.html"`, + "attachment", + m("filename", `"quoting" tested.html`)}, + {`attachment; filename="Here's a semicolon;.html"`, + "attachment", + m("filename", "Here's a semicolon;.html")}, + {`attachment; foo="\"\\";filename="foo.html"`, + "attachment", + m("foo", "\"\\", "filename", "foo.html")}, + {`attachment; filename=foo.html`, + "attachment", + m("filename", "foo.html")}, + {`attachment; filename=foo.html ;`, + "attachment", + m("filename", "foo.html")}, + {`attachment; filename='foo.html'`, + "attachment", + m("filename", "foo.html")}, + {`attachment; filename="foo-%41.html"`, + "attachment", + m("filename", "foo-%41.html")}, + {`attachment; filename="foo-%\41.html"`, + "attachment", + m("filename", "foo-%41.html")}, + {`filename=foo.html`, + "", m()}, + {`x=y; filename=foo.html`, + "", m()}, + {`"foo; filename=bar;baz"; filename=qux`, + "", m()}, + {`inline; attachment; filename=foo.html`, + "", m()}, + {`attachment; filename="foo.html".txt`, + "", m()}, + {`attachment; filename="bar`, + "", m()}, + {`attachment; creation-date="Wed, 12 Feb 1997 16:29:51 -0500"`, + "attachment", + m("creation-date", "Wed, 12 Feb 1997 16:29:51 -0500")}, + {`foobar`, "foobar", m()}, + // TODO(bradfitz): rest of them, including RFC2231 encoded UTF-8 and + // other charsets. } for _, test := range tests { - mt, params := ParseMediaType(test) - if mt != "form-data" { - t.Errorf("expected type form-data for %s, got [%s]", test, mt) + mt, params := ParseMediaType(test.in) + if g, e := mt, test.t; g != e { + t.Errorf("for input %q, expected type %q, got %q", + test.in, e, g) continue } - if params["name"] != "foo" { - t.Errorf("expected name=foo for %s", test) + if len(params) == 0 && len(test.p) == 0 { + continue + } + if !reflect.DeepEqual(params, test.p) { + t.Errorf("for input %q, wrong params.\n"+ + "expected: %#v\n"+ + " got: %#v", + test.in, test.p, params) } } }