Black Lives Matter. Support the Equal Justice Initiative.

Source file src/mime/multipart/multipart_test.go

Documentation: mime/multipart

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package multipart
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"net/textproto"
    13  	"os"
    14  	"reflect"
    15  	"strings"
    16  	"testing"
    17  )
    18  
    19  func TestBoundaryLine(t *testing.T) {
    20  	mr := NewReader(strings.NewReader(""), "myBoundary")
    21  	if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) {
    22  		t.Error("expected")
    23  	}
    24  	if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) {
    25  		t.Error("expected")
    26  	}
    27  	if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) {
    28  		t.Error("expected")
    29  	}
    30  	if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus \n")) {
    31  		t.Error("expected fail")
    32  	}
    33  	if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus--")) {
    34  		t.Error("expected fail")
    35  	}
    36  }
    37  
    38  func escapeString(v string) string {
    39  	bytes, _ := json.Marshal(v)
    40  	return string(bytes)
    41  }
    42  
    43  func expectEq(t *testing.T, expected, actual, what string) {
    44  	if expected == actual {
    45  		return
    46  	}
    47  	t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)",
    48  		what, escapeString(actual), len(actual), escapeString(expected), len(expected))
    49  }
    50  
    51  func TestNameAccessors(t *testing.T) {
    52  	tests := [...][3]string{
    53  		{`form-data; name="foo"`, "foo", ""},
    54  		{` form-data ; name=foo`, "foo", ""},
    55  		{`FORM-DATA;name="foo"`, "foo", ""},
    56  		{` FORM-DATA ; name="foo"`, "foo", ""},
    57  		{` FORM-DATA ; name="foo"`, "foo", ""},
    58  		{` FORM-DATA ; name=foo`, "foo", ""},
    59  		{` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo", "foo.txt"},
    60  		{` not-form-data ; filename="bar.txt"; name=foo; baz=quux`, "", "bar.txt"},
    61  	}
    62  	for i, test := range tests {
    63  		p := &Part{Header: make(map[string][]string)}
    64  		p.Header.Set("Content-Disposition", test[0])
    65  		if g, e := p.FormName(), test[1]; g != e {
    66  			t.Errorf("test %d: FormName() = %q; want %q", i, g, e)
    67  		}
    68  		if g, e := p.FileName(), test[2]; g != e {
    69  			t.Errorf("test %d: FileName() = %q; want %q", i, g, e)
    70  		}
    71  	}
    72  }
    73  
    74  var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8)
    75  
    76  func testMultipartBody(sep string) string {
    77  	testBody := `
    78  This is a multi-part message.  This line is ignored.
    79  --MyBoundary
    80  Header1: value1
    81  HEADER2: value2
    82  foo-bar: baz
    83  
    84  My value
    85  The end.
    86  --MyBoundary
    87  name: bigsection
    88  
    89  [longline]
    90  --MyBoundary
    91  Header1: value1b
    92  HEADER2: value2b
    93  foo-bar: bazb
    94  
    95  Line 1
    96  Line 2
    97  Line 3 ends in a newline, but just one.
    98  
    99  --MyBoundary
   100  
   101  never read data
   102  --MyBoundary--
   103  
   104  
   105  useless trailer
   106  `
   107  	testBody = strings.ReplaceAll(testBody, "\n", sep)
   108  	return strings.Replace(testBody, "[longline]", longLine, 1)
   109  }
   110  
   111  func TestMultipart(t *testing.T) {
   112  	bodyReader := strings.NewReader(testMultipartBody("\r\n"))
   113  	testMultipart(t, bodyReader, false)
   114  }
   115  
   116  func TestMultipartOnlyNewlines(t *testing.T) {
   117  	bodyReader := strings.NewReader(testMultipartBody("\n"))
   118  	testMultipart(t, bodyReader, true)
   119  }
   120  
   121  func TestMultipartSlowInput(t *testing.T) {
   122  	bodyReader := strings.NewReader(testMultipartBody("\r\n"))
   123  	testMultipart(t, &slowReader{bodyReader}, false)
   124  }
   125  
   126  func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) {
   127  	t.Parallel()
   128  	reader := NewReader(r, "MyBoundary")
   129  	buf := new(bytes.Buffer)
   130  
   131  	// Part1
   132  	part, err := reader.NextPart()
   133  	if part == nil || err != nil {
   134  		t.Error("Expected part1")
   135  		return
   136  	}
   137  	if x := part.Header.Get("Header1"); x != "value1" {
   138  		t.Errorf("part.Header.Get(%q) = %q, want %q", "Header1", x, "value1")
   139  	}
   140  	if x := part.Header.Get("foo-bar"); x != "baz" {
   141  		t.Errorf("part.Header.Get(%q) = %q, want %q", "foo-bar", x, "baz")
   142  	}
   143  	if x := part.Header.Get("Foo-Bar"); x != "baz" {
   144  		t.Errorf("part.Header.Get(%q) = %q, want %q", "Foo-Bar", x, "baz")
   145  	}
   146  	buf.Reset()
   147  	if _, err := io.Copy(buf, part); err != nil {
   148  		t.Errorf("part 1 copy: %v", err)
   149  	}
   150  
   151  	adjustNewlines := func(s string) string {
   152  		if onlyNewlines {
   153  			return strings.ReplaceAll(s, "\r\n", "\n")
   154  		}
   155  		return s
   156  	}
   157  
   158  	expectEq(t, adjustNewlines("My value\r\nThe end."), buf.String(), "Value of first part")
   159  
   160  	// Part2
   161  	part, err = reader.NextPart()
   162  	if err != nil {
   163  		t.Fatalf("Expected part2; got: %v", err)
   164  		return
   165  	}
   166  	if e, g := "bigsection", part.Header.Get("name"); e != g {
   167  		t.Errorf("part2's name header: expected %q, got %q", e, g)
   168  	}
   169  	buf.Reset()
   170  	if _, err := io.Copy(buf, part); err != nil {
   171  		t.Errorf("part 2 copy: %v", err)
   172  	}
   173  	s := buf.String()
   174  	if len(s) != len(longLine) {
   175  		t.Errorf("part2 body expected long line of length %d; got length %d",
   176  			len(longLine), len(s))
   177  	}
   178  	if s != longLine {
   179  		t.Errorf("part2 long body didn't match")
   180  	}
   181  
   182  	// Part3
   183  	part, err = reader.NextPart()
   184  	if part == nil || err != nil {
   185  		t.Error("Expected part3")
   186  		return
   187  	}
   188  	if part.Header.Get("foo-bar") != "bazb" {
   189  		t.Error("Expected foo-bar: bazb")
   190  	}
   191  	buf.Reset()
   192  	if _, err := io.Copy(buf, part); err != nil {
   193  		t.Errorf("part 3 copy: %v", err)
   194  	}
   195  	expectEq(t, adjustNewlines("Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n"),
   196  		buf.String(), "body of part 3")
   197  
   198  	// Part4
   199  	part, err = reader.NextPart()
   200  	if part == nil || err != nil {
   201  		t.Error("Expected part 4 without errors")
   202  		return
   203  	}
   204  
   205  	// Non-existent part5
   206  	part, err = reader.NextPart()
   207  	if part != nil {
   208  		t.Error("Didn't expect a fifth part.")
   209  	}
   210  	if err != io.EOF {
   211  		t.Errorf("On fifth part expected io.EOF; got %v", err)
   212  	}
   213  }
   214  
   215  func TestVariousTextLineEndings(t *testing.T) {
   216  	tests := [...]string{
   217  		"Foo\nBar",
   218  		"Foo\nBar\n",
   219  		"Foo\r\nBar",
   220  		"Foo\r\nBar\r\n",
   221  		"Foo\rBar",
   222  		"Foo\rBar\r",
   223  		"\x00\x01\x02\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10",
   224  	}
   225  
   226  	for testNum, expectedBody := range tests {
   227  		body := "--BOUNDARY\r\n" +
   228  			"Content-Disposition: form-data; name=\"value\"\r\n" +
   229  			"\r\n" +
   230  			expectedBody +
   231  			"\r\n--BOUNDARY--\r\n"
   232  		bodyReader := strings.NewReader(body)
   233  
   234  		reader := NewReader(bodyReader, "BOUNDARY")
   235  		buf := new(bytes.Buffer)
   236  		part, err := reader.NextPart()
   237  		if part == nil {
   238  			t.Errorf("Expected a body part on text %d", testNum)
   239  			continue
   240  		}
   241  		if err != nil {
   242  			t.Errorf("Unexpected error on text %d: %v", testNum, err)
   243  			continue
   244  		}
   245  		written, err := io.Copy(buf, part)
   246  		expectEq(t, expectedBody, buf.String(), fmt.Sprintf("test %d", testNum))
   247  		if err != nil {
   248  			t.Errorf("Error copying multipart; bytes=%v, error=%v", written, err)
   249  		}
   250  
   251  		part, err = reader.NextPart()
   252  		if part != nil {
   253  			t.Errorf("Unexpected part in test %d", testNum)
   254  		}
   255  		if err != io.EOF {
   256  			t.Errorf("On test %d expected io.EOF; got %v", testNum, err)
   257  		}
   258  
   259  	}
   260  }
   261  
   262  type maliciousReader struct {
   263  	t *testing.T
   264  	n int
   265  }
   266  
   267  const maxReadThreshold = 1 << 20
   268  
   269  func (mr *maliciousReader) Read(b []byte) (n int, err error) {
   270  	mr.n += len(b)
   271  	if mr.n >= maxReadThreshold {
   272  		mr.t.Fatal("too much was read")
   273  		return 0, io.EOF
   274  	}
   275  	return len(b), nil
   276  }
   277  
   278  func TestLineLimit(t *testing.T) {
   279  	mr := &maliciousReader{t: t}
   280  	r := NewReader(mr, "fooBoundary")
   281  	part, err := r.NextPart()
   282  	if part != nil {
   283  		t.Errorf("unexpected part read")
   284  	}
   285  	if err == nil {
   286  		t.Errorf("expected an error")
   287  	}
   288  	if mr.n >= maxReadThreshold {
   289  		t.Errorf("expected to read < %d bytes; read %d", maxReadThreshold, mr.n)
   290  	}
   291  }
   292  
   293  func TestMultipartTruncated(t *testing.T) {
   294  	testBody := `
   295  This is a multi-part message.  This line is ignored.
   296  --MyBoundary
   297  foo-bar: baz
   298  
   299  Oh no, premature EOF!
   300  `
   301  	body := strings.ReplaceAll(testBody, "\n", "\r\n")
   302  	bodyReader := strings.NewReader(body)
   303  	r := NewReader(bodyReader, "MyBoundary")
   304  
   305  	part, err := r.NextPart()
   306  	if err != nil {
   307  		t.Fatalf("didn't get a part")
   308  	}
   309  	_, err = io.Copy(io.Discard, part)
   310  	if err != io.ErrUnexpectedEOF {
   311  		t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err)
   312  	}
   313  }
   314  
   315  type slowReader struct {
   316  	r io.Reader
   317  }
   318  
   319  func (s *slowReader) Read(p []byte) (int, error) {
   320  	if len(p) == 0 {
   321  		return s.r.Read(p)
   322  	}
   323  	return s.r.Read(p[:1])
   324  }
   325  
   326  type sentinelReader struct {
   327  	// done is closed when this reader is read from.
   328  	done chan struct{}
   329  }
   330  
   331  func (s *sentinelReader) Read([]byte) (int, error) {
   332  	if s.done != nil {
   333  		close(s.done)
   334  		s.done = nil
   335  	}
   336  	return 0, io.EOF
   337  }
   338  
   339  // TestMultipartStreamReadahead tests that PartReader does not block
   340  // on reading past the end of a part, ensuring that it can be used on
   341  // a stream like multipart/x-mixed-replace. See golang.org/issue/15431
   342  func TestMultipartStreamReadahead(t *testing.T) {
   343  	testBody1 := `
   344  This is a multi-part message.  This line is ignored.
   345  --MyBoundary
   346  foo-bar: baz
   347  
   348  Body
   349  --MyBoundary
   350  `
   351  	testBody2 := `foo-bar: bop
   352  
   353  Body 2
   354  --MyBoundary--
   355  `
   356  	done1 := make(chan struct{})
   357  	reader := NewReader(
   358  		io.MultiReader(
   359  			strings.NewReader(testBody1),
   360  			&sentinelReader{done1},
   361  			strings.NewReader(testBody2)),
   362  		"MyBoundary")
   363  
   364  	var i int
   365  	readPart := func(hdr textproto.MIMEHeader, body string) {
   366  		part, err := reader.NextPart()
   367  		if part == nil || err != nil {
   368  			t.Fatalf("Part %d: NextPart failed: %v", i, err)
   369  		}
   370  
   371  		if !reflect.DeepEqual(part.Header, hdr) {
   372  			t.Errorf("Part %d: part.Header = %v, want %v", i, part.Header, hdr)
   373  		}
   374  		data, err := io.ReadAll(part)
   375  		expectEq(t, body, string(data), fmt.Sprintf("Part %d body", i))
   376  		if err != nil {
   377  			t.Fatalf("Part %d: ReadAll failed: %v", i, err)
   378  		}
   379  		i++
   380  	}
   381  
   382  	readPart(textproto.MIMEHeader{"Foo-Bar": {"baz"}}, "Body")
   383  
   384  	select {
   385  	case <-done1:
   386  		t.Errorf("Reader read past second boundary")
   387  	default:
   388  	}
   389  
   390  	readPart(textproto.MIMEHeader{"Foo-Bar": {"bop"}}, "Body 2")
   391  }
   392  
   393  func TestLineContinuation(t *testing.T) {
   394  	// This body, extracted from an email, contains headers that span multiple
   395  	// lines.
   396  
   397  	// TODO: The original mail ended with a double-newline before the
   398  	// final delimiter; this was manually edited to use a CRLF.
   399  	testBody :=
   400  		"\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"
   401  
   402  	r := NewReader(strings.NewReader(testBody), "Apple-Mail-2-292336769")
   403  
   404  	for i := 0; i < 2; i++ {
   405  		part, err := r.NextPart()
   406  		if err != nil {
   407  			t.Fatalf("didn't get a part")
   408  		}
   409  		var buf bytes.Buffer
   410  		n, err := io.Copy(&buf, part)
   411  		if err != nil {
   412  			t.Errorf("error reading part: %v\nread so far: %q", err, buf.String())
   413  		}
   414  		if n <= 0 {
   415  			t.Errorf("read %d bytes; expected >0", n)
   416  		}
   417  	}
   418  }
   419  
   420  func TestQuotedPrintableEncoding(t *testing.T) {
   421  	for _, cte := range []string{"quoted-printable", "Quoted-PRINTABLE"} {
   422  		t.Run(cte, func(t *testing.T) {
   423  			testQuotedPrintableEncoding(t, cte)
   424  		})
   425  	}
   426  }
   427  
   428  func testQuotedPrintableEncoding(t *testing.T, cte string) {
   429  	// From https://golang.org/issue/4411
   430  	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--"
   431  	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
   432  	part, err := r.NextPart()
   433  	if err != nil {
   434  		t.Fatal(err)
   435  	}
   436  	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
   437  		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
   438  	}
   439  	var buf bytes.Buffer
   440  	_, err = io.Copy(&buf, part)
   441  	if err != nil {
   442  		t.Error(err)
   443  	}
   444  	got := buf.String()
   445  	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"
   446  	if got != want {
   447  		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
   448  	}
   449  }
   450  
   451  func TestRawPart(t *testing.T) {
   452  	// https://github.com/golang/go/issues/29090
   453  
   454  	body := strings.Replace(`--0016e68ee29c5d515f04cedf6733
   455  Content-Type: text/plain; charset="utf-8"
   456  Content-Transfer-Encoding: quoted-printable
   457  
   458  <div dir=3D"ltr">Hello World.</div>
   459  --0016e68ee29c5d515f04cedf6733
   460  Content-Type: text/plain; charset="utf-8"
   461  Content-Transfer-Encoding: quoted-printable
   462  
   463  <div dir=3D"ltr">Hello World.</div>
   464  --0016e68ee29c5d515f04cedf6733--`, "\n", "\r\n", -1)
   465  
   466  	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
   467  
   468  	// This part is expected to be raw, bypassing the automatic handling
   469  	// of quoted-printable.
   470  	part, err := r.NextRawPart()
   471  	if err != nil {
   472  		t.Fatal(err)
   473  	}
   474  	if _, ok := part.Header["Content-Transfer-Encoding"]; !ok {
   475  		t.Errorf("missing Content-Transfer-Encoding")
   476  	}
   477  	var buf bytes.Buffer
   478  	_, err = io.Copy(&buf, part)
   479  	if err != nil {
   480  		t.Error(err)
   481  	}
   482  	got := buf.String()
   483  	// Data is still quoted-printable.
   484  	want := `<div dir=3D"ltr">Hello World.</div>`
   485  	if got != want {
   486  		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
   487  	}
   488  
   489  	// This part is expected to have automatic decoding of quoted-printable.
   490  	part, err = r.NextPart()
   491  	if err != nil {
   492  		t.Fatal(err)
   493  	}
   494  	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
   495  		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
   496  	}
   497  
   498  	buf.Reset()
   499  	_, err = io.Copy(&buf, part)
   500  	if err != nil {
   501  		t.Error(err)
   502  	}
   503  	got = buf.String()
   504  	// QP data has been decoded.
   505  	want = `<div dir="ltr">Hello World.</div>`
   506  	if got != want {
   507  		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
   508  	}
   509  }
   510  
   511  // Test parsing an image attachment from gmail, which previously failed.
   512  func TestNested(t *testing.T) {
   513  	// nested-mime is the body part of a multipart/mixed email
   514  	// with boundary e89a8ff1c1e83553e304be640612
   515  	f, err := os.Open("testdata/nested-mime")
   516  	if err != nil {
   517  		t.Fatal(err)
   518  	}
   519  	defer f.Close()
   520  	mr := NewReader(f, "e89a8ff1c1e83553e304be640612")
   521  	p, err := mr.NextPart()
   522  	if err != nil {
   523  		t.Fatalf("error reading first section (alternative): %v", err)
   524  	}
   525  
   526  	// Read the inner text/plain and text/html sections of the multipart/alternative.
   527  	mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610")
   528  	p, err = mr2.NextPart()
   529  	if err != nil {
   530  		t.Fatalf("reading text/plain part: %v", err)
   531  	}
   532  	if b, err := io.ReadAll(p); string(b) != "*body*\r\n" || err != nil {
   533  		t.Fatalf("reading text/plain part: got %q, %v", b, err)
   534  	}
   535  	p, err = mr2.NextPart()
   536  	if err != nil {
   537  		t.Fatalf("reading text/html part: %v", err)
   538  	}
   539  	if b, err := io.ReadAll(p); string(b) != "<b>body</b>\r\n" || err != nil {
   540  		t.Fatalf("reading text/html part: got %q, %v", b, err)
   541  	}
   542  
   543  	p, err = mr2.NextPart()
   544  	if err != io.EOF {
   545  		t.Fatalf("final inner NextPart = %v; want io.EOF", err)
   546  	}
   547  
   548  	// Back to the outer multipart/mixed, reading the image attachment.
   549  	_, err = mr.NextPart()
   550  	if err != nil {
   551  		t.Fatalf("error reading the image attachment at the end: %v", err)
   552  	}
   553  
   554  	_, err = mr.NextPart()
   555  	if err != io.EOF {
   556  		t.Fatalf("final outer NextPart = %v; want io.EOF", err)
   557  	}
   558  }
   559  
   560  type headerBody struct {
   561  	header textproto.MIMEHeader
   562  	body   string
   563  }
   564  
   565  func formData(key, value string) headerBody {
   566  	return headerBody{
   567  		textproto.MIMEHeader{
   568  			"Content-Type":        {"text/plain; charset=ISO-8859-1"},
   569  			"Content-Disposition": {"form-data; name=" + key},
   570  		},
   571  		value,
   572  	}
   573  }
   574  
   575  type parseTest struct {
   576  	name    string
   577  	in, sep string
   578  	want    []headerBody
   579  }
   580  
   581  var parseTests = []parseTest{
   582  	// Actual body from App Engine on a blob upload. The final part (the
   583  	// Content-Type: message/external-body) is what App Engine replaces
   584  	// the uploaded file with. The other form fields (prefixed with
   585  	// "other" in their form-data name) are unchanged. A bug was
   586  	// reported with blob uploads failing when the other fields were
   587  	// empty. This was the MIME POST body that previously failed.
   588  	{
   589  		name: "App Engine post",
   590  		sep:  "00151757727e9583fd04bfbca4c6",
   591  		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--",
   592  		want: []headerBody{
   593  			formData("otherEmpty1", ""),
   594  			formData("otherFoo1", "foo"),
   595  			formData("otherFoo2", "foo"),
   596  			formData("otherEmpty2", ""),
   597  			formData("otherRepeatFoo", "foo"),
   598  			formData("otherRepeatFoo", "foo"),
   599  			formData("otherRepeatEmpty", ""),
   600  			formData("otherRepeatEmpty", ""),
   601  			formData("submit", "Submit"),
   602  			{textproto.MIMEHeader{
   603  				"Content-Type":        {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"},
   604  				"Content-Disposition": {"form-data; name=file; filename=\"fall.png\""},
   605  			}, "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"},
   606  		},
   607  	},
   608  
   609  	// Single empty part, ended with --boundary immediately after headers.
   610  	{
   611  		name: "single empty part, --boundary",
   612  		sep:  "abc",
   613  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc--",
   614  		want: []headerBody{
   615  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   616  		},
   617  	},
   618  
   619  	// Single empty part, ended with \r\n--boundary immediately after headers.
   620  	{
   621  		name: "single empty part, \r\n--boundary",
   622  		sep:  "abc",
   623  		in:   "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--",
   624  		want: []headerBody{
   625  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   626  		},
   627  	},
   628  
   629  	// Final part empty.
   630  	{
   631  		name: "final part empty",
   632  		sep:  "abc",
   633  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--",
   634  		want: []headerBody{
   635  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   636  			{textproto.MIMEHeader{"Foo2": {"bar2"}}, ""},
   637  		},
   638  	},
   639  
   640  	// Final part empty with newlines after final separator.
   641  	{
   642  		name: "final part empty then crlf",
   643  		sep:  "abc",
   644  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n",
   645  		want: []headerBody{
   646  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   647  		},
   648  	},
   649  
   650  	// Final part empty with lwsp-chars after final separator.
   651  	{
   652  		name: "final part empty then lwsp",
   653  		sep:  "abc",
   654  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc-- \t",
   655  		want: []headerBody{
   656  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   657  		},
   658  	},
   659  
   660  	// No parts (empty form as submitted by Chrome)
   661  	{
   662  		name: "no parts",
   663  		sep:  "----WebKitFormBoundaryQfEAfzFOiSemeHfA",
   664  		in:   "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n",
   665  		want: []headerBody{},
   666  	},
   667  
   668  	// Part containing data starting with the boundary, but with additional suffix.
   669  	{
   670  		name: "fake separator as data",
   671  		sep:  "sep",
   672  		in:   "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--",
   673  		want: []headerBody{
   674  			{textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"},
   675  		},
   676  	},
   677  
   678  	// Part containing a boundary with whitespace following it.
   679  	{
   680  		name: "boundary with whitespace",
   681  		sep:  "sep",
   682  		in:   "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--",
   683  		want: []headerBody{
   684  			{textproto.MIMEHeader{"Foo": {"bar"}}, "text"},
   685  		},
   686  	},
   687  
   688  	// With ignored leading line.
   689  	{
   690  		name: "leading line",
   691  		sep:  "MyBoundary",
   692  		in: strings.Replace(`This is a multi-part message.  This line is ignored.
   693  --MyBoundary
   694  foo: bar
   695  
   696  
   697  --MyBoundary--`, "\n", "\r\n", -1),
   698  		want: []headerBody{
   699  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   700  		},
   701  	},
   702  
   703  	// Issue 10616; minimal
   704  	{
   705  		name: "issue 10616 minimal",
   706  		sep:  "sep",
   707  		in: "--sep \r\nFoo: bar\r\n\r\n" +
   708  			"a\r\n" +
   709  			"--sep_alt\r\n" +
   710  			"b\r\n" +
   711  			"\r\n--sep--",
   712  		want: []headerBody{
   713  			{textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"},
   714  		},
   715  	},
   716  
   717  	// Issue 10616; full example from bug.
   718  	{
   719  		name: "nested separator prefix is outer separator",
   720  		sep:  "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9",
   721  		in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9
   722  Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"
   723  
   724  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   725  Content-Type: text/plain; charset="utf-8"
   726  Content-Transfer-Encoding: 8bit
   727  
   728  This is a multi-part message in MIME format.
   729  
   730  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   731  Content-Type: text/html; charset="utf-8"
   732  Content-Transfer-Encoding: 8bit
   733  
   734  html things
   735  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--
   736  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1),
   737  		want: []headerBody{
   738  			{textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}},
   739  				strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   740  Content-Type: text/plain; charset="utf-8"
   741  Content-Transfer-Encoding: 8bit
   742  
   743  This is a multi-part message in MIME format.
   744  
   745  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   746  Content-Type: text/html; charset="utf-8"
   747  Content-Transfer-Encoding: 8bit
   748  
   749  html things
   750  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1),
   751  			},
   752  		},
   753  	},
   754  	// Issue 12662: Check that we don't consume the leading \r if the peekBuffer
   755  	// ends in '\r\n--separator-'
   756  	{
   757  		name: "peek buffer boundary condition",
   758  		sep:  "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
   759  		in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
   760  Content-Disposition: form-data; name="block"; filename="block"
   761  Content-Type: application/octet-stream
   762  
   763  `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1),
   764  		want: []headerBody{
   765  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
   766  				strings.Repeat("A", peekBufferSize-65),
   767  			},
   768  		},
   769  	},
   770  	// Issue 12662: Same test as above with \r\n at the end
   771  	{
   772  		name: "peek buffer boundary condition",
   773  		sep:  "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
   774  		in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
   775  Content-Disposition: form-data; name="block"; filename="block"
   776  Content-Type: application/octet-stream
   777  
   778  `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--\n", "\n", "\r\n", -1),
   779  		want: []headerBody{
   780  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
   781  				strings.Repeat("A", peekBufferSize-65),
   782  			},
   783  		},
   784  	},
   785  	// Issue 12662v2: We want to make sure that for short buffers that end with
   786  	// '\r\n--separator-' we always consume at least one (valid) symbol from the
   787  	// peekBuffer
   788  	{
   789  		name: "peek buffer boundary condition",
   790  		sep:  "aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
   791  		in: strings.Replace(`--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
   792  Content-Disposition: form-data; name="block"; filename="block"
   793  Content-Type: application/octet-stream
   794  
   795  `+strings.Repeat("A", peekBufferSize)+"\n--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1),
   796  		want: []headerBody{
   797  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
   798  				strings.Repeat("A", peekBufferSize),
   799  			},
   800  		},
   801  	},
   802  	// Context: https://github.com/camlistore/camlistore/issues/642
   803  	// If the file contents in the form happens to have a size such as:
   804  	// size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize)
   805  	// then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy
   806  	// cut such as:
   807  	// "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the
   808  	// subsequent Read miss the boundary.
   809  	{
   810  		name: "safeCount off by one",
   811  		sep:  "08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74",
   812  		in: strings.Replace(`--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74
   813  Content-Disposition: form-data; name="myfile"; filename="my-file.txt"
   814  Content-Type: application/octet-stream
   815  
   816  `, "\n", "\r\n", -1) +
   817  			strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)) +
   818  			strings.Replace(`
   819  --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74
   820  Content-Disposition: form-data; name="key"
   821  
   822  val
   823  --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74--
   824  `, "\n", "\r\n", -1),
   825  		want: []headerBody{
   826  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="myfile"; filename="my-file.txt"`}},
   827  				strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)),
   828  			},
   829  			{textproto.MIMEHeader{"Content-Disposition": {`form-data; name="key"`}},
   830  				"val",
   831  			},
   832  		},
   833  	},
   834  
   835  	roundTripParseTest(),
   836  }
   837  
   838  func TestParse(t *testing.T) {
   839  Cases:
   840  	for _, tt := range parseTests {
   841  		r := NewReader(strings.NewReader(tt.in), tt.sep)
   842  		got := []headerBody{}
   843  		for {
   844  			p, err := r.NextPart()
   845  			if err == io.EOF {
   846  				break
   847  			}
   848  			if err != nil {
   849  				t.Errorf("in test %q, NextPart: %v", tt.name, err)
   850  				continue Cases
   851  			}
   852  			pbody, err := io.ReadAll(p)
   853  			if err != nil {
   854  				t.Errorf("in test %q, error reading part: %v", tt.name, err)
   855  				continue Cases
   856  			}
   857  			got = append(got, headerBody{p.Header, string(pbody)})
   858  		}
   859  		if !reflect.DeepEqual(tt.want, got) {
   860  			t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want)
   861  			if len(tt.want) != len(got) {
   862  				t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want))
   863  			} else if len(got) > 1 {
   864  				for pi, wantPart := range tt.want {
   865  					if !reflect.DeepEqual(wantPart, got[pi]) {
   866  						t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart)
   867  					}
   868  				}
   869  			}
   870  		}
   871  	}
   872  }
   873  
   874  func partsFromReader(r *Reader) ([]headerBody, error) {
   875  	got := []headerBody{}
   876  	for {
   877  		p, err := r.NextPart()
   878  		if err == io.EOF {
   879  			return got, nil
   880  		}
   881  		if err != nil {
   882  			return nil, fmt.Errorf("NextPart: %v", err)
   883  		}
   884  		pbody, err := io.ReadAll(p)
   885  		if err != nil {
   886  			return nil, fmt.Errorf("error reading part: %v", err)
   887  		}
   888  		got = append(got, headerBody{p.Header, string(pbody)})
   889  	}
   890  }
   891  
   892  func TestParseAllSizes(t *testing.T) {
   893  	t.Parallel()
   894  	maxSize := 5 << 10
   895  	if testing.Short() {
   896  		maxSize = 512
   897  	}
   898  	var buf bytes.Buffer
   899  	body := strings.Repeat("a", maxSize)
   900  	bodyb := []byte(body)
   901  	for size := 0; size < maxSize; size++ {
   902  		buf.Reset()
   903  		w := NewWriter(&buf)
   904  		part, _ := w.CreateFormField("f")
   905  		part.Write(bodyb[:size])
   906  		part, _ = w.CreateFormField("key")
   907  		part.Write([]byte("val"))
   908  		w.Close()
   909  		r := NewReader(&buf, w.Boundary())
   910  		got, err := partsFromReader(r)
   911  		if err != nil {
   912  			t.Errorf("For size %d: %v", size, err)
   913  			continue
   914  		}
   915  		if len(got) != 2 {
   916  			t.Errorf("For size %d, num parts = %d; want 2", size, len(got))
   917  			continue
   918  		}
   919  		if got[0].body != body[:size] {
   920  			t.Errorf("For size %d, got unexpected len %d: %q", size, len(got[0].body), got[0].body)
   921  		}
   922  	}
   923  }
   924  
   925  func roundTripParseTest() parseTest {
   926  	t := parseTest{
   927  		name: "round trip",
   928  		want: []headerBody{
   929  			formData("empty", ""),
   930  			formData("lf", "\n"),
   931  			formData("cr", "\r"),
   932  			formData("crlf", "\r\n"),
   933  			formData("foo", "bar"),
   934  		},
   935  	}
   936  	var buf bytes.Buffer
   937  	w := NewWriter(&buf)
   938  	for _, p := range t.want {
   939  		pw, err := w.CreatePart(p.header)
   940  		if err != nil {
   941  			panic(err)
   942  		}
   943  		_, err = pw.Write([]byte(p.body))
   944  		if err != nil {
   945  			panic(err)
   946  		}
   947  	}
   948  	w.Close()
   949  	t.in = buf.String()
   950  	t.sep = w.Boundary()
   951  	return t
   952  }
   953  
   954  func TestNoBoundary(t *testing.T) {
   955  	mr := NewReader(strings.NewReader(""), "")
   956  	_, err := mr.NextPart()
   957  	if got, want := fmt.Sprint(err), "multipart: boundary is empty"; got != want {
   958  		t.Errorf("NextPart error = %v; want %v", got, want)
   959  	}
   960  }
   961  

View as plain text