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(strings.Builder)
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 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
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
350
351
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
405
406
407
408
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
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
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
479
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
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
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
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
522 func TestNested(t *testing.T) {
523
524
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
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
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
593
594
595
596
597
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
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
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
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
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
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
671 {
672 name: "no parts",
673 sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA",
674 in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n",
675 want: []headerBody{},
676 },
677
678
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
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
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
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
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
766
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
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
799
800
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
817
818
819
820
821
822
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
850
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
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