summaryrefslogtreecommitdiffstats
path: root/src/mime/multipart
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 19:19:13 +0000
commitccd992355df7192993c666236047820244914598 (patch)
treef00fea65147227b7743083c6148396f74cd66935 /src/mime/multipart
parentInitial commit. (diff)
downloadgolang-1.21-ccd992355df7192993c666236047820244914598.tar.xz
golang-1.21-ccd992355df7192993c666236047820244914598.zip
Adding upstream version 1.21.8.upstream/1.21.8
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/mime/multipart')
-rw-r--r--src/mime/multipart/example_test.go52
-rw-r--r--src/mime/multipart/formdata.go306
-rw-r--r--src/mime/multipart/formdata_test.go544
-rw-r--r--src/mime/multipart/multipart.go488
-rw-r--r--src/mime/multipart/multipart_test.go1020
-rw-r--r--src/mime/multipart/readmimeheader.go14
-rw-r--r--src/mime/multipart/testdata/nested-mime29
-rw-r--r--src/mime/multipart/writer.go201
-rw-r--r--src/mime/multipart/writer_test.go174
9 files changed, 2828 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..85bad2a
--- /dev/null
+++ b/src/mime/multipart/formdata.go
@@ -0,0 +1,306 @@
+// 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"
+ "internal/godebug"
+ "io"
+ "math"
+ "net/textproto"
+ "os"
+ "strconv"
+)
+
+// 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)
+}
+
+var (
+ multipartFiles = godebug.New("#multipartfiles") // TODO: document and remove #
+ multipartMaxParts = godebug.New("multipartmaxparts")
+)
+
+func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
+ form := &Form{make(map[string][]string), make(map[string][]*FileHeader)}
+ var (
+ file *os.File
+ fileOff int64
+ )
+ numDiskFiles := 0
+ combineFiles := true
+ if multipartFiles.Value() == "distinct" {
+ combineFiles = false
+ // multipartFiles.IncNonDefault() // TODO: uncomment after documenting
+ }
+ maxParts := 1000
+ if s := multipartMaxParts.Value(); s != "" {
+ if v, err := strconv.Atoi(s); err == nil && v >= 0 {
+ maxParts = v
+ multipartMaxParts.IncNonDefault()
+ }
+ }
+ maxHeaders := maxMIMEHeaders()
+
+ defer func() {
+ if file != nil {
+ if cerr := file.Close(); err == nil {
+ err = cerr
+ }
+ }
+ if combineFiles && numDiskFiles > 1 {
+ for _, fhs := range form.File {
+ for _, fh := range fhs {
+ fh.tmpshared = true
+ }
+ }
+ }
+ if err != nil {
+ form.RemoveAll()
+ if file != nil {
+ os.Remove(file.Name())
+ }
+ }
+ }()
+
+ // maxFileMemoryBytes is the maximum bytes of file data we will store in memory.
+ // Data past this limit is written to disk.
+ // This limit strictly applies to content, not metadata (filenames, MIME headers, etc.),
+ // since metadata is always stored in memory, not disk.
+ //
+ // maxMemoryBytes is the maximum bytes we will store in memory, including file content,
+ // non-file part values, metadata, and map entry overhead.
+ //
+ // We reserve an additional 10 MB in maxMemoryBytes for non-file data.
+ //
+ // The relationship between these parameters, as well as the overly-large and
+ // unconfigurable 10 MB added on to maxMemory, is unfortunate but difficult to change
+ // within the constraints of the API as documented.
+ maxFileMemoryBytes := maxMemory
+ if maxFileMemoryBytes == math.MaxInt64 {
+ maxFileMemoryBytes--
+ }
+ maxMemoryBytes := maxMemory + int64(10<<20)
+ if maxMemoryBytes <= 0 {
+ if maxMemory < 0 {
+ maxMemoryBytes = 0
+ } else {
+ maxMemoryBytes = math.MaxInt64
+ }
+ }
+ var copyBuf []byte
+ for {
+ p, err := r.nextPart(false, maxMemoryBytes, maxHeaders)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if maxParts <= 0 {
+ return nil, ErrMessageTooLarge
+ }
+ maxParts--
+
+ name := p.FormName()
+ if name == "" {
+ continue
+ }
+ filename := p.FileName()
+
+ // Multiple values for the same key (one map entry, longer slice) are cheaper
+ // than the same number of values for different keys (many map entries), but
+ // using a consistent per-value cost for overhead is simpler.
+ const mapEntryOverhead = 200
+ maxMemoryBytes -= int64(len(name))
+ maxMemoryBytes -= mapEntryOverhead
+ if maxMemoryBytes < 0 {
+ // We can't actually take this path, since nextPart would already have
+ // rejected the MIME headers for being too large. Check anyway.
+ return nil, ErrMessageTooLarge
+ }
+
+ var b bytes.Buffer
+
+ if filename == "" {
+ // value, store as string in memory
+ n, err := io.CopyN(&b, p, maxMemoryBytes+1)
+ if err != nil && err != io.EOF {
+ return nil, err
+ }
+ maxMemoryBytes -= n
+ if maxMemoryBytes < 0 {
+ return nil, ErrMessageTooLarge
+ }
+ form.Value[name] = append(form.Value[name], b.String())
+ continue
+ }
+
+ // file, store in memory or on disk
+ const fileHeaderSize = 100
+ maxMemoryBytes -= mimeHeaderSize(p.Header)
+ maxMemoryBytes -= mapEntryOverhead
+ maxMemoryBytes -= fileHeaderSize
+ if maxMemoryBytes < 0 {
+ return nil, ErrMessageTooLarge
+ }
+ for _, v := range p.Header {
+ maxHeaders -= int64(len(v))
+ }
+ fh := &FileHeader{
+ Filename: filename,
+ Header: p.Header,
+ }
+ n, err := io.CopyN(&b, p, maxFileMemoryBytes+1)
+ if err != nil && err != io.EOF {
+ return nil, err
+ }
+ if n > maxFileMemoryBytes {
+ if file == nil {
+ file, err = os.CreateTemp(r.tempDir, "multipart-")
+ if err != nil {
+ return nil, err
+ }
+ }
+ numDiskFiles++
+ if _, err := file.Write(b.Bytes()); err != nil {
+ return nil, err
+ }
+ if copyBuf == nil {
+ copyBuf = make([]byte, 32*1024) // same buffer size as io.Copy uses
+ }
+ // os.File.ReadFrom will allocate its own copy buffer if we let io.Copy use it.
+ type writerOnly struct{ io.Writer }
+ remainingSize, err := io.CopyBuffer(writerOnly{file}, p, copyBuf)
+ if err != nil {
+ return nil, err
+ }
+ fh.tmpfile = file.Name()
+ fh.Size = int64(b.Len()) + remainingSize
+ fh.tmpoff = fileOff
+ fileOff += fh.Size
+ if !combineFiles {
+ if err := file.Close(); err != nil {
+ return nil, err
+ }
+ file = nil
+ }
+ } else {
+ fh.content = b.Bytes()
+ fh.Size = int64(len(fh.content))
+ maxFileMemoryBytes -= n
+ maxMemoryBytes -= n
+ }
+ form.File[name] = append(form.File[name], fh)
+ }
+
+ return form, nil
+}
+
+func mimeHeaderSize(h textproto.MIMEHeader) (size int64) {
+ size = 400
+ for k, vs := range h {
+ size += int64(len(k))
+ size += 200 // map entry overhead
+ for _, v := range vs {
+ size += int64(len(v))
+ }
+ }
+ return size
+}
+
+// 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 && !errors.Is(e, os.ErrNotExist) && 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
+ tmpoff int64
+ tmpshared bool
+}
+
+// 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}, nil
+ }
+ if fh.tmpshared {
+ f, err := os.Open(fh.tmpfile)
+ if err != nil {
+ return nil, err
+ }
+ r := io.NewSectionReader(f, fh.tmpoff, fh.Size)
+ return sectionReadCloser{r, f}, 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
+ io.Closer
+}
+
+func (rc sectionReadCloser) Close() error {
+ if rc.Closer != nil {
+ return rc.Closer.Close()
+ }
+ return nil
+}
diff --git a/src/mime/multipart/formdata_test.go b/src/mime/multipart/formdata_test.go
new file mode 100644
index 0000000..bfa9f68
--- /dev/null
+++ b/src/mime/multipart/formdata_test.go
@@ -0,0 +1,544 @@
+// 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"
+ "fmt"
+ "io"
+ "math"
+ "net/textproto"
+ "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 58384: Handle ReadForm(math.MaxInt64)
+func TestReadFormWitFileNameMaxMemoryOverflow(t *testing.T) {
+ b := strings.NewReader(strings.ReplaceAll(messageWithFileName, "\n", "\r\n"))
+ r := NewReader(b, boundary)
+ f, err := r.ReadForm(math.MaxInt64)
+ if err != nil {
+ t.Fatalf("ReadForm(MaxInt64): %v", err)
+ }
+ defer f.RemoveAll()
+
+ 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()
+}
+
+// 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")
+ }
+ defer f.RemoveAll()
+
+ if g, e := f.Value["texta"][0], textaValue; g != e {
+ t.Errorf("texta value = %q, want %q", g, e)
+ }
+}
+
+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(strings.Builder)
+ _, 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 messageWithFileName = `
+--MyBoundary
+Content-Disposition: form-data; name="filea"; filename="filea.txt"
+Content-Type: text/plain
+
+` + fileaContents + `
+--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) {
+ if testing.Short() {
+ t.Skip("skipping in -short mode")
+ }
+ n := 10 << 20
+ largeTextValue := strings.Repeat("1", n)
+ message := `--MyBoundary
+Content-Disposition: form-data; name="largetext"
+
+` + largeTextValue + `
+--MyBoundary--
+`
+ testBody := strings.ReplaceAll(message, "\n", "\r\n")
+ // Try parsing the form with increasing maxMemory values.
+ // Changes in how we account for non-file form data may cause the exact point
+ // where we change from rejecting the form as too large to accepting it to vary,
+ // but we should see both successes and failures.
+ const failWhenMaxMemoryLessThan = 128
+ for maxMemory := int64(0); maxMemory < failWhenMaxMemoryLessThan*2; maxMemory += 16 {
+ b := strings.NewReader(testBody)
+ r := NewReader(b, boundary)
+ f, err := r.ReadForm(maxMemory)
+ if err != nil {
+ continue
+ }
+ if g := f.Value["largetext"][0]; g != largeTextValue {
+ t.Errorf("largetext mismatch: got size: %v, expected size: %v", len(g), len(largeTextValue))
+ }
+ f.RemoveAll()
+ if maxMemory < failWhenMaxMemoryLessThan {
+ t.Errorf("ReadForm(%v): no error, expect to hit memory limit when maxMemory < %v", maxMemory, failWhenMaxMemoryLessThan)
+ }
+ return
+ }
+ t.Errorf("ReadForm(x) failed for x < 1024, expect success")
+}
+
+// TestReadForm_MetadataTooLarge verifies that we account for the size of field names,
+// MIME headers, and map entry overhead while limiting the memory consumption of parsed forms.
+func TestReadForm_MetadataTooLarge(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ f func(*Writer)
+ }{{
+ name: "large name",
+ f: func(fw *Writer) {
+ name := strings.Repeat("a", 10<<20)
+ w, _ := fw.CreateFormField(name)
+ w.Write([]byte("value"))
+ },
+ }, {
+ name: "large MIME header",
+ f: func(fw *Writer) {
+ h := make(textproto.MIMEHeader)
+ h.Set("Content-Disposition", `form-data; name="a"`)
+ h.Set("X-Foo", strings.Repeat("a", 10<<20))
+ w, _ := fw.CreatePart(h)
+ w.Write([]byte("value"))
+ },
+ }, {
+ name: "many parts",
+ f: func(fw *Writer) {
+ for i := 0; i < 110000; i++ {
+ w, _ := fw.CreateFormField("f")
+ w.Write([]byte("v"))
+ }
+ },
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ var buf bytes.Buffer
+ fw := NewWriter(&buf)
+ test.f(fw)
+ if err := fw.Close(); err != nil {
+ t.Fatal(err)
+ }
+ fr := NewReader(&buf, fw.Boundary())
+ _, err := fr.ReadForm(0)
+ if err != ErrMessageTooLarge {
+ t.Errorf("fr.ReadForm() = %v, want ErrMessageTooLarge", err)
+ }
+ })
+ }
+}
+
+// TestReadForm_ManyFiles_Combined tests that a multipart form containing many files only
+// results in a single on-disk file.
+func TestReadForm_ManyFiles_Combined(t *testing.T) {
+ const distinct = false
+ testReadFormManyFiles(t, distinct)
+}
+
+// TestReadForm_ManyFiles_Distinct tests that setting GODEBUG=multipartfiles=distinct
+// results in every file in a multipart form being placed in a distinct on-disk file.
+func TestReadForm_ManyFiles_Distinct(t *testing.T) {
+ t.Setenv("GODEBUG", "multipartfiles=distinct")
+ const distinct = true
+ testReadFormManyFiles(t, distinct)
+}
+
+func testReadFormManyFiles(t *testing.T, distinct bool) {
+ var buf bytes.Buffer
+ fw := NewWriter(&buf)
+ const numFiles = 10
+ for i := 0; i < numFiles; i++ {
+ name := fmt.Sprint(i)
+ w, err := fw.CreateFormFile(name, name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ w.Write([]byte(name))
+ }
+ if err := fw.Close(); err != nil {
+ t.Fatal(err)
+ }
+ fr := NewReader(&buf, fw.Boundary())
+ fr.tempDir = t.TempDir()
+ form, err := fr.ReadForm(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for i := 0; i < numFiles; i++ {
+ name := fmt.Sprint(i)
+ if got := len(form.File[name]); got != 1 {
+ t.Fatalf("form.File[%q] has %v entries, want 1", name, got)
+ }
+ fh := form.File[name][0]
+ file, err := fh.Open()
+ if err != nil {
+ t.Fatalf("form.File[%q].Open() = %v", name, err)
+ }
+ if distinct {
+ if _, ok := file.(*os.File); !ok {
+ t.Fatalf("form.File[%q].Open: %T, want *os.File", name, file)
+ }
+ }
+ got, err := io.ReadAll(file)
+ file.Close()
+ if string(got) != name || err != nil {
+ t.Fatalf("read form.File[%q]: %q, %v; want %q, nil", name, string(got), err, name)
+ }
+ }
+ dir, err := os.Open(fr.tempDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer dir.Close()
+ names, err := dir.Readdirnames(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantNames := 1
+ if distinct {
+ wantNames = numFiles
+ }
+ if len(names) != wantNames {
+ t.Fatalf("temp dir contains %v files; want 1", len(names))
+ }
+ if err := form.RemoveAll(); err != nil {
+ t.Fatalf("form.RemoveAll() = %v", err)
+ }
+ names, err = dir.Readdirnames(0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(names) != 0 {
+ t.Fatalf("temp dir contains %v files; want 0", len(names))
+ }
+}
+
+func TestReadFormLimits(t *testing.T) {
+ for _, test := range []struct {
+ values int
+ files int
+ extraKeysPerFile int
+ wantErr error
+ godebug string
+ }{
+ {values: 1000},
+ {values: 1001, wantErr: ErrMessageTooLarge},
+ {values: 500, files: 500},
+ {values: 501, files: 500, wantErr: ErrMessageTooLarge},
+ {files: 1000},
+ {files: 1001, wantErr: ErrMessageTooLarge},
+ {files: 1, extraKeysPerFile: 9998}, // plus Content-Disposition and Content-Type
+ {files: 1, extraKeysPerFile: 10000, wantErr: ErrMessageTooLarge},
+ {godebug: "multipartmaxparts=100", values: 100},
+ {godebug: "multipartmaxparts=100", values: 101, wantErr: ErrMessageTooLarge},
+ {godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 48},
+ {godebug: "multipartmaxheaders=100", files: 2, extraKeysPerFile: 50, wantErr: ErrMessageTooLarge},
+ } {
+ name := fmt.Sprintf("values=%v/files=%v/extraKeysPerFile=%v", test.values, test.files, test.extraKeysPerFile)
+ if test.godebug != "" {
+ name += fmt.Sprintf("/godebug=%v", test.godebug)
+ }
+ t.Run(name, func(t *testing.T) {
+ if test.godebug != "" {
+ t.Setenv("GODEBUG", test.godebug)
+ }
+ var buf bytes.Buffer
+ fw := NewWriter(&buf)
+ for i := 0; i < test.values; i++ {
+ w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i))
+ fmt.Fprintf(w, "value %v", i)
+ }
+ for i := 0; i < test.files; i++ {
+ h := make(textproto.MIMEHeader)
+ h.Set("Content-Disposition",
+ fmt.Sprintf(`form-data; name="file%v"; filename="file%v"`, i, i))
+ h.Set("Content-Type", "application/octet-stream")
+ for j := 0; j < test.extraKeysPerFile; j++ {
+ h.Set(fmt.Sprintf("k%v", j), "v")
+ }
+ w, _ := fw.CreatePart(h)
+ fmt.Fprintf(w, "value %v", i)
+ }
+ if err := fw.Close(); err != nil {
+ t.Fatal(err)
+ }
+ fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary())
+ form, err := fr.ReadForm(1 << 10)
+ if err == nil {
+ defer form.RemoveAll()
+ }
+ if err != test.wantErr {
+ t.Errorf("ReadForm = %v, want %v", err, test.wantErr)
+ }
+ })
+ }
+}
+
+func TestReadFormEndlessHeaderLine(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ prefix string
+ }{{
+ name: "name",
+ prefix: "X-",
+ }, {
+ name: "value",
+ prefix: "X-Header: ",
+ }, {
+ name: "continuation",
+ prefix: "X-Header: foo\r\n ",
+ }} {
+ t.Run(test.name, func(t *testing.T) {
+ const eol = "\r\n"
+ s := `--boundary` + eol
+ s += `Content-Disposition: form-data; name="a"` + eol
+ s += `Content-Type: text/plain` + eol
+ s += test.prefix
+ fr := io.MultiReader(
+ strings.NewReader(s),
+ neverendingReader('X'),
+ )
+ r := NewReader(fr, "boundary")
+ _, err := r.ReadForm(1 << 20)
+ if err != ErrMessageTooLarge {
+ t.Fatalf("ReadForm(1 << 20): %v, want ErrMessageTooLarge", err)
+ }
+ })
+ }
+}
+
+type neverendingReader byte
+
+func (r neverendingReader) Read(p []byte) (n int, err error) {
+ for i := range p {
+ p[i] = byte(r)
+ }
+ return len(p), nil
+}
+
+func BenchmarkReadForm(b *testing.B) {
+ for _, test := range []struct {
+ name string
+ form func(fw *Writer, count int)
+ }{{
+ name: "fields",
+ form: func(fw *Writer, count int) {
+ for i := 0; i < count; i++ {
+ w, _ := fw.CreateFormField(fmt.Sprintf("field%v", i))
+ fmt.Fprintf(w, "value %v", i)
+ }
+ },
+ }, {
+ name: "files",
+ form: func(fw *Writer, count int) {
+ for i := 0; i < count; i++ {
+ w, _ := fw.CreateFormFile(fmt.Sprintf("field%v", i), fmt.Sprintf("file%v", i))
+ fmt.Fprintf(w, "value %v", i)
+ }
+ },
+ }} {
+ b.Run(test.name, func(b *testing.B) {
+ for _, maxMemory := range []int64{
+ 0,
+ 1 << 20,
+ } {
+ var buf bytes.Buffer
+ fw := NewWriter(&buf)
+ test.form(fw, 10)
+ if err := fw.Close(); err != nil {
+ b.Fatal(err)
+ }
+ b.Run(fmt.Sprintf("maxMemory=%v", maxMemory), func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ fr := NewReader(bytes.NewReader(buf.Bytes()), fw.Boundary())
+ form, err := fr.ReadForm(maxMemory)
+ if err != nil {
+ b.Fatal(err)
+ }
+ form.RemoveAll()
+ }
+
+ })
+ }
+ })
+ }
+}
diff --git a/src/mime/multipart/multipart.go b/src/mime/multipart/multipart.go
new file mode 100644
index 0000000..da1f458
--- /dev/null
+++ b/src/mime/multipart/multipart.go
@@ -0,0 +1,488 @@
+// 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.
+
+# Limits
+
+To protect against malicious inputs, this package sets limits on the size
+of the MIME data it processes.
+
+Reader.NextPart and Reader.NextRawPart limit the number of headers in a
+part to 10000 and Reader.ReadForm limits the total number of headers in all
+FileHeaders to 10000.
+These limits may be adjusted with the GODEBUG=multipartmaxheaders=<values>
+setting.
+
+Reader.ReadForm further limits the number of parts in a form to 1000.
+This limit may be adjusted with the GODEBUG=multipartmaxparts=<value>
+setting.
+*/
+package multipart
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "internal/godebug"
+ "io"
+ "mime"
+ "mime/quotedprintable"
+ "net/textproto"
+ "path/filepath"
+ "strconv"
+ "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, maxMIMEHeaderSize, maxMIMEHeaders int64) (*Part, error) {
+ bp := &Part{
+ Header: make(map[string][]string),
+ mr: mr,
+ }
+ if err := bp.populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders); 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 (p *Part) populateHeaders(maxMIMEHeaderSize, maxMIMEHeaders int64) error {
+ r := textproto.NewReader(p.mr.bufReader)
+ header, err := readMIMEHeader(r, maxMIMEHeaderSize, maxMIMEHeaders)
+ if err == nil {
+ p.Header = header
+ }
+ // TODO: Add a distinguishable error to net/textproto.
+ if err != nil && err.Error() == "message too large" {
+ err = ErrMessageTooLarge
+ }
+ 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 double 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' {
+ return +1
+ }
+
+ // Try to detect boundaryDash
+ if c == '-' {
+ if len(buf) == len(prefix)+1 {
+ if readErr != nil {
+ // Prefix + "-" does not match
+ return -1
+ }
+ return 0
+ }
+ if buf[len(prefix)+1] == '-' {
+ 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
+ tempDir string // used in tests
+
+ 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"
+}
+
+// maxMIMEHeaderSize is the maximum size of a MIME header we will parse,
+// including header keys, values, and map overhead.
+const maxMIMEHeaderSize = 10 << 20
+
+// multipartMaxHeaders is the maximum number of header entries NextPart will return,
+// as well as the maximum combined total of header entries Reader.ReadForm will return
+// in FileHeaders.
+var multipartMaxHeaders = godebug.New("multipartmaxheaders")
+
+func maxMIMEHeaders() int64 {
+ if s := multipartMaxHeaders.Value(); s != "" {
+ if v, err := strconv.ParseInt(s, 10, 64); err == nil && v >= 0 {
+ multipartMaxHeaders.IncNonDefault()
+ return v
+ }
+ }
+ return 10000
+}
+
+// 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, maxMIMEHeaderSize, maxMIMEHeaders())
+}
+
+// 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, maxMIMEHeaderSize, maxMIMEHeaders())
+}
+
+func (r *Reader) nextPart(rawPart bool, maxMIMEHeaderSize, maxMIMEHeaders int64) (*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: %w", err)
+ }
+
+ if r.isBoundaryDelimiterLine(line) {
+ r.partsRead++
+ bp, err := newPart(r, rawPart, maxMIMEHeaderSize, maxMIMEHeaders)
+ 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 (r *Reader) isFinalBoundary(line []byte) bool {
+ if !bytes.HasPrefix(line, r.dashBoundaryDash) {
+ return false
+ }
+ rest := line[len(r.dashBoundaryDash):]
+ rest = skipLWSPChar(rest)
+ return len(rest) == 0 || bytes.Equal(rest, r.nl)
+}
+
+func (r *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, r.dashBoundary) {
+ return false
+ }
+ rest := line[len(r.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 r.partsRead == 0 && len(rest) == 1 && rest[0] == '\n' {
+ r.nl = r.nl[1:]
+ r.nlDashBoundary = r.nlDashBoundary[1:]
+ }
+ return bytes.Equal(rest, r.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..e0cb768
--- /dev/null
+++ b/src/mime/multipart/multipart_test.go
@@ -0,0 +1,1020 @@
+// 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(strings.Builder)
+
+ // 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) {
+ for _, body := range []string{
+ `
+This is a multi-part message. This line is ignored.
+--MyBoundary
+foo-bar: baz
+
+Oh no, premature EOF!
+`,
+ `
+This is a multi-part message. This line is ignored.
+--MyBoundary
+foo-bar: baz
+
+Oh no, premature EOF!
+--MyBoundary-`,
+ } {
+ body = strings.ReplaceAll(body, "\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 strings.Builder
+ 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 strings.Builder
+ _, 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 strings.Builder
+ _, 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",
+ },
+ },
+ },
+
+ // Issue 46042; a nested multipart uses the outer separator followed by
+ // a dash.
+ {
+ name: "nested separator prefix is outer separator followed by a dash",
+ sep: "foo",
+ in: strings.Replace(`--foo
+Content-Type: multipart/alternative; boundary="foo-bar"
+
+--foo-bar
+
+Body
+--foo-bar
+
+Body2
+--foo-bar--
+--foo--`, "\n", "\r\n", -1),
+ want: []headerBody{
+ {textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="foo-bar"`}},
+ strings.Replace(`--foo-bar
+
+Body
+--foo-bar
+
+Body2
+--foo-bar--`, "\n", "\r\n", -1),
+ },
+ },
+ },
+
+ // A nested boundary cannot be the outer separator followed by double dash.
+ {
+ name: "nested separator prefix is outer separator followed by double dash",
+ sep: "foo",
+ in: strings.Replace(`--foo
+Content-Type: multipart/alternative; boundary="foo--"
+
+--foo--
+
+Body
+
+--foo--`, "\n", "\r\n", -1),
+ want: []headerBody{
+ {textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="foo--"`}}, ""},
+ },
+ },
+
+ 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 strings.Builder
+ 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/readmimeheader.go b/src/mime/multipart/readmimeheader.go
new file mode 100644
index 0000000..25aa6e2
--- /dev/null
+++ b/src/mime/multipart/readmimeheader.go
@@ -0,0 +1,14 @@
+// Copyright 2023 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 (
+ "net/textproto"
+ _ "unsafe" // for go:linkname
+)
+
+// readMIMEHeader is defined in package net/textproto.
+//
+//go:linkname readMIMEHeader net/textproto.readMIMEHeader
+func readMIMEHeader(r *textproto.Reader, maxMemory, maxHeaders int64) (textproto.MIMEHeader, error)
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..9e0f131
--- /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 strings.Builder
+ 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 strings.Builder
+ 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)
+ }
+}