]> Cypherpunks repositories - gostls13.git/commitdiff
mime/multipart and HTTP multipart/form-data support
authorBrad Fitzpatrick <brad@danga.com>
Thu, 15 Jul 2010 00:26:14 +0000 (17:26 -0700)
committerRuss Cox <rsc@golang.org>
Thu, 15 Jul 2010 00:26:14 +0000 (17:26 -0700)
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
src/pkg/http/request.go
src/pkg/http/request_test.go
src/pkg/mime/Makefile
src/pkg/mime/grammar.go [new file with mode: 0644]
src/pkg/mime/mediatype.go [new file with mode: 0644]
src/pkg/mime/mediatype_test.go [new file with mode: 0644]
src/pkg/mime/multipart/Makefile [new file with mode: 0644]
src/pkg/mime/multipart/multipart.go [new file with mode: 0644]
src/pkg/mime/multipart/multipart_test.go [new file with mode: 0644]
src/pkg/mime/type.go

index e489b71d474187396b2e24b1bc3b343942c205c4..d43174f6518d4547f086d6040e22ce22905cb8ca 100644 (file)
@@ -94,6 +94,7 @@ DIRS=\
        log\
        math\
        mime\
+       mime/multipart\
        net\
        netchan\
        nntp\
index 8a72d6cfad98a5ae0e4cb44c3cc46ca83583da19..a6836856d8801575561e25e2aefeefe96204e287 100644 (file)
@@ -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 != "" {
index 98d5342bbb70b217edeef7fe964f1bae7a532fd1..4ba173a986cdf46fe376c0aeb280e51b3a7e758f 100644 (file)
@@ -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/"
index 57fc7db448dee176a95d59cf7c261913736746c1..1f1296b767461958e140a65f811b46d42f4d43d9 100644 (file)
@@ -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 (file)
index 0000000..98fbe33
--- /dev/null
@@ -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*<any (US-ASCII) CHAR except SPACE, CTLs,
+       //             or tspecials>
+       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        =  <any ASCII character>        ; (  0-177,  0.-127.)
+       // qtext       =  <any CHAR excepting <">,     ; => 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 (file)
index 0000000..eb629aa
--- /dev/null
@@ -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 (file)
index 0000000..42c8a9b
--- /dev/null
@@ -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 (file)
index 0000000..0e6ee42
--- /dev/null
@@ -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 (file)
index 0000000..e009132
--- /dev/null
@@ -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 (file)
index 0000000..f737a90
--- /dev/null
@@ -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)
+               }
+
+       }
+}
index 3706afc473fc9653595aa00bb1881022cca7ceef..b23b503649c090ba4dd82af32c31fc0556716797 100644 (file)
@@ -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]