Commit 43dfb879 authored by Alexandre Cesaro's avatar Alexandre Cesaro Committed by Nigel Tao

mime/quotedprintable: add writer

Updates #4943

Change-Id: I082b97ccd787cf91245d39b8d93783732db6f42c
Reviewed-on: https://go-review.googlesource.com/6171Reviewed-by: default avatarNigel Tao <nigeltao@golang.org>
parent 4d2b3a0b
// Copyright 2015 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 quotedprintable
import "io"
const lineMaxLen = 76
// A Writer is a quoted-printable writer that implements io.WriteCloser.
type Writer struct {
w io.Writer
i int
line [78]byte
cr bool
}
// NewWriter returns a new Writer that writes to w.
func NewWriter(w io.Writer) *Writer {
return &Writer{w: w}
}
// Write encodes p using quoted-printable encoding and writes it to the
// underlying io.Writer. It limits line length to 76 characters. The encoded
// bytes are not necessarily flushed until the Writer is closed.
func (w *Writer) Write(p []byte) (n int, err error) {
for i, b := range p {
switch {
// Simple writes are done in batch.
case b >= '!' && b <= '~' && b != '=':
continue
case isWhitespace(b) || b == '\n' || b == '\r':
continue
}
if i > n {
if err := w.write(p[n:i]); err != nil {
return n, err
}
n = i
}
if err := w.encode(b); err != nil {
return n, err
}
n++
}
if n == len(p) {
return n, nil
}
if err := w.write(p[n:]); err != nil {
return n, err
}
return len(p), nil
}
// Close closes the Writer, flushing any unwritten data to the underlying
// io.Writer, but does not close the underlying io.Writer.
func (w *Writer) Close() error {
if err := w.checkLastByte(); err != nil {
return err
}
return w.flush()
}
// write limits text encoded in quoted-printable to 76 characters per line.
func (w *Writer) write(p []byte) error {
for _, b := range p {
if b == '\n' || b == '\r' {
// If the previous byte was \r, the CRLF has already been inserted.
if w.cr && b == '\n' {
w.cr = false
continue
}
if b == '\r' {
w.cr = true
}
if err := w.checkLastByte(); err != nil {
return err
}
if err := w.insertCRLF(); err != nil {
return err
}
continue
}
if w.i == lineMaxLen-1 {
if err := w.insertSoftLineBreak(); err != nil {
return err
}
}
w.line[w.i] = b
w.i++
w.cr = false
}
return nil
}
func (w *Writer) encode(b byte) error {
if lineMaxLen-1-w.i < 3 {
if err := w.insertSoftLineBreak(); err != nil {
return err
}
}
w.line[w.i] = '='
w.line[w.i+1] = upperhex[b>>4]
w.line[w.i+2] = upperhex[b&0x0f]
w.i += 3
return nil
}
const upperhex = "0123456789ABCDEF"
// checkLastByte encodes the last buffered byte if it is a space or a tab.
func (w *Writer) checkLastByte() error {
if w.i == 0 {
return nil
}
b := w.line[w.i-1]
if isWhitespace(b) {
w.i--
if err := w.encode(b); err != nil {
return err
}
}
return nil
}
func (w *Writer) insertSoftLineBreak() error {
w.line[w.i] = '='
w.i++
return w.insertCRLF()
}
func (w *Writer) insertCRLF() error {
w.line[w.i] = '\r'
w.line[w.i+1] = '\n'
w.i += 2
return w.flush()
}
func (w *Writer) flush() error {
if _, err := w.w.Write(w.line[:w.i]); err != nil {
return err
}
w.i = 0
return nil
}
func isWhitespace(b byte) bool {
return b == ' ' || b == '\t'
}
// Copyright 2015 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 quotedprintable
import (
"bytes"
"io/ioutil"
"strings"
"testing"
)
func TestWriter(t *testing.T) {
tests := []struct {
in, want string
}{
{in: "", want: ""},
{in: "foo bar", want: "foo bar"},
{in: "foo bar=", want: "foo bar=3D"},
{in: "foo bar\r", want: "foo bar\r\n"},
{in: "foo bar\r\r", want: "foo bar\r\n\r\n"},
{in: "foo bar\n", want: "foo bar\r\n"},
{in: "foo bar\r\n", want: "foo bar\r\n"},
{in: "foo bar\r\r\n", want: "foo bar\r\n\r\n"},
{in: "foo bar ", want: "foo bar=20"},
{in: "foo bar\t", want: "foo bar=09"},
{in: "foo bar ", want: "foo bar =20"},
{in: "foo bar \n", want: "foo bar=20\r\n"},
{in: "foo bar \r", want: "foo bar=20\r\n"},
{in: "foo bar \r\n", want: "foo bar=20\r\n"},
{in: "foo bar \n", want: "foo bar =20\r\n"},
{in: "foo bar \n ", want: "foo bar =20\r\n=20"},
{in: "¡Hola Señor!", want: "=C2=A1Hola Se=C3=B1or!"},
{
in: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~",
want: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~",
},
{
in: strings.Repeat("a", 75),
want: strings.Repeat("a", 75),
},
{
in: strings.Repeat("a", 76),
want: strings.Repeat("a", 75) + "=\r\na",
},
{
in: strings.Repeat("a", 72) + "=",
want: strings.Repeat("a", 72) + "=3D",
},
{
in: strings.Repeat("a", 73) + "=",
want: strings.Repeat("a", 73) + "=\r\n=3D",
},
{
in: strings.Repeat("a", 74) + "=",
want: strings.Repeat("a", 74) + "=\r\n=3D",
},
{
in: strings.Repeat("a", 75) + "=",
want: strings.Repeat("a", 75) + "=\r\n=3D",
},
{
in: strings.Repeat(" ", 73),
want: strings.Repeat(" ", 72) + "=20",
},
{
in: strings.Repeat(" ", 74),
want: strings.Repeat(" ", 73) + "=\r\n=20",
},
{
in: strings.Repeat(" ", 75),
want: strings.Repeat(" ", 74) + "=\r\n=20",
},
{
in: strings.Repeat(" ", 76),
want: strings.Repeat(" ", 75) + "=\r\n=20",
},
{
in: strings.Repeat(" ", 77),
want: strings.Repeat(" ", 75) + "=\r\n =20",
},
}
for _, tt := range tests {
buf := new(bytes.Buffer)
w := NewWriter(buf)
if _, err := w.Write([]byte(tt.in)); err != nil {
t.Errorf("Write(%q): %v", tt.in, err)
continue
}
if err := w.Close(); err != nil {
t.Errorf("Close(): %v", err)
continue
}
got := buf.String()
if got != tt.want {
t.Errorf("Write(%q), got:\n%q\nwant:\n%q", tt.in, got, tt.want)
}
}
}
func TestRoundTrip(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
if _, err := w.Write(testMsg); err != nil {
t.Fatalf("Write: %v", err)
}
if err := w.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
r := NewReader(buf)
gotBytes, err := ioutil.ReadAll(r)
if err != nil {
t.Fatalf("Error while reading from Reader: %v", err)
}
got := string(gotBytes)
if got != string(testMsg) {
t.Errorf("Encoding and decoding changed the message, got:\n%s", got)
}
}
// From http://fr.wikipedia.org/wiki/Quoted-Printable
var testMsg = []byte("Quoted-Printable (QP) est un format d'encodage de données codées sur 8 bits, qui utilise exclusivement les caractères alphanumériques imprimables du code ASCII (7 bits).\r\n" +
"\r\n" +
"En effet, les différents codages comprennent de nombreux caractères qui ne sont pas représentables en ASCII (par exemple les caractères accentués), ainsi que des caractères dits « non-imprimables ».\r\n" +
"\r\n" +
"L'encodage Quoted-Printable permet de remédier à ce problème, en procédant de la manière suivante :\r\n" +
"\r\n" +
"Un octet correspondant à un caractère imprimable de l'ASCII sauf le signe égal (donc un caractère de code ASCII entre 33 et 60 ou entre 62 et 126) ou aux caractères de saut de ligne (codes ASCII 13 et 10) ou une suite de tabulations et espaces non situées en fin de ligne (de codes ASCII respectifs 9 et 32) est représenté tel quel.\r\n" +
"Un octet qui ne correspond pas à la définition ci-dessus (caractère non imprimable de l'ASCII, tabulation ou espaces non suivies d'un caractère imprimable avant la fin de la ligne ou signe égal) est représenté par un signe égal, suivi de son numéro, exprimé en hexadécimal.\r\n" +
"Enfin, un signe égal suivi par un saut de ligne (donc la suite des trois caractères de codes ASCII 61, 13 et 10) peut être inséré n'importe où, afin de limiter la taille des lignes produites si nécessaire. Une limite de 76 caractères par ligne est généralement respectée.\r\n")
func BenchmarkWriter(b *testing.B) {
for i := 0; i < b.N; i++ {
w := NewWriter(ioutil.Discard)
w.Write(testMsg)
w.Close()
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment