a84994e6e9
* add tests that cover same behaviors as Convert and ConvertString * align read and write behaviors with bufio to play nice * add methods that allow to customize buffer size * add methods to reset, allowing reuse
182 lines
4.9 KiB
Go
182 lines
4.9 KiB
Go
package iconv
|
|
|
|
import (
|
|
"io"
|
|
"runtime"
|
|
"syscall"
|
|
)
|
|
|
|
const (
|
|
defaultWriteBufferSize = 8 * 1024
|
|
minWriteBufferSize = 16
|
|
)
|
|
|
|
type Writer struct {
|
|
destination io.Writer
|
|
converter *Converter
|
|
readBuffer, writeBuffer []byte
|
|
readPos, writePos int
|
|
}
|
|
|
|
func NewWriter(destination io.Writer, fromEncoding string, toEncoding string) (*Writer, error) {
|
|
return NewWriterSized(destination, fromEncoding, toEncoding, defaultWriteBufferSize)
|
|
}
|
|
|
|
func NewWriterFromConverter(destination io.Writer, converter *Converter) (writer *Writer) {
|
|
return NewWriterFromConverterSized(destination, converter, defaultWriteBufferSize)
|
|
}
|
|
|
|
func NewWriterSized(destination io.Writer, fromEncoding, toEncoding string, size int) (*Writer, error) {
|
|
converter, err := NewConverter(fromEncoding, toEncoding)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// add a finalizer for the converter we created
|
|
runtime.SetFinalizer(converter, finalizeConverter)
|
|
|
|
return NewWriterFromConverterSized(destination, converter, size), nil
|
|
}
|
|
|
|
func NewWriterFromConverterSized(destination io.Writer, converter *Converter, size int) *Writer {
|
|
if size < minWriteBufferSize {
|
|
size = minWriteBufferSize
|
|
}
|
|
|
|
return &Writer{
|
|
destination: destination,
|
|
converter: converter,
|
|
readBuffer: make([]byte, size),
|
|
writeBuffer: make([]byte, size),
|
|
}
|
|
}
|
|
|
|
// Implements io.Writer
|
|
//
|
|
// Will attempt to convert all of p into buffer. If there's not enough room in
|
|
// the buffer to hold all converted bytes, the buffer will be flushed and p will
|
|
// continue to be processed. Close should be called on a writer when finished
|
|
// with all writes, to ensure final shift sequences are written and buffer is
|
|
// flushed to underlying io.Writer.
|
|
//
|
|
// Can return all errors that Convert can, as well as any errors from Flush. Note
|
|
// that some errors from Convert are suppressed if we continue making progress
|
|
// on p.
|
|
func (w *Writer) Write(p []byte) (int, error) {
|
|
var totalBytesRead, bytesRead, bytesWritten int
|
|
var err error
|
|
|
|
if w.readPos == 0 || len(p) == 0 {
|
|
bytesRead, bytesWritten, err = w.converter.Convert(p, w.writeBuffer[w.writePos:])
|
|
totalBytesRead += bytesRead
|
|
w.writePos += bytesWritten
|
|
w.readPos = 0
|
|
} else {
|
|
// we have left over bytes from previous write that weren't complete and there's at least
|
|
// one byte being written, fill read buffer with p and try to convert, if we make progress
|
|
// we can continue conversion from p itself
|
|
bytesCopied := copy(w.readBuffer[w.readPos:], p)
|
|
|
|
bytesRead, bytesWritten, err = w.converter.Convert(w.readBuffer[:w.readPos+bytesCopied], w.writeBuffer[w.writePos:])
|
|
|
|
// if we made no progress, give up
|
|
if bytesRead <= w.readPos {
|
|
return 0, err
|
|
}
|
|
|
|
bytesRead -= w.readPos
|
|
totalBytesRead += bytesRead
|
|
|
|
w.readPos = 0
|
|
w.writePos += bytesWritten
|
|
}
|
|
|
|
// try to process all of p - lots of io functions don't like short writes.
|
|
//
|
|
// There are a few error cases we need to treat specially, as long as we've
|
|
// made progress on p, E2BIG and EILSEQ should not be fatal. EINVAL isn't
|
|
// fatal as long as the rest of p fits in our buffers.
|
|
for err != nil && bytesRead > 0 {
|
|
switch err {
|
|
case syscall.E2BIG:
|
|
err = w.Flush()
|
|
|
|
case syscall.EILSEQ:
|
|
// IGNORE suffix still reports the error on convert
|
|
err = nil
|
|
|
|
// if no more bytes, don't do an empty convert (resets state)
|
|
if totalBytesRead == len(p) {
|
|
break
|
|
}
|
|
|
|
case syscall.EINVAL:
|
|
// if the rest of p fits in read buffer copy it there
|
|
if len(p[totalBytesRead:]) <= len(w.readBuffer) {
|
|
w.readPos = copy(w.readBuffer, p[totalBytesRead:])
|
|
totalBytesRead += w.readPos
|
|
break
|
|
}
|
|
}
|
|
|
|
// if not an ignoreable err or Flush err
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
bytesRead, bytesWritten, err = w.converter.Convert(p[totalBytesRead:], w.writeBuffer[w.writePos:])
|
|
totalBytesRead += bytesRead
|
|
w.writePos += bytesWritten
|
|
}
|
|
|
|
return totalBytesRead, err
|
|
}
|
|
|
|
// Attempt to write any buffered data to destination writer. Returns error from
|
|
// Write call or io.ErrShortWrite if Write didn't report an error but also didn't
|
|
// accept all bytes given.
|
|
func (w *Writer) Flush() error {
|
|
if w.readPos < w.writePos {
|
|
bytesWritten, err := w.destination.Write(w.writeBuffer[:w.writePos])
|
|
|
|
if bytesWritten < 0 {
|
|
panic("iconv: writer returned negative count from Write")
|
|
}
|
|
|
|
if bytesWritten > 0 {
|
|
w.writePos = copy(w.writeBuffer, w.writeBuffer[bytesWritten:w.writePos])
|
|
}
|
|
|
|
if err == nil && w.writePos > 0 {
|
|
err = io.ErrShortWrite
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Perform a final write with empty buffer, which allows iconv to close any shift
|
|
// sequences. A Flush is performed if needed.
|
|
func (w *Writer) Close() error {
|
|
_, err := w.Write(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
// Reset state and direct writes to a new destination writer
|
|
func (w *Writer) Reset(destination io.Writer) {
|
|
w.converter.Reset()
|
|
|
|
*w = Writer{
|
|
destination: destination,
|
|
converter: w.converter,
|
|
readBuffer: w.readBuffer,
|
|
writeBuffer: w.writeBuffer,
|
|
}
|
|
}
|