From 9b64fef71acf71175bc74e2d04ea7fc0a011b03b Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 14 Jul 2010 17:26:14 -0700 Subject: [PATCH] mime/multipart and HTTP multipart/form-data support Somewhat of a work-in-progress (in that MIME is a large spec), but this is functional and enough for discussion and/or code review. In addition to the unit tests, I've tested with curl and Chrome with a variety of test files, making sure the digests of files are unaltered when read via a multipart Part. R=rsc, adg, dsymonds1, agl1 CC=golang-dev https://golang.org/cl/1681049 --- src/pkg/Makefile | 1 + src/pkg/http/request.go | 22 ++ src/pkg/http/request_test.go | 18 ++ src/pkg/mime/Makefile | 2 + src/pkg/mime/grammar.go | 36 +++ src/pkg/mime/mediatype.go | 120 ++++++++++ src/pkg/mime/mediatype_test.go | 117 ++++++++++ src/pkg/mime/multipart/Makefile | 11 + src/pkg/mime/multipart/multipart.go | 280 +++++++++++++++++++++++ src/pkg/mime/multipart/multipart_test.go | 204 +++++++++++++++++ src/pkg/mime/type.go | 17 +- 11 files changed, 820 insertions(+), 8 deletions(-) create mode 100644 src/pkg/mime/grammar.go create mode 100644 src/pkg/mime/mediatype.go create mode 100644 src/pkg/mime/mediatype_test.go create mode 100644 src/pkg/mime/multipart/Makefile create mode 100644 src/pkg/mime/multipart/multipart.go create mode 100644 src/pkg/mime/multipart/multipart_test.go diff --git a/src/pkg/Makefile b/src/pkg/Makefile index e489b71d47..d43174f651 100644 --- a/src/pkg/Makefile +++ b/src/pkg/Makefile @@ -94,6 +94,7 @@ DIRS=\ log\ math\ mime\ + mime/multipart\ net\ netchan\ nntp\ diff --git a/src/pkg/http/request.go b/src/pkg/http/request.go index 8a72d6cfad..a6836856d8 100644 --- a/src/pkg/http/request.go +++ b/src/pkg/http/request.go @@ -16,6 +16,8 @@ import ( "fmt" "io" "io/ioutil" + "mime" + "mime/multipart" "os" "strconv" "strings" @@ -40,6 +42,8 @@ var ( ErrNotSupported = &ProtocolError{"feature not supported"} ErrUnexpectedTrailer = &ProtocolError{"trailer header without chunked transfer encoding"} ErrMissingContentLength = &ProtocolError{"missing ContentLength in HEAD response"} + ErrNotMultipart = &ProtocolError{"request Content-Type isn't multipart/form-data"} + ErrMissingBoundary = &ProtocolError{"no multipart boundary param Content-Type"} ) type badStringError struct { @@ -139,6 +143,24 @@ func (r *Request) ProtoAtLeast(major, minor int) bool { r.ProtoMajor == major && r.ProtoMinor >= minor } +// MultipartReader returns a MIME multipart reader if this is a +// multipart/form-data POST request, else returns nil and an error. +func (r *Request) MultipartReader() (multipart.Reader, os.Error) { + v, ok := r.Header["Content-Type"] + if !ok { + return nil, ErrNotMultipart + } + d, params := mime.ParseMediaType(v) + if d != "multipart/form-data" { + return nil, ErrNotMultipart + } + boundary, ok := params["boundary"] + if !ok { + return nil, ErrMissingBoundary + } + return multipart.NewReader(r.Body, boundary), nil +} + // Return value if nonempty, def otherwise. func valueOrDefault(value, def string) string { if value != "" { diff --git a/src/pkg/http/request_test.go b/src/pkg/http/request_test.go index 98d5342bbb..4ba173a986 100644 --- a/src/pkg/http/request_test.go +++ b/src/pkg/http/request_test.go @@ -101,6 +101,24 @@ func TestPostContentTypeParsing(t *testing.T) { } } +func TestMultipartReader(t *testing.T) { + req := &Request{ + Method: "POST", + Header: stringMap{"Content-Type": `multipart/form-data; boundary="foo123"`}, + Body: nopCloser{new(bytes.Buffer)}, + } + multipart, err := req.MultipartReader() + if multipart == nil { + t.Errorf("expected multipart; error: %v", err) + } + + req.Header = stringMap{"Content-Type": "text/plain"} + multipart, err = req.MultipartReader() + if multipart != nil { + t.Errorf("unexpected multipart for text/plain") + } +} + func TestRedirect(t *testing.T) { const ( start = "http://codesearch.google.com/" diff --git a/src/pkg/mime/Makefile b/src/pkg/mime/Makefile index 57fc7db448..1f1296b767 100644 --- a/src/pkg/mime/Makefile +++ b/src/pkg/mime/Makefile @@ -6,6 +6,8 @@ include ../../Make.$(GOARCH) TARG=mime GOFILES=\ + grammar.go\ + mediatype.go\ type.go\ include ../../Make.pkg diff --git a/src/pkg/mime/grammar.go b/src/pkg/mime/grammar.go new file mode 100644 index 0000000000..98fbe33c6d --- /dev/null +++ b/src/pkg/mime/grammar.go @@ -0,0 +1,36 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mime + +import ( + "strings" +) + +// isTSpecial returns true if rune is in 'tspecials' as defined by RFC +// 1531 and RFC 2045. +func isTSpecial(rune int) bool { + return strings.IndexRune(`()<>@,;:\"/[]?=`, rune) != -1 +} + +// IsTokenChar returns true if rune is in 'token' as defined by RFC +// 1531 and RFC 2045. +func IsTokenChar(rune int) bool { + // token := 1* + return rune > 0x20 && rune < 0x7f && !isTSpecial(rune) +} + +// IsQText returns true if rune is in 'qtext' as defined by RFC 822. +func IsQText(rune int) bool { + // CHAR = ; ( 0-177, 0.-127.) + // qtext = , ; => may be folded + // "\" & CR, and including + // linear-white-space> + switch rune { + case int('"'), int('\\'), int('\r'): + return false + } + return rune < 0x80 +} diff --git a/src/pkg/mime/mediatype.go b/src/pkg/mime/mediatype.go new file mode 100644 index 0000000000..eb629aa6f7 --- /dev/null +++ b/src/pkg/mime/mediatype.go @@ -0,0 +1,120 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mime + +import ( + "bytes" + "strings" + "unicode" +) + +// 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 +// success, ParseMediaType returns the media type converted to +// lowercase and trimmed of white space and a non-nil params. On +// error, it returns an empty string and a nil params. +func ParseMediaType(v string) (mediatype string, params map[string]string) { + i := strings.Index(v, ";") + if i == -1 { + i = len(v) + } + mediatype = strings.TrimSpace(strings.ToLower(v[0:i])) + params = make(map[string]string) + + v = v[i:] + for len(v) > 0 { + v = strings.TrimLeftFunc(v, unicode.IsSpace) + if len(v) == 0 { + return + } + key, value, rest := consumeMediaParam(v) + if key == "" { + // Parse error. + return "", nil + } + params[key] = value + v = rest + } + return +} + +func isNotTokenChar(rune int) bool { + return !IsTokenChar(rune) +} + +// consumeToken consumes a token from the beginning of provided +// string, per RFC 2045 section 5.1 (referenced from 2183), and return +// the token consumed and the rest of the string. Returns ("", v) on +// failure to consume at least one character. +func consumeToken(v string) (token, rest string) { + notPos := strings.IndexFunc(v, isNotTokenChar) + if notPos == -1 { + return v, "" + } + if notPos == 0 { + return "", v + } + return v[0:notPos], v[notPos:] +} + +// consumeValue consumes a "value" per RFC 2045, where a value is +// either a 'token' or a 'quoted-string'. On success, consumeValue +// returns the value consumed (and de-quoted/escaped, if a +// quoted-string) and the rest of the string. On failure, returns +// ("", v). +func consumeValue(v string) (value, rest string) { + if !strings.HasPrefix(v, `"`) { + return consumeToken(v) + } + + // parse a quoted-string + rest = v[1:] // consume the leading quote + buffer := new(bytes.Buffer) + var idx, rune int + var nextIsLiteral bool + for idx, rune = range rest { + switch { + case nextIsLiteral: + if rune >= 0x80 { + return "", v + } + buffer.WriteRune(rune) + nextIsLiteral = false + case rune == '"': + return buffer.String(), rest[idx+1:] + case IsQText(rune): + buffer.WriteRune(rune) + case rune == '\\': + nextIsLiteral = true + default: + return "", v + } + } + return "", v +} + +func consumeMediaParam(v string) (param, value, rest string) { + rest = strings.TrimLeftFunc(v, unicode.IsSpace) + if !strings.HasPrefix(rest, ";") { + return "", "", v + } + + rest = rest[1:] // consume semicolon + rest = strings.TrimLeftFunc(rest, unicode.IsSpace) + param, rest = consumeToken(rest) + if param == "" { + return "", "", v + } + if !strings.HasPrefix(rest, "=") { + return "", "", v + } + rest = rest[1:] // consume equals sign + value, rest = consumeValue(rest) + if value == "" { + return "", "", v + } + return param, value, rest +} diff --git a/src/pkg/mime/mediatype_test.go b/src/pkg/mime/mediatype_test.go new file mode 100644 index 0000000000..42c8a9b074 --- /dev/null +++ b/src/pkg/mime/mediatype_test.go @@ -0,0 +1,117 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mime + +import ( + "testing" +) + +func TestConsumeToken(t *testing.T) { + tests := [...][3]string{ + [3]string{"foo bar", "foo", " bar"}, + [3]string{"bar", "bar", ""}, + [3]string{"", "", ""}, + [3]string{" foo", "", " foo"}, + } + for _, test := range tests { + token, rest := consumeToken(test[0]) + expectedToken := test[1] + expectedRest := test[2] + if token != expectedToken { + t.Errorf("expected to consume token '%s', not '%s' from '%s'", + expectedToken, token, test[0]) + } else if rest != expectedRest { + t.Errorf("expected to have left '%s', not '%s' after reading token '%s' from '%s'", + expectedRest, rest, token, test[0]) + } + } +} + +func TestConsumeValue(t *testing.T) { + tests := [...][3]string{ + [3]string{"foo bar", "foo", " bar"}, + [3]string{"bar", "bar", ""}, + [3]string{" bar ", "", " bar "}, + [3]string{`"My value"end`, "My value", "end"}, + [3]string{`"My value" end`, "My value", " end"}, + [3]string{`"\\" rest`, "\\", " rest"}, + [3]string{`"My \" value"end`, "My \" value", "end"}, + [3]string{`"\" rest`, "", `"\" rest`}, + } + for _, test := range tests { + value, rest := consumeValue(test[0]) + expectedValue := test[1] + expectedRest := test[2] + if value != expectedValue { + t.Errorf("expected to consume value [%s], not [%s] from [%s]", + expectedValue, value, test[0]) + } else if rest != expectedRest { + t.Errorf("expected to have left [%s], not [%s] after reading value [%s] from [%s]", + expectedRest, rest, value, test[0]) + } + } +} + +func TestConsumeMediaParam(t *testing.T) { + tests := [...][4]string{ + [4]string{" ; foo=bar", "foo", "bar", ""}, + [4]string{"; foo=bar", "foo", "bar", ""}, + [4]string{";foo=bar", "foo", "bar", ""}, + [4]string{`;foo="bar"`, "foo", "bar", ""}, + [4]string{`;foo="bar"; `, "foo", "bar", "; "}, + [4]string{`;foo="bar"; foo=baz`, "foo", "bar", "; foo=baz"}, + [4]string{` ; boundary=----CUT;`, "boundary", "----CUT", ";"}, + [4]string{` ; key=value; blah="value";name="foo" `, "key", "value", `; blah="value";name="foo" `}, + [4]string{`; blah="value";name="foo" `, "blah", "value", `;name="foo" `}, + [4]string{`;name="foo" `, "name", "foo", ` `}, + } + for _, test := range tests { + param, value, rest := consumeMediaParam(test[0]) + expectedParam := test[1] + expectedValue := test[2] + expectedRest := test[3] + if param != expectedParam { + t.Errorf("expected to consume param [%s], not [%s] from [%s]", + expectedParam, param, test[0]) + } else if value != expectedValue { + t.Errorf("expected to consume value [%s], not [%s] from [%s]", + expectedValue, value, test[0]) + } else if rest != expectedRest { + t.Errorf("expected to have left [%s], not [%s] after reading [%s/%s] from [%s]", + expectedRest, rest, param, value, test[0]) + } + } +} + +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" `, + } + for _, test := range tests { + mt, params := ParseMediaType(test) + if mt != "form-data" { + t.Errorf("expected type form-data for %s, got [%s]", test, mt) + continue + } + if params["name"] != "foo" { + t.Errorf("expected name=foo for %s", test) + } + } +} + +func TestParseMediaTypeBogus(t *testing.T) { + mt, params := ParseMediaType("bogus ;=========") + if mt != "" { + t.Error("expected empty type") + } + if params != nil { + t.Error("expected nil params") + } +} diff --git a/src/pkg/mime/multipart/Makefile b/src/pkg/mime/multipart/Makefile new file mode 100644 index 0000000000..0e6ee42dcb --- /dev/null +++ b/src/pkg/mime/multipart/Makefile @@ -0,0 +1,11 @@ +# Copyright 2010 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +include ../../../Make.$(GOARCH) + +TARG=mime/multipart +GOFILES=\ + multipart.go\ + +include ../../../Make.pkg diff --git a/src/pkg/mime/multipart/multipart.go b/src/pkg/mime/multipart/multipart.go new file mode 100644 index 0000000000..e009132515 --- /dev/null +++ b/src/pkg/mime/multipart/multipart.go @@ -0,0 +1,280 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// + +/* +Package multipart implements MIME multipart parsing, as defined in RFC +2046. + +The implementation is sufficient for HTTP (RFC 2388) and the multipart +bodies generated by popular browsers. +*/ +package multipart + +import ( + "bufio" + "bytes" + "io" + "mime" + "os" + "regexp" + "strings" +) + +var headerRegexp *regexp.Regexp = regexp.MustCompile("^([a-zA-Z0-9\\-]+): *([^\r\n]+)") + +// Reader is an iterator over parts in a MIME multipart body. +// Reader's underlying parser consumes its input as needed. Seeking +// isn't supported. +type Reader interface { + // NextPart returns the next part in the multipart, or (nil, + // nil) on EOF. An error is returned if the underlying reader + // reports errors, or on truncated or otherwise malformed + // input. + NextPart() (*Part, os.Error) +} + +// A Part represents a single part in a multipart body. +type Part struct { + // The headers of the body, if any, with the keys canonicalized + // in the same fashion that the Go http.Request headers are. + // i.e. "foo-bar" changes case to "Foo-Bar" + Header map[string]string + + buffer *bytes.Buffer + mr *multiReader +} + +// FormName returns the name parameter if p has a Content-Disposition +// of type "form-data". Otherwise it returns the empty string. +func (p *Part) FormName() string { + // See http://tools.ietf.org/html/rfc2183 section 2 for EBNF + // of Content-Disposition value format. + v, ok := p.Header["Content-Disposition"] + if !ok { + return "" + } + d, params := mime.ParseMediaType(v) + if d != "form-data" { + return "" + } + return params["name"] +} + +// NewReader creates a new multipart Reader reading from r using the +// given MIME boundary. +func NewReader(reader io.Reader, boundary string) Reader { + return &multiReader{ + boundary: boundary, + dashBoundary: "--" + boundary, + endLine: "--" + boundary + "--", + bufReader: bufio.NewReader(reader), + } +} + +// Implementation .... + +type devNullWriter bool + +func (*devNullWriter) Write(p []byte) (n int, err os.Error) { + return len(p), nil +} + +var devNull = devNullWriter(false) + +func newPart(mr *multiReader) (bp *Part, err os.Error) { + bp = new(Part) + bp.Header = make(map[string]string) + bp.mr = mr + bp.buffer = new(bytes.Buffer) + if err = bp.populateHeaders(); err != nil { + bp = nil + } + return +} + +func (bp *Part) populateHeaders() os.Error { + for { + line, err := bp.mr.bufReader.ReadString('\n') + if err != nil { + return err + } + if line == "\n" || line == "\r\n" { + return nil + } + if matches := headerRegexp.MatchStrings(line); len(matches) == 3 { + key := matches[1] + value := matches[2] + // TODO: canonicalize headers ala http.Request.Header? + bp.Header[key] = value + continue + } + return os.NewError("Unexpected header line found parsing multipart body") + } + panic("unreachable") +} + +// Read reads the body of a part, after its headers and before the +// next part (if any) begins. +func (bp *Part) Read(p []byte) (n int, err os.Error) { + for { + if bp.buffer.Len() >= len(p) { + // Internal buffer of unconsumed data is large enough for + // the read request. No need to parse more at the moment. + break + } + if !bp.mr.ensureBufferedLine() { + return 0, io.ErrUnexpectedEOF + } + if bp.mr.bufferedLineIsBoundary() { + // Don't consume this line + break + } + + // Write all of this line, except the final CRLF + s := *bp.mr.bufferedLine + if strings.HasSuffix(s, "\r\n") { + bp.mr.consumeLine() + if !bp.mr.ensureBufferedLine() { + return 0, io.ErrUnexpectedEOF + } + if bp.mr.bufferedLineIsBoundary() { + // The final \r\n isn't ours. It logically belongs + // to the boundary line which follows. + bp.buffer.WriteString(s[0 : len(s)-2]) + } else { + bp.buffer.WriteString(s) + } + break + } + if strings.HasSuffix(s, "\n") { + bp.buffer.WriteString(s) + bp.mr.consumeLine() + continue + } + return 0, os.NewError("multipart parse error during Read; unexpected line: " + s) + } + return bp.buffer.Read(p) +} + +func (bp *Part) Close() os.Error { + io.Copy(&devNull, bp) + return nil +} + +type multiReader struct { + boundary string + dashBoundary string // --boundary + endLine string // --boundary-- + + bufferedLine *string + + bufReader *bufio.Reader + currentPart *Part + partsRead int +} + +func (mr *multiReader) eof() bool { + return mr.bufferedLine == nil && + !mr.readLine() +} + +func (mr *multiReader) readLine() bool { + line, err := mr.bufReader.ReadString('\n') + if err != nil { + // TODO: care about err being EOF or not? + return false + } + mr.bufferedLine = &line + return true +} + +func (mr *multiReader) bufferedLineIsBoundary() bool { + return strings.HasPrefix(*mr.bufferedLine, mr.dashBoundary) +} + +func (mr *multiReader) ensureBufferedLine() bool { + if mr.bufferedLine == nil { + return mr.readLine() + } + return true +} + +func (mr *multiReader) consumeLine() { + mr.bufferedLine = nil +} + +func (mr *multiReader) NextPart() (*Part, os.Error) { + if mr.currentPart != nil { + mr.currentPart.Close() + } + + for { + if mr.eof() { + return nil, io.ErrUnexpectedEOF + } + + if isBoundaryDelimiterLine(*mr.bufferedLine, mr.dashBoundary) { + mr.consumeLine() + mr.partsRead++ + bp, err := newPart(mr) + if err != nil { + return nil, err + } + mr.currentPart = bp + return bp, nil + } + + if hasPrefixThenNewline(*mr.bufferedLine, mr.endLine) { + mr.consumeLine() + // Expected EOF (no error) + return nil, nil + } + + if mr.partsRead == 0 { + // skip line + mr.consumeLine() + continue + } + + return nil, os.NewError("Unexpected line in Next().") + } + panic("unreachable") +} + +func isBoundaryDelimiterLine(line, dashPrefix string) bool { + // http://tools.ietf.org/html/rfc2046#section-5.1 + // The boundary delimiter line is then defined as a line + // consisting entirely of two hyphen characters ("-", + // decimal value 45) followed by the boundary parameter + // value from the Content-Type header field, optional linear + // whitespace, and a terminating CRLF. + if !strings.HasPrefix(line, dashPrefix) { + return false + } + if strings.HasSuffix(line, "\r\n") { + return onlyHorizontalWhitespace(line[len(dashPrefix) : len(line)-2]) + } + // Violate the spec and also support newlines without the + // carriage return... + if strings.HasSuffix(line, "\n") { + return onlyHorizontalWhitespace(line[len(dashPrefix) : len(line)-1]) + } + return false +} + +func onlyHorizontalWhitespace(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] != ' ' && s[i] != '\t' { + return false + } + } + return true +} + +func hasPrefixThenNewline(s, prefix string) bool { + return strings.HasPrefix(s, prefix) && + (len(s) == len(prefix)+1 && strings.HasSuffix(s, "\n") || + len(s) == len(prefix)+2 && strings.HasSuffix(s, "\r\n")) +} diff --git a/src/pkg/mime/multipart/multipart_test.go b/src/pkg/mime/multipart/multipart_test.go new file mode 100644 index 0000000000..f737a90700 --- /dev/null +++ b/src/pkg/mime/multipart/multipart_test.go @@ -0,0 +1,204 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package multipart + +import ( + "bytes" + "fmt" + "io" + "json" + "regexp" + "strings" + "testing" +) + +func TestHorizontalWhitespace(t *testing.T) { + if !onlyHorizontalWhitespace(" \t") { + t.Error("expected pass") + } + if onlyHorizontalWhitespace("foo bar") { + t.Error("expected failure") + } +} + +func TestBoundaryLine(t *testing.T) { + boundary := "myBoundary" + prefix := "--" + boundary + if !isBoundaryDelimiterLine("--myBoundary\r\n", prefix) { + t.Error("expected") + } + if !isBoundaryDelimiterLine("--myBoundary \r\n", prefix) { + t.Error("expected") + } + if !isBoundaryDelimiterLine("--myBoundary \n", prefix) { + t.Error("expected") + } + if isBoundaryDelimiterLine("--myBoundary bogus \n", prefix) { + t.Error("expected fail") + } + if isBoundaryDelimiterLine("--myBoundary bogus--", prefix) { + t.Error("expected fail") + } +} + +func escapeString(v string) string { + bytes, _ := json.Marshal(v) + return string(bytes) +} + +func expectEq(t *testing.T, expected, actual, what string) { + if expected == actual { + return + } + t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)", + what, escapeString(actual), len(actual), escapeString(expected), len(expected)) +} + +func TestFormName(t *testing.T) { + p := new(Part) + p.Header = make(map[string]string) + tests := [...][2]string{ + [2]string{`form-data; name="foo"`, "foo"}, + [2]string{` form-data ; name=foo`, "foo"}, + [2]string{`FORM-DATA;name="foo"`, "foo"}, + [2]string{` FORM-DATA ; name="foo"`, "foo"}, + [2]string{` FORM-DATA ; name="foo"`, "foo"}, + [2]string{` FORM-DATA ; name=foo`, "foo"}, + [2]string{` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo"}, + } + for _, test := range tests { + p.Header["Content-Disposition"] = test[0] + expected := test[1] + actual := p.FormName() + if actual != expected { + t.Errorf("expected \"%s\"; got: \"%s\"", expected, actual) + } + } +} + +func TestMultipart(t *testing.T) { + testBody := ` +This is a multi-part message. This line is ignored. +--MyBoundary +Header1: value1 +HEADER2: value2 +foo-bar: baz + +My value +The end. +--MyBoundary +Header1: value1b +HEADER2: value2b +foo-bar: bazb + +Line 1 +Line 2 +Line 3 ends in a newline, but just one. + +--MyBoundary + +never read data +--MyBoundary-- +` + testBody = regexp.MustCompile("\n").ReplaceAllString(testBody, "\r\n") + bodyReader := strings.NewReader(testBody) + + reader := NewReader(bodyReader, "MyBoundary") + buf := new(bytes.Buffer) + + // Part1 + part, err := reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part1") + return + } + if part.Header["Header1"] != "value1" { + t.Error("Expected Header1: value") + } + if part.Header["foo-bar"] != "baz" { + t.Error("Expected foo-bar: baz") + } + buf.Reset() + io.Copy(buf, part) + expectEq(t, "My value\r\nThe end.", + buf.String(), "Value of first part") + + // Part2 + part, err = reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part2") + return + } + if part.Header["foo-bar"] != "bazb" { + t.Error("Expected foo-bar: bazb") + } + buf.Reset() + io.Copy(buf, part) + expectEq(t, "Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n", + buf.String(), "Value of second part") + + // Part3 + part, err = reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part3 without errors") + return + } + + // Non-existent part4 + part, err = reader.NextPart() + if part != nil { + t.Error("Didn't expect a third part.") + } + if err != nil { + t.Errorf("Unexpected error getting third part: %v", err) + } +} + +func TestVariousTextLineEndings(t *testing.T) { + tests := [...]string{ + "Foo\nBar", + "Foo\nBar\n", + "Foo\r\nBar", + "Foo\r\nBar\r\n", + "Foo\rBar", + "Foo\rBar\r", + "\x00\x01\x02\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10", + } + + for testNum, expectedBody := range tests { + body := "--BOUNDARY\r\n" + + "Content-Disposition: form-data; name=\"value\"\r\n" + + "\r\n" + + expectedBody + + "\r\n--BOUNDARY--\r\n" + bodyReader := strings.NewReader(body) + + reader := NewReader(bodyReader, "BOUNDARY") + buf := new(bytes.Buffer) + part, err := reader.NextPart() + if part == nil { + t.Errorf("Expected a body part on text %d", testNum) + continue + } + if err != nil { + t.Errorf("Unexpected error on text %d: %v", testNum, err) + continue + } + written, err := io.Copy(buf, part) + expectEq(t, expectedBody, buf.String(), fmt.Sprintf("test %d", testNum)) + if err != nil { + t.Errorf("Error copying multipart; bytes=%v, error=%v", written, err) + } + + part, err = reader.NextPart() + if part != nil { + t.Errorf("Unexpected part in test %d", testNum) + } + if err != nil { + t.Errorf("Unexpected error in test %d: %v", testNum, err) + } + + } +} diff --git a/src/pkg/mime/type.go b/src/pkg/mime/type.go index 3706afc473..b23b503649 100644 --- a/src/pkg/mime/type.go +++ b/src/pkg/mime/type.go @@ -2,14 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// The mime package translates file name extensions to MIME types. -// It consults the local system's mime.types file, which must be installed -// under one of these names: -// -// /etc/mime.types -// /etc/apache2/mime.types -// /etc/apache/mime.types -// +// The mime package implements parts of the MIME spec. package mime import ( @@ -76,6 +69,14 @@ func initMime() { // TypeByExtension returns the MIME type associated with the file extension ext. // The extension ext should begin with a leading dot, as in ".html". // When ext has no associated type, TypeByExtension returns "". +// +// The built-in table is small but is is augmented by the local +// system's mime.types file(s) if available under one or more of these +// names: +// +// /etc/mime.types +// /etc/apache2/mime.types +// /etc/apache/mime.types func TypeByExtension(ext string) string { once.Do(initMime) return mimeTypes[ext]