RFC2045 section 6.8 states maximum encoded line length of base64 output should be 76 characters or less.
The Golang stream writer base64.NewEncoder
does not have any option for line splitting, as can be seen here.
package main
import (
"encoding/base64"
"io"
"os"
"strings"
)
// See https://www.ietf.org/rfc/rfc2045.txt, section 6.8 for notes on maximum line length of 76 characters
func main() {
data := "It is only the hairs on a gooseberry that prevent it from being a grape! This is long enough to need a line split"
rdr := strings.NewReader(data)
wrt := base64.NewEncoder(base64.StdEncoding, os.Stdout)
io.Copy(wrt, rdr)
}
Output is
SXQgaXMgb25seSB0aGUgaGFpcnMgb24gYSBnb29zZWJlcnJ5IHRoYXQgcHJldmVudCBpdCBmcm9tIGJlaW5nIGEgZ3JhcGUhIEl0IGlzIG9ubHkgdGhlIGhhaXJzIG9uIGEgZ29vc2ViZXJyeSB0aGF0IHByZXZlbnQgaXQgZnJvbSBiZWluZyBhIGdyYXBl
Is there a stream-based solution to splitting lines? The MIME library offers only string based encoding options.
Here's my attempt to create a simple Writer
. It takes into account varying amounts of input data, has configurable chunk length and separator sequence. It uses byte-slice writes for the chunks, which will hopefully be efficient.
package main
import (
"encoding/base64"
"io"
"os"
"strings"
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
type linesplitter struct {
len int
count int
sep []byte
w io.Writer
}
// NewWriter that splits input every len bytes with a sep byte sequence, outputting to writer w
func (ls *linesplitter) NewWriter(len int, sep []byte, w io.Writer) io.WriteCloser {
return &linesplitter{len: len, count: 0, sep: sep, w: w}
}
// Split a line in to ls.len chunks with separator
func (ls *linesplitter) Write(in []byte) (n int, err error) {
writtenThisCall := 0
readPos := 0
// Leading chunk size is limited by: how much input there is; defined split length; and
// any residual from last time
chunkSize := min(len(in), ls.len-ls.count)
// Pass on chunk(s)
for {
ls.w.Write(in[readPos:(readPos + chunkSize)])
readPos += chunkSize // Skip forward ready for next chunk
ls.count += chunkSize
writtenThisCall += chunkSize
// if we have completed a chunk, emit a separator
if ls.count >= ls.len {
ls.w.Write(ls.sep)
writtenThisCall += len(ls.sep)
ls.count = 0
}
inToGo := len(in) - readPos
if inToGo <= 0 {
break // reached end of input data
}
// Determine size of the NEXT chunk
chunkSize = min(inToGo, ls.len)
}
return writtenThisCall, nil
}
func (ls *linesplitter) Close() (err error) {
return nil
}
// See https://www.ietf.org/rfc/rfc2045.txt, section 6.8 for notes on maximum line length of 76 characters
func main() {
data := "It is only the hairs on a gooseberry that prevent it from being a grape! This is long enough to need a line split"
shortData := "hello there"
var ls linesplitter
lsWriter := ls.NewWriter(76, []byte("
"), os.Stdout)
wrt := base64.NewEncoder(base64.StdEncoding, lsWriter)
for i := 0; i < 10; i++ {
io.Copy(wrt, strings.NewReader(shortData))
io.Copy(wrt, strings.NewReader(data))
io.Copy(wrt, strings.NewReader(shortData))
}
}
... comments/improvements welcome.