Home | History | Annotate | Download | only in 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 	"io/ioutil"
     13 	"net/textproto"
     14 	"os"
     15 	"reflect"
     16 	"strings"
     17 	"testing"
     18 )
     19 
     20 func TestBoundaryLine(t *testing.T) {
     21 	mr := NewReader(strings.NewReader(""), "myBoundary")
     22 	if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) {
     23 		t.Error("expected")
     24 	}
     25 	if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) {
     26 		t.Error("expected")
     27 	}
     28 	if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) {
     29 		t.Error("expected")
     30 	}
     31 	if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus \n")) {
     32 		t.Error("expected fail")
     33 	}
     34 	if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus--")) {
     35 		t.Error("expected fail")
     36 	}
     37 }
     38 
     39 func escapeString(v string) string {
     40 	bytes, _ := json.Marshal(v)
     41 	return string(bytes)
     42 }
     43 
     44 func expectEq(t *testing.T, expected, actual, what string) {
     45 	if expected == actual {
     46 		return
     47 	}
     48 	t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)",
     49 		what, escapeString(actual), len(actual), escapeString(expected), len(expected))
     50 }
     51 
     52 func TestNameAccessors(t *testing.T) {
     53 	tests := [...][3]string{
     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 ; name=foo`, "foo", ""},
     60 		{` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo", "foo.txt"},
     61 		{` not-form-data ; filename="bar.txt"; name=foo; baz=quux`, "", "bar.txt"},
     62 	}
     63 	for i, test := range tests {
     64 		p := &Part{Header: make(map[string][]string)}
     65 		p.Header.Set("Content-Disposition", test[0])
     66 		if g, e := p.FormName(), test[1]; g != e {
     67 			t.Errorf("test %d: FormName() = %q; want %q", i, g, e)
     68 		}
     69 		if g, e := p.FileName(), test[2]; g != e {
     70 			t.Errorf("test %d: FileName() = %q; want %q", i, g, e)
     71 		}
     72 	}
     73 }
     74 
     75 var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8)
     76 
     77 func testMultipartBody(sep string) string {
     78 	testBody := `
     79 This is a multi-part message.  This line is ignored.
     80 --MyBoundary
     81 Header1: value1
     82 HEADER2: value2
     83 foo-bar: baz
     84 
     85 My value
     86 The end.
     87 --MyBoundary
     88 name: bigsection
     89 
     90 [longline]
     91 --MyBoundary
     92 Header1: value1b
     93 HEADER2: value2b
     94 foo-bar: bazb
     95 
     96 Line 1
     97 Line 2
     98 Line 3 ends in a newline, but just one.
     99 
    100 --MyBoundary
    101 
    102 never read data
    103 --MyBoundary--
    104 
    105 
    106 useless trailer
    107 `
    108 	testBody = strings.Replace(testBody, "\n", sep, -1)
    109 	return strings.Replace(testBody, "[longline]", longLine, 1)
    110 }
    111 
    112 func TestMultipart(t *testing.T) {
    113 	bodyReader := strings.NewReader(testMultipartBody("\r\n"))
    114 	testMultipart(t, bodyReader, false)
    115 }
    116 
    117 func TestMultipartOnlyNewlines(t *testing.T) {
    118 	bodyReader := strings.NewReader(testMultipartBody("\n"))
    119 	testMultipart(t, bodyReader, true)
    120 }
    121 
    122 func TestMultipartSlowInput(t *testing.T) {
    123 	bodyReader := strings.NewReader(testMultipartBody("\r\n"))
    124 	testMultipart(t, &slowReader{bodyReader}, false)
    125 }
    126 
    127 func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) {
    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.Replace(s, "\r\n", "\n", -1)
    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.Replace(testBody, "\n", "\r\n", -1)
    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(ioutil.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 func TestLineContinuation(t *testing.T) {
    327 	// This body, extracted from an email, contains headers that span multiple
    328 	// lines.
    329 
    330 	// TODO: The original mail ended with a double-newline before the
    331 	// final delimiter; this was manually edited to use a CRLF.
    332 	testBody :=
    333 		"\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"
    334 
    335 	r := NewReader(strings.NewReader(testBody), "Apple-Mail-2-292336769")
    336 
    337 	for i := 0; i < 2; i++ {
    338 		part, err := r.NextPart()
    339 		if err != nil {
    340 			t.Fatalf("didn't get a part")
    341 		}
    342 		var buf bytes.Buffer
    343 		n, err := io.Copy(&buf, part)
    344 		if err != nil {
    345 			t.Errorf("error reading part: %v\nread so far: %q", err, buf.String())
    346 		}
    347 		if n <= 0 {
    348 			t.Errorf("read %d bytes; expected >0", n)
    349 		}
    350 	}
    351 }
    352 
    353 func TestQuotedPrintableEncoding(t *testing.T) {
    354 	// From https://golang.org/issue/4411
    355 	body := "--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=text\r\nContent-Transfer-Encoding: quoted-printable\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--"
    356 	r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
    357 	part, err := r.NextPart()
    358 	if err != nil {
    359 		t.Fatal(err)
    360 	}
    361 	if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
    362 		t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
    363 	}
    364 	var buf bytes.Buffer
    365 	_, err = io.Copy(&buf, part)
    366 	if err != nil {
    367 		t.Error(err)
    368 	}
    369 	got := buf.String()
    370 	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"
    371 	if got != want {
    372 		t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
    373 	}
    374 }
    375 
    376 // Test parsing an image attachment from gmail, which previously failed.
    377 func TestNested(t *testing.T) {
    378 	// nested-mime is the body part of a multipart/mixed email
    379 	// with boundary e89a8ff1c1e83553e304be640612
    380 	f, err := os.Open("testdata/nested-mime")
    381 	if err != nil {
    382 		t.Fatal(err)
    383 	}
    384 	defer f.Close()
    385 	mr := NewReader(f, "e89a8ff1c1e83553e304be640612")
    386 	p, err := mr.NextPart()
    387 	if err != nil {
    388 		t.Fatalf("error reading first section (alternative): %v", err)
    389 	}
    390 
    391 	// Read the inner text/plain and text/html sections of the multipart/alternative.
    392 	mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610")
    393 	p, err = mr2.NextPart()
    394 	if err != nil {
    395 		t.Fatalf("reading text/plain part: %v", err)
    396 	}
    397 	if b, err := ioutil.ReadAll(p); string(b) != "*body*\r\n" || err != nil {
    398 		t.Fatalf("reading text/plain part: got %q, %v", b, err)
    399 	}
    400 	p, err = mr2.NextPart()
    401 	if err != nil {
    402 		t.Fatalf("reading text/html part: %v", err)
    403 	}
    404 	if b, err := ioutil.ReadAll(p); string(b) != "<b>body</b>\r\n" || err != nil {
    405 		t.Fatalf("reading text/html part: got %q, %v", b, err)
    406 	}
    407 
    408 	p, err = mr2.NextPart()
    409 	if err != io.EOF {
    410 		t.Fatalf("final inner NextPart = %v; want io.EOF", err)
    411 	}
    412 
    413 	// Back to the outer multipart/mixed, reading the image attachment.
    414 	_, err = mr.NextPart()
    415 	if err != nil {
    416 		t.Fatalf("error reading the image attachment at the end: %v", err)
    417 	}
    418 
    419 	_, err = mr.NextPart()
    420 	if err != io.EOF {
    421 		t.Fatalf("final outer NextPart = %v; want io.EOF", err)
    422 	}
    423 }
    424 
    425 type headerBody struct {
    426 	header textproto.MIMEHeader
    427 	body   string
    428 }
    429 
    430 func formData(key, value string) headerBody {
    431 	return headerBody{
    432 		textproto.MIMEHeader{
    433 			"Content-Type":        {"text/plain; charset=ISO-8859-1"},
    434 			"Content-Disposition": {"form-data; name=" + key},
    435 		},
    436 		value,
    437 	}
    438 }
    439 
    440 type parseTest struct {
    441 	name    string
    442 	in, sep string
    443 	want    []headerBody
    444 }
    445 
    446 var parseTests = []parseTest{
    447 	// Actual body from App Engine on a blob upload. The final part (the
    448 	// Content-Type: message/external-body) is what App Engine replaces
    449 	// the uploaded file with.  The other form fields (prefixed with
    450 	// "other" in their form-data name) are unchanged.  A bug was
    451 	// reported with blob uploads failing when the other fields were
    452 	// empty. This was the MIME POST body that previously failed.
    453 	{
    454 		name: "App Engine post",
    455 		sep:  "00151757727e9583fd04bfbca4c6",
    456 		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--",
    457 		want: []headerBody{
    458 			formData("otherEmpty1", ""),
    459 			formData("otherFoo1", "foo"),
    460 			formData("otherFoo2", "foo"),
    461 			formData("otherEmpty2", ""),
    462 			formData("otherRepeatFoo", "foo"),
    463 			formData("otherRepeatFoo", "foo"),
    464 			formData("otherRepeatEmpty", ""),
    465 			formData("otherRepeatEmpty", ""),
    466 			formData("submit", "Submit"),
    467 			{textproto.MIMEHeader{
    468 				"Content-Type":        {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"},
    469 				"Content-Disposition": {"form-data; name=file; filename=\"fall.png\""},
    470 			}, "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"},
    471 		},
    472 	},
    473 
    474 	// Single empty part, ended with --boundary immediately after headers.
    475 	{
    476 		name: "single empty part, --boundary",
    477 		sep:  "abc",
    478 		in:   "--abc\r\nFoo: bar\r\n\r\n--abc--",
    479 		want: []headerBody{
    480 			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
    481 		},
    482 	},
    483 
    484 	// Single empty part, ended with \r\n--boundary immediately after headers.
    485 	{
    486 		name: "single empty part, \r\n--boundary",
    487 		sep:  "abc",
    488 		in:   "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--",
    489 		want: []headerBody{
    490 			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
    491 		},
    492 	},
    493 
    494 	// Final part empty.
    495 	{
    496 		name: "final part empty",
    497 		sep:  "abc",
    498 		in:   "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--",
    499 		want: []headerBody{
    500 			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
    501 			{textproto.MIMEHeader{"Foo2": {"bar2"}}, ""},
    502 		},
    503 	},
    504 
    505 	// Final part empty with newlines after final separator.
    506 	{
    507 		name: "final part empty then crlf",
    508 		sep:  "abc",
    509 		in:   "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n",
    510 		want: []headerBody{
    511 			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
    512 		},
    513 	},
    514 
    515 	// Final part empty with lwsp-chars after final separator.
    516 	{
    517 		name: "final part empty then lwsp",
    518 		sep:  "abc",
    519 		in:   "--abc\r\nFoo: bar\r\n\r\n--abc-- \t",
    520 		want: []headerBody{
    521 			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
    522 		},
    523 	},
    524 
    525 	// No parts (empty form as submitted by Chrome)
    526 	{
    527 		name: "no parts",
    528 		sep:  "----WebKitFormBoundaryQfEAfzFOiSemeHfA",
    529 		in:   "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n",
    530 		want: []headerBody{},
    531 	},
    532 
    533 	// Part containing data starting with the boundary, but with additional suffix.
    534 	{
    535 		name: "fake separator as data",
    536 		sep:  "sep",
    537 		in:   "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--",
    538 		want: []headerBody{
    539 			{textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"},
    540 		},
    541 	},
    542 
    543 	// Part containing a boundary with whitespace following it.
    544 	{
    545 		name: "boundary with whitespace",
    546 		sep:  "sep",
    547 		in:   "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--",
    548 		want: []headerBody{
    549 			{textproto.MIMEHeader{"Foo": {"bar"}}, "text"},
    550 		},
    551 	},
    552 
    553 	// With ignored leading line.
    554 	{
    555 		name: "leading line",
    556 		sep:  "MyBoundary",
    557 		in: strings.Replace(`This is a multi-part message.  This line is ignored.
    558 --MyBoundary
    559 foo: bar
    560 
    561 
    562 --MyBoundary--`, "\n", "\r\n", -1),
    563 		want: []headerBody{
    564 			{textproto.MIMEHeader{"Foo": {"bar"}}, ""},
    565 		},
    566 	},
    567 
    568 	// Issue 10616; minimal
    569 	{
    570 		name: "issue 10616 minimal",
    571 		sep:  "sep",
    572 		in: "--sep \r\nFoo: bar\r\n\r\n" +
    573 			"a\r\n" +
    574 			"--sep_alt\r\n" +
    575 			"b\r\n" +
    576 			"\r\n--sep--",
    577 		want: []headerBody{
    578 			{textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"},
    579 		},
    580 	},
    581 
    582 	// Issue 10616; full example from bug.
    583 	{
    584 		name: "nested separator prefix is outer separator",
    585 		sep:  "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9",
    586 		in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9
    587 Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"
    588 
    589 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
    590 Content-Type: text/plain; charset="utf-8"
    591 Content-Transfer-Encoding: 8bit
    592 
    593 This is a multi-part message in MIME format.
    594 
    595 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
    596 Content-Type: text/html; charset="utf-8"
    597 Content-Transfer-Encoding: 8bit
    598 
    599 html things
    600 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--
    601 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1),
    602 		want: []headerBody{
    603 			{textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}},
    604 				strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
    605 Content-Type: text/plain; charset="utf-8"
    606 Content-Transfer-Encoding: 8bit
    607 
    608 This is a multi-part message in MIME format.
    609 
    610 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
    611 Content-Type: text/html; charset="utf-8"
    612 Content-Transfer-Encoding: 8bit
    613 
    614 html things
    615 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1),
    616 			},
    617 		},
    618 	},
    619 
    620 	roundTripParseTest(),
    621 }
    622 
    623 func TestParse(t *testing.T) {
    624 Cases:
    625 	for _, tt := range parseTests {
    626 		r := NewReader(strings.NewReader(tt.in), tt.sep)
    627 		got := []headerBody{}
    628 		for {
    629 			p, err := r.NextPart()
    630 			if err == io.EOF {
    631 				break
    632 			}
    633 			if err != nil {
    634 				t.Errorf("in test %q, NextPart: %v", tt.name, err)
    635 				continue Cases
    636 			}
    637 			pbody, err := ioutil.ReadAll(p)
    638 			if err != nil {
    639 				t.Errorf("in test %q, error reading part: %v", tt.name, err)
    640 				continue Cases
    641 			}
    642 			got = append(got, headerBody{p.Header, string(pbody)})
    643 		}
    644 		if !reflect.DeepEqual(tt.want, got) {
    645 			t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want)
    646 			if len(tt.want) != len(got) {
    647 				t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want))
    648 			} else if len(got) > 1 {
    649 				for pi, wantPart := range tt.want {
    650 					if !reflect.DeepEqual(wantPart, got[pi]) {
    651 						t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart)
    652 					}
    653 				}
    654 			}
    655 		}
    656 	}
    657 }
    658 
    659 func roundTripParseTest() parseTest {
    660 	t := parseTest{
    661 		name: "round trip",
    662 		want: []headerBody{
    663 			formData("empty", ""),
    664 			formData("lf", "\n"),
    665 			formData("cr", "\r"),
    666 			formData("crlf", "\r\n"),
    667 			formData("foo", "bar"),
    668 		},
    669 	}
    670 	var buf bytes.Buffer
    671 	w := NewWriter(&buf)
    672 	for _, p := range t.want {
    673 		pw, err := w.CreatePart(p.header)
    674 		if err != nil {
    675 			panic(err)
    676 		}
    677 		_, err = pw.Write([]byte(p.body))
    678 		if err != nil {
    679 			panic(err)
    680 		}
    681 	}
    682 	w.Close()
    683 	t.in = buf.String()
    684 	t.sep = w.Boundary()
    685 	return t
    686 }
    687