]> Cypherpunks repositories - gostls13.git/commitdiff
mime/multipart: transparently decode quoted-printable transfer encoding
authorBrad Fitzpatrick <bradfitz@golang.org>
Tue, 20 Nov 2012 03:50:19 +0000 (19:50 -0800)
committerBrad Fitzpatrick <bradfitz@golang.org>
Tue, 20 Nov 2012 03:50:19 +0000 (19:50 -0800)
Fixes #4411

R=dsymonds
CC=gobot, golang-dev
https://golang.org/cl/6854067

src/pkg/mime/multipart/multipart.go
src/pkg/mime/multipart/multipart_test.go
src/pkg/mime/multipart/quotedprintable.go [new file with mode: 0644]
src/pkg/mime/multipart/quotedprintable_test.go [new file with mode: 0644]

index fb07e1a56d52985ede360e57750cbc9e8e6f23c8..77e969b41b0ea7b924308572cf6bc3e5d941bee1 100644 (file)
@@ -37,6 +37,11 @@ type Part struct {
 
        disposition       string
        dispositionParams map[string]string
+
+       // r is either a reader directly reading from mr, or it's a
+       // wrapper around such a reader, decoding the
+       // Content-Transfer-Encoding
+       r io.Reader
 }
 
 // FormName returns the name parameter if p has a Content-Disposition
@@ -94,6 +99,12 @@ func newPart(mr *Reader) (*Part, error) {
        if err := bp.populateHeaders(); err != nil {
                return nil, err
        }
+       bp.r = partReader{bp}
+       const cte = "Content-Transfer-Encoding"
+       if bp.Header.Get(cte) == "quoted-printable" {
+               bp.Header.Del(cte)
+               bp.r = newQuotedPrintableReader(bp.r)
+       }
        return bp, nil
 }
 
