mirror of
https://github.com/golang/go
synced 2024-11-20 00:34:43 -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\
|
log\
|
||||||
math\
|
math\
|
||||||
mime\
|
mime\
|
||||||
|
mime/multipart\
|
||||||
net\
|
net\
|
||||||
netchan\
|
netchan\
|
||||||
nntp\
|
nntp\
|
||||||
|
@ -16,6 +16,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"mime"
|
||||||
|
"mime/multipart"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -40,6 +42,8 @@ var (
|
|||||||
ErrNotSupported = &ProtocolError{"feature not supported"}
|
ErrNotSupported = &ProtocolError{"feature not supported"}
|
||||||
ErrUnexpectedTrailer = &ProtocolError{"trailer header without chunked transfer encoding"}
|
ErrUnexpectedTrailer = &ProtocolError{"trailer header without chunked transfer encoding"}
|
||||||
ErrMissingContentLength = &ProtocolError{"missing ContentLength in HEAD response"}
|
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 {
|
type badStringError struct {
|
||||||
@ -139,6 +143,24 @@ func (r *Request) ProtoAtLeast(major, minor int) bool {
|
|||||||
r.ProtoMajor == major && r.ProtoMinor >= minor
|
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.
|
// Return value if nonempty, def otherwise.
|
||||||
func valueOrDefault(value, def string) string {
|
func valueOrDefault(value, def string) string {
|
||||||
if value != "" {
|
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) {
|
func TestRedirect(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
start = "http://codesearch.google.com/"
|
start = "http://codesearch.google.com/"
|
||||||
|
@ -6,6 +6,8 @@ include ../../Make.$(GOARCH)
|
|||||||
|
|
||||||
TARG=mime
|
TARG=mime
|
||||||
GOFILES=\
|
GOFILES=\
|
||||||
|
grammar.go\
|
||||||
|
mediatype.go\
|
||||||
type.go\
|
type.go\
|
||||||
|
|
||||||
include ../../Make.pkg
|
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
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
// The mime package translates file name extensions to MIME types.
|
// The mime package implements parts of the MIME spec.
|
||||||
// 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
|
|
||||||
//
|
|
||||||
package mime
|
package mime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -76,6 +69,14 @@ func initMime() {
|
|||||||
// TypeByExtension returns the MIME type associated with the file extension ext.
|
// TypeByExtension returns the MIME type associated with the file extension ext.
|
||||||
// The extension ext should begin with a leading dot, as in ".html".
|
// The extension ext should begin with a leading dot, as in ".html".
|
||||||
// When ext has no associated type, TypeByExtension returns "".
|
// 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 {
|
func TypeByExtension(ext string) string {
|
||||||
once.Do(initMime)
|
once.Do(initMime)
|
||||||
return mimeTypes[ext]
|
return mimeTypes[ext]
|
||||||
|
Loading…
Reference in New Issue
Block a user