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
5package multipart
6
7import (
8	"bytes"
9	"encoding/json"
10	"fmt"
11	"io"
12	"net/textproto"
13	"os"
14	"reflect"
15	"strings"
16	"testing"
17)
18
19func 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
38func escapeString(v string) string {
39	bytes, _ := json.Marshal(v)
40	return string(bytes)
41}
42
43func 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
51func 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
74var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8)
75
76func testMultipartBody(sep string) string {
77	testBody := `
78This is a multi-part message.  This line is ignored.
79--MyBoundary
80Header1: value1
81HEADER2: value2
82foo-bar: baz
83
84My value
85The end.
86--MyBoundary
87name: bigsection
88
89[longline]
90--MyBoundary
91Header1: value1b
92HEADER2: value2b
93foo-bar: bazb
94
95Line 1
96Line 2
97Line 3 ends in a newline, but just one.
98
99--MyBoundary
100
101never read data
102--MyBoundary--
103
104
105useless trailer
106`
107	testBody = strings.ReplaceAll(testBody, "\n", sep)
108	return strings.Replace(testBody, "[longline]", longLine, 1)
109}
110
111func TestMultipart(t *testing.T) {
112	bodyReader := strings.NewReader(testMultipartBody("\r\n"))
113	testMultipart(t, bodyReader, false)
114}
115
116func TestMultipartOnlyNewlines(t *testing.T) {
117	bodyReader := strings.NewReader(testMultipartBody("\n"))
118	testMultipart(t, bodyReader, true)
119}
120
121func TestMultipartSlowInput(t *testing.T) {
122	bodyReader := strings.NewReader(testMultipartBody("\r\n"))
123	testMultipart(t, &slowReader{bodyReader}, false)
124}
125
126func 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
215func 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
262type maliciousReader struct {
263	t *testing.T
264	n int
265}
266
267const maxReadThreshold = 1 << 20
268
269func (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
278func 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
293func TestMultipartTruncated(t *testing.T) {
294	for _, body := range []string{
295		`
296This is a multi-part message.  This line is ignored.
297--MyBoundary
298foo-bar: baz
299
300Oh no, premature EOF!
301`,
302		`
303This is a multi-part message.  This line is ignored.
304--MyBoundary
305foo-bar: baz
306
307Oh 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
325type slowReader struct {
326	r io.Reader
327}
328
329func (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
336type sentinelReader struct {
337	// done is closed when this reader is read from.
338	done chan struct{}
339}
340
341func (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
352func TestMultipartStreamReadahead(t *testing.T) {
353	testBody1 := `
354This is a multi-part message.  This line is ignored.
355--MyBoundary
356foo-bar: baz
357
358Body
359--MyBoundary
360`
361	testBody2 := `foo-bar: bop
362
363Body 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
403func 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
430func 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
438func 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
461func TestRawPart(t *testing.T) {
462	// https://github.com/golang/go/issues/29090
463
464	body := strings.Replace(`--0016e68ee29c5d515f04cedf6733
465Content-Type: text/plain; charset="utf-8"
466Content-Transfer-Encoding: quoted-printable
467
468<div dir=3D"ltr">Hello World.</div>
469--0016e68ee29c5d515f04cedf6733
470Content-Type: text/plain; charset="utf-8"
471Content-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.
522func 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
570type headerBody struct {
571	header textproto.MIMEHeader
572	body   string
573}
574
575func 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
585type parseTest struct {
586	name    string
587	in, sep string
588	want    []headerBody
589}
590
591var 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
704foo: 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
732Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"
733
734------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
735Content-Type: text/plain; charset="utf-8"
736Content-Transfer-Encoding: 8bit
737
738This is a multi-part message in MIME format.
739
740------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
741Content-Type: text/html; charset="utf-8"
742Content-Transfer-Encoding: 8bit
743
744html 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
750Content-Type: text/plain; charset="utf-8"
751Content-Transfer-Encoding: 8bit
752
753This is a multi-part message in MIME format.
754
755------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt
756Content-Type: text/html; charset="utf-8"
757Content-Transfer-Encoding: 8bit
758
759html 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
771Content-Disposition: form-data; name="block"; filename="block"
772Content-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
787Content-Disposition: form-data; name="block"; filename="block"
788Content-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
805Content-Disposition: form-data; name="block"; filename="block"
806Content-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
827Content-Disposition: form-data; name="myfile"; filename="my-file.txt"
828Content-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
834Content-Disposition: form-data; name="key"
835
836val
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
855Content-Type: multipart/alternative; boundary="foo-bar"
856
857--foo-bar
858
859Body
860--foo-bar
861
862Body2
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
869Body
870--foo-bar
871
872Body2
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
883Content-Type: multipart/alternative; boundary="foo--"
884
885--foo--
886
887Body
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
898func TestParse(t *testing.T) {
899Cases:
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
934func 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
952func 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
985func 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
1014func 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