feat(levin): portable storage section encode/decode
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
abc88f5c7a
commit
101ef37985
2 changed files with 1025 additions and 0 deletions
688
node/levin/storage.go
Normal file
688
node/levin/storage.go
Normal file
|
|
@ -0,0 +1,688 @@
|
|||
// Copyright (c) 2024-2026 Lethean Contributors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package levin
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Portable storage signatures and version (9-byte header).
|
||||
const (
|
||||
StorageSignatureA uint32 = 0x01011101
|
||||
StorageSignatureB uint32 = 0x01020101
|
||||
StorageVersion uint8 = 1
|
||||
StorageHeaderSize = 9
|
||||
)
|
||||
|
||||
// Type tags for portable storage entries.
|
||||
const (
|
||||
TypeInt64 uint8 = 1
|
||||
TypeInt32 uint8 = 2
|
||||
TypeInt16 uint8 = 3
|
||||
TypeInt8 uint8 = 4
|
||||
TypeUint64 uint8 = 5
|
||||
TypeUint32 uint8 = 6
|
||||
TypeUint16 uint8 = 7
|
||||
TypeUint8 uint8 = 8
|
||||
TypeDouble uint8 = 9
|
||||
TypeString uint8 = 10
|
||||
TypeBool uint8 = 11
|
||||
TypeObject uint8 = 12
|
||||
|
||||
ArrayFlag uint8 = 0x80
|
||||
)
|
||||
|
||||
// Sentinel errors for storage encoding and decoding.
|
||||
var (
|
||||
ErrStorageBadSignature = errors.New("levin: bad storage signature")
|
||||
ErrStorageTruncated = errors.New("levin: truncated storage data")
|
||||
ErrStorageBadVersion = errors.New("levin: unsupported storage version")
|
||||
ErrStorageNameTooLong = errors.New("levin: entry name exceeds 255 bytes")
|
||||
ErrStorageTypeMismatch = errors.New("levin: value type mismatch")
|
||||
ErrStorageUnknownType = errors.New("levin: unknown type tag")
|
||||
)
|
||||
|
||||
// Section is an ordered map of named values forming a portable storage section.
|
||||
// Field iteration order is always alphabetical by key for deterministic encoding.
|
||||
type Section map[string]Value
|
||||
|
||||
// Value holds a typed portable storage value. Use the constructor functions
|
||||
// (Uint64Val, StringVal, ObjectVal, etc.) to create instances.
|
||||
type Value struct {
|
||||
Type uint8
|
||||
|
||||
// Exactly one of these is populated, determined by Type.
|
||||
intVal int64
|
||||
uintVal uint64
|
||||
floatVal float64
|
||||
boolVal bool
|
||||
bytesVal []byte
|
||||
objectVal Section
|
||||
|
||||
// Arrays — exactly one populated when Type has ArrayFlag set.
|
||||
uint64Array []uint64
|
||||
uint32Array []uint32
|
||||
stringArray [][]byte
|
||||
objectArray []Section
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scalar constructors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Uint64Val creates a Value of TypeUint64.
|
||||
func Uint64Val(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} }
|
||||
|
||||
// Uint32Val creates a Value of TypeUint32.
|
||||
func Uint32Val(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} }
|
||||
|
||||
// Uint16Val creates a Value of TypeUint16.
|
||||
func Uint16Val(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} }
|
||||
|
||||
// Uint8Val creates a Value of TypeUint8.
|
||||
func Uint8Val(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} }
|
||||
|
||||
// Int64Val creates a Value of TypeInt64.
|
||||
func Int64Val(v int64) Value { return Value{Type: TypeInt64, intVal: v} }
|
||||
|
||||
// Int32Val creates a Value of TypeInt32.
|
||||
func Int32Val(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} }
|
||||
|
||||
// Int16Val creates a Value of TypeInt16.
|
||||
func Int16Val(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} }
|
||||
|
||||
// Int8Val creates a Value of TypeInt8.
|
||||
func Int8Val(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} }
|
||||
|
||||
// BoolVal creates a Value of TypeBool.
|
||||
func BoolVal(v bool) Value { return Value{Type: TypeBool, boolVal: v} }
|
||||
|
||||
// DoubleVal creates a Value of TypeDouble.
|
||||
func DoubleVal(v float64) Value { return Value{Type: TypeDouble, floatVal: v} }
|
||||
|
||||
// StringVal creates a Value of TypeString. The slice is not copied.
|
||||
func StringVal(v []byte) Value { return Value{Type: TypeString, bytesVal: v} }
|
||||
|
||||
// ObjectVal creates a Value of TypeObject wrapping a nested Section.
|
||||
func ObjectVal(s Section) Value { return Value{Type: TypeObject, objectVal: s} }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Array constructors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Uint64ArrayVal creates a typed array of uint64 values.
|
||||
func Uint64ArrayVal(vs []uint64) Value {
|
||||
return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs}
|
||||
}
|
||||
|
||||
// Uint32ArrayVal creates a typed array of uint32 values.
|
||||
func Uint32ArrayVal(vs []uint32) Value {
|
||||
return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs}
|
||||
}
|
||||
|
||||
// StringArrayVal creates a typed array of byte-string values.
|
||||
func StringArrayVal(vs [][]byte) Value {
|
||||
return Value{Type: ArrayFlag | TypeString, stringArray: vs}
|
||||
}
|
||||
|
||||
// ObjectArrayVal creates a typed array of Section values.
|
||||
func ObjectArrayVal(vs []Section) Value {
|
||||
return Value{Type: ArrayFlag | TypeObject, objectArray: vs}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Scalar accessors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// AsUint64 returns the uint64 value or an error on type mismatch.
|
||||
func (v Value) AsUint64() (uint64, error) {
|
||||
if v.Type != TypeUint64 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.uintVal, nil
|
||||
}
|
||||
|
||||
// AsUint32 returns the uint32 value or an error on type mismatch.
|
||||
func (v Value) AsUint32() (uint32, error) {
|
||||
if v.Type != TypeUint32 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return uint32(v.uintVal), nil
|
||||
}
|
||||
|
||||
// AsUint16 returns the uint16 value or an error on type mismatch.
|
||||
func (v Value) AsUint16() (uint16, error) {
|
||||
if v.Type != TypeUint16 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return uint16(v.uintVal), nil
|
||||
}
|
||||
|
||||
// AsUint8 returns the uint8 value or an error on type mismatch.
|
||||
func (v Value) AsUint8() (uint8, error) {
|
||||
if v.Type != TypeUint8 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return uint8(v.uintVal), nil
|
||||
}
|
||||
|
||||
// AsInt64 returns the int64 value or an error on type mismatch.
|
||||
func (v Value) AsInt64() (int64, error) {
|
||||
if v.Type != TypeInt64 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.intVal, nil
|
||||
}
|
||||
|
||||
// AsInt32 returns the int32 value or an error on type mismatch.
|
||||
func (v Value) AsInt32() (int32, error) {
|
||||
if v.Type != TypeInt32 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return int32(v.intVal), nil
|
||||
}
|
||||
|
||||
// AsInt16 returns the int16 value or an error on type mismatch.
|
||||
func (v Value) AsInt16() (int16, error) {
|
||||
if v.Type != TypeInt16 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return int16(v.intVal), nil
|
||||
}
|
||||
|
||||
// AsInt8 returns the int8 value or an error on type mismatch.
|
||||
func (v Value) AsInt8() (int8, error) {
|
||||
if v.Type != TypeInt8 {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return int8(v.intVal), nil
|
||||
}
|
||||
|
||||
// AsBool returns the bool value or an error on type mismatch.
|
||||
func (v Value) AsBool() (bool, error) {
|
||||
if v.Type != TypeBool {
|
||||
return false, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.boolVal, nil
|
||||
}
|
||||
|
||||
// AsDouble returns the float64 value or an error on type mismatch.
|
||||
func (v Value) AsDouble() (float64, error) {
|
||||
if v.Type != TypeDouble {
|
||||
return 0, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.floatVal, nil
|
||||
}
|
||||
|
||||
// AsString returns the byte-string value or an error on type mismatch.
|
||||
func (v Value) AsString() ([]byte, error) {
|
||||
if v.Type != TypeString {
|
||||
return nil, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.bytesVal, nil
|
||||
}
|
||||
|
||||
// AsSection returns the nested Section or an error on type mismatch.
|
||||
func (v Value) AsSection() (Section, error) {
|
||||
if v.Type != TypeObject {
|
||||
return nil, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.objectVal, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Array accessors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// AsUint64Array returns the []uint64 array or an error on type mismatch.
|
||||
func (v Value) AsUint64Array() ([]uint64, error) {
|
||||
if v.Type != (ArrayFlag | TypeUint64) {
|
||||
return nil, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.uint64Array, nil
|
||||
}
|
||||
|
||||
// AsUint32Array returns the []uint32 array or an error on type mismatch.
|
||||
func (v Value) AsUint32Array() ([]uint32, error) {
|
||||
if v.Type != (ArrayFlag | TypeUint32) {
|
||||
return nil, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.uint32Array, nil
|
||||
}
|
||||
|
||||
// AsStringArray returns the [][]byte array or an error on type mismatch.
|
||||
func (v Value) AsStringArray() ([][]byte, error) {
|
||||
if v.Type != (ArrayFlag | TypeString) {
|
||||
return nil, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.stringArray, nil
|
||||
}
|
||||
|
||||
// AsSectionArray returns the []Section array or an error on type mismatch.
|
||||
func (v Value) AsSectionArray() ([]Section, error) {
|
||||
if v.Type != (ArrayFlag | TypeObject) {
|
||||
return nil, ErrStorageTypeMismatch
|
||||
}
|
||||
return v.objectArray, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Encoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// EncodeStorage serialises a Section to the portable storage binary format,
|
||||
// including the 9-byte header. Keys are sorted alphabetically to ensure
|
||||
// deterministic output.
|
||||
func EncodeStorage(s Section) ([]byte, error) {
|
||||
buf := make([]byte, 0, 256)
|
||||
|
||||
// 9-byte storage header.
|
||||
var hdr [StorageHeaderSize]byte
|
||||
binary.LittleEndian.PutUint32(hdr[0:4], StorageSignatureA)
|
||||
binary.LittleEndian.PutUint32(hdr[4:8], StorageSignatureB)
|
||||
hdr[8] = StorageVersion
|
||||
buf = append(buf, hdr[:]...)
|
||||
|
||||
// Encode root section.
|
||||
out, err := encodeSection(buf, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// encodeSection appends a section (entry count + entries) to buf.
|
||||
func encodeSection(buf []byte, s Section) ([]byte, error) {
|
||||
// Sort keys for deterministic output.
|
||||
keys := make([]string, 0, len(s))
|
||||
for k := range s {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// Entry count as varint.
|
||||
buf = append(buf, PackVarint(uint64(len(keys)))...)
|
||||
|
||||
for _, name := range keys {
|
||||
v := s[name]
|
||||
|
||||
// Name: uint8 length + raw bytes.
|
||||
if len(name) > 255 {
|
||||
return nil, ErrStorageNameTooLong
|
||||
}
|
||||
buf = append(buf, byte(len(name)))
|
||||
buf = append(buf, name...)
|
||||
|
||||
// Type tag.
|
||||
buf = append(buf, v.Type)
|
||||
|
||||
// Value.
|
||||
var err error
|
||||
buf, err = encodeValue(buf, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// encodeValue appends the encoded representation of a value (without the
|
||||
// type tag, which is written by the caller).
|
||||
func encodeValue(buf []byte, v Value) ([]byte, error) {
|
||||
// Array types.
|
||||
if v.Type&ArrayFlag != 0 {
|
||||
return encodeArray(buf, v)
|
||||
}
|
||||
|
||||
switch v.Type {
|
||||
case TypeUint64:
|
||||
var tmp [8]byte
|
||||
binary.LittleEndian.PutUint64(tmp[:], v.uintVal)
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeInt64:
|
||||
var tmp [8]byte
|
||||
binary.LittleEndian.PutUint64(tmp[:], uint64(v.intVal))
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeDouble:
|
||||
var tmp [8]byte
|
||||
binary.LittleEndian.PutUint64(tmp[:], math.Float64bits(v.floatVal))
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeUint32:
|
||||
var tmp [4]byte
|
||||
binary.LittleEndian.PutUint32(tmp[:], uint32(v.uintVal))
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeInt32:
|
||||
var tmp [4]byte
|
||||
binary.LittleEndian.PutUint32(tmp[:], uint32(v.intVal))
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeUint16:
|
||||
var tmp [2]byte
|
||||
binary.LittleEndian.PutUint16(tmp[:], uint16(v.uintVal))
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeInt16:
|
||||
var tmp [2]byte
|
||||
binary.LittleEndian.PutUint16(tmp[:], uint16(v.intVal))
|
||||
return append(buf, tmp[:]...), nil
|
||||
|
||||
case TypeUint8:
|
||||
return append(buf, byte(v.uintVal)), nil
|
||||
|
||||
case TypeInt8:
|
||||
return append(buf, byte(v.intVal)), nil
|
||||
|
||||
case TypeBool:
|
||||
if v.boolVal {
|
||||
return append(buf, 1), nil
|
||||
}
|
||||
return append(buf, 0), nil
|
||||
|
||||
case TypeString:
|
||||
buf = append(buf, PackVarint(uint64(len(v.bytesVal)))...)
|
||||
return append(buf, v.bytesVal...), nil
|
||||
|
||||
case TypeObject:
|
||||
return encodeSection(buf, v.objectVal)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: 0x%02x", ErrStorageUnknownType, v.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// encodeArray appends array data: varint(count) + packed elements.
|
||||
func encodeArray(buf []byte, v Value) ([]byte, error) {
|
||||
elemType := v.Type & ^ArrayFlag
|
||||
|
||||
switch elemType {
|
||||
case TypeUint64:
|
||||
buf = append(buf, PackVarint(uint64(len(v.uint64Array)))...)
|
||||
for _, n := range v.uint64Array {
|
||||
var tmp [8]byte
|
||||
binary.LittleEndian.PutUint64(tmp[:], n)
|
||||
buf = append(buf, tmp[:]...)
|
||||
}
|
||||
return buf, nil
|
||||
|
||||
case TypeUint32:
|
||||
buf = append(buf, PackVarint(uint64(len(v.uint32Array)))...)
|
||||
for _, n := range v.uint32Array {
|
||||
var tmp [4]byte
|
||||
binary.LittleEndian.PutUint32(tmp[:], n)
|
||||
buf = append(buf, tmp[:]...)
|
||||
}
|
||||
return buf, nil
|
||||
|
||||
case TypeString:
|
||||
buf = append(buf, PackVarint(uint64(len(v.stringArray)))...)
|
||||
for _, s := range v.stringArray {
|
||||
buf = append(buf, PackVarint(uint64(len(s)))...)
|
||||
buf = append(buf, s...)
|
||||
}
|
||||
return buf, nil
|
||||
|
||||
case TypeObject:
|
||||
buf = append(buf, PackVarint(uint64(len(v.objectArray)))...)
|
||||
var err error
|
||||
for _, sec := range v.objectArray {
|
||||
buf, err = encodeSection(buf, sec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: array of 0x%02x", ErrStorageUnknownType, elemType)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Decoder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// DecodeStorage deserialises portable storage binary data (including the
|
||||
// 9-byte header) into a Section.
|
||||
func DecodeStorage(data []byte) (Section, error) {
|
||||
if len(data) < StorageHeaderSize {
|
||||
return nil, ErrStorageTruncated
|
||||
}
|
||||
|
||||
sigA := binary.LittleEndian.Uint32(data[0:4])
|
||||
sigB := binary.LittleEndian.Uint32(data[4:8])
|
||||
ver := data[8]
|
||||
|
||||
if sigA != StorageSignatureA || sigB != StorageSignatureB {
|
||||
return nil, ErrStorageBadSignature
|
||||
}
|
||||
if ver != StorageVersion {
|
||||
return nil, ErrStorageBadVersion
|
||||
}
|
||||
|
||||
s, _, err := decodeSection(data[StorageHeaderSize:])
|
||||
return s, err
|
||||
}
|
||||
|
||||
// decodeSection reads a section from buf and returns the section plus
|
||||
// the number of bytes consumed.
|
||||
func decodeSection(buf []byte) (Section, int, error) {
|
||||
count, n, err := UnpackVarint(buf)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("section entry count: %w", err)
|
||||
}
|
||||
off := n
|
||||
|
||||
s := make(Section, int(count))
|
||||
|
||||
for i := uint64(0); i < count; i++ {
|
||||
// Name length (1 byte).
|
||||
if off >= len(buf) {
|
||||
return nil, 0, ErrStorageTruncated
|
||||
}
|
||||
nameLen := int(buf[off])
|
||||
off++
|
||||
|
||||
// Name bytes.
|
||||
if off+nameLen > len(buf) {
|
||||
return nil, 0, ErrStorageTruncated
|
||||
}
|
||||
name := string(buf[off : off+nameLen])
|
||||
off += nameLen
|
||||
|
||||
// Type tag (1 byte).
|
||||
if off >= len(buf) {
|
||||
return nil, 0, ErrStorageTruncated
|
||||
}
|
||||
tag := buf[off]
|
||||
off++
|
||||
|
||||
// Value.
|
||||
val, consumed, err := decodeValue(buf[off:], tag)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("field %q: %w", name, err)
|
||||
}
|
||||
off += consumed
|
||||
|
||||
s[name] = val
|
||||
}
|
||||
|
||||
return s, off, nil
|
||||
}
|
||||
|
||||
// decodeValue reads a value of the given type tag from buf and returns
|
||||
// the value plus bytes consumed.
|
||||
func decodeValue(buf []byte, tag uint8) (Value, int, error) {
|
||||
// Array types.
|
||||
if tag&ArrayFlag != 0 {
|
||||
return decodeArray(buf, tag)
|
||||
}
|
||||
|
||||
switch tag {
|
||||
case TypeUint64:
|
||||
if len(buf) < 8 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
v := binary.LittleEndian.Uint64(buf[:8])
|
||||
return Value{Type: TypeUint64, uintVal: v}, 8, nil
|
||||
|
||||
case TypeInt64:
|
||||
if len(buf) < 8 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
v := int64(binary.LittleEndian.Uint64(buf[:8]))
|
||||
return Value{Type: TypeInt64, intVal: v}, 8, nil
|
||||
|
||||
case TypeDouble:
|
||||
if len(buf) < 8 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
bits := binary.LittleEndian.Uint64(buf[:8])
|
||||
return Value{Type: TypeDouble, floatVal: math.Float64frombits(bits)}, 8, nil
|
||||
|
||||
case TypeUint32:
|
||||
if len(buf) < 4 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
v := binary.LittleEndian.Uint32(buf[:4])
|
||||
return Value{Type: TypeUint32, uintVal: uint64(v)}, 4, nil
|
||||
|
||||
case TypeInt32:
|
||||
if len(buf) < 4 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
v := int32(binary.LittleEndian.Uint32(buf[:4]))
|
||||
return Value{Type: TypeInt32, intVal: int64(v)}, 4, nil
|
||||
|
||||
case TypeUint16:
|
||||
if len(buf) < 2 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
v := binary.LittleEndian.Uint16(buf[:2])
|
||||
return Value{Type: TypeUint16, uintVal: uint64(v)}, 2, nil
|
||||
|
||||
case TypeInt16:
|
||||
if len(buf) < 2 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
v := int16(binary.LittleEndian.Uint16(buf[:2]))
|
||||
return Value{Type: TypeInt16, intVal: int64(v)}, 2, nil
|
||||
|
||||
case TypeUint8:
|
||||
if len(buf) < 1 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
return Value{Type: TypeUint8, uintVal: uint64(buf[0])}, 1, nil
|
||||
|
||||
case TypeInt8:
|
||||
if len(buf) < 1 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
return Value{Type: TypeInt8, intVal: int64(int8(buf[0]))}, 1, nil
|
||||
|
||||
case TypeBool:
|
||||
if len(buf) < 1 {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
return Value{Type: TypeBool, boolVal: buf[0] != 0}, 1, nil
|
||||
|
||||
case TypeString:
|
||||
strLen, n, err := UnpackVarint(buf)
|
||||
if err != nil {
|
||||
return Value{}, 0, err
|
||||
}
|
||||
if uint64(len(buf)-n) < strLen {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
data := make([]byte, strLen)
|
||||
copy(data, buf[n:n+int(strLen)])
|
||||
return Value{Type: TypeString, bytesVal: data}, n + int(strLen), nil
|
||||
|
||||
case TypeObject:
|
||||
sec, consumed, err := decodeSection(buf)
|
||||
if err != nil {
|
||||
return Value{}, 0, err
|
||||
}
|
||||
return Value{Type: TypeObject, objectVal: sec}, consumed, nil
|
||||
|
||||
default:
|
||||
return Value{}, 0, fmt.Errorf("%w: 0x%02x", ErrStorageUnknownType, tag)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeArray reads a typed array from buf (tag has ArrayFlag set).
|
||||
func decodeArray(buf []byte, tag uint8) (Value, int, error) {
|
||||
elemType := tag & ^ArrayFlag
|
||||
|
||||
count, n, err := UnpackVarint(buf)
|
||||
if err != nil {
|
||||
return Value{}, 0, err
|
||||
}
|
||||
off := n
|
||||
|
||||
switch elemType {
|
||||
case TypeUint64:
|
||||
arr := make([]uint64, count)
|
||||
for i := uint64(0); i < count; i++ {
|
||||
if off+8 > len(buf) {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
arr[i] = binary.LittleEndian.Uint64(buf[off : off+8])
|
||||
off += 8
|
||||
}
|
||||
return Value{Type: tag, uint64Array: arr}, off, nil
|
||||
|
||||
case TypeUint32:
|
||||
arr := make([]uint32, count)
|
||||
for i := uint64(0); i < count; i++ {
|
||||
if off+4 > len(buf) {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
arr[i] = binary.LittleEndian.Uint32(buf[off : off+4])
|
||||
off += 4
|
||||
}
|
||||
return Value{Type: tag, uint32Array: arr}, off, nil
|
||||
|
||||
case TypeString:
|
||||
arr := make([][]byte, count)
|
||||
for i := uint64(0); i < count; i++ {
|
||||
strLen, sn, err := UnpackVarint(buf[off:])
|
||||
if err != nil {
|
||||
return Value{}, 0, err
|
||||
}
|
||||
off += sn
|
||||
if uint64(len(buf)-off) < strLen {
|
||||
return Value{}, 0, ErrStorageTruncated
|
||||
}
|
||||
data := make([]byte, strLen)
|
||||
copy(data, buf[off:off+int(strLen)])
|
||||
arr[i] = data
|
||||
off += int(strLen)
|
||||
}
|
||||
return Value{Type: tag, stringArray: arr}, off, nil
|
||||
|
||||
case TypeObject:
|
||||
arr := make([]Section, count)
|
||||
for i := uint64(0); i < count; i++ {
|
||||
sec, consumed, err := decodeSection(buf[off:])
|
||||
if err != nil {
|
||||
return Value{}, 0, err
|
||||
}
|
||||
arr[i] = sec
|
||||
off += consumed
|
||||
}
|
||||
return Value{Type: tag, objectArray: arr}, off, nil
|
||||
|
||||
default:
|
||||
return Value{}, 0, fmt.Errorf("%w: array of 0x%02x", ErrStorageUnknownType, elemType)
|
||||
}
|
||||
}
|
||||
337
node/levin/storage_test.go
Normal file
337
node/levin/storage_test.go
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
// Copyright (c) 2024-2026 Lethean Contributors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package levin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodeStorage_EmptySection(t *testing.T) {
|
||||
s := Section{}
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
// 9-byte header + 1-byte varint(0) = 10 bytes.
|
||||
assert.Len(t, data, 10)
|
||||
|
||||
// Verify storage header signatures.
|
||||
assert.Equal(t, byte(0x01), data[0])
|
||||
assert.Equal(t, byte(0x11), data[1])
|
||||
assert.Equal(t, byte(0x01), data[2])
|
||||
assert.Equal(t, byte(0x01), data[3])
|
||||
assert.Equal(t, byte(0x01), data[4])
|
||||
assert.Equal(t, byte(0x01), data[5])
|
||||
assert.Equal(t, byte(0x02), data[6])
|
||||
assert.Equal(t, byte(0x01), data[7])
|
||||
|
||||
// Version byte.
|
||||
assert.Equal(t, byte(1), data[8])
|
||||
|
||||
// Entry count varint: 0.
|
||||
assert.Equal(t, byte(0x00), data[9])
|
||||
}
|
||||
|
||||
func TestStorage_PrimitivesRoundTrip(t *testing.T) {
|
||||
s := Section{
|
||||
"u64": Uint64Val(0xDEADBEEFCAFEBABE),
|
||||
"u32": Uint32Val(0xCAFEBABE),
|
||||
"u16": Uint16Val(0xBEEF),
|
||||
"u8": Uint8Val(42),
|
||||
"i64": Int64Val(-9223372036854775808),
|
||||
"i32": Int32Val(-2147483648),
|
||||
"i16": Int16Val(-32768),
|
||||
"i8": Int8Val(-128),
|
||||
"flag": BoolVal(true),
|
||||
"height": StringVal([]byte("hello world")),
|
||||
"pi": DoubleVal(3.141592653589793),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unsigned integers.
|
||||
u64, err := decoded["u64"].AsUint64()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint64(0xDEADBEEFCAFEBABE), u64)
|
||||
|
||||
u32, err := decoded["u32"].AsUint32()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(0xCAFEBABE), u32)
|
||||
|
||||
u16, err := decoded["u16"].AsUint16()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint16(0xBEEF), u16)
|
||||
|
||||
u8, err := decoded["u8"].AsUint8()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint8(42), u8)
|
||||
|
||||
// Signed integers.
|
||||
i64, err := decoded["i64"].AsInt64()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(-9223372036854775808), i64)
|
||||
|
||||
i32, err := decoded["i32"].AsInt32()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(-2147483648), i32)
|
||||
|
||||
i16, err := decoded["i16"].AsInt16()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int16(-32768), i16)
|
||||
|
||||
i8, err := decoded["i8"].AsInt8()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int8(-128), i8)
|
||||
|
||||
// Bool.
|
||||
flag, err := decoded["flag"].AsBool()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, flag)
|
||||
|
||||
// String.
|
||||
str, err := decoded["height"].AsString()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("hello world"), str)
|
||||
|
||||
// Double.
|
||||
pi, err := decoded["pi"].AsDouble()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3.141592653589793, pi)
|
||||
}
|
||||
|
||||
func TestStorage_NestedObject(t *testing.T) {
|
||||
inner := Section{
|
||||
"port": Uint16Val(18080),
|
||||
"host": StringVal([]byte("127.0.0.1")),
|
||||
}
|
||||
outer := Section{
|
||||
"node_data": ObjectVal(inner),
|
||||
"version": Uint32Val(1),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(outer)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
ver, err := decoded["version"].AsUint32()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(1), ver)
|
||||
|
||||
innerDec, err := decoded["node_data"].AsSection()
|
||||
require.NoError(t, err)
|
||||
|
||||
port, err := innerDec["port"].AsUint16()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint16(18080), port)
|
||||
|
||||
host, err := innerDec["host"].AsString()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("127.0.0.1"), host)
|
||||
}
|
||||
|
||||
func TestStorage_Uint64Array(t *testing.T) {
|
||||
s := Section{
|
||||
"heights": Uint64ArrayVal([]uint64{10, 20, 30}),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
arr, err := decoded["heights"].AsUint64Array()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []uint64{10, 20, 30}, arr)
|
||||
}
|
||||
|
||||
func TestStorage_StringArray(t *testing.T) {
|
||||
s := Section{
|
||||
"peers": StringArrayVal([][]byte{[]byte("foo"), []byte("bar")}),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
arr, err := decoded["peers"].AsStringArray()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, arr, 2)
|
||||
assert.Equal(t, []byte("foo"), arr[0])
|
||||
assert.Equal(t, []byte("bar"), arr[1])
|
||||
}
|
||||
|
||||
func TestStorage_ObjectArray(t *testing.T) {
|
||||
sections := []Section{
|
||||
{"id": Uint32Val(1), "name": StringVal([]byte("alice"))},
|
||||
{"id": Uint32Val(2), "name": StringVal([]byte("bob"))},
|
||||
}
|
||||
s := Section{
|
||||
"nodes": ObjectArrayVal(sections),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
arr, err := decoded["nodes"].AsSectionArray()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, arr, 2)
|
||||
|
||||
id1, err := arr[0]["id"].AsUint32()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(1), id1)
|
||||
|
||||
name1, err := arr[0]["name"].AsString()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("alice"), name1)
|
||||
|
||||
id2, err := arr[1]["id"].AsUint32()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint32(2), id2)
|
||||
|
||||
name2, err := arr[1]["name"].AsString()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []byte("bob"), name2)
|
||||
}
|
||||
|
||||
func TestDecodeStorage_BadSignature(t *testing.T) {
|
||||
// Corrupt the first 4 bytes.
|
||||
data := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x01, 0x02, 0x01, 0x01, 0x00}
|
||||
_, err := DecodeStorage(data)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrStorageBadSignature)
|
||||
}
|
||||
|
||||
func TestDecodeStorage_TooShort(t *testing.T) {
|
||||
_, err := DecodeStorage([]byte{0x01, 0x11})
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrStorageTruncated)
|
||||
}
|
||||
|
||||
func TestStorage_ByteIdenticalReencode(t *testing.T) {
|
||||
s := Section{
|
||||
"alpha": Uint64Val(999),
|
||||
"bravo": StringVal([]byte("deterministic")),
|
||||
"charlie": BoolVal(false),
|
||||
"delta": ObjectVal(Section{
|
||||
"x": Int32Val(-42),
|
||||
"y": Int32Val(100),
|
||||
}),
|
||||
"echo": Uint64ArrayVal([]uint64{1, 2, 3}),
|
||||
}
|
||||
|
||||
data1, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data1)
|
||||
require.NoError(t, err)
|
||||
|
||||
data2, err := EncodeStorage(decoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, data1, data2, "re-encoded bytes must be identical")
|
||||
}
|
||||
|
||||
func TestStorage_TypeMismatchErrors(t *testing.T) {
|
||||
v := Uint64Val(42)
|
||||
|
||||
_, err := v.AsUint32()
|
||||
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
|
||||
|
||||
_, err = v.AsString()
|
||||
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
|
||||
|
||||
_, err = v.AsBool()
|
||||
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
|
||||
|
||||
_, err = v.AsSection()
|
||||
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
|
||||
|
||||
_, err = v.AsUint64Array()
|
||||
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
|
||||
}
|
||||
|
||||
func TestStorage_Uint32Array(t *testing.T) {
|
||||
s := Section{
|
||||
"ports": Uint32ArrayVal([]uint32{8080, 8443, 9090}),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
arr, err := decoded["ports"].AsUint32Array()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []uint32{8080, 8443, 9090}, arr)
|
||||
}
|
||||
|
||||
func TestDecodeStorage_BadVersion(t *testing.T) {
|
||||
// Valid signatures but version 2 instead of 1.
|
||||
data := []byte{0x01, 0x11, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x00}
|
||||
_, err := DecodeStorage(data)
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrStorageBadVersion)
|
||||
}
|
||||
|
||||
func TestStorage_EmptyArrays(t *testing.T) {
|
||||
s := Section{
|
||||
"empty_u64": Uint64ArrayVal([]uint64{}),
|
||||
"empty_str": StringArrayVal([][]byte{}),
|
||||
"empty_obj": ObjectArrayVal([]Section{}),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
u64arr, err := decoded["empty_u64"].AsUint64Array()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, u64arr)
|
||||
|
||||
strarr, err := decoded["empty_str"].AsStringArray()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, strarr)
|
||||
|
||||
objarr, err := decoded["empty_obj"].AsSectionArray()
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, objarr)
|
||||
}
|
||||
|
||||
func TestStorage_BoolFalseRoundTrip(t *testing.T) {
|
||||
s := Section{
|
||||
"off": BoolVal(false),
|
||||
"on": BoolVal(true),
|
||||
}
|
||||
|
||||
data, err := EncodeStorage(s)
|
||||
require.NoError(t, err)
|
||||
|
||||
decoded, err := DecodeStorage(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
off, err := decoded["off"].AsBool()
|
||||
require.NoError(t, err)
|
||||
assert.False(t, off)
|
||||
|
||||
on, err := decoded["on"].AsBool()
|
||||
require.NoError(t, err)
|
||||
assert.True(t, on)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue