aboutsummaryrefslogtreecommitdiff
path: root/libgo/go/mime
diff options
context:
space:
mode:
authorIan Lance Taylor <iant@golang.org>2017-01-14 00:05:42 +0000
committerIan Lance Taylor <ian@gcc.gnu.org>2017-01-14 00:05:42 +0000
commitc2047754c300b68c05d65faa8dc2925fe67b71b4 (patch)
treee183ae81a1f48a02945cb6de463a70c5be1b06f6 /libgo/go/mime
parent829afb8f05602bb31c9c597b24df7377fed4f059 (diff)
downloadgcc-c2047754c300b68c05d65faa8dc2925fe67b71b4.zip
gcc-c2047754c300b68c05d65faa8dc2925fe67b71b4.tar.gz
gcc-c2047754c300b68c05d65faa8dc2925fe67b71b4.tar.bz2
libgo: update to Go 1.8 release candidate 1
Compiler changes: * Change map assignment to use mapassign and assign value directly. * Change string iteration to use decoderune, faster for ASCII strings. * Change makeslice to take int, and use makeslice64 for larger values. * Add new noverflow field to hmap struct used for maps. Unresolved problems, to be fixed later: * Commented out test in go/types/sizes_test.go that doesn't compile. * Commented out reflect.TestStructOf test for padding after zero-sized field. Reviewed-on: https://go-review.googlesource.com/35231 gotools/: Updates for Go 1.8rc1. * Makefile.am (go_cmd_go_files): Add bug.go. (s-zdefaultcc): Write defaultPkgConfig. * Makefile.in: Rebuild. From-SVN: r244456
Diffstat (limited to 'libgo/go/mime')
-rw-r--r--libgo/go/mime/mediatype.go37
-rw-r--r--libgo/go/mime/mediatype_test.go14
-rw-r--r--libgo/go/mime/multipart/formdata.go4
-rw-r--r--libgo/go/mime/multipart/multipart.go219
-rw-r--r--libgo/go/mime/multipart/multipart_test.go69
-rw-r--r--libgo/go/mime/quotedprintable/example_test.go39
-rw-r--r--libgo/go/mime/quotedprintable/reader.go13
-rw-r--r--libgo/go/mime/quotedprintable/reader_test.go16
8 files changed, 271 insertions, 140 deletions
diff --git a/libgo/go/mime/mediatype.go b/libgo/go/mime/mediatype.go
index 1845401..75cc903 100644
--- a/libgo/go/mime/mediatype.go
+++ b/libgo/go/mime/mediatype.go
@@ -248,24 +248,33 @@ func consumeValue(v string) (value, rest string) {
}
// parse a quoted-string
- rest = v[1:] // consume the leading quote
buffer := new(bytes.Buffer)
- var nextIsLiteral bool
- for idx, r := range rest {
- switch {
- case nextIsLiteral:
- buffer.WriteRune(r)
- nextIsLiteral = false
- case r == '"':
- return buffer.String(), rest[idx+1:]
- case r == '\\':
- nextIsLiteral = true
- case r != '\r' && r != '\n':
- buffer.WriteRune(r)
- default:
+ for i := 1; i < len(v); i++ {
+ r := v[i]
+ if r == '"' {
+ return buffer.String(), v[i+1:]
+ }
+ // When MSIE sends a full file path (in "intranet mode"), it does not
+ // escape backslashes: "C:\dev\go\foo.txt", not "C:\\dev\\go\\foo.txt".
+ //
+ // No known MIME generators emit unnecessary backslash escapes
+ // for simple token characters like numbers and letters.
+ //
+ // If we see an unnecessary backslash escape, assume it is from MSIE
+ // and intended as a literal backslash. This makes Go servers deal better
+ // with MSIE without affecting the way they handle conforming MIME
+ // generators.
+ if r == '\\' && i+1 < len(v) && !isTokenChar(rune(v[i+1])) {
+ buffer.WriteByte(v[i+1])
+ i++
+ continue
+ }
+ if r == '\r' || r == '\n' {
return "", v
}
+ buffer.WriteByte(v[i])
}
+ // Did not find end quote.
return "", v
}
diff --git a/libgo/go/mime/mediatype_test.go b/libgo/go/mime/mediatype_test.go
index 9afa558..c5fc906 100644
--- a/libgo/go/mime/mediatype_test.go
+++ b/libgo/go/mime/mediatype_test.go
@@ -138,10 +138,11 @@ func TestParseMediaType(t *testing.T) {
m("title", "This is even more ***fun*** isn't it!")},
// Tests from http://greenbytes.de/tech/tc2231/
+ // Note: Backslash escape handling is a bit loose, like MSIE.
// TODO(bradfitz): add the rest of the tests from that site.
{`attachment; filename="f\oo.html"`,
"attachment",
- m("filename", "foo.html")},
+ m("filename", "f\\oo.html")},
{`attachment; filename="\"quoting\" tested.html"`,
"attachment",
m("filename", `"quoting" tested.html`)},
@@ -165,7 +166,7 @@ func TestParseMediaType(t *testing.T) {
m("filename", "foo-%41.html")},
{`attachment; filename="foo-%\41.html"`,
"attachment",
- m("filename", "foo-%41.html")},
+ m("filename", "foo-%\\41.html")},
{`filename=foo.html`,
"", m()},
{`x=y; filename=foo.html`,
@@ -220,18 +221,21 @@ func TestParseMediaType(t *testing.T) {
// Empty string used to be mishandled.
{`foo; bar=""`, "foo", m("bar", "")},
+
+ // Microsoft browers in intranet mode do not think they need to escape \ in file name.
+ {`form-data; name="file"; filename="C:\dev\go\robots.txt"`, "form-data", m("name", "file", "filename", `C:\dev\go\robots.txt`)},
}
for _, test := range tests {
mt, params, err := ParseMediaType(test.in)
if err != nil {
if test.t != "" {
- t.Errorf("for input %q, unexpected error: %v", test.in, err)
+ t.Errorf("for input %#q, unexpected error: %v", test.in, err)
continue
}
continue
}
if g, e := mt, test.t; g != e {
- t.Errorf("for input %q, expected type %q, got %q",
+ t.Errorf("for input %#q, expected type %q, got %q",
test.in, e, g)
continue
}
@@ -239,7 +243,7 @@ func TestParseMediaType(t *testing.T) {
continue
}
if !reflect.DeepEqual(params, test.p) {
- t.Errorf("for input %q, wrong params.\n"+
+ t.Errorf("for input %#q, wrong params.\n"+
"expected: %#v\n"+
" got: %#v",
test.in, test.p, params)
diff --git a/libgo/go/mime/multipart/formdata.go b/libgo/go/mime/multipart/formdata.go
index 8085bd3..c9e3188 100644
--- a/libgo/go/mime/multipart/formdata.go
+++ b/libgo/go/mime/multipart/formdata.go
@@ -79,8 +79,10 @@ func (r *Reader) readForm(maxMemory int64) (_ *Form, err error) {
if err != nil {
return nil, err
}
- defer file.Close()
_, err = io.Copy(file, io.MultiReader(&b, p))
+ if cerr := file.Close(); err == nil {
+ err = cerr
+ }
if err != nil {
os.Remove(file.Name())
return nil, err
diff --git a/libgo/go/mime/multipart/multipart.go b/libgo/go/mime/multipart/multipart.go
index 205348c..1954808 100644
--- a/libgo/go/mime/multipart/multipart.go
+++ b/libgo/go/mime/multipart/multipart.go
@@ -42,9 +42,7 @@ type Part struct {
// during Read calls.
Header textproto.MIMEHeader
- buffer *bytes.Buffer
- mr *Reader
- bytesRead int
+ mr *Reader
disposition string
dispositionParams map[string]string
@@ -53,6 +51,11 @@ type Part struct {
// wrapper around such a reader, decoding the
// Content-Transfer-Encoding
r io.Reader
+
+ n int // known data bytes waiting in mr.bufReader
+ total int64 // total data bytes read already
+ err error // error to return when n == 0
+ readErr error // read error observed from mr.bufReader
}
// FormName returns the name parameter if p has a Content-Disposition
@@ -126,7 +129,6 @@ func newPart(mr *Reader) (*Part, error) {
bp := &Part{
Header: make(map[string][]string),
mr: mr,
- buffer: new(bytes.Buffer),
}
if err := bp.populateHeaders(); err != nil {
return nil, err
@@ -161,65 +163,118 @@ type partReader struct {
p *Part
}
-func (pr partReader) Read(d []byte) (n int, err error) {
+func (pr partReader) Read(d []byte) (int, error) {
p := pr.p
- defer func() {
- p.bytesRead += n
- }()
- if p.buffer.Len() >= len(d) {
- // Internal buffer of unconsumed data is large enough for
- // the read request. No need to parse more at the moment.
- return p.buffer.Read(d)
+ br := p.mr.bufReader
+
+ // Read into buffer until we identify some data to return,
+ // or we find a reason to stop (boundary or read error).
+ for p.n == 0 && p.err == nil {
+ peek, _ := br.Peek(br.Buffered())
+ p.n, p.err = scanUntilBoundary(peek, p.mr.dashBoundary, p.mr.nlDashBoundary, p.total, p.readErr)
+ if p.n == 0 && p.err == nil {
+ // Force buffered I/O to read more into buffer.
+ _, p.readErr = br.Peek(len(peek) + 1)
+ if p.readErr == io.EOF {
+ p.readErr = io.ErrUnexpectedEOF
+ }
+ }
}
- peek, err := p.mr.bufReader.Peek(peekBufferSize) // TODO(bradfitz): add buffer size accessor
-
- // Look for an immediate empty part without a leading \r\n
- // before the boundary separator. Some MIME code makes empty
- // parts like this. Most browsers, however, write the \r\n
- // before the subsequent boundary even for empty parts and
- // won't hit this path.
- if p.bytesRead == 0 && p.mr.peekBufferIsEmptyPart(peek) {
- return 0, io.EOF
+
+ // Read out from "data to return" part of buffer.
+ if p.n == 0 {
+ return 0, p.err
}
- unexpectedEOF := err == io.EOF
- if err != nil && !unexpectedEOF {
- return 0, fmt.Errorf("multipart: Part Read: %v", err)
+ n := len(d)
+ if n > p.n {
+ n = p.n
}
- if peek == nil {
- panic("nil peek buf")
+ n, _ = br.Read(d[:n])
+ p.total += int64(n)
+ p.n -= n
+ if p.n == 0 {
+ return n, p.err
}
- // Search the peek buffer for "\r\n--boundary". If found,
- // consume everything up to the boundary. If not, consume only
- // as much of the peek buffer as cannot hold the boundary
- // string.
- nCopy := 0
- foundBoundary := false
- if idx, isEnd := p.mr.peekBufferSeparatorIndex(peek); idx != -1 {
- nCopy = idx
- foundBoundary = isEnd
- if !isEnd && nCopy == 0 {
- nCopy = 1 // make some progress.
+ return n, nil
+}
+
+// scanUntilBoundary scans buf to identify how much of it can be safely
+// returned as part of the Part body.
+// dashBoundary is "--boundary".
+// nlDashBoundary is "\r\n--boundary" or "\n--boundary", depending on what mode we are in.
+// The comments below (and the name) assume "\n--boundary", but either is accepted.
+// total is the number of bytes read out so far. If total == 0, then a leading "--boundary" is recognized.
+// readErr is the read error, if any, that followed reading the bytes in buf.
+// scanUntilBoundary returns the number of data bytes from buf that can be
+// returned as part of the Part body and also the error to return (if any)
+// once those data bytes are done.
+func scanUntilBoundary(buf, dashBoundary, nlDashBoundary []byte, total int64, readErr error) (int, error) {
+ if total == 0 {
+ // At beginning of body, allow dashBoundary.
+ if bytes.HasPrefix(buf, dashBoundary) {
+ switch matchAfterPrefix(buf, dashBoundary, readErr) {
+ case -1:
+ return len(dashBoundary), nil
+ case 0:
+ return 0, nil
+ case +1:
+ return 0, io.EOF
+ }
+ }
+ if bytes.HasPrefix(dashBoundary, buf) {
+ return 0, readErr
}
- } else if safeCount := len(peek) - len(p.mr.nlDashBoundary); safeCount > 0 {
- nCopy = safeCount
- } else if unexpectedEOF {
- // If we've run out of peek buffer and the boundary
- // wasn't found (and can't possibly fit), we must have
- // hit the end of the file unexpectedly.
- return 0, io.ErrUnexpectedEOF
}
- if nCopy > 0 {
- if _, err := io.CopyN(p.buffer, p.mr.bufReader, int64(nCopy)); err != nil {
- return 0, err
+
+ // Search for "\n--boundary".
+ if i := bytes.Index(buf, nlDashBoundary); i >= 0 {
+ switch matchAfterPrefix(buf[i:], nlDashBoundary, readErr) {
+ case -1:
+ return i + len(nlDashBoundary), nil
+ case 0:
+ return i, nil
+ case +1:
+ return i, io.EOF
+ }
+ }
+ if bytes.HasPrefix(nlDashBoundary, buf) {
+ return 0, readErr
+ }
+
+ // Otherwise, anything up to the final \n is not part of the boundary
+ // and so must be part of the body.
+ // Also if the section from the final \n onward is not a prefix of the boundary,
+ // it too must be part of the body.
+ i := bytes.LastIndexByte(buf, nlDashBoundary[0])
+ if i >= 0 && bytes.HasPrefix(nlDashBoundary, buf[i:]) {
+ return i, nil
+ }
+ return len(buf), readErr
+}
+
+// matchAfterPrefix checks whether buf should be considered to match the boundary.
+// The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary",
+// and the caller has verified already that bytes.HasPrefix(buf, prefix) is true.
+//
+// matchAfterPrefix returns +1 if the buffer does match the boundary,
+// meaning the prefix is followed by a dash, space, tab, cr, nl, or end of input.
+// It returns -1 if the buffer definitely does NOT match the boundary,
+// meaning the prefix is followed by some other character.
+// For example, "--foobar" does not match "--foo".
+// It returns 0 more input needs to be read to make the decision,
+// meaning that len(buf) == len(prefix) and readErr == nil.
+func matchAfterPrefix(buf, prefix []byte, readErr error) int {
+ if len(buf) == len(prefix) {
+ if readErr != nil {
+ return +1
}
+ return 0
}
- n, err = p.buffer.Read(d)
- if err == io.EOF && !foundBoundary {
- // If the boundary hasn't been reached there's more to
- // read, so don't pass through an EOF from the buffer
- err = nil
+ c := buf[len(prefix)]
+ if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '-' {
+ return +1
}
- return
+ return -1
}
func (p *Part) Close() error {
@@ -337,64 +392,6 @@ func (mr *Reader) isBoundaryDelimiterLine(line []byte) (ret bool) {
return bytes.Equal(rest, mr.nl)
}
-// peekBufferIsEmptyPart reports whether the provided peek-ahead
-// buffer represents an empty part. It is called only if we've not
-// already read any bytes in this part and checks for the case of MIME
-// software not writing the \r\n on empty parts. Some does, some
-// doesn't.
-//
-// This checks that what follows the "--boundary" is actually the end
-// ("--boundary--" with optional whitespace) or optional whitespace
-// and then a newline, so we don't catch "--boundaryFAKE", in which
-// case the whole line is part of the data.
-func (mr *Reader) peekBufferIsEmptyPart(peek []byte) bool {
- // End of parts case.
- // Test whether peek matches `^--boundary--[ \t]*(?:\r\n|$)`
- if bytes.HasPrefix(peek, mr.dashBoundaryDash) {
- rest := peek[len(mr.dashBoundaryDash):]
- rest = skipLWSPChar(rest)
- return bytes.HasPrefix(rest, mr.nl) || len(rest) == 0
- }
- if !bytes.HasPrefix(peek, mr.dashBoundary) {
- return false
- }
- // Test whether rest matches `^[ \t]*\r\n`)
- rest := peek[len(mr.dashBoundary):]
- rest = skipLWSPChar(rest)
- return bytes.HasPrefix(rest, mr.nl)
-}
-
-// peekBufferSeparatorIndex returns the index of mr.nlDashBoundary in
-// peek and whether it is a real boundary (and not a prefix of an
-// unrelated separator). To be the end, the peek buffer must contain a
-// newline after the boundary or contain the ending boundary (--separator--).
-func (mr *Reader) peekBufferSeparatorIndex(peek []byte) (idx int, isEnd bool) {
- idx = bytes.Index(peek, mr.nlDashBoundary)
- if idx == -1 {
- return
- }
-
- peek = peek[idx+len(mr.nlDashBoundary):]
- if len(peek) == 0 || len(peek) == 1 && peek[0] == '-' {
- return idx, false
- }
- if len(peek) > 1 && peek[0] == '-' && peek[1] == '-' {
- return idx, true
- }
- peek = skipLWSPChar(peek)
- // Don't have a complete line after the peek.
- if bytes.IndexByte(peek, '\n') == -1 {
- return idx, false
- }
- if len(peek) > 0 && peek[0] == '\n' {
- return idx, true
- }
- if len(peek) > 1 && peek[0] == '\r' && peek[1] == '\n' {
- return idx, true
- }
- return idx, false
-}
-
// skipLWSPChar returns b with leading spaces and tabs removed.
// RFC 822 defines:
// LWSP-char = SPACE / HTAB
diff --git a/libgo/go/mime/multipart/multipart_test.go b/libgo/go/mime/multipart/multipart_test.go
index 82a7f86..7fbee90 100644
--- a/libgo/go/mime/multipart/multipart_test.go
+++ b/libgo/go/mime/multipart/multipart_test.go
@@ -125,6 +125,7 @@ func TestMultipartSlowInput(t *testing.T) {
}
func testMultipart(t *testing.T, r io.Reader, onlyNewlines bool) {
+ t.Parallel()
reader := NewReader(r, "MyBoundary")
buf := new(bytes.Buffer)
@@ -323,6 +324,73 @@ func (s *slowReader) Read(p []byte) (int, error) {
return s.r.Read(p[:1])
}
+type sentinelReader struct {
+ // done is closed when this reader is read from.
+ done chan struct{}
+}
+
+func (s *sentinelReader) Read([]byte) (int, error) {
+ if s.done != nil {
+ close(s.done)
+ s.done = nil
+ }
+ return 0, io.EOF
+}
+
+// TestMultipartStreamReadahead tests that PartReader does not block
+// on reading past the end of a part, ensuring that it can be used on
+// a stream like multipart/x-mixed-replace. See golang.org/issue/15431
+func TestMultipartStreamReadahead(t *testing.T) {
+ testBody1 := `
+This is a multi-part message. This line is ignored.
+--MyBoundary
+foo-bar: baz
+
+Body
+--MyBoundary
+`
+ testBody2 := `foo-bar: bop
+
+Body 2
+--MyBoundary--
+`
+ done1 := make(chan struct{})
+ reader := NewReader(
+ io.MultiReader(
+ strings.NewReader(testBody1),
+ &sentinelReader{done1},
+ strings.NewReader(testBody2)),
+ "MyBoundary")
+
+ var i int
+ readPart := func(hdr textproto.MIMEHeader, body string) {
+ part, err := reader.NextPart()
+ if part == nil || err != nil {
+ t.Fatalf("Part %d: NextPart failed: %v", i, err)
+ }
+
+ if !reflect.DeepEqual(part.Header, hdr) {
+ t.Errorf("Part %d: part.Header = %v, want %v", i, part.Header, hdr)
+ }
+ data, err := ioutil.ReadAll(part)
+ expectEq(t, body, string(data), fmt.Sprintf("Part %d body", i))
+ if err != nil {
+ t.Fatalf("Part %d: ReadAll failed: %v", i, err)
+ }
+ i++
+ }
+
+ readPart(textproto.MIMEHeader{"Foo-Bar": {"baz"}}, "Body")
+
+ select {
+ case <-done1:
+ t.Errorf("Reader read past second boundary")
+ default:
+ }
+
+ readPart(textproto.MIMEHeader{"Foo-Bar": {"bop"}}, "Body 2")
+}
+
func TestLineContinuation(t *testing.T) {
// This body, extracted from an email, contains headers that span multiple
// lines.
@@ -755,6 +823,7 @@ func partsFromReader(r *Reader) ([]headerBody, error) {
}
func TestParseAllSizes(t *testing.T) {
+ t.Parallel()
const maxSize = 5 << 10
var buf bytes.Buffer
body := strings.Repeat("a", maxSize)
diff --git a/libgo/go/mime/quotedprintable/example_test.go b/libgo/go/mime/quotedprintable/example_test.go
new file mode 100644
index 0000000..0593b04
--- /dev/null
+++ b/libgo/go/mime/quotedprintable/example_test.go
@@ -0,0 +1,39 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+
+package quotedprintable_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "mime/quotedprintable"
+ "os"
+ "strings"
+)
+
+func ExampleNewReader() {
+ for _, s := range []string{
+ `=48=65=6C=6C=6F=2C=20=47=6F=70=68=65=72=73=21`,
+ `invalid escape: <b style="font-size: 200%">hello</b>`,
+ "Hello, Gophers! This symbol will be unescaped: =3D and this will be written in =\r\none line.",
+ } {
+ b, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(s)))
+ fmt.Printf("%s %v\n", b, err)
+ }
+ // Output:
+ // Hello, Gophers! <nil>
+ // invalid escape: <b style="font-size: 200%">hello</b> <nil>
+ // Hello, Gophers! This symbol will be unescaped: = and this will be written in one line. <nil>
+}
+
+func ExampleNewWriter() {
+ w := quotedprintable.NewWriter(os.Stdout)
+ w.Write([]byte("These symbols will be escaped: = \t"))
+ w.Close()
+
+ // Output:
+ // These symbols will be escaped: =3D =09
+}
diff --git a/libgo/go/mime/quotedprintable/reader.go b/libgo/go/mime/quotedprintable/reader.go
index 3bd6833..b142240 100644
--- a/libgo/go/mime/quotedprintable/reader.go
+++ b/libgo/go/mime/quotedprintable/reader.go
@@ -74,6 +74,11 @@ func (r *Reader) Read(p []byte) (n int, err error) {
// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
// with other broken QP encoders & decoders.
+ // 3. it accepts soft line-break (=) at end of message (issue 15486); i.e.
+ // the final byte read from the underlying reader is allowed to be '=',
+ // and it will be silently ignored.
+ // 4. it takes = as literal = if not followed by two hex digits
+ // but not at end of line (issue 13219).
for len(p) > 0 {
if len(r.line) == 0 {
if r.rerr != nil {
@@ -89,7 +94,8 @@ func (r *Reader) Read(p []byte) (n int, err error) {
if bytes.HasSuffix(r.line, softSuffix) {
rightStripped := wholeLine[len(r.line):]
r.line = r.line[:len(r.line)-1]
- if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
+ if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) &&
+ !(len(rightStripped) == 0 && len(r.line) > 0 && r.rerr == io.EOF) {
r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
}
} else if hasLF {
@@ -107,6 +113,11 @@ func (r *Reader) Read(p []byte) (n int, err error) {
case b == '=':
b, err = readHexByte(r.line[1:])
if err != nil {
+ if len(r.line) >= 2 && r.line[1] != '\r' && r.line[1] != '\n' {
+ // Take the = as a literal =.
+ b = '='
+ break
+ }
return n, err
}
r.line = r.line[2:] // 2 of the 3; other 1 is done below
diff --git a/libgo/go/mime/quotedprintable/reader_test.go b/libgo/go/mime/quotedprintable/reader_test.go
index e77b261..ca016f9 100644
--- a/libgo/go/mime/quotedprintable/reader_test.go
+++ b/libgo/go/mime/quotedprintable/reader_test.go
@@ -30,7 +30,7 @@ func TestReader(t *testing.T) {
{in: "foo bar=3d", want: "foo bar="}, // lax.
{in: "foo bar=\n", want: "foo bar"},
{in: "foo bar\n", want: "foo bar\n"}, // somewhat lax.
- {in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF},
+ {in: "foo bar=0", want: "foo bar=0"}, // lax
{in: "foo bar=0D=0A", want: "foo bar\r\n"},
{in: " A B \r\n C ", want: " A B\r\n C"},
{in: " A B =\r\n C ", want: " A B C"},
@@ -58,6 +58,9 @@ func TestReader(t *testing.T) {
{in: "foo=\nbar", want: "foobar"},
{in: "foo=\rbar", want: "foo", err: "quotedprintable: invalid hex byte 0x0d"},
{in: "foo=\r\r\r \nbar", want: "foo", err: `quotedprintable: invalid bytes after =: "\r\r\r \n"`},
+ // Issue 15486, accept trailing soft line-break at end of input.
+ {in: "foo=", want: "foo"},
+ {in: "=", want: "", err: `quotedprintable: invalid bytes after =: ""`},
// Example from RFC 2045:
{in: "Now's the time =\n" + "for all folk to come=\n" + " to the aid of their country.",
@@ -191,13 +194,10 @@ func TestExhaustive(t *testing.T) {
}
sort.Strings(outcomes)
got := strings.Join(outcomes, "\n")
- want := `OK: 21576
-invalid bytes after =: 3397
-quotedprintable: invalid hex byte 0x0a: 1400
-quotedprintable: invalid hex byte 0x0d: 2700
-quotedprintable: invalid hex byte 0x20: 2490
-quotedprintable: invalid hex byte 0x3d: 440
-unexpected EOF: 3122`
+ want := `OK: 28934
+invalid bytes after =: 3949
+quotedprintable: invalid hex byte 0x0d: 2048
+unexpected EOF: 194`
if got != want {
t.Errorf("Got:\n%s\nWant:\n%s", got, want)
}