...

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(strings.Builder)
   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  	for _, body := range []string{
   295  		`
   296  This is a multi-part message.  This line is ignored.
   297  --MyBoundary
   298  foo-bar: baz
   299  
   300  Oh no, premature EOF!
   301  `,
   302  		`
   303  This is a multi-part message.  This line is ignored.
   304  --MyBoundary
   305  foo-bar: baz
   306  
   307  Oh no, premature EOF!
   308  --MyBoundary-`,
   309  	} {
   310  		body = strings.ReplaceAll(body, "\n", "\r\n")
   311  		bodyReader := strings.NewReader(body)
   312  		r := NewReader(bodyReader, "MyBoundary")
   313  
   314  		part, err := r.NextPart()
   315  		if err != nil {
   316  			t.Fatalf("didn't get a part")
   317  		}
   318  		_, err = io.Copy(io.Discard, part)
   319  		if err != io.ErrUnexpectedEOF {
   320  			t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err)
   321  		}
   322  	}
   323  }
   324  
   325  type slowReader struct {
   326  	r io.Reader
   327  }
   328  
   329  func (s *slowReader) Read(p []byte) (int, error) {
   330  	if len(p) == 0 {
   331  		return s.r.Read(p)
   332  	}
   333  	return s.r.Read(p[:1])
   334  }
   335  
   336  type sentinelReader struct {
   337  	// done is closed when this reader is read from.
   338  	done chan struct{}
   339  }
   340  
   341  func (s *sentinelReader) Read([]byte) (int, error) {
   342  	if s.done != nil {
   343  		close(s.done)
   344  		s.done = nil
   345  	}
   346  	return 0, io.EOF
   347  }
   348  
   349  // TestMultipartStreamReadahead tests that PartReader does not block
   350  // on reading past the end of a part, ensuring that it can be used on
   351  // a stream like multipart/x-mixed-replace. See golang.org/issue/15431
   352  func TestMultipartStreamReadahead(t *testing.T) {
   353  	testBody1 := `
   354  This is a multi-part message.  This line is ignored.
   355  --MyBoundary
   356  foo-bar: baz
   357  
   358  Body
   359  --MyBoundary
   360  `
   361  	testBody2 := `foo-bar: bop
   362  
   363  Body 2
   364  --MyBoundary--
   365  `
   366  	done1 := make(chan struct{})
   367  	reader := NewReader(
   368  		io.MultiReader(
   369  			strings.NewReader(testBody1),
   370  			&sentinelReader{done1},
   371  			strings.NewReader(testBody2)),
   372  		"MyBoundary")
   373  
   374  	var i int
   375  	readPart := func(hdr textproto.MIMEHeader, body string) {
   376  		part, err := reader.NextPart()
   377  		if part == nil || err != nil {
   378  			t.Fatalf("Part %d: NextPart failed: %v", i, err)
   379  		}
   380  
   381  		if !reflect.DeepEqual(part.Header, hdr) {
   382  			t.Errorf("Part %d: part.Header = %v, want %v", i, part.Header, hdr)
   383  		}
   384  		data, err := io.ReadAll(part)
   385  		expectEq(t, body, string(data), fmt.Sprintf("Part %d body", i))
   386  		if err != nil {
   387  			t.Fatalf("Part %d: ReadAll failed: %v", i, err)
   388  		}
   389  		i++
   390  	}
   391  
   392  	readPart(textproto.MIMEHeader{"Foo-Bar": {"baz"}}, "Body")
   393  
   394  	select {
   395  	case <-done1:
   396  		t.Errorf("Reader read past second boundary")
   397  	default:
   398  	}
   399  
   400  	readPart(textproto.MIMEHeader{"Foo-Bar": {"bop"}}, "Body 2")
   401  }
   402  
   403  func TestLineContinuation(t *testing.T) {
   404  	// This body, extracted from an email, contains headers that span multiple
   405  	// lines.
   406  
   407  	// TODO: The original mail ended with a double-newline before the
   408  	// final delimiter; this was manually edited to use a CRLF.
   409  	testBody :=
   410  		"\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"
   411  
   412  	r := NewReader(strings.NewReader(testBody), "Apple-Mail-2-292336769")
   413  
   414  	for i := 0; i < 2; i++ {
   415  		part, err := r.NextPart()
   416  		if err != nil {
   417  			t.Fatalf("didn't get a part")
   418  		}
   419  		var buf strings.Builder
   420  		n, err := io.Copy(&buf, part)
   421  		if err != nil {
   422  			t.Errorf("error reading part: %v\nread so far: %q", err, buf.String())
   423  		}
   424  		if n <= 0 {
   425  			t.Errorf("read %d bytes; expected >0", n)
   426  		}
   427  	}
   428  }
   429  
   430  func TestQuotedPrintableEncoding(t *testing.T) {
   431  	for _, cte := range []string{"quoted-printable", "Quoted-PRINTABLE"} {
   432  		t.Run(cte, func(t *testing.T) {
   433  			testQuotedPrintableEncoding(t, cte)
   434  		})
   435  	}
   436  }
   437  
   438  func testQuotedPrintableEncoding(t *testing.T, cte string) {
   439  	// From https://golang.org/issue/4411
   440  	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--"
   441  	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
   442  	part, err := r.NextPart()
   443  	if err != nil {
   444  		t.Fatal(err)
   445  	}
   446  	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
   447  		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
   448  	}
   449  	var buf strings.Builder
   450  	_, err = io.Copy(&buf, part)
   451  	if err != nil {
   452  		t.Error(err)
   453  	}
   454  	got := buf.String()
   455  	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"
   456  	if got != want {
   457  		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
   458  	}
   459  }
   460  
   461  func TestRawPart(t *testing.T) {
   462  	// https://github.com/golang/go/issues/29090
   463  
   464  	body := strings.Replace(`--0016e68ee29c5d515f04cedf6733
   465  Content-Type: text/plain; charset="utf-8"
   466  Content-Transfer-Encoding: quoted-printable
   467  
   468  <div dir=3D"ltr">Hello World.</div>
   469  --0016e68ee29c5d515f04cedf6733
   470  Content-Type: text/plain; charset="utf-8"
   471  Content-Transfer-Encoding: quoted-printable
   472  
   473  <div dir=3D"ltr">Hello World.</div>
   474  --0016e68ee29c5d515f04cedf6733--`, "\n", "\r\n", -1)
   475  
   476  	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
   477  
   478  	// This part is expected to be raw, bypassing the automatic handling
   479  	// of quoted-printable.
   480  	part, err := r.NextRawPart()
   481  	if err != nil {
   482  		t.Fatal(err)
   483  	}
   484  	if _, ok := part.Header["Content-Transfer-Encoding"]; !ok {
   485  		t.Errorf("missing Content-Transfer-Encoding")
   486  	}
   487  	var buf strings.Builder
   488  	_, err = io.Copy(&buf, part)
   489  	if err != nil {
   490  		t.Error(err)
   491  	}
   492  	got := buf.String()
   493  	// Data is still quoted-printable.
   494  	want := `<div dir=3D"ltr">Hello World.</div>`
   495  	if got != want {
   496  		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
   497  	}
   498  
   499  	// This part is expected to have automatic decoding of quoted-printable.
   500  	part, err = r.NextPart()
   501  	if err != nil {
   502  		t.Fatal(err)
   503  	}
   504  	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
   505  		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
   506  	}
   507  
   508  	buf.Reset()
   509  	_, err = io.Copy(&buf, part)
   510  	if err != nil {
   511  		t.Error(err)
   512  	}
   513  	got = buf.String()
   514  	// QP data has been decoded.
   515  	want = `<div dir="ltr">Hello World.</div>`
   516  	if got != want {
   517  		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
   518  	}
   519  }
   520  
   521  // Test parsing an image attachment from gmail, which previously failed.
   522  func TestNested(t *testing.T) {
   523  	// nested-mime is the body part of a multipart/mixed email
   524  	// with boundary e89a8ff1c1e83553e304be640612
   525  	f, err := os.Open("testdata/nested-mime")
   526  	if err != nil {
   527  		t.Fatal(err)
   528  	}
   529  	defer f.Close()
   530  	mr := NewReader(f, "e89a8ff1c1e83553e304be640612")
   531  	p, err := mr.NextPart()
   532  	if err != nil {
   533  		t.Fatalf("error reading first section (alternative): %v", err)
   534  	}
   535  
   536  	// Read the inner text/plain and text/html sections of the multipart/alternative.
   537  	mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610")
   538  	p, err = mr2.NextPart()
   539  	if err != nil {
   540  		t.Fatalf("reading text/plain part: %v", err)
   541  	}
   542  	if b, err := io.ReadAll(p); string(b) != "*body*\r\n" || err != nil {
   543  		t.Fatalf("reading text/plain part: got %q, %v", b, err)
   544  	}
   545  	p, err = mr2.NextPart()
   546  	if err != nil {
   547  		t.Fatalf("reading text/html part: %v", err)
   548  	}
   549  	if b, err := io.ReadAll(p); string(b) != "<b>body</b>\r\n" || err != nil {
   550  		t.Fatalf("reading text/html part: got %q, %v", b, err)
   551  	}
   552  
   553  	p, err = mr2.NextPart()
   554  	if err != io.EOF {
   555  		t.Fatalf("final inner NextPart = %v; want io.EOF", err)
   556  	}
   557  
   558  	// Back to the outer multipart/mixed, reading the image attachment.
   559  	_, err = mr.NextPart()
   560  	if err != nil {
   561  		t.Fatalf("error reading the image attachment at the end: %v", err)
   562  	}
   563  
   564  	_, err = mr.NextPart()
   565  	if err != io.EOF {
   566  		t.Fatalf("final outer NextPart = %v; want io.EOF", err)
   567  	}
   568  }
   569  
   570  type headerBody struct {
   571  	header textproto.MIMEHeader
   572  	body   string
   573  }
   574  
   575  func formData(key, value string) headerBody {
   576  	return headerBody{
   577  		textproto.MIMEHeader{
   578  			"Content-Type":        {"text/plain; charset=ISO-8859-1"},
   579  			"Content-Disposition": {"form-data; name=" + key},
   580  		},
   581  		value,
   582  	}
   583  }
   584  
   585  type parseTest struct {
   586  	name    string
   587  	in, sep string
   588  	want    []headerBody
   589  }
   590  
   591  var parseTests = []parseTest{
   592  	// Actual body from App Engine on a blob upload. The final part (the
   593  	// Content-Type: message/external-body) is what App Engine replaces
   594  	// the uploaded file with. The other form fields (prefixed with
   595  	// "other" in their form-data name) are unchanged. A bug was
   596  	// reported with blob uploads failing when the other fields were
   597  	// empty. This was the MIME POST body that previously failed.
   598  	{
   599  		name: "App Engine post",
   600  		sep:  "00151757727e9583fd04bfbca4c6",
   601  		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--",
   602  		want: []headerBody{
   603  			formData("otherEmpty1", ""),
   604  			formData("otherFoo1", "foo"),
   605  			formData("otherFoo2", "foo"),
   606  			formData("otherEmpty2", ""),
   607  			formData("otherRepeatFoo", "foo"),
   608  			formData("otherRepeatFoo", "foo"),
   609  			formData("otherRepeatEmpty", ""),
   610  			formData("otherRepeatEmpty", ""),
   611  			formData("submit", "Submit"),
   612  			{textproto.MIMEHeader{
   613  				"Content-Type":        {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"},
   614  				"Content-Disposition": {"form-data; name=file; filename=\"fall.png\""},
   615  			}, "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"},
   616  		},
   617  	},
   618  
   619  	// Single empty part, ended with --boundary immediately after headers.
   620  	{
   621  		name: "single empty part, --boundary",
   622  		sep:  "abc",
   623  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc--",
   624  		want: []headerBody{
   625  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   626  		},
   627  	},
   628  
   629  	// Single empty part, ended with \r\n--boundary immediately after headers.
   630  	{
   631  		name: "single empty part, \r\n--boundary",
   632  		sep:  "abc",
   633  		in:   "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--",
   634  		want: []headerBody{
   635  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   636  		},
   637  	},
   638  
   639  	// Final part empty.
   640  	{
   641  		name: "final part empty",
   642  		sep:  "abc",
   643  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--",
   644  		want: []headerBody{
   645  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   646  			{textproto.MIMEHeader{"Foo2": {"bar2"}}, ""},
   647  		},
   648  	},
   649  
   650  	// Final part empty with newlines after final separator.
   651  	{
   652  		name: "final part empty then crlf",
   653  		sep:  "abc",
   654  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n",
   655  		want: []headerBody{
   656  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   657  		},
   658  	},
   659  
   660  	// Final part empty with lwsp-chars after final separator.
   661  	{
   662  		name: "final part empty then lwsp",
   663  		sep:  "abc",
   664  		in:   "--abc\r\nFoo: bar\r\n\r\n--abc-- \t",
   665  		want: []headerBody{
   666  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   667  		},
   668  	},
   669  
   670  	// No parts (empty form as submitted by Chrome)
   671  	{
   672  		name: "no parts",
   673  		sep:  "----WebKitFormBoundaryQfEAfzFOiSemeHfA",
   674  		in:   "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n",
   675  		want: []headerBody{},
   676  	},
   677  
   678  	// Part containing data starting with the boundary, but with additional suffix.
   679  	{
   680  		name: "fake separator as data",
   681  		sep:  "sep",
   682  		in:   "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--",
   683  		want: []headerBody{
   684  			{textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"},
   685  		},
   686  	},
   687  
   688  	// Part containing a boundary with whitespace following it.
   689  	{
   690  		name: "boundary with whitespace",
   691  		sep:  "sep",
   692  		in:   "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--",
   693  		want: []headerBody{
   694  			{textproto.MIMEHeader{"Foo": {"bar"}}, "text"},
   695  		},
   696  	},
   697  
   698  	// With ignored leading line.
   699  	{
   700  		name: "leading line",
   701  		sep:  "MyBoundary",
   702  		in: strings.Replace(`This is a multi-part message.  This line is ignored.
   703  --MyBoundary
   704  foo: bar
   705  
   706  
   707  --MyBoundary--`, "\n", "\r\n", -1),
   708  		want: []headerBody{
   709  			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
   710  		},
   711  	},
   712  
   713  	// Issue 10616; minimal
   714  	{
   715  		name: "issue 10616 minimal",
   716  		sep:  "sep",
   717  		in: "--sep \r\nFoo: bar\r\n\r\n" +
   718  			"a\r\n" +
   719  			"--sep_alt\r\n" +
   720  			"b\r\n" +
   721  			"\r\n--sep--",
   722  		want: []headerBody{
   723  			{textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"},
   724  		},
   725  	},
   726  
   727  	// Issue 10616; full example from bug.
   728  	{
   729  		name: "nested separator prefix is outer separator",
   730  		sep:  "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9",
   731  		in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9
   732  Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"
   733  
   734  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   735  Content-Type: text/plain; charset="utf-8"
   736  Content-Transfer-Encoding: 8bit
   737  
   738  This is a multi-part message in MIME format.
   739  
   740  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   741  Content-Type: text/html; charset="utf-8"
   742  Content-Transfer-Encoding: 8bit
   743  
   744  html things
   745  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--
   746  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1),
   747  		want: []headerBody{
   748  			{textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}},
   749  				strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   750  Content-Type: text/plain; charset="utf-8"
   751  Content-Transfer-Encoding: 8bit
   752  
   753  This is a multi-part message in MIME format.
   754  
   755  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
   756  Content-Type: text/html; charset="utf-8"
   757  Content-Transfer-Encoding: 8bit
   758  
   759  html things
   760  ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1),
   761  			},
   762  		},
   763  	},
   764  
   765  	// Issue 12662: Check that we don't consume the leading \r if the peekBuffer
   766  	// ends in '\r\n--separator-'
   767  	{
   768  		name: "peek buffer boundary condition",
   769  		sep:  "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
   770  		in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
   771  Content-Disposition: form-data; name="block"; filename="block"
   772  Content-Type: application/octet-stream
   773  
   774  `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1),
   775  		want: []headerBody{
   776  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
   777  				strings.Repeat("A", peekBufferSize-65),
   778  			},
   779  		},
   780  	},
   781  
   782  	// Issue 12662: Same test as above with \r\n at the end
   783  	{
   784  		name: "peek buffer boundary condition",
   785  		sep:  "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
   786  		in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
   787  Content-Disposition: form-data; name="block"; filename="block"
   788  Content-Type: application/octet-stream
   789  
   790  `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--\n", "\n", "\r\n", -1),
   791  		want: []headerBody{
   792  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
   793  				strings.Repeat("A", peekBufferSize-65),
   794  			},
   795  		},
   796  	},
   797  
   798  	// Issue 12662v2: We want to make sure that for short buffers that end with
   799  	// '\r\n--separator-' we always consume at least one (valid) symbol from the
   800  	// peekBuffer
   801  	{
   802  		name: "peek buffer boundary condition",
   803  		sep:  "aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db",
   804  		in: strings.Replace(`--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db
   805  Content-Disposition: form-data; name="block"; filename="block"
   806  Content-Type: application/octet-stream
   807  
   808  `+strings.Repeat("A", peekBufferSize)+"\n--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1),
   809  		want: []headerBody{
   810  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}},
   811  				strings.Repeat("A", peekBufferSize),
   812  			},
   813  		},
   814  	},
   815  
   816  	// Context: https://github.com/camlistore/camlistore/issues/642
   817  	// If the file contents in the form happens to have a size such as:
   818  	// size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize)
   819  	// then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy
   820  	// cut such as:
   821  	// "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the
   822  	// subsequent Read miss the boundary.
   823  	{
   824  		name: "safeCount off by one",
   825  		sep:  "08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74",
   826  		in: strings.Replace(`--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74
   827  Content-Disposition: form-data; name="myfile"; filename="my-file.txt"
   828  Content-Type: application/octet-stream
   829  
   830  `, "\n", "\r\n", -1) +
   831  			strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)) +
   832  			strings.Replace(`
   833  --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74
   834  Content-Disposition: form-data; name="key"
   835  
   836  val
   837  --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74--
   838  `, "\n", "\r\n", -1),
   839  		want: []headerBody{
   840  			{textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="myfile"; filename="my-file.txt"`}},
   841  				strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)),
   842  			},
   843  			{textproto.MIMEHeader{"Content-Disposition": {`form-data; name="key"`}},
   844  				"val",
   845  			},
   846  		},
   847  	},
   848  
   849  	// Issue 46042; a nested multipart uses the outer separator followed by
   850  	// a dash.
   851  	{
   852  		name: "nested separator prefix is outer separator followed by a dash",
   853  		sep:  "foo",
   854  		in: strings.Replace(`--foo
   855  Content-Type: multipart/alternative; boundary="foo-bar"
   856  
   857  --foo-bar
   858  
   859  Body
   860  --foo-bar
   861  
   862  Body2
   863  --foo-bar--
   864  --foo--`, "\n", "\r\n", -1),
   865  		want: []headerBody{
   866  			{textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="foo-bar"`}},
   867  				strings.Replace(`--foo-bar
   868  
   869  Body
   870  --foo-bar
   871  
   872  Body2
   873  --foo-bar--`, "\n", "\r\n", -1),
   874  			},
   875  		},
   876  	},
   877  
   878  	// A nested boundary cannot be the outer separator followed by double dash.
   879  	{
   880  		name: "nested separator prefix is outer separator followed by double dash",
   881  		sep:  "foo",
   882  		in: strings.Replace(`--foo
   883  Content-Type: multipart/alternative; boundary="foo--"
   884  
   885  --foo--
   886  
   887  Body
   888  
   889  --foo--`, "\n", "\r\n", -1),
   890  		want: []headerBody{
   891  			{textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="foo--"`}}, ""},
   892  		},
   893  	},
   894  
   895  	roundTripParseTest(),
   896  }
   897  
   898  func TestParse(t *testing.T) {
   899  Cases:
   900  	for _, tt := range parseTests {
   901  		r := NewReader(strings.NewReader(tt.in), tt.sep)
   902  		got := []headerBody{}
   903  		for {
   904  			p, err := r.NextPart()
   905  			if err == io.EOF {
   906  				break
   907  			}
   908  			if err != nil {
   909  				t.Errorf("in test %q, NextPart: %v", tt.name, err)
   910  				continue Cases
   911  			}
   912  			pbody, err := io.ReadAll(p)
   913  			if err != nil {
   914  				t.Errorf("in test %q, error reading part: %v", tt.name, err)
   915  				continue Cases
   916  			}
   917  			got = append(got, headerBody{p.Header, string(pbody)})
   918  		}
   919  		if !reflect.DeepEqual(tt.want, got) {
   920  			t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want)
   921  			if len(tt.want) != len(got) {
   922  				t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want))
   923  			} else if len(got) > 1 {
   924  				for pi, wantPart := range tt.want {
   925  					if !reflect.DeepEqual(wantPart, got[pi]) {
   926  						t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart)
   927  					}
   928  				}
   929  			}
   930  		}
   931  	}
   932  }
   933  
   934  func partsFromReader(r *Reader) ([]headerBody, error) {
   935  	got := []headerBody{}
   936  	for {
   937  		p, err := r.NextPart()
   938  		if err == io.EOF {
   939  			return got, nil
   940  		}
   941  		if err != nil {
   942  			return nil, fmt.Errorf("NextPart: %v", err)
   943  		}
   944  		pbody, err := io.ReadAll(p)
   945  		if err != nil {
   946  			return nil, fmt.Errorf("error reading part: %v", err)
   947  		}
   948  		got = append(got, headerBody{p.Header, string(pbody)})
   949  	}
   950  }
   951  
   952  func TestParseAllSizes(t *testing.T) {
   953  	t.Parallel()
   954  	maxSize := 5 << 10
   955  	if testing.Short() {
   956  		maxSize = 512
   957  	}
   958  	var buf bytes.Buffer
   959  	body := strings.Repeat("a", maxSize)
   960  	bodyb := []byte(body)
   961  	for size := 0; size < maxSize; size++ {
   962  		buf.Reset()
   963  		w := NewWriter(&buf)
   964  		part, _ := w.CreateFormField("f")
   965  		part.Write(bodyb[:size])
   966  		part, _ = w.CreateFormField("key")
   967  		part.Write([]byte("val"))
   968  		w.Close()
   969  		r := NewReader(&buf, w.Boundary())
   970  		got, err := partsFromReader(r)
   971  		if err != nil {
   972  			t.Errorf("For size %d: %v", size, err)
   973  			continue
   974  		}
   975  		if len(got) != 2 {
   976  			t.Errorf("For size %d, num parts = %d; want 2", size, len(got))
   977  			continue
   978  		}
   979  		if got[0].body != body[:size] {
   980  			t.Errorf("For size %d, got unexpected len %d: %q", size, len(got[0].body), got[0].body)
   981  		}
   982  	}
   983  }
   984  
   985  func roundTripParseTest() parseTest {
   986  	t := parseTest{
   987  		name: "round trip",
   988  		want: []headerBody{
   989  			formData("empty", ""),
   990  			formData("lf", "\n"),
   991  			formData("cr", "\r"),
   992  			formData("crlf", "\r\n"),
   993  			formData("foo", "bar"),
   994  		},
   995  	}
   996  	var buf strings.Builder
   997  	w := NewWriter(&buf)
   998  	for _, p := range t.want {
   999  		pw, err := w.CreatePart(p.header)
  1000  		if err != nil {
  1001  			panic(err)
  1002  		}
  1003  		_, err = pw.Write([]byte(p.body))
  1004  		if err != nil {
  1005  			panic(err)
  1006  		}
  1007  	}
  1008  	w.Close()
  1009  	t.in = buf.String()
  1010  	t.sep = w.Boundary()
  1011  	return t
  1012  }
  1013  
  1014  func TestNoBoundary(t *testing.T) {
  1015  	mr := NewReader(strings.NewReader(""), "")
  1016  	_, err := mr.NextPart()
  1017  	if got, want := fmt.Sprint(err), "multipart: boundary is empty"; got != want {
  1018  		t.Errorf("NextPart error = %v; want %v", got, want)
  1019  	}
  1020  }
  1021  

View as plain text