This commit is contained in:
2025-04-11 13:31:02 +02:00
parent ba1196a962
commit 03f30f1968
48 changed files with 1685 additions and 474 deletions

81
varn/buffer.go Normal file
View File

@@ -0,0 +1,81 @@
package varn
import "io"
// Buffer is a simple buffer reader that allows reading bytes and
// variable-length integers. It is not thread-safe and should not be used
// concurrently.
type Buffer struct {
pos *int
buf []byte
}
// NewBuffer creates a new Buffer instance.
func NewBuffer(buf []byte) Buffer {
zero := 0
return Buffer{&zero, buf}
}
// ReadBytes reads n bytes from the buffer.
func (buf Buffer) ReadBytes(n int) ([]byte, error) {
if *buf.pos >= len(buf.buf) {
return nil, io.EOF
}
if *buf.pos+n > len(buf.buf) {
return nil, io.ErrUnexpectedEOF
}
res := buf.buf[*buf.pos : *buf.pos+n]
*buf.pos = *buf.pos + n
return res, nil
}
func (buf Buffer) ReadByte() (byte, error) {
b, err := buf.ReadBytes(1)
if err != nil {
return 0, err
}
return b[0], nil
}
// ReadVarUint reads a variable-length unsigned integer from the buffer.
// Returns io.EOF if the end of the buffer is reached before a complete integer
// is read.
func (buf Buffer) ReadVarUint() (uint64, error) {
var value uint64 = 0
var shift uint64 = 0
for {
b, err := buf.ReadByte()
if err != nil {
return 0, err
}
value |= (uint64(b) & 0b01111111) << shift
shift += 7
if b&0b10000000 == 0 {
break
}
}
return value, nil
}
// ReadVarBytes reads a variable-length byte array from the buffer.
// It first reads a variable-length unsigned integer to determine the length
// of the byte array, and then reads that many bytes from the buffer.
// Returns the byte array and an error if any occurs during reading.
// The function will return io.EOF if the end of the buffer is reached before
// the specified number of bytes is read.
// If the length of the byte array is 0, it will return an empty byte slice.
// If the length is greater than the remaining bytes in the buffer, it will
// return io.EOF.
func (buf Buffer) ReadVarBytes() ([]byte, error) {
v, err := buf.ReadVarUint()
if err != nil {
return nil, err
}
b, err := buf.ReadBytes(int(v))
if err != nil {
return nil, err
}
return b, nil
}

155
varn/buffer_test.go Normal file
View File

@@ -0,0 +1,155 @@
package varn_test
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"git.intruders.space/public/opentimestamps/varn"
)
func TestNewBuffer(t *testing.T) {
data := []byte{0x01, 0x02, 0x03}
buf := varn.NewBuffer(data)
// Test the first byte to verify initialization
b, err := buf.ReadByte()
assert.NoError(t, err)
assert.Equal(t, byte(0x01), b)
// Test reading the second byte
b, err = buf.ReadByte()
assert.NoError(t, err)
assert.Equal(t, byte(0x02), b)
// Test reading the third byte
b, err = buf.ReadByte()
assert.NoError(t, err)
assert.Equal(t, byte(0x03), b)
// Test reading beyond the buffer
_, err = buf.ReadByte()
assert.ErrorIs(t, err, io.EOF)
// Test reading from an empty buffer
emptyBuf := varn.NewBuffer([]byte{})
_, err = emptyBuf.ReadByte()
assert.ErrorIs(t, err, io.EOF)
// Test reading from a nil buffer
nilBuf := varn.NewBuffer(nil)
_, err = nilBuf.ReadByte()
assert.ErrorIs(t, err, io.EOF)
// Test reading from a buffer with a single byte
singleByteBuf := varn.NewBuffer([]byte{0xFF})
b, err = singleByteBuf.ReadByte()
assert.NoError(t, err)
assert.Equal(t, byte(0xFF), b)
}
func TestReadBytes(t *testing.T) {
data := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
buf := varn.NewBuffer(data)
// Read 3 bytes
bytes, err := buf.ReadBytes(3)
assert.NoError(t, err)
assert.Equal(t, []byte{0x01, 0x02, 0x03}, bytes)
// Read 2 more bytes
bytes, err = buf.ReadBytes(2)
assert.NoError(t, err)
assert.Equal(t, []byte{0x04, 0x05}, bytes)
// Try to read beyond the buffer
_, err = buf.ReadBytes(1)
assert.ErrorIs(t, err, io.EOF)
}
func TestReadByte(t *testing.T) {
data := []byte{0x0A, 0x0B}
buf := varn.NewBuffer(data)
// Read first byte
b, err := buf.ReadByte()
assert.NoError(t, err)
assert.Equal(t, byte(0x0A), b)
// Read second byte
b, err = buf.ReadByte()
assert.NoError(t, err)
assert.Equal(t, byte(0x0B), b)
// Try to read beyond the buffer
_, err = buf.ReadByte()
assert.ErrorIs(t, err, io.EOF)
}
func TestReadVarUint(t *testing.T) {
testCases := []struct {
name string
input []byte
expected uint64
}{
{
name: "Single byte",
input: []byte{0x01},
expected: 1,
},
{
name: "Two bytes",
input: []byte{0x81, 0x01}, // 129 in varint encoding
expected: 129,
},
{
name: "Large number",
input: []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01}, // 536870911 in varint encoding
expected: 536870911,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
buf := varn.NewBuffer(tc.input)
val, err := buf.ReadVarUint()
assert.NoError(t, err)
assert.Equal(t, tc.expected, val)
})
}
// Test EOF
buf := varn.NewBuffer([]byte{0x81}) // Incomplete varint
_, err := buf.ReadVarUint()
assert.ErrorIs(t, err, io.EOF)
}
func TestReadVarBytes(t *testing.T) {
// Test normal case: length followed by data
data := []byte{0x03, 0x0A, 0x0B, 0x0C, 0x0D}
buf := varn.NewBuffer(data)
bytes, err := buf.ReadVarBytes()
assert.NoError(t, err)
assert.Equal(t, []byte{0x0A, 0x0B, 0x0C}, bytes)
// Test empty array
data = []byte{0x00, 0x01}
buf = varn.NewBuffer(data)
bytes, err = buf.ReadVarBytes()
assert.NoError(t, err)
assert.Equal(t, []byte{}, bytes)
// Test EOF during length read
buf = varn.NewBuffer([]byte{})
_, err = buf.ReadVarBytes()
assert.ErrorIs(t, err, io.EOF)
// Test error during data read (insufficient bytes)
data = []byte{0x03, 0x01}
buf = varn.NewBuffer(data)
_, err = buf.ReadVarBytes()
assert.ErrorIs(t, err, io.ErrUnexpectedEOF)
}

