1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
|
package coding
import (
"bufio"
"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 *bufio.Reader
overflow []byte
lineLen int
}
// MaxQPLineLen is the maximum line length we allow before inserting `=\r\n`. Prevents buffer
// overflows in mime/quotedprintable.Reader.
const MaxQPLineLen = 1024
var (
_ io.Reader = &QPCleaner{} // Assert QPCleaner implements io.Reader.
escapedEquals = []byte("=3D") // QP encoded value of an equals sign.
lineBreak = []byte("=\r\n")
)
// NewQPCleaner returns a QPCleaner for the specified reader.
func NewQPCleaner(r io.Reader) *QPCleaner {
return &QPCleaner{
in: bufio.NewReader(r),
overflow: nil,
lineLen: 0,
}
}
// Read method for io.Reader interface.
func (qp *QPCleaner) Read(dest []byte) (n int, err error) {
destLen := len(dest)
if len(qp.overflow) > 0 {
// Copy bytes that didn't fit into dest buffer during previous read.
n = copy(dest, qp.overflow)
qp.overflow = qp.overflow[n:]
}
// writeByte outputs a single byte, space for which will have already been ensured by the loop
// condition. Updates counters.
writeByte := func(in byte) {
dest[n] = in
n++
qp.lineLen++
}
// writeBytes outputs multiple bytes, storing overflow for next read. Updates counters.
writeBytes := func(in []byte) {
nc := copy(dest[n:], in)
if nc < len(in) {
// Stash unwritten bytes into overflow.
qp.overflow = append(qp.overflow, []byte(in[nc:])...)
}
n += nc
qp.lineLen += len(in)
}
// ensureLineLen ensures there is room to write `requested` bytes, preventing a line break being
// inserted in the middle of the escaped string. The requested count is in addition to the
// byte that was already reserved for this loop iteration.
ensureLineLen := func(requested int) {
if qp.lineLen+requested >= MaxQPLineLen {
writeBytes(lineBreak)
qp.lineLen = 0
}
}
// Loop over bytes in qp.in ByteReader while there is space in dest.
for n < destLen {
var b byte
b, err = qp.in.ReadByte()
if err != nil {
return n, err
}
if qp.lineLen >= MaxQPLineLen {
writeBytes(lineBreak)
qp.lineLen = 0
if n == destLen {
break
}
}
switch {
// Pass valid hex bytes through, otherwise escapes the equals symbol.
case b == '=':
ensureLineLen(2)
var hexBytes []byte
hexBytes, err = qp.in.Peek(2)
if err != nil && err != io.EOF {
return 0, err
}
if validHexBytes(hexBytes) {
dest[n] = b
n++
} else {
writeBytes(escapedEquals)
}
// Valid special character.
case b == '\t':
writeByte(b)
// Valid special characters that reset line length.
case b == '\r' || b == '\n':
writeByte(b)
qp.lineLen = 0
// Invalid characters, render as quoted-printable.
case b < ' ' || '~' < b:
ensureLineLen(2)
writeBytes([]byte(fmt.Sprintf("=%02X", b)))
// Acceptable characters.
default:
writeByte(b)
}
}
return n, err
}
func validHexByte(b byte) bool {
return '0' <= b && b <= '9' || 'A' <= b && b <= 'F' || 'a' <= b && b <= 'f'
}
// validHexBytes returns true if this byte sequence represents a valid quoted-printable escape
// sequence or line break, minus the initial equals sign.
func validHexBytes(v []byte) bool {
if len(v) > 0 && v[0] == '\n' {
// Soft line break.
return true
}
if len(v) < 2 {
return false
}
if v[0] == '\r' && v[1] == '\n' {
// Soft line break.
return true
}
return validHexByte(v[0]) && validHexByte(v[1])
}
|