github.com/q45/go@v0.0.0-20151101211701-a4fb8c13db3f/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 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 // Issue 12662: Check that we don't consume the leading \r if the peekBuffer 620 // ends in '\r\n--separator-' 621 { 622 name: "peek buffer boundary condition", 623 sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", 624 in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db 625 Content-Disposition: form-data; name="block"; filename="block" 626 Content-Type: application/octet-stream 627 628 `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1), 629 want: []headerBody{ 630 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, 631 strings.Repeat("A", peekBufferSize-65), 632 }, 633 }, 634 }, 635 // Issue 12662: Same test as above with \r\n at the end 636 { 637 name: "peek buffer boundary condition", 638 sep: "00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", 639 in: strings.Replace(`--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db 640 Content-Disposition: form-data; name="block"; filename="block" 641 Content-Type: application/octet-stream 642 643 `+strings.Repeat("A", peekBufferSize-65)+"\n--00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--\n", "\n", "\r\n", -1), 644 want: []headerBody{ 645 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, 646 strings.Repeat("A", peekBufferSize-65), 647 }, 648 }, 649 }, 650 // Issue 12662v2: We want to make sure that for short buffers that end with 651 // '\r\n--separator-' we always consume at least one (valid) symbol from the 652 // peekBuffer 653 { 654 name: "peek buffer boundary condition", 655 sep: "aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db", 656 in: strings.Replace(`--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db 657 Content-Disposition: form-data; name="block"; filename="block" 658 Content-Type: application/octet-stream 659 660 `+strings.Repeat("A", peekBufferSize)+"\n--aaaaaaaaaa00ffded004d4dd0fdf945fbdef9d9050cfd6a13a821846299b27fc71b9db--", "\n", "\r\n", -1), 661 want: []headerBody{ 662 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="block"; filename="block"`}}, 663 strings.Repeat("A", peekBufferSize), 664 }, 665 }, 666 }, 667 // Context: https://github.com/camlistore/camlistore/issues/642 668 // If the file contents in the form happens to have a size such as: 669 // size = peekBufferSize - (len("\n--") + len(boundary) + len("\r") + 1), (modulo peekBufferSize) 670 // then peekBufferSeparatorIndex was wrongly returning (-1, false), which was leading to an nCopy 671 // cut such as: 672 // "somedata\r| |\n--Boundary\r" (instead of "somedata| |\r\n--Boundary\r"), which was making the 673 // subsequent Read miss the boundary. 674 { 675 name: "safeCount off by one", 676 sep: "08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74", 677 in: strings.Replace(`--08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74 678 Content-Disposition: form-data; name="myfile"; filename="my-file.txt" 679 Content-Type: application/octet-stream 680 681 `, "\n", "\r\n", -1) + 682 strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)) + 683 strings.Replace(` 684 --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74 685 Content-Disposition: form-data; name="key" 686 687 val 688 --08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74-- 689 `, "\n", "\r\n", -1), 690 want: []headerBody{ 691 {textproto.MIMEHeader{"Content-Type": {`application/octet-stream`}, "Content-Disposition": {`form-data; name="myfile"; filename="my-file.txt"`}}, 692 strings.Repeat("A", peekBufferSize-(len("\n--")+len("08b84578eabc563dcba967a945cdf0d9f613864a8f4a716f0e81caa71a74")+len("\r")+1)), 693 }, 694 {textproto.MIMEHeader{"Content-Disposition": {`form-data; name="key"`}}, 695 "val", 696 }, 697 }, 698 }, 699 700 roundTripParseTest(), 701 } 702 703 func TestParse(t *testing.T) { 704 Cases: 705 for _, tt := range parseTests { 706 r := NewReader(strings.NewReader(tt.in), tt.sep) 707 got := []headerBody{} 708 for { 709 p, err := r.NextPart() 710 if err == io.EOF { 711 break 712 } 713 if err != nil { 714 t.Errorf("in test %q, NextPart: %v", tt.name, err) 715 continue Cases 716 } 717 pbody, err := ioutil.ReadAll(p) 718 if err != nil { 719 t.Errorf("in test %q, error reading part: %v", tt.name, err) 720 continue Cases 721 } 722 got = append(got, headerBody{p.Header, string(pbody)}) 723 } 724 if !reflect.DeepEqual(tt.want, got) { 725 t.Errorf("test %q:\n got: %v\nwant: %v", tt.name, got, tt.want) 726 if len(tt.want) != len(got) { 727 t.Errorf("test %q: got %d parts, want %d", tt.name, len(got), len(tt.want)) 728 } else if len(got) > 1 { 729 for pi, wantPart := range tt.want { 730 if !reflect.DeepEqual(wantPart, got[pi]) { 731 t.Errorf("test %q, part %d:\n got: %v\nwant: %v", tt.name, pi, got[pi], wantPart) 732 } 733 } 734 } 735 } 736 } 737 } 738 739 func partsFromReader(r *Reader) ([]headerBody, error) { 740 got := []headerBody{} 741 for { 742 p, err := r.NextPart() 743 if err == io.EOF { 744 return got, nil 745 } 746 if err != nil { 747 return nil, fmt.Errorf("NextPart: %v", err) 748 } 749 pbody, err := ioutil.ReadAll(p) 750 if err != nil { 751 return nil, fmt.Errorf("error reading part: %v", err) 752 } 753 got = append(got, headerBody{p.Header, string(pbody)}) 754 } 755 } 756 757 func TestParseAllSizes(t *testing.T) { 758 const maxSize = 5 << 10 759 var buf bytes.Buffer 760 body := strings.Repeat("a", maxSize) 761 bodyb := []byte(body) 762 for size := 0; size < maxSize; size++ { 763 buf.Reset() 764 w := NewWriter(&buf) 765 part, _ := w.CreateFormField("f") 766 part.Write(bodyb[:size]) 767 part, _ = w.CreateFormField("key") 768 part.Write([]byte("val")) 769 w.Close() 770 r := NewReader(&buf, w.Boundary()) 771 got, err := partsFromReader(r) 772 if err != nil { 773 t.Errorf("For size %d: %v", size, err) 774 continue 775 } 776 if len(got) != 2 { 777 t.Errorf("For size %d, num parts = %d; want 2", size, len(got)) 778 continue 779 } 780 if got[0].body != body[:size] { 781 t.Errorf("For size %d, got unexpected len %d: %q", size, len(got[0].body), got[0].body) 782 } 783 } 784 } 785 786 func roundTripParseTest() parseTest { 787 t := parseTest{ 788 name: "round trip", 789 want: []headerBody{ 790 formData("empty", ""), 791 formData("lf", "\n"), 792 formData("cr", "\r"), 793 formData("crlf", "\r\n"), 794 formData("foo", "bar"), 795 }, 796 } 797 var buf bytes.Buffer 798 w := NewWriter(&buf) 799 for _, p := range t.want { 800 pw, err := w.CreatePart(p.header) 801 if err != nil { 802 panic(err) 803 } 804 _, err = pw.Write([]byte(p.body)) 805 if err != nil { 806 panic(err) 807 } 808 } 809 w.Close() 810 t.in = buf.String() 811 t.sep = w.Boundary() 812 return t 813 }