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