@@ -109,6 +120,17 @@ func (bp *Part) populateHeaders() error {
 // Read reads the body of a part, after its headers and before the
 // next part (if any) begins.
 func (p *Part) Read(d []byte) (n int, err error) {
+       return p.r.Read(d)
+}
+
+// partReader implements io.Reader by reading raw bytes directly from the
+// wrapped *Part, without doing any Transfer-Encoding decoding.
+type partReader struct {
+       p *Part
+}
+
+func (pr partReader) Read(d []byte) (n int, err error) {
+       p := pr.p
        defer func() {
                p.bytesRead += n
        }()
index cd65e177e852e3a80539ec6c688ef0b14338ebbd..d662e834059d80afee347aa0214889f7a77f16cd 100644 (file)
@@ -339,9 +339,10 @@ func TestLineContinuation(t *testing.T) {
                if err != nil {
                        t.Fatalf("didn't get a part")
                }
-               n, err := io.Copy(ioutil.Discard, part)
+               var buf bytes.Buffer
+               n, err := io.Copy(&buf, part)
                if err != nil {
-                       t.Errorf("error reading part: %v", err)
+                       t.Errorf("error reading part: %v\nread so far: %q", err, buf.String())
                }
                if n <= 0 {
                        t.Errorf("read %d bytes; expected >0", n)
@@ -349,6 +350,29 @@ func TestLineContinuation(t *testing.T) {
        }
 }
 
+func TestQuotedPrintableEncoding(t *testing.T) {
+       // From http://golang.org/issue/4411
+       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--"
+       r := NewReader(strings.NewReader(body), "0016e68ee29c5d515f04cedf6733")
+       part, err := r.NextPart()
+       if err != nil {
+               t.Fatal(err)
+       }
+       if te, ok := part.Header["Content-Transfer-Encoding"]; ok {
+               t.Errorf("unexpected Content-Transfer-Encoding of %q", te)
+       }
+       var buf bytes.Buffer
+       _, err = io.Copy(&buf, part)
+       if err != nil {
+               t.Error(err)
+       }
+       got := buf.String()
+       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"
+       if got != want {
+               t.Errorf("wrong part value:\n got: %q\nwant: %q", got, want)
+       }
+}
+
 // Test parsing an image attachment from gmail, which previously failed.
 func TestNested(t *testing.T) {
        // nested-mime is the body part of a multipart/mixed email
diff --git a/src/pkg/mime/multipart/quotedprintable.go b/src/pkg/mime/multipart/quotedprintable.go
new file mode 100644 (file)
index 0000000..0a60a6e
--- /dev/null
@@ -0,0 +1,92 @@
+// Copyright 2012 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.
+
+// The file define a quoted-printable decoder, as specified in RFC 2045.
+
+package multipart
+
+import (
+       "bufio"
+       "bytes"
+       "fmt"
+       "io"
+)
+
+type qpReader struct {
+       br   *bufio.Reader
+       rerr error  // last read error
+       line []byte // to be consumed before more of br
+}
+
+func newQuotedPrintableReader(r io.Reader) io.Reader {
+       return &qpReader{
+               br: bufio.NewReader(r),
+       }
+}
+
+func fromHex(b byte) (byte, error) {
+       switch {
+       case b >= '0' && b <= '9':
+               return b - '0', nil
+       case b >= 'A' && b <= 'F':
+               return b - 'A' + 10, nil
+       }
+       return 0, fmt.Errorf("multipart: invalid quoted-printable hex byte 0x%02x", b)
+}
+
+func (q *qpReader) readHexByte(v []byte) (b byte, err error) {
+       if len(v) < 2 {
+               return 0, io.ErrUnexpectedEOF
+       }
+       var hb, lb byte
+       if hb, err = fromHex(v[0]); err != nil {
+               return 0, err
+       }
+       if lb, err = fromHex(v[1]); err != nil {
+               return 0, err
+       }
+       return hb<<4 | lb, nil
+}
+
+func isQPDiscardWhitespace(r rune) bool {
+       switch r {
+       case '\n', '\r', ' ', '\t':
+               return true
+       }
+       return false
+}
+
+func (q *qpReader) Read(p []byte) (n int, err error) {
+       for len(p) > 0 {
+               if len(q.line) == 0 {
+                       if q.rerr != nil {
+                               return n, q.rerr
+                       }
+                       q.line, q.rerr = q.br.ReadSlice('\n')
+                       q.line = bytes.TrimRightFunc(q.line, isQPDiscardWhitespace)
+                       continue
+               }
+               if len(q.line) == 1 && q.line[0] == '=' {
+                       // Soft newline; skipped.
+                       q.line = nil
+                       continue
+               }
+               b := q.line[0]
+               switch {
+               case b == '=':
+                       b, err = q.readHexByte(q.line[1:])
+                       if err != nil {
+                               return n, err
+                       }
+                       q.line = q.line[2:] // 2 of the 3; other 1 is done below
+               case b != '\t' && (b < ' ' || b > '~'):
+                       return n, fmt.Errorf("multipart: invalid unescaped byte 0x%02x in quoted-printable body", b)
+               }
+               p[0] = b
+               p = p[1:]
+               q.line = q.line[1:]
+               n++
+       }
+       return n, nil
+}
diff --git a/src/pkg/mime/multipart/quotedprintable_test.go b/src/pkg/mime/multipart/quotedprintable_test.go
new file mode 100644 (file)
index 0000000..796a41f
--- /dev/null
@@ -0,0 +1,52 @@
+// Copyright 2012 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.
+
+package multipart
+
+import (
+       "bytes"
+       "fmt"
+       "io"
+       "strings"
+       "testing"
+)
+
+func TestQuotedPrintable(t *testing.T) {
+       tests := []struct {
+               in, want string
+               err      interface{}
+       }{
+               {in: "foo bar", want: "foo bar"},
+               {in: "foo bar=3D", want: "foo bar="},
+               {in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF},
+               {in: "foo bar=ab", want: "foo bar", err: "multipart: invalid quoted-printable hex byte 0x61"},
+               {in: "foo bar=0D=0A", want: "foo bar\r\n"},
+               {in: "foo bar=\r\n baz", want: "foo bar baz"},
+               {in: "foo=\nbar", want: "foobar"},
+               {in: "foo\x00bar", want: "foo", err: "multipart: invalid unescaped byte 0x00 in quoted-printable body"},
+               {in: "foo bar\xff", want: "foo bar", err: "multipart: invalid unescaped byte 0xff in quoted-printable body"},
+       }
+       for _, tt := range tests {
+               var buf bytes.Buffer
+               _, err := io.Copy(&buf, newQuotedPrintableReader(strings.NewReader(tt.in)))
+               if got := buf.String(); got != tt.want {
+                       t.Errorf("for %q, got %q; want %q", tt.in, got, tt.want)
+               }
+               switch verr := tt.err.(type) {
+               case nil:
+                       if err != nil {
+                               t.Errorf("for %q, got unexpected error: %v", tt.in, err)
+                       }
+               case string:
+                       if got := fmt.Sprint(err); got != verr {
+                               t.Errorf("for %q, got error %q; want %q", tt.in, got, verr)
+                       }
+               case error:
+                       if err != verr {
+                               t.Errorf("for %q, got error %q; want %q", tt.in, err, verr)
+                       }
+               }
+       }
+
+}