Skip to content

Instantly share code, notes, and snippets.

@alexjoedt
Created June 29, 2024 06:28
Show Gist options
  • Save alexjoedt/7fb5bdcf7da4a94d8ef59d18d96e6712 to your computer and use it in GitHub Desktop.
Save alexjoedt/7fb5bdcf7da4a94d8ef59d18d96e6712 to your computer and use it in GitHub Desktop.
package bytes
import (
"encoding/binary"
"io"
)
// ErrBufferOverflow is triggered when the buffer overflows.
var (
ErrBufferOverflow = errors.New("buffer overflow")
)
// FixedBuffer is a builder for binary data with a fixed size.
// This builder allows for efficient management
// of a fixed buffer without dynamic memory allocations during runtime.
type FixedBuffer struct {
readIndex int
writeIndex int
buf []byte
order binary.ByteOrder
}
// NewFixedBuffer creates a new instance of FixedBuffer with the
// specified size and a default byte order.
func NewFixedBuffer(size int, order binary.ByteOrder) *FixedBuffer {
return &FixedBuffer{
buf: make([]byte, size),
order: order,
}
}
// checkSize checks if there is enough space in the buffer to write n bytes.
func (bb *FixedBuffer) checkSize(n int) error {
if bb.writeIndex+n > len(bb.buf) {
return ErrBufferOverflow
}
return nil
}
// WriteData writes data to the buffer using the default byte order.
func (bb *FixedBuffer) WriteData(data any) error {
return bb.writeData(data, bb.order)
}
// WriteDataOrder writes data to the buffer using the specified byte order.
func (bb *FixedBuffer) WriteDataOrder(data any, order binary.ByteOrder) error {
return bb.writeData(data, order)
}
// writeData is a helper function that writes data to the buffer.
func (bb *FixedBuffer) writeData(data any, order binary.ByteOrder) error {
switch v := data.(type) {
case string:
_, err := bb.Write([]byte(v))
if err != nil {
return err
}
default:
return binary.Write(bb, order, v)
}
return nil
}
// Bytes returns the current content of the buffer.
func (bb *FixedBuffer) Bytes() []byte {
return bb.buf[bb.readIndex:bb.writeIndex]
}
// Write implements the io.Writer interface and writes data to the buffer.
func (bb *FixedBuffer) Write(p []byte) (int, error) {
if err := bb.checkSize(len(p)); err != nil {
return 0, err
}
n := copy(bb.buf[bb.writeIndex:], p)
bb.writeIndex += n
return n, nil
}
// Read implements the io.Reader interface and reads data from the buffer.
func (bb *FixedBuffer) Read(p []byte) (int, error) {
if bb.readIndex >= bb.writeIndex {
return 0, io.EOF
}
n := copy(p, bb.buf[bb.readIndex:bb.writeIndex])
bb.readIndex += n
return n, nil
}
package bytes
import (
"bytes"
"encoding/binary"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWriteData(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
size int
order binary.ByteOrder
dataToWrite []any
expectedBytes []byte
expectedError error
}{
{
name: "write data",
order: binary.BigEndian,
size: 22,
dataToWrite: []any{
uint8(1),
uint16(2),
[]uint16{3, 4},
uint32(5),
uint64(6),
[]byte{7, 8},
"9",
},
expectedBytes: []byte{1, 0, 2, 0, 3, 0, 4, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 6, 7, 8, 57},
},
{
name: "write data which size is too big",
order: binary.BigEndian,
size: 1,
dataToWrite: []any{
[]byte{7, 8},
},
expectedBytes: []byte{},
expectedError: ErrBufferOverflow,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
a := assert.New(t)
bb := NewFixedBuffer(tc.size, tc.order)
for _, p := range tc.dataToWrite {
err := bb.WriteData(p)
a.Equal(tc.expectedError, err)
}
a.Equal(tc.expectedBytes, bb.Bytes())
})
}
}
func TestWriteRead(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
size int
order binary.ByteOrder
toWrite []byte
expectedBytes []byte
expWriteError error
expReadError error
}{
{
name: "simple write and read",
size: 100,
order: binary.BigEndian,
toWrite: []byte("this is a test"),
expectedBytes: []byte("this is a test"),
},
{
name: "buffer overflow",
size: 10,
order: binary.BigEndian,
toWrite: []byte("this string is too long for the buffer"),
expectedBytes: nil,
expWriteError: ErrBufferOverflow,
},
{
name: "empty write and read",
size: 100,
order: binary.BigEndian,
toWrite: []byte(""),
expectedBytes: []byte(""),
expReadError: io.EOF,
},
{
name: "write and read with exact buffer size",
size: 17,
order: binary.BigEndian,
toWrite: []byte("exact buffer size"),
expectedBytes: []byte("exact buffer size"),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
a := assert.New(t)
bb := NewFixedBuffer(tc.size, tc.order)
n, err := bb.Write(tc.toWrite)
if tc.expWriteError != nil {
a.Equal(tc.expWriteError, err)
a.Equal(0, n)
} else {
require.NoError(t, err)
toRead := make([]byte, n)
m, err := bb.Read(toRead)
assert.Equal(t, tc.expReadError, err)
a.Equal(n, m)
a.Equal(tc.expectedBytes, toRead)
}
})
}
}
func TestReader(t *testing.T) {
toWrite := "123456789"
bb := NewFixedBuffer(10, binary.BigEndian)
err := bb.WriteData(toWrite)
require.NoError(t, err)
buf := new(bytes.Buffer)
n, err := io.Copy(buf, bb)
require.NoError(t, err)
assert.Equal(t, len(toWrite), int(n))
assert.Equal(t, buf.Bytes(), []byte(toWrite))
}
func TestWriter(t *testing.T) {
toWrite := "123456789"
buf := new(bytes.Buffer)
n, err := buf.WriteString(toWrite)
require.NoError(t, err)
bb := NewFixedBuffer(10, binary.BigEndian)
require.NoError(t, err)
nCopy, err := io.Copy(bb, buf)
require.NoError(t, err)
assert.Equal(t, int(nCopy), n)
assert.Equal(t, []byte(toWrite), bb.Bytes())
}
func BenchmarkAlloc(b *testing.B) {
bb := NewFixedBuffer(10, binary.BigEndian)
for i := 0; i < b.N; i++ {
bb.WriteData(uint8(1))
bb.WriteData(uint16(1))
bb.WriteData(uint32(1))
bb.WriteData(uint64(1))
bb.Write([]byte("123456789"))
bb.Write([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9})
b.ReportAllocs()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment