From 101ef3798517dab334bb3702bd12307616a70fab Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 20 Feb 2026 19:27:31 +0000 Subject: [PATCH] feat(levin): portable storage section encode/decode Co-Authored-By: Charon --- node/levin/storage.go | 688 +++++++++++++++++++++++++++++++++++++ node/levin/storage_test.go | 337 ++++++++++++++++++ 2 files changed, 1025 insertions(+) create mode 100644 node/levin/storage.go create mode 100644 node/levin/storage_test.go diff --git a/node/levin/storage.go b/node/levin/storage.go new file mode 100644 index 0000000..173ac0b --- /dev/null +++ b/node/levin/storage.go @@ -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) + } +} diff --git a/node/levin/storage_test.go b/node/levin/storage_test.go new file mode 100644 index 0000000..ae16c52 --- /dev/null +++ b/node/levin/storage_test.go @@ -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) +}