mirror of
https://github.com/golang/go
synced 2024-11-24 03:20:02 -07:00
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
This commit is contained in:
parent
e9bcbc5398
commit
9b64fef71a
@ -94,6 +94,7 @@ DIRS=\
|
||||
log\
|
||||
math\
|
||||
mime\
|
||||
mime/multipart\
|
||||
net\
|
||||
netchan\
|
||||
nntp\
|
||||
|
@ -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 != "" {
|
||||
|
@ -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/"
|
||||
|
@ -6,6 +6,8 @@ include ../../Make.$(GOARCH)
|
||||
|
||||
TARG=mime
|
||||
GOFILES=\
|
||||
grammar.go\
|
||||
mediatype.go\
|
||||
type.go\
|
||||
|
||||
include ../../Make.pkg
|
||||
|
36
src/pkg/mime/grammar.go
Normal file
36
src/pkg/mime/grammar.go
Normal file
@ -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
|
||||
}
|
120
src/pkg/mime/mediatype.go
Normal file
120
src/pkg/mime/mediatype.go
Normal file
@ -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
|
||||
}
|
117
src/pkg/mime/mediatype_test.go
Normal file
117
src/pkg/mime/mediatype_test.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
11
src/pkg/mime/multipart/Makefile
Normal file
11
src/pkg/mime/multipart/Makefile
Normal file
@ -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
|
280
src/pkg/mime/multipart/multipart.go
Normal file
280
src/pkg/mime/multipart/multipart.go
Normal file
@ -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"))
|
||||
}
|
204
src/pkg/mime/multipart/multipart_test.go
Normal file
204
src/pkg/mime/multipart/multipart_test.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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]
|
||||
|
Loading…
Reference in New Issue
Block a user