github.com/geraldss/go/src@v0.0.0-20210511222824-ac7d0ebfc235/mime/multipart/multipart_test.go (about) 1 // Copyright 2010 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package multipart 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "io" 12 "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 // 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 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 // done is closed when this reader is read from. 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 // TestMultipartStreamReadahead tests that PartReader does not block 340 // on reading past the end of a part, ensuring that it can be used on 341 // a stream like multipart/x-mixed-replace. See golang.org/issue/15431 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 // This body, extracted from an email, contains headers that span multiple 395 // lines. 396 397 // TODO: The original mail ended with a double-newline before the 398 // final delimiter; this was manually edited to use a CRLF. 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 // From https://golang.org/issue/4411 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 // https://github.com/golang/go/issues/29090 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 // This part is expected to be raw, bypassing the automatic handling 469 // of quoted-printable. 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 // Data is still quoted-printable. 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 // This part is expected to have automatic decoding of quoted-printable. 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 // QP data has been decoded. 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 // Test parsing an image attachment from gmail, which previously failed. 512 func TestNested(t *testing.T) { 513 // nested-mime is the body part of a multipart/mixed email 514 // with boundary e89a8ff1c1e83553e304be640612 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 // Read the inner text/plain and text/html sections of the multipart/alternative. 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 // Back to the outer multipart/mixed, reading the image attachment. 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 // Actual body from App Engine on a blob upload. The final part (the 583 // Content-Type: message/external-body) is what App Engine replaces 584 // the uploaded file with. The other form fields (prefixed with 585 // "other" in their form-data name) are unchanged. A bug was 586 // reported with blob uploads failing when the other fields were 587 // empty. This was the MIME POST body that previously failed. 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 // Single empty part, ended with --boundary immediately after headers. 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 // Single empty part, ended with \r\n--boundary immediately after headers. 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 // Final part empty. 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 // Final part empty with newlines after final separator. 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 // Final part empty with lwsp-chars after final separator. 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 // No parts (empty form as submitted by Chrome) 661 { 662 name: "no parts", 663 sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA", 664 in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n", 665 want: []headerBody{}, 666 }, 667 668 // Part containing data starting with the boundary, but with additional suffix. 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 // Part containing a boundary with whitespace following it. 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 // With ignored leading line. 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 // Issue 10616; minimal 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 // Issue 10616; full example from bug. 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 // Issue 12662: Check that we don't consume the leading \r if the peekBuffer 755 // ends in '\r\n--separator-' 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 // Issue 12662: Same test as above with \r\n at the end 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 // Issue 12662v2: We want to make sure that for short buffers that end with 786 // '\r\n--separator-' we always consume at least one (valid) symbol from the 787 // peekBuffer 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 // Context: https://github.com/camlistore/camlistore/issues/642 803 // If the file contents in the form happens to have a size such as: 804 // size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize) 805 // then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy 806 // cut such as: 807 // "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the 808 // subsequent Read miss the boundary. 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 }