diff options
Diffstat (limited to 'libgo/go/mime')
-rw-r--r-- | libgo/go/mime/grammar.go | 4 | ||||
-rw-r--r-- | libgo/go/mime/mediatype.go | 15 | ||||
-rw-r--r-- | libgo/go/mime/mediatype_test.go | 3 | ||||
-rw-r--r-- | libgo/go/mime/multipart/formdata.go | 2 | ||||
-rw-r--r-- | libgo/go/mime/multipart/formdata_test.go | 4 | ||||
-rw-r--r-- | libgo/go/mime/multipart/multipart.go | 90 | ||||
-rw-r--r-- | libgo/go/mime/multipart/multipart_test.go | 97 | ||||
-rw-r--r-- | libgo/go/mime/multipart/writer.go | 157 | ||||
-rw-r--r-- | libgo/go/mime/multipart/writer_test.go | 78 | ||||
-rw-r--r-- | libgo/go/mime/type.go | 2 |
10 files changed, 369 insertions, 83 deletions
diff --git a/libgo/go/mime/grammar.go b/libgo/go/mime/grammar.go index e60cbb8df74..6e319ff8be8 100644 --- a/libgo/go/mime/grammar.go +++ b/libgo/go/mime/grammar.go @@ -9,13 +9,13 @@ import ( ) // isTSpecial returns true if rune is in 'tspecials' as defined by RFC -// 1531 and RFC 2045. +// 1521 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. +// 1521 and RFC 2045. func IsTokenChar(rune int) bool { // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, // or tspecials> diff --git a/libgo/go/mime/mediatype.go b/libgo/go/mime/mediatype.go index f28ff3e9681..40c735c5baa 100644 --- a/libgo/go/mime/mediatype.go +++ b/libgo/go/mime/mediatype.go @@ -31,11 +31,13 @@ func validMediaTypeOrDisposition(s string) bool { } // 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. +// parameters, per RFC 1521. 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. The returned params +// is always a non-nil map. Params maps from the lowercase +// attribute to the attribute value with its case preserved. +// 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 { @@ -132,7 +134,7 @@ func ParseMediaType(v string) (mediatype string, params map[string]string) { } func decode2231Enc(v string) string { - sv := strings.Split(v, "'", 3) + sv := strings.SplitN(v, "'", 3) if len(sv) != 3 { return "" } @@ -211,6 +213,7 @@ func consumeMediaParam(v string) (param, value, rest string) { rest = rest[1:] // consume semicolon rest = strings.TrimLeftFunc(rest, unicode.IsSpace) param, rest = consumeToken(rest) + param = strings.ToLower(param) if param == "" { return "", "", v } diff --git a/libgo/go/mime/mediatype_test.go b/libgo/go/mime/mediatype_test.go index 454ddd03778..93264bd09a1 100644 --- a/libgo/go/mime/mediatype_test.go +++ b/libgo/go/mime/mediatype_test.go @@ -60,6 +60,7 @@ func TestConsumeMediaParam(t *testing.T) { {" ; foo=bar", "foo", "bar", ""}, {"; foo=bar", "foo", "bar", ""}, {";foo=bar", "foo", "bar", ""}, + {";FOO=bar", "foo", "bar", ""}, {`;foo="bar"`, "foo", "bar", ""}, {`;foo="bar"; `, "foo", "bar", "; "}, {`;foo="bar"; foo=baz`, "foo", "bar", "; foo=baz"}, @@ -127,7 +128,7 @@ func TestParseMediaType(t *testing.T) { `URL*1="cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar"`, "message/external-body", m("access-type", "URL", - "URL", "ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar")}, + "url", "ftp://cs.utk.edu/pub/moore/bulk-mailer/bulk-mailer.tar")}, {`application/x-stuff; ` + `title*0*=us-ascii'en'This%20is%20even%20more%20; ` + diff --git a/libgo/go/mime/multipart/formdata.go b/libgo/go/mime/multipart/formdata.go index 5f328656590..91404d6f41c 100644 --- a/libgo/go/mime/multipart/formdata.go +++ b/libgo/go/mime/multipart/formdata.go @@ -19,7 +19,7 @@ import ( // a Content-Disposition of "form-data". // It stores up to maxMemory bytes of the file parts in memory // and the remainder on disk in temporary files. -func (r *multiReader) ReadForm(maxMemory int64) (f *Form, err os.Error) { +func (r *Reader) ReadForm(maxMemory int64) (f *Form, err os.Error) { form := &Form{make(map[string][]string), make(map[string][]*FileHeader)} defer func() { if err != nil { diff --git a/libgo/go/mime/multipart/formdata_test.go b/libgo/go/mime/multipart/formdata_test.go index b56e2a430e0..4bc4649317a 100644 --- a/libgo/go/mime/multipart/formdata_test.go +++ b/libgo/go/mime/multipart/formdata_test.go @@ -31,10 +31,12 @@ func TestReadForm(t *testing.T) { if _, ok := fd.(*os.File); ok { t.Error("file is *os.File, should not be") } + fd.Close() fd = testFile(t, f.File["fileb"][0], "fileb.txt", filebContents) if _, ok := fd.(*os.File); !ok { - t.Error("file has unexpected underlying type %T", fd) + t.Errorf("file has unexpected underlying type %T", fd) } + fd.Close() } func testFile(t *testing.T, fh *FileHeader, efn, econtent string) File { diff --git a/libgo/go/mime/multipart/multipart.go b/libgo/go/mime/multipart/multipart.go index 9affa112611..2533bd337dc 100644 --- a/libgo/go/mime/multipart/multipart.go +++ b/libgo/go/mime/multipart/multipart.go @@ -21,28 +21,15 @@ import ( "mime" "net/textproto" "os" - "regexp" ) -var headerRegexp *regexp.Regexp = regexp.MustCompile("^([a-zA-Z0-9\\-]+): *([^\r\n]+)") +// TODO(bradfitz): inline these once the compiler can inline them in +// read-only situation (such as bytes.HasSuffix) +var lf = []byte("\n") +var crlf = []byte("\r\n") var emptyParams = make(map[string]string) -// 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 an error. - // When there are no more parts, the error os.EOF is returned. - NextPart() (*Part, os.Error) - - // ReadForm parses an entire multipart message whose parts have - // a Content-Disposition of "form-data". - // It stores up to maxMemory bytes of the file parts in memory - // and the remainder on disk in temporary files. - ReadForm(maxMemory int64) (*Form, 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 @@ -51,7 +38,7 @@ type Part struct { Header textproto.MIMEHeader buffer *bytes.Buffer - mr *multiReader + mr *Reader disposition string dispositionParams map[string]string @@ -71,7 +58,6 @@ func (p *Part) FormName() string { return p.dispositionParams["name"] } - // FileName returns the filename parameter of the Part's // Content-Disposition header. func (p *Part) FileName() string { @@ -91,20 +77,19 @@ func (p *Part) parseContentDisposition() { // NewReader creates a new multipart Reader reading from r using the // given MIME boundary. -func NewReader(reader io.Reader, boundary string) Reader { +func NewReader(reader io.Reader, boundary string) *Reader { b := []byte("\r\n--" + boundary + "--") - return &multiReader{ + return &Reader{ bufReader: bufio.NewReader(reader), + nl: b[:2], nlDashBoundary: b[:len(b)-2], dashBoundaryDash: b[2:], dashBoundary: b[2 : len(b)-2], } } -// Implementation .... - -func newPart(mr *multiReader) (*Part, os.Error) { +func newPart(mr *Reader) (*Part, os.Error) { bp := &Part{ Header: make(map[string][]string), mr: mr, @@ -117,22 +102,12 @@ func newPart(mr *multiReader) (*Part, os.Error) { } func (bp *Part) populateHeaders() os.Error { - for { - lineBytes, err := bp.mr.bufReader.ReadSlice('\n') - if err != nil { - return err - } - line := string(lineBytes) - if line == "\n" || line == "\r\n" { - return nil - } - if matches := headerRegexp.FindStringSubmatch(line); len(matches) == 3 { - bp.Header.Add(matches[1], matches[2]) - continue - } - return os.NewError("Unexpected header line found parsing multipart body") + r := textproto.NewReader(bp.mr.bufReader) + header, err := r.ReadMIMEHeader() + if err == nil { + bp.Header = header } - panic("unreachable") + return err } // Read reads the body of a part, after its headers and before the @@ -188,16 +163,21 @@ func (bp *Part) Close() os.Error { return nil } -type multiReader struct { +// 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 struct { bufReader *bufio.Reader currentPart *Part partsRead int - nlDashBoundary, dashBoundaryDash, dashBoundary []byte + nl, nlDashBoundary, dashBoundaryDash, dashBoundary []byte } -func (mr *multiReader) NextPart() (*Part, os.Error) { +// NextPart returns the next part in the multipart or an error. +// When there are no more parts, the error os.EOF is returned. +func (mr *Reader) NextPart() (*Part, os.Error) { if mr.currentPart != nil { mr.currentPart.Close() } @@ -233,11 +213,11 @@ func (mr *multiReader) NextPart() (*Part, os.Error) { continue } - if bytes.Equal(line, []byte("\r\n")) { - // Consume the "\r\n" separator between the - // body of the previous part and the boundary - // line we now expect will follow. (either a - // new part or the end boundary) + // Consume the "\n" or "\r\n" separator between the + // body of the previous part and the boundary line we + // now expect will follow. (either a new part or the + // end boundary) + if bytes.Equal(line, mr.nl) { expectNewPart = true continue } @@ -247,7 +227,7 @@ func (mr *multiReader) NextPart() (*Part, os.Error) { panic("unreachable") } -func (mr *multiReader) isBoundaryDelimiterLine(line []byte) bool { +func (mr *Reader) isBoundaryDelimiterLine(line []byte) 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 ("-", @@ -257,13 +237,17 @@ func (mr *multiReader) isBoundaryDelimiterLine(line []byte) bool { if !bytes.HasPrefix(line, mr.dashBoundary) { return false } - if bytes.HasSuffix(line, []byte("\r\n")) { - return onlyHorizontalWhitespace(line[len(mr.dashBoundary) : len(line)-2]) + if bytes.HasSuffix(line, mr.nl) { + return onlyHorizontalWhitespace(line[len(mr.dashBoundary) : len(line)-len(mr.nl)]) } // Violate the spec and also support newlines without the // carriage return... - if bytes.HasSuffix(line, []byte("\n")) { - return onlyHorizontalWhitespace(line[len(mr.dashBoundary) : len(line)-1]) + if mr.partsRead == 0 && bytes.HasSuffix(line, lf) { + if onlyHorizontalWhitespace(line[len(mr.dashBoundary) : len(line)-1]) { + mr.nl = mr.nl[1:] + mr.nlDashBoundary = mr.nlDashBoundary[1:] + return true + } } return false } @@ -280,5 +264,5 @@ func onlyHorizontalWhitespace(s []byte) bool { func hasPrefixThenNewline(s, prefix []byte) bool { return bytes.HasPrefix(s, prefix) && (len(s) == len(prefix)+1 && s[len(s)-1] == '\n' || - len(s) == len(prefix)+2 && bytes.HasSuffix(s, []byte("\r\n"))) + len(s) == len(prefix)+2 && bytes.HasSuffix(s, crlf)) } diff --git a/libgo/go/mime/multipart/multipart_test.go b/libgo/go/mime/multipart/multipart_test.go index 8222fbd8a4d..38079e53a19 100644 --- a/libgo/go/mime/multipart/multipart_test.go +++ b/libgo/go/mime/multipart/multipart_test.go @@ -25,7 +25,7 @@ func TestHorizontalWhitespace(t *testing.T) { } func TestBoundaryLine(t *testing.T) { - mr := NewReader(strings.NewReader(""), "myBoundary").(*multiReader) + mr := NewReader(strings.NewReader(""), "myBoundary") if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) { t.Error("expected") } @@ -81,7 +81,7 @@ func TestNameAccessors(t *testing.T) { var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8) -func testMultipartBody() string { +func testMultipartBody(sep string) string { testBody := ` This is a multi-part message. This line is ignored. --MyBoundary @@ -112,21 +112,26 @@ never read data useless trailer ` - testBody = strings.Replace(testBody, "\n", "\r\n", -1) + testBody = strings.Replace(testBody, "\n", sep, -1) return strings.Replace(testBody, "[longline]", longLine, 1) } func TestMultipart(t *testing.T) { - bodyReader := strings.NewReader(testMultipartBody()) - testMultipart(t, bodyReader) + bodyReader := strings.NewReader(testMultipartBody("\r\n")) + testMultipart(t, bodyReader, false) +} + +func TestMultipartOnlyNewlines(t *testing.T) { + bodyReader := strings.NewReader(testMultipartBody("\n")) + testMultipart(t, bodyReader, true) } func TestMultipartSlowInput(t *testing.T) { - bodyReader := strings.NewReader(testMultipartBody()) - testMultipart(t, &slowReader{bodyReader}) + bodyReader := strings.NewReader(testMultipartBody("\r\n")) + testMultipart(t, &slowReader{bodyReader}, false) } -func testMultipart(t *testing.T, r io.Reader) { +func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) { reader := NewReader(r, "MyBoundary") buf := new(bytes.Buffer) @@ -136,21 +141,28 @@ func testMultipart(t *testing.T, r io.Reader) { t.Error("Expected part1") return } - if part.Header.Get("Header1") != "value1" { - t.Error("Expected Header1: value") + if x := part.Header.Get("Header1"); x != "value1" { + t.Errorf("part.Header.Get(%q) = %q, want %q", "Header1", x, "value1") } - if part.Header.Get("foo-bar") != "baz" { - t.Error("Expected foo-bar: baz") + if x := part.Header.Get("foo-bar"); x != "baz" { + t.Errorf("part.Header.Get(%q) = %q, want %q", "foo-bar", x, "baz") } - if part.Header.Get("Foo-Bar") != "baz" { - t.Error("Expected Foo-Bar: baz") + if x := part.Header.Get("Foo-Bar"); x != "baz" { + t.Errorf("part.Header.Get(%q) = %q, want %q", "Foo-Bar", x, "baz") } buf.Reset() if _, err := io.Copy(buf, part); err != nil { t.Errorf("part 1 copy: %v", err) } - expectEq(t, "My value\r\nThe end.", - buf.String(), "Value of first part") + + adjustNewlines := func(s string) string { + if onlyNewlines { + return strings.Replace(s, "\r\n", "\n", -1) + } + return s + } + + expectEq(t, adjustNewlines("My value\r\nThe end."), buf.String(), "Value of first part") // Part2 part, err = reader.NextPart() @@ -187,7 +199,7 @@ func testMultipart(t *testing.T, r io.Reader) { if _, err := io.Copy(buf, part); err != nil { t.Errorf("part 3 copy: %v", err) } - expectEq(t, "Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n", + expectEq(t, adjustNewlines("Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n"), buf.String(), "body of part 3") // Part4 @@ -203,7 +215,7 @@ func testMultipart(t *testing.T, r io.Reader) { t.Error("Didn't expect a fifth part.") } if err != os.EOF { - t.Errorf("On fifth part expected os.EOF; got %v", err) + t.Errorf("On fifth part expected os.EOF; got %v", err) } } @@ -307,6 +319,29 @@ Oh no, premature EOF! } } +func TestZeroLengthBody(t *testing.T) { + testBody := strings.Replace(` +This is a multi-part message. This line is ignored. +--MyBoundary +foo: bar + + +--MyBoundary-- +`, "\n", "\r\n", -1) + r := NewReader(strings.NewReader(testBody), "MyBoundary") + part, err := r.NextPart() + if err != nil { + t.Fatalf("didn't get a part") + } + n, err := io.Copy(ioutil.Discard, part) + if err != nil { + t.Errorf("error reading part: %v", err) + } + if n != 0 { + t.Errorf("read %d bytes; expected 0", n) + } +} + type slowReader struct { r io.Reader } @@ -317,3 +352,29 @@ func (s *slowReader) Read(p []byte) (int, os.Error) { } return s.r.Read(p[:1]) } + +func TestLineContinuation(t *testing.T) { + // This body, extracted from an email, contains headers that span multiple + // lines. + + // TODO: The original mail ended with a double-newline before the + // final delimiter; this was manually edited to use a CRLF. + testBody := + "\n--Apple-Mail-2-292336769\nContent-Transfer-Encoding: 7bit\nContent-Type: text/plain;\n\tcharset=US-ASCII;\n\tdelsp=yes;\n\tformat=flowed\n\nI'm finding the same thing happening on my system (10.4.1).\n\n\n--Apple-Mail-2-292336769\nContent-Transfer-Encoding: quoted-printable\nContent-Type: text/html;\n\tcharset=ISO-8859-1\n\n<HTML><BODY>I'm finding the same thing =\nhappening on my system (10.4.1).=A0 But I built it with XCode =\n2.0.</BODY></=\nHTML>=\n\r\n--Apple-Mail-2-292336769--\n" + + r := NewReader(strings.NewReader(testBody), "Apple-Mail-2-292336769") + + for i := 0; i < 2; i++ { + part, err := r.NextPart() + if err != nil { + t.Fatalf("didn't get a part") + } + n, err := io.Copy(ioutil.Discard, part) + if err != nil { + t.Errorf("error reading part: %v", err) + } + if n <= 0 { + t.Errorf("read %d bytes; expected >0", n) + } + } +} diff --git a/libgo/go/mime/multipart/writer.go b/libgo/go/mime/multipart/writer.go new file mode 100644 index 00000000000..97a8897b299 --- /dev/null +++ b/libgo/go/mime/multipart/writer.go @@ -0,0 +1,157 @@ +// Copyright 2011 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" + "crypto/rand" + "fmt" + "io" + "net/textproto" + "os" + "strings" +) + +// A Writer generates multipart messages. +type Writer struct { + w io.Writer + boundary string + lastpart *part +} + +// NewWriter returns a new multipart Writer with a random boundary, +// writing to w. +func NewWriter(w io.Writer) *Writer { + return &Writer{ + w: w, + boundary: randomBoundary(), + } +} + +// Boundary returns the Writer's randomly selected boundary string. +func (w *Writer) Boundary() string { + return w.boundary +} + +// FormDataContentType returns the Content-Type for an HTTP +// multipart/form-data with this Writer's Boundary. +func (w *Writer) FormDataContentType() string { + return "multipart/form-data; boundary=" + w.boundary +} + +func randomBoundary() string { + var buf [30]byte + _, err := io.ReadFull(rand.Reader, buf[:]) + if err != nil { + panic(err) + } + return fmt.Sprintf("%x", buf[:]) +} + +// CreatePart creates a new multipart section with the provided +// header. The body of the part should be written to the returned +// Writer. After calling CreatePart, any previous part may no longer +// be written to. +func (w *Writer) CreatePart(header textproto.MIMEHeader) (io.Writer, os.Error) { + if w.lastpart != nil { + if err := w.lastpart.close(); err != nil { + return nil, err + } + } + var b bytes.Buffer + if w.lastpart != nil { + fmt.Fprintf(&b, "\r\n--%s\r\n", w.boundary) + } else { + fmt.Fprintf(&b, "--%s\r\n", w.boundary) + } + // TODO(bradfitz): move this to textproto.MimeHeader.Write(w), have it sort + // and clean, like http.Header.Write(w) does. + for k, vv := range header { + for _, v := range vv { + fmt.Fprintf(&b, "%s: %s\r\n", k, v) + } + } + fmt.Fprintf(&b, "\r\n") + _, err := io.Copy(w.w, &b) + if err != nil { + return nil, err + } + p := &part{ + mw: w, + } + w.lastpart = p + return p, nil +} + +func escapeQuotes(s string) string { + s = strings.Replace(s, "\\", "\\\\", -1) + s = strings.Replace(s, "\"", "\\\"", -1) + return s +} + +// CreateFormFile is a convenience wrapper around CreatePart. It creates +// a new form-data header with the provided field name and file name. +func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, os.Error) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", + fmt.Sprintf(`form-data; name="%s"; filename="%s"`, + escapeQuotes(fieldname), escapeQuotes(filename))) + h.Set("Content-Type", "application/octet-stream") + return w.CreatePart(h) +} + +// CreateFormField calls CreatePart with a header using the +// given field name. +func (w *Writer) CreateFormField(fieldname string) (io.Writer, os.Error) { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", + fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname))) + return w.CreatePart(h) +} + +// WriteField calls CreateFormField and then writes the given value. +func (w *Writer) WriteField(fieldname, value string) os.Error { + p, err := w.CreateFormField(fieldname) + if err != nil { + return err + } + _, err = p.Write([]byte(value)) + return err +} + +// Close finishes the multipart message and writes the trailing +// boundary end line to the output. +func (w *Writer) Close() os.Error { + if w.lastpart != nil { + if err := w.lastpart.close(); err != nil { + return err + } + w.lastpart = nil + } + _, err := fmt.Fprintf(w.w, "\r\n--%s--\r\n", w.boundary) + return err +} + +type part struct { + mw *Writer + closed bool + we os.Error // last error that occurred writing +} + +func (p *part) close() os.Error { + p.closed = true + return p.we +} + +func (p *part) Write(d []byte) (n int, err os.Error) { + if p.closed { + return 0, os.NewError("multipart: can't write to finished part") + } + n, err = p.mw.w.Write(d) + if err != nil { + p.we = err + } + return +} diff --git a/libgo/go/mime/multipart/writer_test.go b/libgo/go/mime/multipart/writer_test.go new file mode 100644 index 00000000000..494e936c4c6 --- /dev/null +++ b/libgo/go/mime/multipart/writer_test.go @@ -0,0 +1,78 @@ +// Copyright 2011 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" + "io/ioutil" + "testing" +) + +func TestWriter(t *testing.T) { + fileContents := []byte("my file contents") + + var b bytes.Buffer + w := NewWriter(&b) + { + part, err := w.CreateFormFile("myfile", "my-file.txt") + if err != nil { + t.Fatalf("CreateFormFile: %v", err) + } + part.Write(fileContents) + err = w.WriteField("key", "val") + if err != nil { + t.Fatalf("WriteField: %v", err) + } + part.Write([]byte("val")) + err = w.Close() + if err != nil { + t.Fatalf("Close: %v", err) + } + s := b.String() + if len(s) == 0 { + t.Fatal("String: unexpected empty result") + } + if s[0] == '\r' || s[0] == '\n' { + t.Fatal("String: unexpected newline") + } + } + + r := NewReader(&b, w.Boundary()) + + part, err := r.NextPart() + if err != nil { + t.Fatalf("part 1: %v", err) + } + if g, e := part.FormName(), "myfile"; g != e { + t.Errorf("part 1: want form name %q, got %q", e, g) + } + slurp, err := ioutil.ReadAll(part) + if err != nil { + t.Fatalf("part 1: ReadAll: %v", err) + } + if e, g := string(fileContents), string(slurp); e != g { + t.Errorf("part 1: want contents %q, got %q", e, g) + } + + part, err = r.NextPart() + if err != nil { + t.Fatalf("part 2: %v", err) + } + if g, e := part.FormName(), "key"; g != e { + t.Errorf("part 2: want form name %q, got %q", e, g) + } + slurp, err = ioutil.ReadAll(part) + if err != nil { + t.Fatalf("part 2: ReadAll: %v", err) + } + if e, g := "val", string(slurp); e != g { + t.Errorf("part 2: want contents %q, got %q", e, g) + } + + part, err = r.NextPart() + if part != nil || err == nil { + t.Fatalf("expected end of parts; got %v, %v", part, err) + } +} diff --git a/libgo/go/mime/type.go b/libgo/go/mime/type.go index 8c43b81b0c5..8ecfe9a37b1 100644 --- a/libgo/go/mime/type.go +++ b/libgo/go/mime/type.go @@ -19,7 +19,7 @@ var typeFiles = []string{ } var mimeTypes = map[string]string{ - ".css": "text/css", + ".css": "text/css; charset=utf-8", ".gif": "image/gif", ".htm": "text/html; charset=utf-8", ".html": "text/html; charset=utf-8", |