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 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.Replace(s, "\r\n", "\n", -1) 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.Replace(testBody, "\n", "\r\n", -1) 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(ioutil.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 func TestLineContinuation(t *testing.T) { 327 // This body, extracted from an email, contains headers that span multiple 328 // lines. 329 330 // TODO: The original mail ended with a double-newline before the 331 // final delimiter; this was manually edited to use a CRLF. 332 testBody := 333 "\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" 334 335 r := NewReader(strings.NewReader(testBody), "Apple-Mail-2-292336769") 336 337 for i := 0; i < 2; i++ { 338 part, err := r.NextPart() 339 if err != nil { 340 t.Fatalf("didn't get a part") 341 } 342 var buf bytes.Buffer 343 n, err := io.Copy(&buf, part) 344 if err != nil { 345 t.Errorf("error reading part: %v\nread so far: %q", err, buf.String()) 346 } 347 if n <= 0 { 348 t.Errorf("read %d bytes; expected >0", n) 349 } 350 } 351 } 352 353 func TestQuotedPrintableEncoding(t *testing.T) { 354 // From https://golang.org/issue/4411 355 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--" 356 r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733") 357 part, err := r.NextPart() 358 if err != nil { 359 t.Fatal(err) 360 } 361 if te, ok := part.Header["Content-Transfer-Encoding"]; ok { 362 t.Errorf("unexpected Content-Transfer-Encoding of %q", te) 363 } 364 var buf bytes.Buffer 365 _, err = io.Copy(&buf, part) 366 if err != nil { 367 t.Error(err) 368 } 369 got := buf.String() 370 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" 371 if got != want { 372 t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want) 373 } 374 } 375 376 // Test parsing an image attachment from gmail, which previously failed. 377 func TestNested(t *testing.T) { 378 // nested-mime is the body part of a multipart/mixed email 379 // with boundary e89a8ff1c1e83553e304be640612 380 f, err := os.Open("testdata/nested-mime") 381 if err != nil { 382 t.Fatal(err) 383 } 384 defer f.Close() 385 mr := NewReader(f, "e89a8ff1c1e83553e304be640612") 386 p, err := mr.NextPart() 387 if err != nil { 388 t.Fatalf("error reading first section (alternative): %v", err) 389 } 390 391 // Read the inner text/plain and text/html sections of the multipart/alternative. 392 mr2 := NewReader(p, "e89a8ff1c1e83553e004be640610") 393 p, err = mr2.NextPart() 394 if err != nil { 395 t.Fatalf("reading text/plain part: %v", err) 396 } 397 if b, err := ioutil.ReadAll(p); string(b) != "*body*\r\n" || err != nil { 398 t.Fatalf("reading text/plain part: got %q, %v", b, err) 399 } 400 p, err = mr2.NextPart() 401 if err != nil { 402 t.Fatalf("reading text/html part: %v", err) 403 } 404 if b, err := ioutil.ReadAll(p); string(b) != "<b>body</b>\r\n" || err != nil { 405 t.Fatalf("reading text/html part: got %q, %v", b, err) 406 } 407 408 p, err = mr2.NextPart() 409 if err != io.EOF { 410 t.Fatalf("final inner NextPart = %v; want io.EOF", err) 411 } 412 413 // Back to the outer multipart/mixed, reading the image attachment. 414 _, err = mr.NextPart() 415 if err != nil { 416 t.Fatalf("error reading the image attachment at the end: %v", err) 417 } 418 419 _, err = mr.NextPart() 420 if err != io.EOF { 421 t.Fatalf("final outer NextPart = %v; want io.EOF", err) 422 } 423 } 424 425 type headerBody struct { 426 header textproto.MIMEHeader 427 body string 428 } 429 430 func formData(key, value string) headerBody { 431 return headerBody{ 432 textproto.MIMEHeader{ 433 "Content-Type": {"text/plain; charset=ISO-8859-1"}, 434 "Content-Disposition": {"form-data; name=" + key}, 435 }, 436 value, 437 } 438 } 439 440 type parseTest struct { 441 name string 442 in, sep string 443 want []headerBody 444 } 445 446 var parseTests = []parseTest{ 447 // Actual body from App Engine on a blob upload. The final part (the 448 // Content-Type: message/external-body) is what App Engine replaces 449 // the uploaded file with. The other form fields (prefixed with 450 // "other" in their form-data name) are unchanged. A bug was 451 // reported with blob uploads failing when the other fields were 452 // empty. This was the MIME POST body that previously failed. 453 { 454 name: "App Engine post", 455 sep: "00151757727e9583fd04bfbca4c6", 456 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--", 457 want: []headerBody{ 458 formData("otherEmpty1", ""), 459 formData("otherFoo1", "foo"), 460 formData("otherFoo2", "foo"), 461 formData("otherEmpty2", ""), 462 formData("otherRepeatFoo", "foo"), 463 formData("otherRepeatFoo", "foo"), 464 formData("otherRepeatEmpty", ""), 465 formData("otherRepeatEmpty", ""), 466 formData("submit", "Submit"), 467 {textproto.MIMEHeader{ 468 "Content-Type": {"message/external-body; charset=ISO-8859-1; blob-key=AHAZQqG84qllx7HUqO_oou5EvdYQNS3Mbbkb0RjjBoM_Kc1UqEN2ygDxWiyCPulIhpHRPx-VbpB6RX4MrsqhWAi_ZxJ48O9P2cTIACbvATHvg7IgbvZytyGMpL7xO1tlIvgwcM47JNfv_tGhy1XwyEUO8oldjPqg5Q"}, 469 "Content-Disposition": {"form-data; name=file; filename=\"fall.png\""}, 470 }, "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"}, 471 }, 472 }, 473 474 // Single empty part, ended with --boundary immediately after headers. 475 { 476 name: "single empty part, --boundary", 477 sep: "abc", 478 in: "--abc\r\nFoo: bar\r\n\r\n--abc--", 479 want: []headerBody{ 480 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 481 }, 482 }, 483 484 // Single empty part, ended with \r\n--boundary immediately after headers. 485 { 486 name: "single empty part, \r\n--boundary", 487 sep: "abc", 488 in: "--abc\r\nFoo: bar\r\n\r\n\r\n--abc--", 489 want: []headerBody{ 490 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 491 }, 492 }, 493 494 // Final part empty. 495 { 496 name: "final part empty", 497 sep: "abc", 498 in: "--abc\r\nFoo: bar\r\n\r\n--abc\r\nFoo2: bar2\r\n\r\n--abc--", 499 want: []headerBody{ 500 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 501 {textproto.MIMEHeader{"Foo2": {"bar2"}}, ""}, 502 }, 503 }, 504 505 // Final part empty with newlines after final separator. 506 { 507 name: "final part empty then crlf", 508 sep: "abc", 509 in: "--abc\r\nFoo: bar\r\n\r\n--abc--\r\n", 510 want: []headerBody{ 511 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 512 }, 513 }, 514 515 // Final part empty with lwsp-chars after final separator. 516 { 517 name: "final part empty then lwsp", 518 sep: "abc", 519 in: "--abc\r\nFoo: bar\r\n\r\n--abc-- \t", 520 want: []headerBody{ 521 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 522 }, 523 }, 524 525 // No parts (empty form as submitted by Chrome) 526 { 527 name: "no parts", 528 sep: "----WebKitFormBoundaryQfEAfzFOiSemeHfA", 529 in: "------WebKitFormBoundaryQfEAfzFOiSemeHfA--\r\n", 530 want: []headerBody{}, 531 }, 532 533 // Part containing data starting with the boundary, but with additional suffix. 534 { 535 name: "fake separator as data", 536 sep: "sep", 537 in: "--sep\r\nFoo: bar\r\n\r\n--sepFAKE\r\n--sep--", 538 want: []headerBody{ 539 {textproto.MIMEHeader{"Foo": {"bar"}}, "--sepFAKE"}, 540 }, 541 }, 542 543 // Part containing a boundary with whitespace following it. 544 { 545 name: "boundary with whitespace", 546 sep: "sep", 547 in: "--sep \r\nFoo: bar\r\n\r\ntext\r\n--sep--", 548 want: []headerBody{ 549 {textproto.MIMEHeader{"Foo": {"bar"}}, "text"}, 550 }, 551 }, 552 553 // With ignored leading line. 554 { 555 name: "leading line", 556 sep: "MyBoundary", 557 in: strings.Replace(`This is a multi-part message. This line is ignored. 558 --MyBoundary 559 foo: bar 560 561 562 --MyBoundary--`, "\n", "\r\n", -1), 563 want: []headerBody{ 564 {textproto.MIMEHeader{"Foo": {"bar"}}, ""}, 565 }, 566 }, 567 568 // Issue 10616; minimal 569 { 570 name: "issue 10616 minimal", 571 sep: "sep", 572 in: "--sep \r\nFoo: bar\r\n\r\n" + 573 "a\r\n" + 574 "--sep_alt\r\n" + 575 "b\r\n" + 576 "\r\n--sep--", 577 want: []headerBody{ 578 {textproto.MIMEHeader{"Foo": {"bar"}}, "a\r\n--sep_alt\r\nb\r\n"}, 579 }, 580 }, 581 582 // Issue 10616; full example from bug. 583 { 584 name: "nested separator prefix is outer separator", 585 sep: "----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9", 586 in: strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9 587 Content-Type: multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt" 588 589 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 590 Content-Type: text/plain; charset="utf-8" 591 Content-Transfer-Encoding: 8bit 592 593 This is a multi-part message in MIME format. 594 595 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 596 Content-Type: text/html; charset="utf-8" 597 Content-Transfer-Encoding: 8bit 598 599 html things 600 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt-- 601 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9--`, "\n", "\r\n", -1), 602 want: []headerBody{ 603 {textproto.MIMEHeader{"Content-Type": {`multipart/alternative; boundary="----=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt"`}}, 604 strings.Replace(`------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 605 Content-Type: text/plain; charset="utf-8" 606 Content-Transfer-Encoding: 8bit 607 608 This is a multi-part message in MIME format. 609 610 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt 611 Content-Type: text/html; charset="utf-8" 612 Content-Transfer-Encoding: 8bit 613 614 html things 615 ------=_NextPart_4c2fbafd7ec4c8bf08034fe724b608d9_alt--`, "\n", "\r\n", -1), 616 }, 617 }, 618 }, 619 620 roundTripParseTest(), 621 } 622 623 func TestParse(t *testing.T) { 624 Cases: 625 for _, tt := range parseTests { 626 r := NewReader(strings.NewReader(tt.in), tt.sep) 627 got := []headerBody{} 628 for { 629 p, err := r.NextPart() 630 if err == io.EOF { 631 break 632 } 633 if err != nil { 634 t.Errorf("in test %q, NextPart: %v", tt.name, err) 635 continue Cases 636 } 637 pbody, err := ioutil.ReadAll(p) 638 if err != nil { 639 t.Errorf("in test %q, error reading part: %v", tt.name, err) 640 continue Cases 641 } 642 got = append(got, headerBody{p.Header, string(pbody)}) 643 } 644 if !reflect.DeepEqual(tt.want, got) { 645 t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want) 646 if len(tt.want) != len(got) { 647 t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want)) 648 } else if len(got) > 1 { 649 for pi, wantPart := range tt.want { 650 if !reflect.DeepEqual(wantPart, got[pi]) { 651 t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart) 652 } 653 } 654 } 655 } 656 } 657 } 658 659 func roundTripParseTest() parseTest { 660 t := parseTest{ 661 name: "round trip", 662 want: []headerBody{ 663 formData("empty", ""), 664 formData("lf", "\n"), 665 formData("cr", "\r"), 666 formData("crlf", "\r\n"), 667 formData("foo", "bar"), 668 }, 669 } 670 var buf bytes.Buffer 671 w := NewWriter(&buf) 672 for _, p := range t.want { 673 pw, err := w.CreatePart(p.header) 674 if err != nil { 675 panic(err) 676 } 677 _, err = pw.Write([]byte(p.body)) 678 if err != nil { 679 panic(err) 680 } 681 } 682 w.Close() 683 t.in = buf.String() 684 t.sep = w.Boundary() 685 return t 686 } 687