38
varn/varn.go Normal file
View File

@@ -0,0 +1,38 @@
// Package varn implements variable-length encoding for unsigned integers and
// byte slices. It is used in the OpenTimestamps protocol to encode
// instructions and attestations. The encoding is similar to the one used in
// Protocol Buffers, but with a different format for the variable-length
// integers. The encoding is designed to be compact and efficient, while still
// being easy to decode. The package provides functions to read and write
// variable-length integers and byte slices, as well as a Buffer type for
// reading and writing data in a more convenient way. The package is not
// thread-safe and should not be used concurrently. It is intended for use in
// the OpenTimestamps protocol and is not a general-purpose encoding library.
package varn
// AppendVarUint appends a variable-length unsigned integer to the buffer
func AppendVarUint(buf []byte, value uint64) []byte {
if value == 0 {
buf = append(buf, 0)
} else {
for value != 0 {
b := byte(value & 0b01111111)
if value > 0b01111111 {
b |= 0b10000000
}
buf = append(buf, b)
if value <= 0b01111111 {
break
}
value >>= 7
}
}
return buf
}
func AppendVarBytes(buf []byte, value []byte) []byte {
buf = AppendVarUint(buf, uint64(len(value)))
buf = append(buf, value...)
return buf
}

118
varn/varn_test.go Normal file
View File

@@ -0,0 +1,118 @@
package varn_test
import (
"bytes"
"encoding/hex"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.intruders.space/public/opentimestamps/varn"
)
// Helper functions for decoding (to verify encoding)
func readVarUint(t *testing.T, data []byte) (uint64, int) {
t.Helper()
require.NotZero(t, data, "empty buffer")
if data[0] == 0 {
return 0, 1
}
var value uint64
var bytesRead int
for i, b := range data {
bytesRead = i + 1
value |= uint64(b&0x7F) << uint(7*i)
if b&0x80 == 0 {
break
}
require.Less(t, i, 9, "varint too long") // 9 is max bytes needed for uint64
}
return value, bytesRead
}
func readVarBytes(t *testing.T, data []byte) ([]byte, int) {
t.Helper()
length, bytesRead := readVarUint(t, data)
require.GreaterOrEqual(t, uint64(len(data)-bytesRead), length, "buffer too short")
end := bytesRead + int(length)
return data[bytesRead:end], end
}
func TestAppendVarUint(t *testing.T) {
tests := []struct {
name string
value uint64
expected string // hex representation of expected bytes
}{
{"zero", 0, "00"},
{"small value", 42, "2a"},
{"value below 128", 127, "7f"},
{"value requiring 2 bytes", 128, "8001"},
{"medium value", 300, "ac02"},
{"large value", 1234567890, "d285d8cc04"}, // Updated from "d2a6e58e07" to correct encoding
{"max uint64", 18446744073709551615, "ffffffffffffffffff01"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf []byte
result := varn.AppendVarUint(buf, tt.value)
// Check encoding
expected, err := hex.DecodeString(tt.expected)
assert.NoError(t, err)
assert.Equal(t, expected, result)
// Verify by decoding
decoded, _ := readVarUint(t, result)
assert.Equal(t, tt.value, decoded)
})
}
}
func TestAppendVarBytes(t *testing.T) {
tests := []struct {
name string
value []byte
}{
{"empty", []byte{}},
{"small", []byte{0x01, 0x02, 0x03}},
{"medium", bytes.Repeat([]byte{0xAA}, 127)},
{"large", bytes.Repeat([]byte{0xBB}, 256)},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf []byte
result := varn.AppendVarBytes(buf, tt.value)
// Verify by decoding
decoded, _ := readVarBytes(t, result)
assert.Equal(t, tt.value, decoded)
// Check that length is properly encoded
lenEncoded, _ := readVarUint(t, result)
assert.Equal(t, uint64(len(tt.value)), lenEncoded)
})
}
}
func TestAppendingToExistingBuffer(t *testing.T) {
existing := []byte{0xFF, 0xFF}
// Test AppendVarUint
result := varn.AppendVarUint(existing, 42)
assert.Equal(t, []byte{0xFF, 0xFF, 0x2A}, result)
// Test AppendVarBytes
data := []byte{0x01, 0x02, 0x03}
result = varn.AppendVarBytes(existing, data)
assert.Equal(t, []byte{0xFF, 0xFF, 0x03, 0x01, 0x02, 0x03}, result)
}