LEFT | RIGHT |
(no file at all) | |
1 // Copyright 2010 The Go Authors. All rights reserved. | 1 // Copyright 2010 The Go Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style | 2 // Use of this source code is governed by a BSD-style |
3 // license that can be found in the LICENSE file. | 3 // license that can be found in the LICENSE file. |
4 | 4 |
5 package multipart | 5 package multipart |
6 | 6 |
7 import ( | 7 import ( |
8 "bytes" | 8 "bytes" |
9 "encoding/json" | 9 "encoding/json" |
10 "fmt" | 10 "fmt" |
11 "io" | 11 "io" |
12 "io/ioutil" | 12 "io/ioutil" |
| 13 "net/textproto" |
13 "os" | 14 "os" |
| 15 "reflect" |
14 "strings" | 16 "strings" |
15 "testing" | 17 "testing" |
16 ) | 18 ) |
17 | |
18 func TestHorizontalWhitespace(t *testing.T) { | |
19 if !onlyHorizontalWhitespace([]byte(" \t")) { | |
20 t.Error("expected pass") | |
21 } | |
22 if onlyHorizontalWhitespace([]byte("foo bar")) { | |
23 t.Error("expected failure") | |
24 } | |
25 } | |
26 | 19 |
27 func TestBoundaryLine(t *testing.T) { | 20 func TestBoundaryLine(t *testing.T) { |
28 mr := NewReader(strings.NewReader(""), "myBoundary") | 21 mr := NewReader(strings.NewReader(""), "myBoundary") |
29 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) { | 22 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary\r\n")) { |
30 t.Error("expected") | 23 t.Error("expected") |
31 } | 24 } |
32 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) { | 25 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \r\n")) { |
33 t.Error("expected") | 26 t.Error("expected") |
34 } | 27 } |
35 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) { | 28 if !mr.isBoundaryDelimiterLine([]byte("--myBoundary \n")) { |
(...skipping 276 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
312 part, err := r.NextPart() | 305 part, err := r.NextPart() |
313 if err != nil { | 306 if err != nil { |
314 t.Fatalf("didn't get a part") | 307 t.Fatalf("didn't get a part") |
315 } | 308 } |
316 _, err = io.Copy(ioutil.Discard, part) | 309 _, err = io.Copy(ioutil.Discard, part) |
317 if err != io.ErrUnexpectedEOF { | 310 if err != io.ErrUnexpectedEOF { |
318 t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err) | 311 t.Fatalf("expected error io.ErrUnexpectedEOF; got %v", err) |
319 } | 312 } |
320 } | 313 } |
321 | 314 |
322 func TestZeroLengthBody(t *testing.T) { | |
323 testBody := strings.Replace(` | |
324 This is a multi-part message. This line is ignored. | |
325 --MyBoundary | |
326 foo: bar | |
327 | |
328 | |
329 --MyBoundary-- | |
330 `, "\n", "\r\n", -1) | |
331 r := NewReader(strings.NewReader(testBody), "MyBoundary") | |
332 part, err := r.NextPart() | |
333 if err != nil { | |
334 t.Fatalf("didn't get a part") | |
335 } | |
336 n, err := io.Copy(ioutil.Discard, part) | |
337 if err != nil { | |
338 t.Errorf("error reading part: %v", err) | |
339 } | |
340 if n != 0 { | |
341 t.Errorf("read %d bytes; expected 0", n) | |
342 } | |
343 } | |
344 | |
345 type slowReader struct { | 315 type slowReader struct { |
346 r io.Reader | 316 r io.Reader |
347 } | 317 } |
348 | 318 |
349 func (s *slowReader) Read(p []byte) (int, error) { | 319 func (s *slowReader) Read(p []byte) (int, error) { |
350 if len(p) == 0 { | 320 if len(p) == 0 { |
351 return s.r.Read(p) | 321 return s.r.Read(p) |
352 } | 322 } |
353 return s.r.Read(p[:1]) | 323 return s.r.Read(p[:1]) |
354 } | 324 } |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
420 _, err = mr.NextPart() | 390 _, err = mr.NextPart() |
421 if err != nil { | 391 if err != nil { |
422 t.Fatalf("error reading the image attachment at the end: %v", er
r) | 392 t.Fatalf("error reading the image attachment at the end: %v", er
r) |
423 } | 393 } |
424 | 394 |
425 _, err = mr.NextPart() | 395 _, err = mr.NextPart() |
426 if err != io.EOF { | 396 if err != io.EOF { |
427 t.Fatalf("final outer NextPart = %v; want io.EOF", err) | 397 t.Fatalf("final outer NextPart = %v; want io.EOF", err) |
428 } | 398 } |
429 } | 399 } |
| 400 |
| 401 type headerBody struct { |
| 402 header textproto.MIMEHeader |
| 403 body string |
| 404 } |
| 405 |
| 406 func formData(key, value string) headerBody { |
| 407 return headerBody{ |
| 408 textproto.MIMEHeader{ |
| 409 "Content-Type": {"text/plain; charset=ISO-8859-1"
}, |
| 410 "Content-Disposition": {"form-data; name=" + key}, |
| 411 }, |
| 412 value, |
| 413 } |
| 414 } |
| 415 |
| 416 type parseTest struct { |
| 417 name string |
| 418 in, sep string |
| 419 want []headerBody |
| 420 } |
| 421 |
| 422 var parseTests = []parseTest{ |
| 423 // Actual body from App Engine on a blob upload. The final part (the |
| 424 // Content-Type: message/external-body) is what App Engine replaces |
| 425 // the uploaded file with. The other form fields (prefixed with |
| 426 // "other" in their form-data name) are unchanged. A bug was |
| 427 // reported with blob uploads failing when the other fields were |
| 428 // empty. This was the MIME POST body that previously failed. |
| 429 { |
| 430 name: "App Engine post", |
| 431 sep: "00151757727e9583fd04bfbca4c6", |
| 432 in: "--00151757727e9583fd04bfbca4c6\r\nContent-Type: text/plai
n; 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--00151757727e95
83fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Dispos
ition: 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/pl
ain; 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--00151757727e95
83fd04bfbca4c6\r\nContent-Type: text/plain; charset=ISO-8859-1\r\nContent-Dispos
ition: 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: me
ssage/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS
3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7
IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q\r\nContent-Disposition: for
m-data; name=file; filename=\"fall.png\"\r\n\r\nContent-Type: image/png\r\nConte
nt-Length: 232303\r\nX-AppEngine-Upload-Creation: 2012-05-10 23:14:02.715173\r\n
Content-MD5: MzRjODU1ZDZhZGU1NmRlOWEwZmMwMDdlODBmZTA0NzA=\r\nContent-Disposition
: form-data; name=file; filename=\"fall.png\"\r\n\r\n\r\n--00151757727e9583fd04b
fbca4c6--", |
| 433 want: []headerBody{ |
| 434 formData("otherEmpty1", ""), |
| 435 formData("otherFoo1", "foo"), |
| 436 formData("otherFoo2", "foo"), |
| 437 formData("otherEmpty2", ""), |
| 438 formData("otherRepeatFoo", "foo"), |
| 439 formData("otherRepeatFoo", "foo"), |
| 440 formData("otherRepeatEmpty", ""), |
| 441 formData("otherRepeatEmpty", ""), |
| 442 formData("submit", "Submit"), |
| 443 {textproto.MIMEHeader{ |
| 444 "Content-Type": {"message/external-body;
charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN
2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvg
wcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"}, |
| 445 "Content-Disposition": {"form-data; name=file; f
ilename=\"fall.png\""}, |
| 446 }, "Content-Type: image/png\r\nContent-Length: 232303\r\
nX-AppEngine-Upload-Creation: 2012-05-10 23:14:02.715173\r\nContent-MD5: MzRjODU
1ZDZhZGU1NmRlOWEwZmMwMDdlODBmZTA0NzA=\r\nContent-Disposition: form-data; name=fi
le; filename=\"fall.png\"\r\n\r\n"}, |
| 447 }, |
| 448 }, |
| 449 |
| 450 // Single empty part, ended with --boundary immediately after headers. |
| 451 { |
| 452 name: "single empty part, --boundary", |
| 453 sep: "abc", |
| 454 in: "--abc\r\nFoo: bar\r\n\r\n--abc--", |
| 455 want: []headerBody{ |
| 456 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, |
| 457 }, |
| 458 }, |
| 459 |
| 460 // Single empty part, ended with \r\n--boundary immediately after header
s. |
| 461 { |
| 462 name: "single empty part, \r\n--boundary", |
| 463 sep: "abc", |
| 464 in: "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--", |
| 465 want: []headerBody{ |
| 466 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, |
| 467 }, |
| 468 }, |
| 469 |
| 470 // Final part empty. |
| 471 { |
| 472 name: "final part empty", |
| 473 sep: "abc", |
| 474 in: "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc
--", |
| 475 want: []headerBody{ |
| 476 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, |
| 477 {textproto.MIMEHeader{"Foo2": {"bar2"}}, ""}, |
| 478 }, |
| 479 }, |
| 480 |
| 481 // Final part empty with newlines after final separator. |
| 482 { |
| 483 name: "final part empty then crlf", |
| 484 sep: "abc", |
| 485 in: "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n", |
| 486 want: []headerBody{ |
| 487 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, |
| 488 }, |
| 489 }, |
| 490 |
| 491 // Final part empty with lwsp-chars after final separator. |
| 492 { |
| 493 name: "final part empty then lwsp", |
| 494 sep: "abc", |
| 495 in: "--abc\r\nFoo: bar\r\n\r\n--abc-- \t", |
| 496 want: []headerBody{ |
| 497 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, |
| 498 }, |
| 499 }, |
| 500 |
| 501 // No parts (empty form as submitted by Chrome) |
| 502 { |
| 503 name: "no parts", |
| 504 sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA", |
| 505 in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n", |
| 506 want: []headerBody{}, |
| 507 }, |
| 508 |
| 509 // Part containing data starting with the boundary, but with additional
suffix. |
| 510 { |
| 511 name: "fake separator as data", |
| 512 sep: "sep", |
| 513 in: "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--", |
| 514 want: []headerBody{ |
| 515 {textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"}, |
| 516 }, |
| 517 }, |
| 518 |
| 519 // Part containing a boundary with whitespace following it. |
| 520 { |
| 521 name: "boundary with whitespace", |
| 522 sep: "sep", |
| 523 in: "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--", |
| 524 want: []headerBody{ |
| 525 {textproto.MIMEHeader{"Foo": {"bar"}}, "text"}, |
| 526 }, |
| 527 }, |
| 528 |
| 529 // With ignored leading line. |
| 530 { |
| 531 name: "leading line", |
| 532 sep: "MyBoundary", |
| 533 in: strings.Replace(`This is a multi-part message. This line is
ignored. |
| 534 --MyBoundary |
| 535 foo: bar |
| 536 |
| 537 |
| 538 --MyBoundary--`, "\n", "\r\n", -1), |
| 539 want: []headerBody{ |
| 540 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, |
| 541 }, |
| 542 }, |
| 543 |
| 544 roundTripParseTest(), |
| 545 } |
| 546 |
| 547 func TestParse(t *testing.T) { |
| 548 Cases: |
| 549 for _, tt := range parseTests { |
| 550 r := NewReader(strings.NewReader(tt.in), tt.sep) |
| 551 got := []headerBody{} |
| 552 for { |
| 553 p, err := r.NextPart() |
| 554 if err == io.EOF { |
| 555 break |
| 556 } |
| 557 if err != nil { |
| 558 t.Errorf("in test %q, NextPart: %v", tt.name, er
r) |
| 559 continue Cases |
| 560 } |
| 561 pbody, err := ioutil.ReadAll(p) |
| 562 if err != nil { |
| 563 t.Errorf("in test %q, error reading part: %v", t
t.name, err) |
| 564 continue Cases |
| 565 } |
| 566 got = append(got, headerBody{p.Header, string(pbody)}) |
| 567 } |
| 568 if !reflect.DeepEqual(tt.want, got) { |
| 569 t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, t
t.want) |
| 570 if len(tt.want) != len(got) { |
| 571 t.Errorf("test %q: got %d parts, want %d", tt.na
me, len(got), len(tt.want)) |
| 572 } else if len(got) > 1 { |
| 573 for pi, wantPart := range tt.want { |
| 574 if !reflect.DeepEqual(wantPart, got[pi])
{ |
| 575 t.Errorf("test %q, part %d:\n go
t: %v\nwant: %v", tt.name, pi, got[pi], wantPart) |
| 576 } |
| 577 } |
| 578 } |
| 579 } |
| 580 } |
| 581 } |
| 582 |
| 583 func roundTripParseTest() parseTest { |
| 584 t := parseTest{ |
| 585 name: "round trip", |
| 586 want: []headerBody{ |
| 587 formData("empty", ""), |
| 588 formData("lf", "\n"), |
| 589 formData("cr", "\r"), |
| 590 formData("crlf", "\r\n"), |
| 591 formData("foo", "bar"), |
| 592 }, |
| 593 } |
| 594 var buf bytes.Buffer |
| 595 w := NewWriter(&buf) |
| 596 for _, p := range t.want { |
| 597 pw, err := w.CreatePart(p.header) |
| 598 if err != nil { |
| 599 panic(err) |
| 600 } |
| 601 _, err = pw.Write([]byte(p.body)) |
| 602 if err != nil { |
| 603 panic(err) |
| 604 } |
| 605 } |
| 606 w.Close() |
| 607 t.in = buf.String() |
| 608 t.sep = w.Boundary() |
| 609 return t |
| 610 } |
LEFT | RIGHT |