diff options
Diffstat (limited to 'src/mime/multipart')
-rw-r--r-- | src/mime/multipart/example_test.go | 52 | ||||
-rw-r--r-- | src/mime/multipart/formdata.go | 182 | ||||
-rw-r--r-- | src/mime/multipart/formdata_test.go | 236 | ||||
-rw-r--r-- | src/mime/multipart/multipart.go | 429 | ||||
-rw-r--r-- | src/mime/multipart/multipart_test.go | 960 | ||||
-rw-r--r-- | src/mime/multipart/testdata/nested-mime | 29 | ||||
-rw-r--r-- | src/mime/multipart/writer.go | 201 | ||||
-rw-r--r-- | src/mime/multipart/writer_test.go | 174 |
8 files changed, 2263 insertions, 0 deletions
diff --git a/src/mime/multipart/example_test.go b/src/mime/multipart/example_test.go new file mode 100644 index 0000000..fe154ac --- /dev/null +++ b/src/mime/multipart/example_test.go @@ -0,0 +1,52 @@ +// Copyright 2014 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_test + +import ( + "fmt" + "io" + "log" + "mime" + "mime/multipart" + "net/mail" + "strings" +) + +func ExampleNewReader() { + msg := &mail.Message{ + Header: map[string][]string{ + "Content-Type": {"multipart/mixed; boundary=foo"}, + }, + Body: strings.NewReader( + "--foo\r\nFoo: one\r\n\r\nA section\r\n" + + "--foo\r\nFoo: two\r\n\r\nAnd another\r\n" + + "--foo--\r\n"), + } + mediaType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type")) + if err != nil { + log.Fatal(err) + } + if strings.HasPrefix(mediaType, "multipart/") { + mr := multipart.NewReader(msg.Body, params["boundary"]) + for { + p, err := mr.NextPart() + if err == io.EOF { + return + } + if err != nil { + log.Fatal(err) + } + slurp, err := io.ReadAll(p) + if err != nil { + log.Fatal(err) + } + fmt.Printf("Part %q: %q\n", p.Header.Get("Foo"), slurp) + } + } + + // Output: + // Part "one": "A section" + // Part "two": "And another" +} diff --git a/src/mime/multipart/formdata.go b/src/mime/multipart/formdata.go new file mode 100644 index 0000000..fca5f9e --- /dev/null +++ b/src/mime/multipart/formdata.go @@ -0,0 +1,182 @@ +// 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" + "errors" + "io" + "math" + "net/textproto" + "os" +) + +// ErrMessageTooLarge is returned by ReadForm if the message form +// data is too large to be processed. +var ErrMessageTooLarge = errors.New("multipart: message too large") + +// TODO(adg,bradfitz): find a way to unify the DoS-prevention strategy here +// with that of the http package's ParseForm. + +// ReadForm parses an entire multipart message whose parts have +// a Content-Disposition of "form-data". +// It stores up to maxMemory bytes + 10MB (reserved for non-file parts) +// in memory. File parts which can't be stored in memory will be stored on +// disk in temporary files. +// It returns ErrMessageTooLarge if all non-file parts can't be stored in +// memory. +func (r *Reader) ReadForm(maxMemory int64) (*Form, error) { + return r.readForm(maxMemory) +} + +func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) { + form := &Form{make(map[string][]string), make(map[string][]*FileHeader)} + defer func() { + if err != nil { + form.RemoveAll() + } + }() + + // Reserve an additional 10 MB for non-file parts. + maxValueBytes := maxMemory + int64(10<<20) + if maxValueBytes <= 0 { + if maxMemory < 0 { + maxValueBytes = 0 + } else { + maxValueBytes = math.MaxInt64 + } + } + for { + p, err := r.NextPart() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + + name := p.FormName() + if name == "" { + continue + } + filename := p.FileName() + + var b bytes.Buffer + + if filename == "" { + // value, store as string in memory + n, err := io.CopyN(&b, p, maxValueBytes+1) + if err != nil && err != io.EOF { + return nil, err + } + maxValueBytes -= n + if maxValueBytes < 0 { + return nil, ErrMessageTooLarge + } + form.Value[name] = append(form.Value[name], b.String()) + continue + } + + // file, store in memory or on disk + fh := &FileHeader{ + Filename: filename, + Header: p.Header, + } + n, err := io.CopyN(&b, p, maxMemory+1) + if err != nil && err != io.EOF { + return nil, err + } + if n > maxMemory { + // too big, write to disk and flush buffer + file, err := os.CreateTemp("", "multipart-") + if err != nil { + return nil, err + } + size, err := io.Copy(file, io.MultiReader(&b, p)) + if cerr := file.Close(); err == nil { + err = cerr + } + if err != nil { + os.Remove(file.Name()) + return nil, err + } + fh.tmpfile = file.Name() + fh.Size = size + } else { + fh.content = b.Bytes() + fh.Size = int64(len(fh.content)) + maxMemory -= n + maxValueBytes -= n + } + form.File[name] = append(form.File[name], fh) + } + + return form, nil +} + +// Form is a parsed multipart form. +// Its File parts are stored either in memory or on disk, +// and are accessible via the *FileHeader's Open method. +// Its Value parts are stored as strings. +// Both are keyed by field name. +type Form struct { + Value map[string][]string + File map[string][]*FileHeader +} + +// RemoveAll removes any temporary files associated with a Form. +func (f *Form) RemoveAll() error { + var err error + for _, fhs := range f.File { + for _, fh := range fhs { + if fh.tmpfile != "" { + e := os.Remove(fh.tmpfile) + if e != nil && err == nil { + err = e + } + } + } + } + return err +} + +// A FileHeader describes a file part of a multipart request. +type FileHeader struct { + Filename string + Header textproto.MIMEHeader + Size int64 + + content []byte + tmpfile string +} + +// Open opens and returns the FileHeader's associated File. +func (fh *FileHeader) Open() (File, error) { + if b := fh.content; b != nil { + r := io.NewSectionReader(bytes.NewReader(b), 0, int64(len(b))) + return sectionReadCloser{r}, nil + } + return os.Open(fh.tmpfile) +} + +// File is an interface to access the file part of a multipart message. +// Its contents may be either stored in memory or on disk. +// If stored on disk, the File's underlying concrete type will be an *os.File. +type File interface { + io.Reader + io.ReaderAt + io.Seeker + io.Closer +} + +// helper types to turn a []byte into a File + +type sectionReadCloser struct { + *io.SectionReader +} + +func (rc sectionReadCloser) Close() error { + return nil +} diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go new file mode 100644 index 0000000..e3a3a3e --- /dev/null +++ b/src/mime/multipart/formdata_test.go @@ -0,0 +1,236 @@ +// 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" + "math" + "os" + "strings" + "testing" +) + +func TestReadForm(t *testing.T) { + b := strings.NewReader(strings.ReplaceAll(message, "\n", "\r\n")) + r := NewReader(b, boundary) + f, err := r.ReadForm(25) + if err != nil { + t.Fatal("ReadForm:", err) + } + defer f.RemoveAll() + if g, e := f.Value["texta"][0], textaValue; g != e { + t.Errorf("texta value = %q, want %q", g, e) + } + if g, e := f.Value["textb"][0], textbValue; g != e { + t.Errorf("texta value = %q, want %q", g, e) + } + fd := testFile(t, f.File["filea"][0], "filea.txt", fileaContents) + 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.Errorf("file has unexpected underlying type %T", fd) + } + fd.Close() +} + +func TestReadFormWithNamelessFile(t *testing.T) { + b := strings.NewReader(strings.ReplaceAll(messageWithFileWithoutName, "\n", "\r\n")) + r := NewReader(b, boundary) + f, err := r.ReadForm(25) + if err != nil { + t.Fatal("ReadForm:", err) + } + defer f.RemoveAll() + + if g, e := f.Value["hiddenfile"][0], filebContents; g != e { + t.Errorf("hiddenfile value = %q, want %q", g, e) + } +} + +// Issue 40430: Handle ReadForm(math.MaxInt64) +func TestReadFormMaxMemoryOverflow(t *testing.T) { + b := strings.NewReader(strings.ReplaceAll(messageWithTextContentType, "\n", "\r\n")) + r := NewReader(b, boundary) + f, err := r.ReadForm(math.MaxInt64) + if err != nil { + t.Fatalf("ReadForm(MaxInt64): %v", err) + } + if f == nil { + t.Fatal("ReadForm(MaxInt64): missing form") + } +} + +func TestReadFormWithTextContentType(t *testing.T) { + // From https://github.com/golang/go/issues/24041 + b := strings.NewReader(strings.ReplaceAll(messageWithTextContentType, "\n", "\r\n")) + r := NewReader(b, boundary) + f, err := r.ReadForm(25) + if err != nil { + t.Fatal("ReadForm:", err) + } + defer f.RemoveAll() + + if g, e := f.Value["texta"][0], textaValue; g != e { + t.Errorf("texta value = %q, want %q", g, e) + } +} + +func testFile(t *testing.T, fh *FileHeader, efn, econtent string) File { + if fh.Filename != efn { + t.Errorf("filename = %q, want %q", fh.Filename, efn) + } + if fh.Size != int64(len(econtent)) { + t.Errorf("size = %d, want %d", fh.Size, len(econtent)) + } + f, err := fh.Open() + if err != nil { + t.Fatal("opening file:", err) + } + b := new(bytes.Buffer) + _, err = io.Copy(b, f) + if err != nil { + t.Fatal("copying contents:", err) + } + if g := b.String(); g != econtent { + t.Errorf("contents = %q, want %q", g, econtent) + } + return f +} + +const ( + fileaContents = "This is a test file." + filebContents = "Another test file." + textaValue = "foo" + textbValue = "bar" + boundary = `MyBoundary` +) + +const messageWithFileWithoutName = ` +--MyBoundary +Content-Disposition: form-data; name="hiddenfile"; filename="" +Content-Type: text/plain + +` + filebContents + ` +--MyBoundary-- +` + +const messageWithTextContentType = ` +--MyBoundary +Content-Disposition: form-data; name="texta" +Content-Type: text/plain + +` + textaValue + ` +--MyBoundary +` + +const message = ` +--MyBoundary +Content-Disposition: form-data; name="filea"; filename="filea.txt" +Content-Type: text/plain + +` + fileaContents + ` +--MyBoundary +Content-Disposition: form-data; name="fileb"; filename="fileb.txt" +Content-Type: text/plain + +` + filebContents + ` +--MyBoundary +Content-Disposition: form-data; name="texta" + +` + textaValue + ` +--MyBoundary +Content-Disposition: form-data; name="textb" + +` + textbValue + ` +--MyBoundary-- +` + +func TestReadForm_NoReadAfterEOF(t *testing.T) { + maxMemory := int64(32) << 20 + boundary := `---------------------------8d345eef0d38dc9` + body := ` +-----------------------------8d345eef0d38dc9 +Content-Disposition: form-data; name="version" + +171 +-----------------------------8d345eef0d38dc9--` + + mr := NewReader(&failOnReadAfterErrorReader{t: t, r: strings.NewReader(body)}, boundary) + + f, err := mr.ReadForm(maxMemory) + if err != nil { + t.Fatal(err) + } + t.Logf("Got: %#v", f) +} + +// failOnReadAfterErrorReader is an io.Reader wrapping r. +// It fails t if any Read is called after a failing Read. +type failOnReadAfterErrorReader struct { + t *testing.T + r io.Reader + sawErr error +} + +func (r *failOnReadAfterErrorReader) Read(p []byte) (n int, err error) { + if r.sawErr != nil { + r.t.Fatalf("unexpected Read on Reader after previous read saw error %v", r.sawErr) + } + n, err = r.r.Read(p) + r.sawErr = err + return +} + +// TestReadForm_NonFileMaxMemory asserts that the ReadForm maxMemory limit is applied +// while processing non-file form data as well as file form data. +func TestReadForm_NonFileMaxMemory(t *testing.T) { + n := 10<<20 + 25 + if testing.Short() { + n = 10<<10 + 25 + } + largeTextValue := strings.Repeat("1", n) + message := `--MyBoundary +Content-Disposition: form-data; name="largetext" + +` + largeTextValue + ` +--MyBoundary-- +` + + testBody := strings.ReplaceAll(message, "\n", "\r\n") + testCases := []struct { + name string + maxMemory int64 + err error + }{ + {"smaller", 50, nil}, + {"exact-fit", 25, nil}, + {"too-large", 0, ErrMessageTooLarge}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.maxMemory == 0 && testing.Short() { + t.Skip("skipping in -short mode") + } + b := strings.NewReader(testBody) + r := NewReader(b, boundary) + f, err := r.ReadForm(tc.maxMemory) + if err == nil { + defer f.RemoveAll() + } + if tc.err != err { + t.Fatalf("ReadForm error - got: %v; expected: %v", tc.err, err) + } + if err == nil { + if g := f.Value["largetext"][0]; g != largeTextValue { + t.Errorf("largetext mismatch: got size: %v, expected size: %v", len(g), len(largeTextValue)) + } + } + }) + } +} diff --git a/src/mime/multipart/multipart.go b/src/mime/multipart/multipart.go new file mode 100644 index 0000000..81bf722 --- /dev/null +++ b/src/mime/multipart/multipart.go @@ -0,0 +1,429 @@ +// 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" + "fmt" + "io" + "mime" + "mime/quotedprintable" + "net/textproto" + "path/filepath" + "strings" +) + +var emptyParams = make(map[string]string) + +// This constant needs to be at least 76 for this package to work correctly. +// This is because \r\n--separator_of_len_70- would fill the buffer and it +// wouldn't be safe to consume a single byte from it. +const peekBufferSize = 4096 + +// 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. + // For example, "foo-bar" changes case to "Foo-Bar" + Header textproto.MIMEHeader + + mr *Reader + + disposition string + dispositionParams map[string]string + + // r is either a reader directly reading from mr, or it's a + // wrapper around such a reader, decoding the + // Content-Transfer-Encoding + r io.Reader + + n int // known data bytes waiting in mr.bufReader + total int64 // total data bytes read already + err error // error to return when n == 0 + readErr error // read error observed from mr.bufReader +} + +// 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 https://tools.ietf.org/html/rfc2183 section 2 for EBNF + // of Content-Disposition value format. + if p.dispositionParams == nil { + p.parseContentDisposition() + } + if p.disposition != "form-data" { + return "" + } + return p.dispositionParams["name"] +} + +// FileName returns the filename parameter of the Part's Content-Disposition +// header. If not empty, the filename is passed through filepath.Base (which is +// platform dependent) before being returned. +func (p *Part) FileName() string { + if p.dispositionParams == nil { + p.parseContentDisposition() + } + filename := p.dispositionParams["filename"] + if filename == "" { + return "" + } + // RFC 7578, Section 4.2 requires that if a filename is provided, the + // directory path information must not be used. + return filepath.Base(filename) +} + +func (p *Part) parseContentDisposition() { + v := p.Header.Get("Content-Disposition") + var err error + p.disposition, p.dispositionParams, err = mime.ParseMediaType(v) + if err != nil { + p.dispositionParams = emptyParams + } +} + +// NewReader creates a new multipart Reader reading from r using the +// given MIME boundary. +// +// The boundary is usually obtained from the "boundary" parameter of +// the message's "Content-Type" header. Use mime.ParseMediaType to +// parse such headers. +func NewReader(r io.Reader, boundary string) *Reader { + b := []byte("\r\n--" + boundary + "--") + return &Reader{ + bufReader: bufio.NewReaderSize(&stickyErrorReader{r: r}, peekBufferSize), + nl: b[:2], + nlDashBoundary: b[:len(b)-2], + dashBoundaryDash: b[2:], + dashBoundary: b[2 : len(b)-2], + } +} + +// stickyErrorReader is an io.Reader which never calls Read on its +// underlying Reader once an error has been seen. (the io.Reader +// interface's contract promises nothing about the return values of +// Read calls after an error, yet this package does do multiple Reads +// after error) +type stickyErrorReader struct { + r io.Reader + err error +} + +func (r *stickyErrorReader) Read(p []byte) (n int, _ error) { + if r.err != nil { + return 0, r.err + } + n, r.err = r.r.Read(p) + return n, r.err +} + +func newPart(mr *Reader, rawPart bool) (*Part, error) { + bp := &Part{ + Header: make(map[string][]string), + mr: mr, + } + if err := bp.populateHeaders(); err != nil { + return nil, err + } + bp.r = partReader{bp} + + // rawPart is used to switch between Part.NextPart and Part.NextRawPart. + if !rawPart { + const cte = "Content-Transfer-Encoding" + if strings.EqualFold(bp.Header.Get(cte), "quoted-printable") { + bp.Header.Del(cte) + bp.r = quotedprintable.NewReader(bp.r) + } + } + return bp, nil +} + +func (bp *Part) populateHeaders() error { + r := textproto.NewReader(bp.mr.bufReader) + header, err := r.ReadMIMEHeader() + if err == nil { + bp.Header = header + } + return err +} + +// Read reads the body of a part, after its headers and before the +// next part (if any) begins. +func (p *Part) Read(d []byte) (n int, err error) { + return p.r.Read(d) +} + +// partReader implements io.Reader by reading raw bytes directly from the +// wrapped *Part, without doing any Transfer-Encoding decoding. +type partReader struct { + p *Part +} + +func (pr partReader) Read(d []byte) (int, error) { + p := pr.p + br := p.mr.bufReader + + // Read into buffer until we identify some data to return, + // or we find a reason to stop (boundary or read error). + for p.n == 0 && p.err == nil { + peek, _ := br.Peek(br.Buffered()) + p.n, p.err = scanUntilBoundary(peek, p.mr.dashBoundary, p.mr.nlDashBoundary, p.total, p.readErr) + if p.n == 0 && p.err == nil { + // Force buffered I/O to read more into buffer. + _, p.readErr = br.Peek(len(peek) + 1) + if p.readErr == io.EOF { + p.readErr = io.ErrUnexpectedEOF + } + } + } + + // Read out from "data to return" part of buffer. + if p.n == 0 { + return 0, p.err + } + n := len(d) + if n > p.n { + n = p.n + } + n, _ = br.Read(d[:n]) + p.total += int64(n) + p.n -= n + if p.n == 0 { + return n, p.err + } + return n, nil +} + +// scanUntilBoundary scans buf to identify how much of it can be safely +// returned as part of the Part body. +// dashBoundary is "--boundary". +// nlDashBoundary is "\r\n--boundary" or "\n--boundary", depending on what mode we are in. +// The comments below (and the name) assume "\n--boundary", but either is accepted. +// total is the number of bytes read out so far. If total == 0, then a leading "--boundary" is recognized. +// readErr is the read error, if any, that followed reading the bytes in buf. +// scanUntilBoundary returns the number of data bytes from buf that can be +// returned as part of the Part body and also the error to return (if any) +// once those data bytes are done. +func scanUntilBoundary(buf, dashBoundary, nlDashBoundary []byte, total int64, readErr error) (int, error) { + if total == 0 { + // At beginning of body, allow dashBoundary. + if bytes.HasPrefix(buf, dashBoundary) { + switch matchAfterPrefix(buf, dashBoundary, readErr) { + case -1: + return len(dashBoundary), nil + case 0: + return 0, nil + case +1: + return 0, io.EOF + } + } + if bytes.HasPrefix(dashBoundary, buf) { + return 0, readErr + } + } + + // Search for "\n--boundary". + if i := bytes.Index(buf, nlDashBoundary); i >= 0 { + switch matchAfterPrefix(buf[i:], nlDashBoundary, readErr) { + case -1: + return i + len(nlDashBoundary), nil + case 0: + return i, nil + case +1: + return i, io.EOF + } + } + if bytes.HasPrefix(nlDashBoundary, buf) { + return 0, readErr + } + + // Otherwise, anything up to the final \n is not part of the boundary + // and so must be part of the body. + // Also if the section from the final \n onward is not a prefix of the boundary, + // it too must be part of the body. + i := bytes.LastIndexByte(buf, nlDashBoundary[0]) + if i >= 0 && bytes.HasPrefix(nlDashBoundary, buf[i:]) { + return i, nil + } + return len(buf), readErr +} + +// matchAfterPrefix checks whether buf should be considered to match the boundary. +// The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary", +// and the caller has verified already that bytes.HasPrefix(buf, prefix) is true. +// +// matchAfterPrefix returns +1 if the buffer does match the boundary, +// meaning the prefix is followed by a dash, space, tab, cr, nl, or end of input. +// It returns -1 if the buffer definitely does NOT match the boundary, +// meaning the prefix is followed by some other character. +// For example, "--foobar" does not match "--foo". +// It returns 0 more input needs to be read to make the decision, +// meaning that len(buf) == len(prefix) and readErr == nil. +func matchAfterPrefix(buf, prefix []byte, readErr error) int { + if len(buf) == len(prefix) { + if readErr != nil { + return +1 + } + return 0 + } + c := buf[len(prefix)] + if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '-' { + return +1 + } + return -1 +} + +func (p *Part) Close() error { + io.Copy(io.Discard, p) + return nil +} + +// 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 + + nl []byte // "\r\n" or "\n" (set after seeing first boundary line) + nlDashBoundary []byte // nl + "--boundary" + dashBoundaryDash []byte // "--boundary--" + dashBoundary []byte // "--boundary" +} + +// NextPart returns the next part in the multipart or an error. +// When there are no more parts, the error io.EOF is returned. +// +// As a special case, if the "Content-Transfer-Encoding" header +// has a value of "quoted-printable", that header is instead +// hidden and the body is transparently decoded during Read calls. +func (r *Reader) NextPart() (*Part, error) { + return r.nextPart(false) +} + +// NextRawPart returns the next part in the multipart or an error. +// When there are no more parts, the error io.EOF is returned. +// +// Unlike NextPart, it does not have special handling for +// "Content-Transfer-Encoding: quoted-printable". +func (r *Reader) NextRawPart() (*Part, error) { + return r.nextPart(true) +} + +func (r *Reader) nextPart(rawPart bool) (*Part, error) { + if r.currentPart != nil { + r.currentPart.Close() + } + if string(r.dashBoundary) == "--" { + return nil, fmt.Errorf("multipart: boundary is empty") + } + expectNewPart := false + for { + line, err := r.bufReader.ReadSlice('\n') + + if err == io.EOF && r.isFinalBoundary(line) { + // If the buffer ends in "--boundary--" without the + // trailing "\r\n", ReadSlice will return an error + // (since it's missing the '\n'), but this is a valid + // multipart EOF so we need to return io.EOF instead of + // a fmt-wrapped one. + return nil, io.EOF + } + if err != nil { + return nil, fmt.Errorf("multipart: NextPart: %v", err) + } + + if r.isBoundaryDelimiterLine(line) { + r.partsRead++ + bp, err := newPart(r, rawPart) + if err != nil { + return nil, err + } + r.currentPart = bp + return bp, nil + } + + if r.isFinalBoundary(line) { + // Expected EOF + return nil, io.EOF + } + + if expectNewPart { + return nil, fmt.Errorf("multipart: expecting a new Part; got line %q", string(line)) + } + + if r.partsRead == 0 { + // skip line + continue + } + + // 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, r.nl) { + expectNewPart = true + continue + } + + return nil, fmt.Errorf("multipart: unexpected line in Next(): %q", line) + } +} + +// isFinalBoundary reports whether line is the final boundary line +// indicating that all parts are over. +// It matches `^--boundary--[ \t]*(\r\n)?$` +func (mr *Reader) isFinalBoundary(line []byte) bool { + if !bytes.HasPrefix(line, mr.dashBoundaryDash) { + return false + } + rest := line[len(mr.dashBoundaryDash):] + rest = skipLWSPChar(rest) + return len(rest) == 0 || bytes.Equal(rest, mr.nl) +} + +func (mr *Reader) isBoundaryDelimiterLine(line []byte) (ret bool) { + // https://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 !bytes.HasPrefix(line, mr.dashBoundary) { + return false + } + rest := line[len(mr.dashBoundary):] + rest = skipLWSPChar(rest) + + // On the first part, see our lines are ending in \n instead of \r\n + // and switch into that mode if so. This is a violation of the spec, + // but occurs in practice. + if mr.partsRead == 0 && len(rest) == 1 && rest[0] == '\n' { + mr.nl = mr.nl[1:] + mr.nlDashBoundary = mr.nlDashBoundary[1:] + } + return bytes.Equal(rest, mr.nl) +} + +// skipLWSPChar returns b with leading spaces and tabs removed. +// RFC 822 defines: +// LWSP-char = SPACE / HTAB +func skipLWSPChar(b []byte) []byte { + for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') { + b = b[1:] + } + return b +} diff --git a/src/mime/multipart/multipart_test.go b/src/mime/multipart/multipart_test.go new file mode 100644 index 0000000..741d230 --- /dev/null +++ b/src/mime/multipart/multipart_test.go @@ -0,0 +1,960 @@ +// 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" + "encoding/json" + "fmt" + "io" + "net/textproto" + "os" + "reflect" + "strings" + "testing" +) + +func TestBoundaryLine(t *testing.T) { + mr := NewReader(strings.NewReader(""), "myBoundary") + if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) { + t.Error("expected") + } + if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) { + t.Error("expected") + } + if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) { + t.Error("expected") + } + if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus \n")) { + t.Error("expected fail") + } + if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus--")) { + 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 TestNameAccessors(t *testing.T) { + tests := [...][3]string{ + {`form-data; name="foo"`, "foo", ""}, + {` form-data ; name=foo`, "foo", ""}, + {`FORM-DATA;name="foo"`, "foo", ""}, + {` FORM-DATA ; name="foo"`, "foo", ""}, + {` FORM-DATA ; name="foo"`, "foo", ""}, + {` FORM-DATA ; name=foo`, "foo", ""}, + {` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo", "foo.txt"}, + {` not-form-data ; filename="bar.txt"; name=foo; baz=quux`, "", "bar.txt"}, + } + for i, test := range tests { + p := &Part{Header: make(map[string][]string)} + p.Header.Set("Content-Disposition", test[0]) + if g, e := p.FormName(), test[1]; g != e { + t.Errorf("test %d: FormName() = %q; want %q", i, g, e) + } + if g, e := p.FileName(), test[2]; g != e { + t.Errorf("test %d: FileName() = %q; want %q", i, g, e) + } + } +} + +var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8) + +func testMultipartBody(sep string) string { + testBody := ` +This is a multi-part message. This line is ignored. +--MyBoundary +Header1: value1 +HEADER2: value2 +foo-bar: baz + +My value +The end. +--MyBoundary +name: bigsection + +[longline] +--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-- + + +useless trailer +` + testBody = strings.ReplaceAll(testBody, "\n", sep) + return strings.Replace(testBody, "[longline]", longLine, 1) +} + +func TestMultipart(t *testing.T) { + 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("\r\n")) + testMultipart(t, &slowReader{bodyReader}, false) +} + +func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) { + t.Parallel() + reader := NewReader(r, "MyBoundary") + buf := new(bytes.Buffer) + + // Part1 + part, err := reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part1") + return + } + if x := part.Header.Get("Header1"); x != "value1" { + t.Errorf("part.Header.Get(%q) = %q, want %q", "Header1", x, "value1") + } + if x := part.Header.Get("foo-bar"); x != "baz" { + t.Errorf("part.Header.Get(%q) = %q, want %q", "foo-bar", x, "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) + } + + adjustNewlines := func(s string) string { + if onlyNewlines { + return strings.ReplaceAll(s, "\r\n", "\n") + } + return s + } + + expectEq(t, adjustNewlines("My value\r\nThe end."), buf.String(), "Value of first part") + + // Part2 + part, err = reader.NextPart() + if err != nil { + t.Fatalf("Expected part2; got: %v", err) + return + } + if e, g := "bigsection", part.Header.Get("name"); e != g { + t.Errorf("part2's name header: expected %q, got %q", e, g) + } + buf.Reset() + if _, err := io.Copy(buf, part); err != nil { + t.Errorf("part 2 copy: %v", err) + } + s := buf.String() + if len(s) != len(longLine) { + t.Errorf("part2 body expected long line of length %d; got length %d", + len(longLine), len(s)) + } + if s != longLine { + t.Errorf("part2 long body didn't match") + } + + // Part3 + part, err = reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part3") + return + } + if part.Header.Get("foo-bar") != "bazb" { + t.Error("Expected foo-bar: bazb") + } + buf.Reset() + if _, err := io.Copy(buf, part); err != nil { + t.Errorf("part 3 copy: %v", err) + } + 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 + part, err = reader.NextPart() + if part == nil || err != nil { + t.Error("Expected part 4 without errors") + return + } + + // Non-existent part5 + part, err = reader.NextPart() + if part != nil { + t.Error("Didn't expect a fifth part.") + } + if err != io.EOF { + t.Errorf("On fifth part expected io.EOF; got %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 != io.EOF { + t.Errorf("On test %d expected io.EOF; got %v", testNum, err) + } + + } +} + +type maliciousReader struct { + t *testing.T + n int +} + +const maxReadThreshold = 1 << 20 + +func (mr *maliciousReader) Read(b []byte) (n int, err error) { + mr.n += len(b) + if mr.n >= maxReadThreshold { + mr.t.Fatal("too much was read") + return 0, io.EOF + } + return len(b), nil +} + +func TestLineLimit(t *testing.T) { + mr := &maliciousReader{t: t} + r := NewReader(mr, "fooBoundary") + part, err := r.NextPart() + if part != nil { + t.Errorf("unexpected part read") + } + if err == nil { + t.Errorf("expected an error") + } + if mr.n >= maxReadThreshold { + t.Errorf("expected to read < %d bytes; read %d", maxReadThreshold, mr.n) + } +} + +func TestMultipartTruncated(t *testing.T) { + testBody := ` +This is a multi-part message. This line is ignored. +--MyBoundary +foo-bar: baz + +Oh no, premature EOF! +` + body := strings.ReplaceAll(testBody, "\n", "\r\n") + bodyReader := strings.NewReader(body) + r := NewReader(bodyReader, "MyBoundary") + + part, err := r.NextPart() + if err != nil { + t.Fatalf("didn't get a part") + } + _, err = io.Copy(io.Discard, part) + if err != io.ErrUnexpectedEOF { + t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err) + } +} + +type slowReader struct { + r io.Reader +} + +func (s *slowReader) Read(p []byte) (int, error) { + if len(p) == 0 { + return s.r.Read(p) + } + return s.r.Read(p[:1]) +} + +type sentinelReader struct { + // done is closed when this reader is read from. + done chan struct{} +} + +func (s *sentinelReader) Read([]byte) (int, error) { + if s.done != nil { + close(s.done) + s.done = nil + } + return 0, io.EOF +} + +// TestMultipartStreamReadahead tests that PartReader does not block +// on reading past the end of a part, ensuring that it can be used on +// a stream like multipart/x-mixed-replace. See golang.org/issue/15431 +func TestMultipartStreamReadahead(t *testing.T) { + testBody1 := ` +This is a multi-part message. This line is ignored. +--MyBoundary +foo-bar: baz + +Body +--MyBoundary +` + testBody2 := `foo-bar: bop + +Body 2 +--MyBoundary-- +` + done1 := make(chan struct{}) + reader := NewReader( + io.MultiReader( + strings.NewReader(testBody1), + &sentinelReader{done1}, + strings.NewReader(testBody2)), + "MyBoundary") + + var i int + readPart := func(hdr textproto.MIMEHeader, body string) { + part, err := reader.NextPart() + if part == nil || err != nil { + t.Fatalf("Part %d: NextPart failed: %v", i, err) + } + + if !reflect.DeepEqual(part.Header, hdr) { + t.Errorf("Part %d: part.Header = %v, want %v", i, part.Header, hdr) + } + data, err := io.ReadAll(part) + expectEq(t, body, string(data), fmt.Sprintf("Part %d body", i)) + if err != nil { + t.Fatalf("Part %d: ReadAll failed: %v", i, err) + } + i++ + } + + readPart(textproto.MIMEHeader{"Foo-Bar": {"baz"}}, "Body") + + select { + case <-done1: + t.Errorf("Reader read past second boundary") + default: + } + + readPart(textproto.MIMEHeader{"Foo-Bar": {"bop"}}, "Body 2") +} + +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") + } + var buf bytes.Buffer + n, err := io.Copy(&buf, part) + if err != nil { + t.Errorf("error reading part: %v\nread so far: %q", err, buf.String()) + } + if n <= 0 { + t.Errorf("read %d bytes; expected >0", n) + } + } +} + +func TestQuotedPrintableEncoding(t *testing.T) { + for _, cte := range []string{"quoted-printable", "Quoted-PRINTABLE"} { + t.Run(cte, func(t *testing.T) { + testQuotedPrintableEncoding(t, cte) + }) + } +} + +func testQuotedPrintableEncoding(t *testing.T, cte string) { + // From https://golang.org/issue/4411 + body := "--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=text\r\nContent-Transfer-Encoding: " + cte + "\r\n\r\nwords words words words words words words words words words words words wor=\r\nds words words words words words words words words words words words words =\r\nwords words words words words words words words words words words words wor=\r\nds words words words words words words words words words words words words =\r\nwords words words words words words words words words\r\n--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=submit\r\n\r\nSubmit\r\n--0016e68ee29c5d515f04cedf6733--" + r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733") + part, err := r.NextPart() + if err != nil { + t.Fatal(err) + } + if te, ok := part.Header["Content-Transfer-Encoding"]; ok { + t.Errorf("unexpected Content-Transfer-Encoding of %q", te) + } + var buf bytes.Buffer + _, err = io.Copy(&buf, part) + if err != nil { + t.Error(err) + } + got := buf.String() + want := "words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words words" + if got != want { + t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want) + } +} + +func TestRawPart(t *testing.T) { + // https://github.com/golang/go/issues/29090 + + body := strings.Replace(`--0016e68ee29c5d515f04cedf6733 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr">Hello World.</div> +--0016e68ee29c5d515f04cedf6733 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr">Hello World.</div> +--0016e68ee29c5d515f04cedf6733--`, "\n", "\r\n", -1) + + r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733") + + // This part is expected to be raw, bypassing the automatic handling + // of quoted-printable. + part, err := r.NextRawPart() + if err != nil { + t.Fatal(err) + } + if _, ok := part.Header["Content-Transfer-Encoding"]; !ok { + t.Errorf("missing Content-Transfer-Encoding") + } + var buf bytes.Buffer + _, err = io.Copy(&buf, part) + if err != nil { + t.Error(err) + } + got := buf.String() + // Data is still quoted-printable. + want := `<div dir=3D"ltr">Hello World.</div>` + if got != want { + t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want) + } + + // This part is expected to have automatic decoding of quoted-printable. + part, err = r.NextPart() + if err != nil { + t.Fatal(err) + } + if te, ok := part.Header["Content-Transfer-Encoding"]; ok { + t.Errorf("unexpected Content-Transfer-Encoding of %q", te) + } + + buf.Reset() + _, err = io.Copy(&buf, part) + if err != nil { + t.Error(err) + } + got = buf.String() + // QP data has been decoded. + want = `<div dir="ltr">Hello World.</div>` + if got != want { + t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want) + } +} + +// Test parsing an image attachment from gmail, which previously failed. +func TestNested(t *testing.T) { + // nested-mime is the body part of a multipart/mixed email + // with boundary e89a8ff1c1e83553e304be640612 + f, err := os.Open("testdata/nested-mime") + if err != nil { + t.Fatal(err) + } + defer f.Close() + mr := NewReader(f, "e89a8ff1c1e83553e304be640612") + p, err := mr.NextPart() + if err != nil { + t.Fatalf("error reading first section (alternative): %v", err) + } + + // Read the inner text/plain and text/html sections of the multipart/alternative. + mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610") + p, err = mr2.NextPart() + if err != nil { + t.Fatalf("reading text/plain part: %v", err) + } + if b, err := io.ReadAll(p); string(b) != "*body*\r\n" || err != nil { + t.Fatalf("reading text/plain part: got %q, %v", b, err) + } + p, err = mr2.NextPart() + if err != nil { + t.Fatalf("reading text/html part: %v", err) + } + if b, err := io.ReadAll(p); string(b) != "<b>body</b>\r\n" || err != nil { + t.Fatalf("reading text/html part: got %q, %v", b, err) + } + + p, err = mr2.NextPart() + if err != io.EOF { + t.Fatalf("final inner NextPart = %v; want io.EOF", err) + } + + // Back to the outer multipart/mixed, reading the image attachment. + _, err = mr.NextPart() + if err != nil { + t.Fatalf("error reading the image attachment at the end: %v", err) + } + + _, err = mr.NextPart() + if err != io.EOF { + t.Fatalf("final outer NextPart = %v; want io.EOF", err) + } +} + +type headerBody struct { + header textproto.MIMEHeader + body string +} + +func formData(key, value string) headerBody { + return headerBody{ + textproto.MIMEHeader{ + "Content-Type": {"text/plain; charset=ISO-8859-1"}, + "Content-Disposition": {"form-data; name=" + key}, + }, + value, + } +} + +type parseTest struct { + name string + in, sep string + want []headerBody +} + +var parseTests = []parseTest{ + // Actual body from App Engine on a blob upload. The final part (the + // Content-Type: message/external-body) is what App Engine replaces + // the uploaded file with. The other form fields (prefixed with + // "other" in their form-data name) are unchanged. A bug was + // reported with blob uploads failing when the other fields were + // empty. This was the MIME POST body that previously failed. + { + name: "App Engine post", + sep: "00151757727e9583fd04bfbca4c6", + in: "--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherEmpty1\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherFoo1\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherFoo2\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherEmpty2\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatFoo\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatFoo\r\n\r\nfoo\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatEmpty\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=otherRepeatEmpty\r\n\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=submit\r\n\r\nSubmit\r\n--00151757727e9583fd04bfbca4c6\r\nContent-Type: message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q\r\nContent-Disposition: form-data; name=file; filename=\"fall.png\"\r\n\r\nContent-Type: image/png\r\nContent-Length: 232303\r\nX-AppEngine-Upload-Creation: 2012-05-10 23:14:02.715173\r\nContent-MD5: MzRjODU1ZDZhZGU1NmRlOWEwZmMwMDdlODBmZTA0NzA=\r\nContent-Disposition: form-data; name=file; filename=\"fall.png\"\r\n\r\n\r\n--00151757727e9583fd04bfbca4c6--", + want: []headerBody{ + formData("otherEmpty1", ""), + formData("otherFoo1", "foo"), + formData("otherFoo2", "foo"), + formData("otherEmpty2", ""), + formData("otherRepeatFoo", "foo"), + formData("otherRepeatFoo", "foo"), + formData("otherRepeatEmpty", ""), + formData("otherRepeatEmpty", ""), + formData("submit", "Submit"), + {textproto.MIMEHeader{ + "Content-Type": {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"}, + "Content-Disposition": {"form-data; name=file; filename=\"fall.png\""}, + }, "Content-Type: image/png\r\nContent-Length: 232303\r\nX-AppEngine-Upload-Creation: 2012-05-10 23:14:02.715173\r\nContent-MD5: MzRjODU1ZDZhZGU1NmRlOWEwZmMwMDdlODBmZTA0NzA=\r\nContent-Disposition: form-data; name=file; filename=\"fall.png\"\r\n\r\n"}, + }, + }, + + // Single empty part, ended with --boundary immediately after headers. + { + name: "single empty part, --boundary", + sep: "abc", + in: "--abc\r\nFoo: bar\r\n\r\n--abc--", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, + }, + }, + + // Single empty part, ended with \r\n--boundary immediately after headers. + { + name: "single empty part, \r\n--boundary", + sep: "abc", + in: "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, + }, + }, + + // Final part empty. + { + name: "final part empty", + sep: "abc", + in: "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, + {textproto.MIMEHeader{"Foo2": {"bar2"}}, ""}, + }, + }, + + // Final part empty with newlines after final separator. + { + name: "final part empty then crlf", + sep: "abc", + in: "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, + }, + }, + + // Final part empty with lwsp-chars after final separator. + { + name: "final part empty then lwsp", + sep: "abc", + in: "--abc\r\nFoo: bar\r\n\r\n--abc-- \t", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, + }, + }, + + // No parts (empty form as submitted by Chrome) + { + name: "no parts", + sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA", + in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n", + want: []headerBody{}, + }, + + // Part containing data starting with the boundary, but with additional suffix. + { + name: "fake separator as data", + sep: "sep", + in: "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"}, + }, + }, + + // Part containing a boundary with whitespace following it. + { + name: "boundary with whitespace", + sep: "sep", + in: "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, "text"}, + }, + }, + + // With ignored leading line. + { + name: "leading line", + sep: "MyBoundary", + in: strings.Replace(`This is a multi-part message. This line is ignored. +--MyBoundary +foo: bar + + +--MyBoundary--`, "\n", "\r\n", -1), + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, + }, + }, + + // Issue 10616; minimal + { + name: "issue 10616 minimal", + sep: "sep", + in: "--sep \r\nFoo: bar\r\n\r\n" + + "a\r\n" + + "--sep_alt\r\n" + + "b\r\n" + + "\r\n--sep--", + want: []headerBody{ + {textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"}, + }, + }, + + // Issue 10616; full example from bug. + { + name: "nested separator prefix is outer separator", + sep: "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9", + in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9 +Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt" + +------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +This is a multi-part message in MIME format. + +------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: 8bit + +html things +------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt-- +------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1), + want: []headerBody{ + {textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}}, + strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: 8bit + +This is a multi-part message in MIME format. + +------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: 8bit + +html things +------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1), + }, + }, + }, + // Issue 12662: Check that we don't consume the leading \r if the peekBuffer + // ends in '\r\n--separator-' + { + name: "peek buffer boundary condition", + sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", + in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db +Content-Disposition: form-data; name="block"; filename="block" +Content-Type: application/octet-stream + +`+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1), + want: []headerBody{ + {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, + strings.Repeat("A", peekBufferSize-65), + }, + }, + }, + // Issue 12662: Same test as above with \r\n at the end + { + name: "peek buffer boundary condition", + sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", + in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db +Content-Disposition: form-data; name="block"; filename="block" +Content-Type: application/octet-stream + +`+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--\n", "\n", "\r\n", -1), + want: []headerBody{ + {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, + strings.Repeat("A", peekBufferSize-65), + }, + }, + }, + // Issue 12662v2: We want to make sure that for short buffers that end with + // '\r\n--separator-' we always consume at least one (valid) symbol from the + // peekBuffer + { + name: "peek buffer boundary condition", + sep: "aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", + in: strings.Replace(`--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db +Content-Disposition: form-data; name="block"; filename="block" +Content-Type: application/octet-stream + +`+strings.Repeat("A", peekBufferSize)+"\n--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1), + want: []headerBody{ + {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, + strings.Repeat("A", peekBufferSize), + }, + }, + }, + // Context: https://github.com/camlistore/camlistore/issues/642 + // If the file contents in the form happens to have a size such as: + // size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize) + // then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy + // cut such as: + // "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the + // subsequent Read miss the boundary. + { + name: "safeCount off by one", + sep: "08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74", + in: strings.Replace(`--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74 +Content-Disposition: form-data; name="myfile"; filename="my-file.txt" +Content-Type: application/octet-stream + +`, "\n", "\r\n", -1) + + strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)) + + strings.Replace(` +--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74 +Content-Disposition: form-data; name="key" + +val +--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74-- +`, "\n", "\r\n", -1), + want: []headerBody{ + {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="myfile"; filename="my-file.txt"`}}, + strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)), + }, + {textproto.MIMEHeader{"Content-Disposition": {`form-data; name="key"`}}, + "val", + }, + }, + }, + + roundTripParseTest(), +} + +func TestParse(t *testing.T) { +Cases: + for _, tt := range parseTests { + r := NewReader(strings.NewReader(tt.in), tt.sep) + got := []headerBody{} + for { + p, err := r.NextPart() + if err == io.EOF { + break + } + if err != nil { + t.Errorf("in test %q, NextPart: %v", tt.name, err) + continue Cases + } + pbody, err := io.ReadAll(p) + if err != nil { + t.Errorf("in test %q, error reading part: %v", tt.name, err) + continue Cases + } + got = append(got, headerBody{p.Header, string(pbody)}) + } + if !reflect.DeepEqual(tt.want, got) { + t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want) + if len(tt.want) != len(got) { + t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want)) + } else if len(got) > 1 { + for pi, wantPart := range tt.want { + if !reflect.DeepEqual(wantPart, got[pi]) { + t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart) + } + } + } + } + } +} + +func partsFromReader(r *Reader) ([]headerBody, error) { + got := []headerBody{} + for { + p, err := r.NextPart() + if err == io.EOF { + return got, nil + } + if err != nil { + return nil, fmt.Errorf("NextPart: %v", err) + } + pbody, err := io.ReadAll(p) + if err != nil { + return nil, fmt.Errorf("error reading part: %v", err) + } + got = append(got, headerBody{p.Header, string(pbody)}) + } +} + +func TestParseAllSizes(t *testing.T) { + t.Parallel() + maxSize := 5 << 10 + if testing.Short() { + maxSize = 512 + } + var buf bytes.Buffer + body := strings.Repeat("a", maxSize) + bodyb := []byte(body) + for size := 0; size < maxSize; size++ { + buf.Reset() + w := NewWriter(&buf) + part, _ := w.CreateFormField("f") + part.Write(bodyb[:size]) + part, _ = w.CreateFormField("key") + part.Write([]byte("val")) + w.Close() + r := NewReader(&buf, w.Boundary()) + got, err := partsFromReader(r) + if err != nil { + t.Errorf("For size %d: %v", size, err) + continue + } + if len(got) != 2 { + t.Errorf("For size %d, num parts = %d; want 2", size, len(got)) + continue + } + if got[0].body != body[:size] { + t.Errorf("For size %d, got unexpected len %d: %q", size, len(got[0].body), got[0].body) + } + } +} + +func roundTripParseTest() parseTest { + t := parseTest{ + name: "round trip", + want: []headerBody{ + formData("empty", ""), + formData("lf", "\n"), + formData("cr", "\r"), + formData("crlf", "\r\n"), + formData("foo", "bar"), + }, + } + var buf bytes.Buffer + w := NewWriter(&buf) + for _, p := range t.want { + pw, err := w.CreatePart(p.header) + if err != nil { + panic(err) + } + _, err = pw.Write([]byte(p.body)) + if err != nil { + panic(err) + } + } + w.Close() + t.in = buf.String() + t.sep = w.Boundary() + return t +} + +func TestNoBoundary(t *testing.T) { + mr := NewReader(strings.NewReader(""), "") + _, err := mr.NextPart() + if got, want := fmt.Sprint(err), "multipart: boundary is empty"; got != want { + t.Errorf("NextPart error = %v; want %v", got, want) + } +} diff --git a/src/mime/multipart/testdata/nested-mime b/src/mime/multipart/testdata/nested-mime new file mode 100644 index 0000000..71c238e --- /dev/null +++ b/src/mime/multipart/testdata/nested-mime @@ -0,0 +1,29 @@ +--e89a8ff1c1e83553e304be640612
+Content-Type: multipart/alternative; boundary=e89a8ff1c1e83553e004be640610
+
+--e89a8ff1c1e83553e004be640610
+Content-Type: text/plain; charset=UTF-8
+
+*body*
+
+--e89a8ff1c1e83553e004be640610
+Content-Type: text/html; charset=UTF-8
+
+<b>body</b>
+
+--e89a8ff1c1e83553e004be640610--
+--e89a8ff1c1e83553e304be640612
+Content-Type: image/png; name="x.png"
+Content-Disposition: attachment;
+ filename="x.png"
+Content-Transfer-Encoding: base64
+X-Attachment-Id: f_h1edgigu0
+
+iVBORw0KGgoAAAANSUhEUgAAAagAAADrCAIAAACza5XhAAAKMWlDQ1BJQ0MgUHJvZmlsZQAASImd
+lndUU9kWh8+9N71QkhCKlNBraFICSA29SJEuKjEJEErAkAAiNkRUcERRkaYIMijggKNDkbEiioUB
+8b2kqeGaj4aTNftesu5mob4pr07ecMywRwLBvDCJOksqlUyldAZD7g9fxIZRWWPMvXRNJROJRBIG
+Y7Vx0mva1HAwYqibdKONXye3dW4iUonhWFJnqK7OaanU1gGkErFYEgaj0cg8wK+zVPh2ziwnHy07
+U8lYTNapezSzOuevRwLB7CFkqQQCwaJDiBQIBIJFhwh8AoFg0SHUqQUCASRJKkwkhMy/JfODWPEJ
+BIJFhwh8AoFg0TFnQqQ55GtPFopcJsN97e1nYtNuIBYeGBgYCmYrmE3jZ05iaGAoMX0xzxkWz6Hv
+yO7WvrlwzA0uLzrD+VkKqViwl9IfTBVNFMyc/x9alloiPPlqhQAAAABJRU5ErkJggg==
+--e89a8ff1c1e83553e304be640612--
diff --git a/src/mime/multipart/writer.go b/src/mime/multipart/writer.go new file mode 100644 index 0000000..d1ff151 --- /dev/null +++ b/src/mime/multipart/writer.go @@ -0,0 +1,201 @@ +// 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" + "errors" + "fmt" + "io" + "net/textproto" + "sort" + "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 boundary. +func (w *Writer) Boundary() string { + return w.boundary +} + +// SetBoundary overrides the Writer's default randomly-generated +// boundary separator with an explicit value. +// +// SetBoundary must be called before any parts are created, may only +// contain certain ASCII characters, and must be non-empty and +// at most 70 bytes long. +func (w *Writer) SetBoundary(boundary string) error { + if w.lastpart != nil { + return errors.New("mime: SetBoundary called after write") + } + // rfc2046#section-5.1.1 + if len(boundary) < 1 || len(boundary) > 70 { + return errors.New("mime: invalid boundary length") + } + end := len(boundary) - 1 + for i, b := range boundary { + if 'A' <= b && b <= 'Z' || 'a' <= b && b <= 'z' || '0' <= b && b <= '9' { + continue + } + switch b { + case '\'', '(', ')', '+', '_', ',', '-', '.', '/', ':', '=', '?': + continue + case ' ': + if i != end { + continue + } + } + return errors.New("mime: invalid boundary character") + } + w.boundary = boundary + return nil +} + +// FormDataContentType returns the Content-Type for an HTTP +// multipart/form-data with this Writer's Boundary. +func (w *Writer) FormDataContentType() string { + b := w.boundary + // We must quote the boundary if it contains any of the + // tspecials characters defined by RFC 2045, or space. + if strings.ContainsAny(b, `()<>@,;:\"/[]?= `) { + b = `"` + b + `"` + } + return "multipart/form-data; boundary=" + b +} + +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, 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) + } + + keys := make([]string, 0, len(header)) + for k := range header { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + for _, v := range header[k] { + 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 +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +func escapeQuotes(s string) string { + return quoteEscaper.Replace(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, 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, 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) 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() 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 error // last error that occurred writing +} + +func (p *part) close() error { + p.closed = true + return p.we +} + +func (p *part) Write(d []byte) (n int, err error) { + if p.closed { + return 0, errors.New("multipart: can't write to finished part") + } + n, err = p.mw.w.Write(d) + if err != nil { + p.we = err + } + return +} diff --git a/src/mime/multipart/writer_test.go b/src/mime/multipart/writer_test.go new file mode 100644 index 0000000..cfc0f09 --- /dev/null +++ b/src/mime/multipart/writer_test.go @@ -0,0 +1,174 @@ +// 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" + "mime" + "net/textproto" + "strings" + "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 := io.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 = io.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) + } +} + +func TestWriterSetBoundary(t *testing.T) { + tests := []struct { + b string + ok bool + }{ + {"abc", true}, + {"", false}, + {"ungültig", false}, + {"!", false}, + {strings.Repeat("x", 70), true}, + {strings.Repeat("x", 71), false}, + {"bad!ascii!", false}, + {"my-separator", true}, + {"with space", true}, + {"badspace ", false}, + {"(boundary)", true}, + } + for i, tt := range tests { + var b bytes.Buffer + w := NewWriter(&b) + err := w.SetBoundary(tt.b) + got := err == nil + if got != tt.ok { + t.Errorf("%d. boundary %q = %v (%v); want %v", i, tt.b, got, err, tt.ok) + } else if tt.ok { + got := w.Boundary() + if got != tt.b { + t.Errorf("boundary = %q; want %q", got, tt.b) + } + + ct := w.FormDataContentType() + mt, params, err := mime.ParseMediaType(ct) + if err != nil { + t.Errorf("could not parse Content-Type %q: %v", ct, err) + } else if mt != "multipart/form-data" { + t.Errorf("unexpected media type %q; want %q", mt, "multipart/form-data") + } else if b := params["boundary"]; b != tt.b { + t.Errorf("unexpected boundary parameter %q; want %q", b, tt.b) + } + + w.Close() + wantSub := "\r\n--" + tt.b + "--\r\n" + if got := b.String(); !strings.Contains(got, wantSub) { + t.Errorf("expected %q in output. got: %q", wantSub, got) + } + } + } +} + +func TestWriterBoundaryGoroutines(t *testing.T) { + // Verify there's no data race accessing any lazy boundary if it's used by + // different goroutines. This was previously broken by + // https://codereview.appspot.com/95760043/ and reverted in + // https://codereview.appspot.com/117600043/ + w := NewWriter(io.Discard) + done := make(chan int) + go func() { + w.CreateFormField("foo") + done <- 1 + }() + w.Boundary() + <-done +} + +func TestSortedHeader(t *testing.T) { + var buf bytes.Buffer + w := NewWriter(&buf) + if err := w.SetBoundary("MIMEBOUNDARY"); err != nil { + t.Fatalf("Error setting mime boundary: %v", err) + } + + header := textproto.MIMEHeader{ + "A": {"2"}, + "B": {"5", "7", "6"}, + "C": {"4"}, + "M": {"3"}, + "Z": {"1"}, + } + + part, err := w.CreatePart(header) + if err != nil { + t.Fatalf("Unable to create part: %v", err) + } + part.Write([]byte("foo")) + + w.Close() + + want := "--MIMEBOUNDARY\r\nA: 2\r\nB: 5\r\nB: 7\r\nB: 6\r\nC: 4\r\nM: 3\r\nZ: 1\r\n\r\nfoo\r\n--MIMEBOUNDARY--\r\n" + if want != buf.String() { + t.Fatalf("\n got: %q\nwant: %q\n", buf.String(), want) + } +} |