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