github.com/zebozhuang/go@v0.0.0-20200207033046-f8a98f6f5c5d/src/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 "io/ioutil" 13 "net/textproto" 14 "os" 15 "reflect" 16 "strings" 17 "testing" 18 ) 19 20 func TestBoundaryLine(t *testing.T) { 21 mr := NewReader(strings.NewReader(""), "myBoundary") 22 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) { 23 t.Error("expected") 24 } 25 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) { 26 t.Error("expected") 27 } 28 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) { 29 t.Error("expected") 30 } 31 if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus \n")) { 32 t.Error("expected fail") 33 } 34 if mr.isBoundaryDelimiterLine([]byte("--myBoundary bogus--")) { 35 t.Error("expected fail") 36 } 37 } 38 39 func escapeString(v string) string { 40 bytes, _ := json.Marshal(v) 41 return string(bytes) 42 } 43 44 func expectEq(t *testing.T, expected, actual, what string) { 45 if expected == actual { 46 return 47 } 48 t.Errorf("Unexpected value for %s; got %s (len %d) but expected: %s (len %d)", 49 what, escapeString(actual), len(actual), escapeString(expected), len(expected)) 50 } 51 52 func TestNameAccessors(t *testing.T) { 53 tests := [...][3]string{ 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 ; name=foo`, "foo", ""}, 60 {` FORM-DATA ; filename="foo.txt"; name=foo; baz=quux`, "foo", "foo.txt"}, 61 {` not-form-data ; filename="bar.txt"; name=foo; baz=quux`, "", "bar.txt"}, 62 } 63 for i, test := range tests { 64 p := &Part{Header: make(map[string][]string)} 65 p.Header.Set("Content-Disposition", test[0]) 66 if g, e := p.FormName(), test[1]; g != e { 67 t.Errorf("test %d: FormName() = %q; want %q", i, g, e) 68 } 69 if g, e := p.FileName(), test[2]; g != e { 70 t.Errorf("test %d: FileName() = %q; want %q", i, g, e) 71 } 72 } 73 } 74 75 var longLine = strings.Repeat("\n\n\r\r\r\n\r\000", (1<<20)/8) 76 77 func testMultipartBody(sep string) string { 78 testBody := ` 79 This is a multi-part message. This line is ignored. 80 --MyBoundary 81 Header1: value1 82 HEADER2: value2 83 foo-bar: baz 84 85 My value 86 The end. 87 --MyBoundary 88 name: bigsection 89 90 [longline] 91 --MyBoundary 92 Header1: value1b 93 HEADER2: value2b 94 foo-bar: bazb 95 96 Line 1 97 Line 2 98 Line 3 ends in a newline, but just one. 99 100 --MyBoundary 101 102 never read data 103 --MyBoundary-- 104 105 106 useless trailer 107 ` 108 testBody = strings.Replace(testBody, "\n", sep, -1) 109 return strings.Replace(testBody, "[longline]", longLine, 1) 110 } 111 112 func TestMultipart(t *testing.T) { 113 bodyReader := strings.NewReader(testMultipartBody("\r\n")) 114 testMultipart(t, bodyReader, false) 115 } 116 117 func TestMultipartOnlyNewlines(t *testing.T) { 118 bodyReader := strings.NewReader(testMultipartBody("\n")) 119 testMultipart(t, bodyReader, true) 120 } 121 122 func TestMultipartSlowInput(t *testing.T) { 123 bodyReader := strings.NewReader(testMultipartBody("\r\n")) 124 testMultipart(t, &slowReader{bodyReader}, false) 125 } 126 127 func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) { 128 t.Parallel() 129 reader := NewReader(r, "MyBoundary") 130 buf := new(bytes.Buffer) 131 132 // Part1 133 part, err := reader.NextPart() 134 if part == nil || err != nil { 135 t.Error("Expected part1") 136 return 137 } 138 if x := part.Header.Get("Header1"); x != "value1" { 139 t.Errorf("part.Header.Get(%q) = %q, want %q", "Header1", x, "value1") 140 } 141 if x := part.Header.Get("foo-bar"); x != "baz" { 142 t.Errorf("part.Header.Get(%q) = %q, want %q", "foo-bar", x, "baz") 143 } 144 if x := part.Header.Get("Foo-Bar"); x != "baz" { 145 t.Errorf("part.Header.Get(%q) = %q, want %q", "Foo-Bar", x, "baz") 146 } 147 buf.Reset() 148 if _, err := io.Copy(buf, part); err != nil { 149 t.Errorf("part 1 copy: %v", err) 150 } 151 152 adjustNewlines := func(s string) string { 153 if onlyNewlines { 154 return strings.Replace(s, "\r\n", "\n", -1) 155 } 156 return s 157 } 158 159 expectEq(t, adjustNewlines("My value\r\nThe end."), buf.String(), "Value of first part") 160 161 // Part2 162 part, err = reader.NextPart() 163 if err != nil { 164 t.Fatalf("Expected part2; got: %v", err) 165 return 166 } 167 if e, g := "bigsection", part.Header.Get("name"); e != g { 168 t.Errorf("part2's name header: expected %q, got %q", e, g) 169 } 170 buf.Reset() 171 if _, err := io.Copy(buf, part); err != nil { 172 t.Errorf("part 2 copy: %v", err) 173 } 174 s := buf.String() 175 if len(s) != len(longLine) { 176 t.Errorf("part2 body expected long line of length %d; got length %d", 177 len(longLine), len(s)) 178 } 179 if s != longLine { 180 t.Errorf("part2 long body didn't match") 181 } 182 183 // Part3 184 part, err = reader.NextPart() 185 if part == nil || err != nil { 186 t.Error("Expected part3") 187 return 188 } 189 if part.Header.Get("foo-bar") != "bazb" { 190 t.Error("Expected foo-bar: bazb") 191 } 192 buf.Reset() 193 if _, err := io.Copy(buf, part); err != nil { 194 t.Errorf("part 3 copy: %v", err) 195 } 196 expectEq(t, adjustNewlines("Line 1\r\nLine 2\r\nLine 3 ends in a newline, but just one.\r\n"), 197 buf.String(), "body of part 3") 198 199 // Part4 200 part, err = reader.NextPart() 201 if part == nil || err != nil { 202 t.Error("Expected part 4 without errors") 203 return 204 } 205 206 // Non-existent part5 207 part, err = reader.NextPart() 208 if part != nil { 209 t.Error("Didn't expect a fifth part.") 210 } 211 if err != io.EOF { 212 t.Errorf("On fifth part expected io.EOF; got %v", err) 213 } 214 } 215 216 func TestVariousTextLineEndings(t *testing.T) { 217 tests := [...]string{ 218 "Foo\nBar", 219 "Foo\nBar\n", 220 "Foo\r\nBar", 221 "Foo\r\nBar\r\n", 222 "Foo\rBar", 223 "Foo\rBar\r", 224 "\x00\x01\x02\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10", 225 } 226 227 for testNum, expectedBody := range tests { 228 body := "--BOUNDARY\r\n" + 229 "Content-Disposition: form-data; name=\"value\"\r\n" + 230 "\r\n" + 231 expectedBody + 232 "\r\n--BOUNDARY--\r\n" 233 bodyReader := strings.NewReader(body) 234 235 reader := NewReader(bodyReader, "BOUNDARY") 236 buf := new(bytes.Buffer) 237 part, err := reader.NextPart() 238 if part == nil { 239 t.Errorf("Expected a body part on text %d", testNum) 240 continue 241 } 242 if err != nil { 243 t.Errorf("Unexpected error on text %d: %v", testNum, err) 244 continue 245 } 246 written, err := io.Copy(buf, part) 247 expectEq(t, expectedBody, buf.String(), fmt.Sprintf("test %d", testNum)) 248 if err != nil { 249 t.Errorf("Error copying multipart; bytes=%v, error=%v", written, err) 250 } 251 252 part, err = reader.NextPart() 253 if part != nil { 254 t.Errorf("Unexpected part in test %d", testNum) 255 } 256 if err != io.EOF { 257 t.Errorf("On test %d expected io.EOF; got %v", testNum, err) 258 } 259 260 } 261 } 262 263 type maliciousReader struct { 264 t *testing.T 265 n int 266 } 267 268 const maxReadThreshold = 1 << 20 269 270 func (mr *maliciousReader) Read(b []byte) (n int, err error) { 271 mr.n += len(b) 272 if mr.n >= maxReadThreshold { 273 mr.t.Fatal("too much was read") 274 return 0, io.EOF 275 } 276 return len(b), nil 277 } 278 279 func TestLineLimit(t *testing.T) { 280 mr := &maliciousReader{t: t} 281 r := NewReader(mr, "fooBoundary") 282 part, err := r.NextPart() 283 if part != nil { 284 t.Errorf("unexpected part read") 285 } 286 if err == nil { 287 t.Errorf("expected an error") 288 } 289 if mr.n >= maxReadThreshold { 290 t.Errorf("expected to read < %d bytes; read %d", maxReadThreshold, mr.n) 291 } 292 } 293 294 func TestMultipartTruncated(t *testing.T) { 295 testBody := ` 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 body := strings.Replace(testBody, "\n", "\r\n", -1) 303 bodyReader := strings.NewReader(body) 304 r := NewReader(bodyReader, "MyBoundary") 305 306 part, err := r.NextPart() 307 if err != nil { 308 t.Fatalf("didn't get a part") 309 } 310 _, err = io.Copy(ioutil.Discard, part) 311 if err != io.ErrUnexpectedEOF { 312 t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err) 313 } 314 } 315 316 type slowReader struct { 317 r io.Reader 318 } 319 320 func (s *slowReader) Read(p []byte) (int, error) { 321 if len(p) == 0 { 322 return s.r.Read(p) 323 } 324 return s.r.Read(p[:1]) 325 } 326 327 type sentinelReader struct { 328 // done is closed when this reader is read from. 329 done chan struct{} 330 } 331 332 func (s *sentinelReader) Read([]byte) (int, error) { 333 if s.done != nil { 334 close(s.done) 335 s.done = nil 336 } 337 return 0, io.EOF 338 } 339 340 // TestMultipartStreamReadahead tests that PartReader does not block 341 // on reading past the end of a part, ensuring that it can be used on 342 // a stream like multipart/x-mixed-replace. See golang.org/issue/15431 343 func TestMultipartStreamReadahead(t *testing.T) { 344 testBody1 := ` 345 This is a multi-part message. This line is ignored. 346 --MyBoundary 347 foo-bar: baz 348 349 Body 350 --MyBoundary 351 ` 352 testBody2 := `foo-bar: bop 353 354 Body 2 355 --MyBoundary-- 356 ` 357 done1 := make(chan struct{}) 358 reader := NewReader( 359 io.MultiReader( 360 strings.NewReader(testBody1), 361 &sentinelReader{done1}, 362 strings.NewReader(testBody2)), 363 "MyBoundary") 364 365 var i int 366 readPart := func(hdr textproto.MIMEHeader, body string) { 367 part, err := reader.NextPart() 368 if part == nil || err != nil { 369 t.Fatalf("Part %d: NextPart failed: %v", i, err) 370 } 371 372 if !reflect.DeepEqual(part.Header, hdr) { 373 t.Errorf("Part %d: part.Header = %v, want %v", i, part.Header, hdr) 374 } 375 data, err := ioutil.ReadAll(part) 376 expectEq(t, body, string(data), fmt.Sprintf("Part %d body", i)) 377 if err != nil { 378 t.Fatalf("Part %d: ReadAll failed: %v", i, err) 379 } 380 i++ 381 } 382 383 readPart(textproto.MIMEHeader{"Foo-Bar": {"baz"}}, "Body") 384 385 select { 386 case <-done1: 387 t.Errorf("Reader read past second boundary") 388 default: 389 } 390 391 readPart(textproto.MIMEHeader{"Foo-Bar": {"bop"}}, "Body 2") 392 } 393 394 func TestLineContinuation(t *testing.T) { 395 // This body, extracted from an email, contains headers that span multiple 396 // lines. 397 398 // TODO: The original mail ended with a double-newline before the 399 // final delimiter; this was manually edited to use a CRLF. 400 testBody := 401 "\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" 402 403 r := NewReader(strings.NewReader(testBody), "Apple-Mail-2-292336769") 404 405 for i := 0; i < 2; i++ { 406 part, err := r.NextPart() 407 if err != nil { 408 t.Fatalf("didn't get a part") 409 } 410 var buf bytes.Buffer 411 n, err := io.Copy(&buf, part) 412 if err != nil { 413 t.Errorf("error reading part: %v\nread so far: %q", err, buf.String()) 414 } 415 if n <= 0 { 416 t.Errorf("read %d bytes; expected >0", n) 417 } 418 } 419 } 420 421 func TestQuotedPrintableEncoding(t *testing.T) { 422 // From https://golang.org/issue/4411 423 body := "--0016e68ee29c5d515f04cedf6733\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Disposition: form-data; name=text\r\nContent-Transfer-Encoding: quoted-printable\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--" 424 r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733") 425 part, err := r.NextPart() 426 if err != nil { 427 t.Fatal(err) 428 } 429 if te, ok := part.Header["Content-Transfer-Encoding"]; ok { 430 t.Errorf("unexpected Content-Transfer-Encoding of %q", te) 431 } 432 var buf bytes.Buffer 433 _, err = io.Copy(&buf, part) 434 if err != nil { 435 t.Error(err) 436 } 437 got := buf.String() 438 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" 439 if got != want { 440 t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want) 441 } 442 } 443 444 // Test parsing an image attachment from gmail, which previously failed. 445 func TestNested(t *testing.T) { 446 // nested-mime is the body part of a multipart/mixed email 447 // with boundary e89a8ff1c1e83553e304be640612 448 f, err := os.Open("testdata/nested-mime") 449 if err != nil { 450 t.Fatal(err) 451 } 452 defer f.Close() 453 mr := NewReader(f, "e89a8ff1c1e83553e304be640612") 454 p, err := mr.NextPart() 455 if err != nil { 456 t.Fatalf("error reading first section (alternative): %v", err) 457 } 458 459 // Read the inner text/plain and text/html sections of the multipart/alternative. 460 mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610") 461 p, err = mr2.NextPart() 462 if err != nil { 463 t.Fatalf("reading text/plain part: %v", err) 464 } 465 if b, err := ioutil.ReadAll(p); string(b) != "*body*\r\n" || err != nil { 466 t.Fatalf("reading text/plain part: got %q, %v", b, err) 467 } 468 p, err = mr2.NextPart() 469 if err != nil { 470 t.Fatalf("reading text/html part: %v", err) 471 } 472 if b, err := ioutil.ReadAll(p); string(b) != "<b>body</b>\r\n" || err != nil { 473 t.Fatalf("reading text/html part: got %q, %v", b, err) 474 } 475 476 p, err = mr2.NextPart() 477 if err != io.EOF { 478 t.Fatalf("final inner NextPart = %v; want io.EOF", err) 479 } 480 481 // Back to the outer multipart/mixed, reading the image attachment. 482 _, err = mr.NextPart() 483 if err != nil { 484 t.Fatalf("error reading the image attachment at the end: %v", err) 485 } 486 487 _, err = mr.NextPart() 488 if err != io.EOF { 489 t.Fatalf("final outer NextPart = %v; want io.EOF", err) 490 } 491 } 492 493 type headerBody struct { 494 header textproto.MIMEHeader 495 body string 496 } 497 498 func formData(key, value string) headerBody { 499 return headerBody{ 500 textproto.MIMEHeader{ 501 "Content-Type": {"text/plain; charset=ISO-8859-1"}, 502 "Content-Disposition": {"form-data; name=" + key}, 503 }, 504 value, 505 } 506 } 507 508 type parseTest struct { 509 name string 510 in, sep string 511 want []headerBody 512 } 513 514 var parseTests = []parseTest{ 515 // Actual body from App Engine on a blob upload. The final part (the 516 // Content-Type: message/external-body) is what App Engine replaces 517 // the uploaded file with. The other form fields (prefixed with 518 // "other" in their form-data name) are unchanged. A bug was 519 // reported with blob uploads failing when the other fields were 520 // empty. This was the MIME POST body that previously failed. 521 { 522 name: "App Engine post", 523 sep: "00151757727e9583fd04bfbca4c6", 524 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--", 525 want: []headerBody{ 526 formData("otherEmpty1", ""), 527 formData("otherFoo1", "foo"), 528 formData("otherFoo2", "foo"), 529 formData("otherEmpty2", ""), 530 formData("otherRepeatFoo", "foo"), 531 formData("otherRepeatFoo", "foo"), 532 formData("otherRepeatEmpty", ""), 533 formData("otherRepeatEmpty", ""), 534 formData("submit", "Submit"), 535 {textproto.MIMEHeader{ 536 "Content-Type": {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"}, 537 "Content-Disposition": {"form-data; name=file; filename=\"fall.png\""}, 538 }, "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"}, 539 }, 540 }, 541 542 // Single empty part, ended with --boundary immediately after headers. 543 { 544 name: "single empty part, --boundary", 545 sep: "abc", 546 in: "--abc\r\nFoo: bar\r\n\r\n--abc--", 547 want: []headerBody{ 548 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 549 }, 550 }, 551 552 // Single empty part, ended with \r\n--boundary immediately after headers. 553 { 554 name: "single empty part, \r\n--boundary", 555 sep: "abc", 556 in: "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--", 557 want: []headerBody{ 558 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 559 }, 560 }, 561 562 // Final part empty. 563 { 564 name: "final part empty", 565 sep: "abc", 566 in: "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--", 567 want: []headerBody{ 568 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 569 {textproto.MIMEHeader{"Foo2": {"bar2"}}, ""}, 570 }, 571 }, 572 573 // Final part empty with newlines after final separator. 574 { 575 name: "final part empty then crlf", 576 sep: "abc", 577 in: "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n", 578 want: []headerBody{ 579 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 580 }, 581 }, 582 583 // Final part empty with lwsp-chars after final separator. 584 { 585 name: "final part empty then lwsp", 586 sep: "abc", 587 in: "--abc\r\nFoo: bar\r\n\r\n--abc-- \t", 588 want: []headerBody{ 589 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 590 }, 591 }, 592 593 // No parts (empty form as submitted by Chrome) 594 { 595 name: "no parts", 596 sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA", 597 in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n", 598 want: []headerBody{}, 599 }, 600 601 // Part containing data starting with the boundary, but with additional suffix. 602 { 603 name: "fake separator as data", 604 sep: "sep", 605 in: "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--", 606 want: []headerBody{ 607 {textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"}, 608 }, 609 }, 610 611 // Part containing a boundary with whitespace following it. 612 { 613 name: "boundary with whitespace", 614 sep: "sep", 615 in: "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--", 616 want: []headerBody{ 617 {textproto.MIMEHeader{"Foo": {"bar"}}, "text"}, 618 }, 619 }, 620 621 // With ignored leading line. 622 { 623 name: "leading line", 624 sep: "MyBoundary", 625 in: strings.Replace(`This is a multi-part message. This line is ignored. 626 --MyBoundary 627 foo: bar 628 629 630 --MyBoundary--`, "\n", "\r\n", -1), 631 want: []headerBody{ 632 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 633 }, 634 }, 635 636 // Issue 10616; minimal 637 { 638 name: "issue 10616 minimal", 639 sep: "sep", 640 in: "--sep \r\nFoo: bar\r\n\r\n" + 641 "a\r\n" + 642 "--sep_alt\r\n" + 643 "b\r\n" + 644 "\r\n--sep--", 645 want: []headerBody{ 646 {textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"}, 647 }, 648 }, 649 650 // Issue 10616; full example from bug. 651 { 652 name: "nested separator prefix is outer separator", 653 sep: "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9", 654 in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9 655 Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt" 656 657 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 658 Content-Type: text/plain; charset="utf-8" 659 Content-Transfer-Encoding: 8bit 660 661 This is a multi-part message in MIME format. 662 663 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 664 Content-Type: text/html; charset="utf-8" 665 Content-Transfer-Encoding: 8bit 666 667 html things 668 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt-- 669 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1), 670 want: []headerBody{ 671 {textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}}, 672 strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 673 Content-Type: text/plain; charset="utf-8" 674 Content-Transfer-Encoding: 8bit 675 676 This is a multi-part message in MIME format. 677 678 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 679 Content-Type: text/html; charset="utf-8" 680 Content-Transfer-Encoding: 8bit 681 682 html things 683 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1), 684 }, 685 }, 686 }, 687 // Issue 12662: Check that we don't consume the leading \r if the peekBuffer 688 // ends in '\r\n--separator-' 689 { 690 name: "peek buffer boundary condition", 691 sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", 692 in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db 693 Content-Disposition: form-data; name="block"; filename="block" 694 Content-Type: application/octet-stream 695 696 `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1), 697 want: []headerBody{ 698 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, 699 strings.Repeat("A", peekBufferSize-65), 700 }, 701 }, 702 }, 703 // Issue 12662: Same test as above with \r\n at the end 704 { 705 name: "peek buffer boundary condition", 706 sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", 707 in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db 708 Content-Disposition: form-data; name="block"; filename="block" 709 Content-Type: application/octet-stream 710 711 `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--\n", "\n", "\r\n", -1), 712 want: []headerBody{ 713 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, 714 strings.Repeat("A", peekBufferSize-65), 715 }, 716 }, 717 }, 718 // Issue 12662v2: We want to make sure that for short buffers that end with 719 // '\r\n--separator-' we always consume at least one (valid) symbol from the 720 // peekBuffer 721 { 722 name: "peek buffer boundary condition", 723 sep: "aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", 724 in: strings.Replace(`--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db 725 Content-Disposition: form-data; name="block"; filename="block" 726 Content-Type: application/octet-stream 727 728 `+strings.Repeat("A", peekBufferSize)+"\n--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1), 729 want: []headerBody{ 730 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, 731 strings.Repeat("A", peekBufferSize), 732 }, 733 }, 734 }, 735 // Context: https://github.com/camlistore/camlistore/issues/642 736 // If the file contents in the form happens to have a size such as: 737 // size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize) 738 // then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy 739 // cut such as: 740 // "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the 741 // subsequent Read miss the boundary. 742 { 743 name: "safeCount off by one", 744 sep: "08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74", 745 in: strings.Replace(`--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74 746 Content-Disposition: form-data; name="myfile"; filename="my-file.txt" 747 Content-Type: application/octet-stream 748 749 `, "\n", "\r\n", -1) + 750 strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)) + 751 strings.Replace(` 752 --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74 753 Content-Disposition: form-data; name="key" 754 755 val 756 --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74-- 757 `, "\n", "\r\n", -1), 758 want: []headerBody{ 759 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="myfile"; filename="my-file.txt"`}}, 760 strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)), 761 }, 762 {textproto.MIMEHeader{"Content-Disposition": {`form-data; name="key"`}}, 763 "val", 764 }, 765 }, 766 }, 767 768 roundTripParseTest(), 769 } 770 771 func TestParse(t *testing.T) { 772 Cases: 773 for _, tt := range parseTests { 774 r := NewReader(strings.NewReader(tt.in), tt.sep) 775 got := []headerBody{} 776 for { 777 p, err := r.NextPart() 778 if err == io.EOF { 779 break 780 } 781 if err != nil { 782 t.Errorf("in test %q, NextPart: %v", tt.name, err) 783 continue Cases 784 } 785 pbody, err := ioutil.ReadAll(p) 786 if err != nil { 787 t.Errorf("in test %q, error reading part: %v", tt.name, err) 788 continue Cases 789 } 790 got = append(got, headerBody{p.Header, string(pbody)}) 791 } 792 if !reflect.DeepEqual(tt.want, got) { 793 t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want) 794 if len(tt.want) != len(got) { 795 t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want)) 796 } else if len(got) > 1 { 797 for pi, wantPart := range tt.want { 798 if !reflect.DeepEqual(wantPart, got[pi]) { 799 t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart) 800 } 801 } 802 } 803 } 804 } 805 } 806 807 func partsFromReader(r *Reader) ([]headerBody, error) { 808 got := []headerBody{} 809 for { 810 p, err := r.NextPart() 811 if err == io.EOF { 812 return got, nil 813 } 814 if err != nil { 815 return nil, fmt.Errorf("NextPart: %v", err) 816 } 817 pbody, err := ioutil.ReadAll(p) 818 if err != nil { 819 return nil, fmt.Errorf("error reading part: %v", err) 820 } 821 got = append(got, headerBody{p.Header, string(pbody)}) 822 } 823 } 824 825 func TestParseAllSizes(t *testing.T) { 826 t.Parallel() 827 const maxSize = 5 << 10 828 var buf bytes.Buffer 829 body := strings.Repeat("a", maxSize) 830 bodyb := []byte(body) 831 for size := 0; size < maxSize; size++ { 832 buf.Reset() 833 w := NewWriter(&buf) 834 part, _ := w.CreateFormField("f") 835 part.Write(bodyb[:size]) 836 part, _ = w.CreateFormField("key") 837 part.Write([]byte("val")) 838 w.Close() 839 r := NewReader(&buf, w.Boundary()) 840 got, err := partsFromReader(r) 841 if err != nil { 842 t.Errorf("For size %d: %v", size, err) 843 continue 844 } 845 if len(got) != 2 { 846 t.Errorf("For size %d, num parts = %d; want 2", size, len(got)) 847 continue 848 } 849 if got[0].body != body[:size] { 850 t.Errorf("For size %d, got unexpected len %d: %q", size, len(got[0].body), got[0].body) 851 } 852 } 853 } 854 855 func roundTripParseTest() parseTest { 856 t := parseTest{ 857 name: "round trip", 858 want: []headerBody{ 859 formData("empty", ""), 860 formData("lf", "\n"), 861 formData("cr", "\r"), 862 formData("crlf", "\r\n"), 863 formData("foo", "bar"), 864 }, 865 } 866 var buf bytes.Buffer 867 w := NewWriter(&buf) 868 for _, p := range t.want { 869 pw, err := w.CreatePart(p.header) 870 if err != nil { 871 panic(err) 872 } 873 _, err = pw.Write([]byte(p.body)) 874 if err != nil { 875 panic(err) 876 } 877 } 878 w.Close() 879 t.in = buf.String() 880 t.sep = w.Boundary() 881 return t 882 }