diff --git a/util/buffer/buffer.go b/util/buffer/delayed_buffer.go similarity index 100% rename from util/buffer/buffer.go rename to util/buffer/delayed_buffer.go diff --git a/util/buffer/buffer_test.go b/util/buffer/delayed_buffer_test.go similarity index 100% rename from util/buffer/buffer_test.go rename to util/buffer/delayed_buffer_test.go diff --git a/util/buffer/seeker_buffer.go b/util/buffer/seeker_buffer.go new file mode 100644 index 00000000..0ecfc71e --- /dev/null +++ b/util/buffer/seeker_buffer.go @@ -0,0 +1,78 @@ +package buffer + +import "io" + +var _ interface { + io.ReadCloser + io.ReadSeeker +} = (*SeekerBuffer)(nil) + +// Buffer is a ReadWriteCloser that supports seeking. It's intended to +// replicate the functionality of bytes.Buffer that I use in my projects. +// +// Note that the seeking is limited to the read marker; all writes are +// append-only. +type SeekerBuffer struct { + data []byte + pos int64 +} + +func NewSeekerBuffer(data []byte) *SeekerBuffer { + return &SeekerBuffer{ + data: data, + } +} + +func (b *SeekerBuffer) Read(p []byte) (int, error) { + if b.pos >= int64(len(b.data)) { + return 0, io.EOF + } + + n := copy(p, b.data[b.pos:]) + b.pos += int64(n) + return n, nil +} + +func (b *SeekerBuffer) Write(p []byte) (int, error) { + b.data = append(b.data, p...) + return len(p), nil +} + +// Seek sets the read pointer to pos. +func (b *SeekerBuffer) Seek(offset int64, whence int) (int64, error) { + switch whence { + case io.SeekStart: + b.pos = offset + case io.SeekEnd: + b.pos = int64(len(b.data)) + offset + case io.SeekCurrent: + b.pos += offset + } + + return b.pos, nil +} + +// Rewind resets the read pointer to 0. +func (b *SeekerBuffer) Rewind() error { + if _, err := b.Seek(0, io.SeekStart); err != nil { + return err + } + return nil +} + +// Close clears all the data out of the buffer and sets the read position to 0. +func (b *SeekerBuffer) Close() error { + b.data = nil + b.pos = 0 + return nil +} + +// Len returns the length of data remaining to be read. +func (b *SeekerBuffer) Len() int { + return len(b.data[b.pos:]) +} + +// Bytes returns the underlying bytes from the current position. +func (b *SeekerBuffer) Bytes() []byte { + return b.data[b.pos:] +} diff --git a/util/buffer/seeker_buffer_test.go b/util/buffer/seeker_buffer_test.go new file mode 100644 index 00000000..915aa19d --- /dev/null +++ b/util/buffer/seeker_buffer_test.go @@ -0,0 +1,55 @@ +package buffer + +import ( + "fmt" + "strings" + "testing" +) + +func noErrorT(t *testing.T, err error) { + if nil != err { + t.Fatalf("%s", err) + } +} + +func boolT(t *testing.T, cond bool, s ...string) { + if !cond { + what := strings.Join(s, ", ") + if len(what) > 0 { + what = ": " + what + } + t.Fatalf("assert.Bool failed%s", what) + } +} + +func TestSeeking(t *testing.T) { + partA := []byte("hello, ") + partB := []byte("world!") + + buf := NewSeekerBuffer(partA) + + boolT(t, buf.Len() == len(partA), fmt.Sprintf("on init: have length %d, want length %d", buf.Len(), len(partA))) + + b := make([]byte, 32) + + n, err := buf.Read(b) + noErrorT(t, err) + boolT(t, buf.Len() == 0, fmt.Sprintf("after reading 1: have length %d, want length 0", buf.Len())) + boolT(t, n == len(partA), fmt.Sprintf("after reading 2: have length %d, want length %d", n, len(partA))) + + n, err = buf.Write(partB) + noErrorT(t, err) + boolT(t, n == len(partB), fmt.Sprintf("after writing: have length %d, want length %d", n, len(partB))) + + n, err = buf.Read(b) + noErrorT(t, err) + boolT(t, buf.Len() == 0, fmt.Sprintf("after rereading 1: have length %d, want length 0", buf.Len())) + boolT(t, n == len(partB), fmt.Sprintf("after rereading 2: have length %d, want length %d", n, len(partB))) + + partsLen := len(partA) + len(partB) + _ = buf.Rewind() + boolT(t, buf.Len() == partsLen, fmt.Sprintf("after rewinding: have length %d, want length %d", buf.Len(), partsLen)) + + buf.Close() + boolT(t, buf.Len() == 0, fmt.Sprintf("after closing, have length %d, want length 0", buf.Len())) +}