1// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package quotedprintable
6
7import "io"
8
9const lineMaxLen = 76
10
11// A Writer is a quoted-printable writer that implements io.WriteCloser.
12type Writer struct {
13	// Binary mode treats the writer's input as pure binary and processes end of
14	// line bytes as binary data.
15	Binary bool
16
17	w    io.Writer
18	i    int
19	line [78]byte
20	cr   bool
21}
22
23// NewWriter returns a new Writer that writes to w.
24func NewWriter(w io.Writer) *Writer {
25	return &Writer{w: w}
26}
27
28// Write encodes p using quoted-printable encoding and writes it to the
29// underlying io.Writer. It limits line length to 76 characters. The encoded
30// bytes are not necessarily flushed until the Writer is closed.
31func (w *Writer) Write(p []byte) (n int, err error) {
32	for i, b := range p {
33		switch {
34		// Simple writes are done in batch.
35		case b >= '!' && b <= '~' && b != '=':
36			continue
37		case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
38			continue
39		}
40
41		if i > n {
42			if err := w.write(p[n:i]); err != nil {
43				return n, err
44			}
45			n = i
46		}
47
48		if err := w.encode(b); err != nil {
49			return n, err
50		}
51		n++
52	}
53
54	if n == len(p) {
55		return n, nil
56	}
57
58	if err := w.write(p[n:]); err != nil {
59		return n, err
60	}
61
62	return len(p), nil
63}
64
65// Close closes the Writer, flushing any unwritten data to the underlying
66// io.Writer, but does not close the underlying io.Writer.
67func (w *Writer) Close() error {
68	if err := w.checkLastByte(); err != nil {
69		return err
70	}
71
72	return w.flush()
73}
74
75// write limits text encoded in quoted-printable to 76 characters per line.
76func (w *Writer) write(p []byte) error {
77	for _, b := range p {
78		if b == '\n' || b == '\r' {
79			// If the previous byte was \r, the CRLF has already been inserted.
80			if w.cr && b == '\n' {
81				w.cr = false
82				continue
83			}
84
85			if b == '\r' {
86				w.cr = true
87			}
88
89			if err := w.checkLastByte(); err != nil {
90				return err
91			}
92			if err := w.insertCRLF(); err != nil {
93				return err
94			}
95			continue
96		}
97
98		if w.i == lineMaxLen-1 {
99			if err := w.insertSoftLineBreak(); err != nil {
100				return err
101			}
102		}
103
104		w.line[w.i] = b
105		w.i++
106		w.cr = false
107	}
108
109	return nil
110}
111
112func (w *Writer) encode(b byte) error {
113	if lineMaxLen-1-w.i < 3 {
114		if err := w.insertSoftLineBreak(); err != nil {
115			return err
116		}
117	}
118
119	w.line[w.i] = '='
120	w.line[w.i+1] = upperhex[b>>4]
121	w.line[w.i+2] = upperhex[b&0x0f]
122	w.i += 3
123
124	return nil
125}
126
127const upperhex = "0123456789ABCDEF"
128
129// checkLastByte encodes the last buffered byte if it is a space or a tab.
130func (w *Writer) checkLastByte() error {
131	if w.i == 0 {
132		return nil
133	}
134
135	b := w.line[w.i-1]
136	if isWhitespace(b) {
137		w.i--
138		if err := w.encode(b); err != nil {
139			return err
140		}
141	}
142
143	return nil
144}
145
146func (w *Writer) insertSoftLineBreak() error {
147	w.line[w.i] = '='
148	w.i++
149
150	return w.insertCRLF()
151}
152
153func (w *Writer) insertCRLF() error {
154	w.line[w.i] = '\r'
155	w.line[w.i+1] = '\n'
156	w.i += 2
157
158	return w.flush()
159}
160
161func (w *Writer) flush() error {
162	if _, err := w.w.Write(w.line[:w.i]); err != nil {
163		return err
164	}
165
166	w.i = 0
167	return nil
168}
169
170func isWhitespace(b byte) bool {
171	return b == ' ' || b == '\t'
172}
173