-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add qpCleaner, a Reader that fixes quoted-printable
- qpCleaner escapes invalid bytes in a quoted-printable encoded stream, this prevents Golang's quoted-printable decoder from blowing up on poorly encoded MIME parts. - Benchmarked this ByteReader implementation against one that used a bytes.Buffer. This was slightly faster and much easier to read.
- Loading branch information
Showing
3 changed files
with
141 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package enmime | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
) | ||
|
||
// qpCleaner scans quoted printable content for invalid characters and encodes them so that | ||
// Go's quoted-printable decoder does not abort with an error. | ||
type qpCleaner struct { | ||
in io.ByteReader | ||
} | ||
|
||
// Assert qpCleaner implements io.Reader | ||
var _ io.Reader = &qpCleaner{} | ||
|
||
// newBase64Cleaner returns a Base64Cleaner object for the specified reader. Base64Cleaner | ||
// implements the io.Reader interface. | ||
func newQPCleaner(r io.ByteReader) *qpCleaner { | ||
return &qpCleaner{ | ||
in: r, | ||
} | ||
} | ||
|
||
// Read method for io.Reader interface. Reasonably efficient for well-formed quoted-printable | ||
// streams. Less so when invalid characters are encountered; reads will be short. | ||
func (qp *qpCleaner) Read(dest []byte) (n int, err error) { | ||
// Ensure room to write a byte or =XX string | ||
destLen := len(dest) - 3 | ||
// Loop over bytes in qp.in ByteReader | ||
for n < destLen { | ||
b, err := qp.in.ReadByte() | ||
if err != nil { | ||
return n, err | ||
} | ||
// Test character type | ||
switch { | ||
case b == '\t' || b == '\r' || b == '\n': | ||
// Valid special characters | ||
dest[n] = b | ||
n++ | ||
case b < ' ' || '~' < b: | ||
// Invalid character, render quoted-printable into buffer | ||
s := fmt.Sprintf("=%02X", b) | ||
n += copy(dest[n:], s) | ||
default: | ||
// Acceptable character | ||
dest[n] = b | ||
n++ | ||
} | ||
} | ||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package enmime | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
func TestQPCleaner(t *testing.T) { | ||
ttable := []struct { | ||
input string | ||
want string | ||
}{ | ||
{"", ""}, | ||
{"abcDEF_", "abcDEF_"}, | ||
{"=5bSlack=5d", "=5bSlack=5d"}, | ||
{"low: ,high:~", "low: ,high:~"}, | ||
{"\r\n\t", "\r\n\t"}, | ||
{"pédagogues", "p=C3=A9dagogues"}, | ||
{"Stuffs’s", "Stuffs=E2=80=99s"}, | ||
} | ||
|
||
for _, tc := range ttable { | ||
// Run cleaner | ||
cleaner := newQPCleaner(strings.NewReader(tc.input)) | ||
buf := new(bytes.Buffer) | ||
_, err := buf.ReadFrom(cleaner) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
got := buf.String() | ||
if got != tc.want { | ||
t.Errorf("Got: %q, want: %q", got, tc.want) | ||
} | ||
} | ||
} | ||
|
||
// TestQPCleanerOverflow attempts to confuse the cleaner by issuing a smaller subsequent read | ||
func TestQPCleanerOverflow(t *testing.T) { | ||
input := bytes.Repeat([]byte("pédagogues =\r\n"), 1000) | ||
want := bytes.Repeat([]byte("p=C3=A9dagogues =\r\n"), 1000) | ||
inbuf := bytes.NewBuffer(input) | ||
qp := newQPCleaner(inbuf) | ||
|
||
offset := 0 | ||
for len := 1000; len > 0; len -= 100 { | ||
p := make([]byte, len) | ||
n, err := qp.Read(p) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if n < 1 { | ||
t.Fatalf("Read(p) = %v, wanted >0", n) | ||
} | ||
for i := 0; i < n; i++ { | ||
if p[i] != want[offset] { | ||
t.Errorf("p[%v] = %q, want: %q (want[%v])", i, p[i], want[offset], offset) | ||
} | ||
offset++ | ||
} | ||
} | ||
} | ||
|
||
var result int | ||
|
||
func BenchmarkQPCleaner(b *testing.B) { | ||
b.StopTimer() | ||
input := bytes.Repeat([]byte("pédagogues =\r\n"), b.N) | ||
b.SetBytes(int64(len(input))) | ||
inbuf := bytes.NewBuffer(input) | ||
qp := newQPCleaner(inbuf) | ||
p := make([]byte, 1024) | ||
b.StartTimer() | ||
|
||
for { | ||
n, err := qp.Read(p) | ||
result += n | ||
if err == io.EOF { | ||
break | ||
} | ||
if err != nil { | ||
b.Fatalf("Read(): %v", err) | ||
} | ||
} | ||
} |