go-p2p/node/levin/storage.go
Virgil c03b3410e6
All checks were successful
Security Scan / security (push) Successful in 10s
Test / test (push) Successful in 1m29s
refactor(node): remove legacy compatibility aliases
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-30 21:27:45 +00:00

725 lines
19 KiB
Go

// Copyright (c) 2024-2026 Lethean Contributors
// SPDX-License-Identifier: EUPL-1.2
package levin
import (
"encoding/binary"
"maps"
"math"
"slices"
core "dappco.re/go/core"
)
// 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 (
ErrorStorageBadSignature = core.E("levin.storage", "bad storage signature", nil)
ErrorStorageTruncated = core.E("levin.storage", "truncated storage data", nil)
ErrorStorageBadVersion = core.E("levin.storage", "unsupported storage version", nil)
ErrorStorageNameTooLong = core.E("levin.storage", "entry name exceeds 255 bytes", nil)
ErrorStorageTypeMismatch = core.E("levin.storage", "value type mismatch", nil)
ErrorStorageUnknownType = core.E("levin.storage", "unknown type tag", nil)
)
// Section is an ordered map of named values forming a portable storage section.
// Field iteration order is always alphabetical by key for deterministic encoding.
//
// section := Section{"id": StringValue([]byte("peer-1"))}
type Section map[string]Value
// Value holds a typed portable storage value. Use the constructor functions
// (Uint64Value, StringValue, ObjectValue, etc.) to create instances.
//
// value := StringValue([]byte("peer-1"))
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
// ---------------------------------------------------------------------------
// Uint64Value creates a Value of TypeUint64.
//
// value := Uint64Value(42)
func Uint64Value(v uint64) Value { return Value{Type: TypeUint64, uintVal: v} }
// Uint32Value creates a Value of TypeUint32.
//
// value := Uint32Value(42)
func Uint32Value(v uint32) Value { return Value{Type: TypeUint32, uintVal: uint64(v)} }
// Uint16Value creates a Value of TypeUint16.
//
// value := Uint16Value(42)
func Uint16Value(v uint16) Value { return Value{Type: TypeUint16, uintVal: uint64(v)} }
// Uint8Value creates a Value of TypeUint8.
//
// value := Uint8Value(42)
func Uint8Value(v uint8) Value { return Value{Type: TypeUint8, uintVal: uint64(v)} }
// Int64Value creates a Value of TypeInt64.
//
// value := Int64Value(42)
func Int64Value(v int64) Value { return Value{Type: TypeInt64, intVal: v} }
// Int32Value creates a Value of TypeInt32.
//
// value := Int32Value(42)
func Int32Value(v int32) Value { return Value{Type: TypeInt32, intVal: int64(v)} }
// Int16Value creates a Value of TypeInt16.
//
// value := Int16Value(42)
func Int16Value(v int16) Value { return Value{Type: TypeInt16, intVal: int64(v)} }
// Int8Value creates a Value of TypeInt8.
//
// value := Int8Value(42)
func Int8Value(v int8) Value { return Value{Type: TypeInt8, intVal: int64(v)} }
// BoolValue creates a Value of TypeBool.
//
// value := BoolValue(true)
func BoolValue(v bool) Value { return Value{Type: TypeBool, boolVal: v} }
// DoubleValue creates a Value of TypeDouble.
//
// value := DoubleValue(3.14)
func DoubleValue(v float64) Value { return Value{Type: TypeDouble, floatVal: v} }
// StringValue creates a Value of TypeString. The slice is not copied.
//
// value := StringValue([]byte("hello"))
func StringValue(v []byte) Value { return Value{Type: TypeString, bytesVal: v} }
// ObjectValue creates a Value of TypeObject wrapping a nested Section.
//
// value := ObjectValue(Section{"id": StringValue([]byte("peer-1"))})
func ObjectValue(s Section) Value { return Value{Type: TypeObject, objectVal: s} }
// ---------------------------------------------------------------------------
// Array constructors
// ---------------------------------------------------------------------------
// Uint64ArrayValue creates a typed array of uint64 values.
//
// value := Uint64ArrayValue([]uint64{1, 2, 3})
func Uint64ArrayValue(vs []uint64) Value {
return Value{Type: ArrayFlag | TypeUint64, uint64Array: vs}
}
// Uint32ArrayValue creates a typed array of uint32 values.
//
// value := Uint32ArrayValue([]uint32{1, 2, 3})
func Uint32ArrayValue(vs []uint32) Value {
return Value{Type: ArrayFlag | TypeUint32, uint32Array: vs}
}
// StringArrayValue creates a typed array of byte-string values.
//
// value := StringArrayValue([][]byte{[]byte("a"), []byte("b")})
func StringArrayValue(vs [][]byte) Value {
return Value{Type: ArrayFlag | TypeString, stringArray: vs}
}
// ObjectArrayValue creates a typed array of Section values.
//
// value := ObjectArrayValue([]Section{{"id": StringValue([]byte("peer-1"))}})
func ObjectArrayValue(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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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, ErrorStorageTypeMismatch
}
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.
//
// data, err := EncodeStorage(section)
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 := slices.Sorted(maps.Keys(s))
// 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, ErrorStorageNameTooLong
}
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, core.E("levin.encodeValue", core.Sprintf("unknown type tag: 0x%02x", v.Type), ErrorStorageUnknownType)
}
}
// 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, core.E("levin.encodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrorStorageUnknownType)
}
}
// ---------------------------------------------------------------------------
// Decoder
// ---------------------------------------------------------------------------
// DecodeStorage deserialises portable storage binary data (including the
// 9-byte header) into a Section.
//
// section, err := DecodeStorage(data)
func DecodeStorage(data []byte) (Section, error) {
if len(data) < StorageHeaderSize {
return nil, ErrorStorageTruncated
}
sigA := binary.LittleEndian.Uint32(data[0:4])
sigB := binary.LittleEndian.Uint32(data[4:8])
ver := data[8]
if sigA != StorageSignatureA || sigB != StorageSignatureB {
return nil, ErrorStorageBadSignature
}
if ver != StorageVersion {
return nil, ErrorStorageBadVersion
}
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, core.E("levin.decodeSection", "section entry count", err)
}
off := n
s := make(Section, int(count))
for range count {
// Name length (1 byte).
if off >= len(buf) {
return nil, 0, ErrorStorageTruncated
}
nameLen := int(buf[off])
off++
// Name bytes.
if off+nameLen > len(buf) {
return nil, 0, ErrorStorageTruncated
}
name := string(buf[off : off+nameLen])
off += nameLen
// Type tag (1 byte).
if off >= len(buf) {
return nil, 0, ErrorStorageTruncated
}
tag := buf[off]
off++
// Value.
val, consumed, err := decodeValue(buf[off:], tag)
if err != nil {
return nil, 0, core.E("levin.decodeSection", "field "+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, ErrorStorageTruncated
}
v := binary.LittleEndian.Uint64(buf[:8])
return Value{Type: TypeUint64, uintVal: v}, 8, nil
case TypeInt64:
if len(buf) < 8 {
return Value{}, 0, ErrorStorageTruncated
}
v := int64(binary.LittleEndian.Uint64(buf[:8]))
return Value{Type: TypeInt64, intVal: v}, 8, nil
case TypeDouble:
if len(buf) < 8 {
return Value{}, 0, ErrorStorageTruncated
}
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, ErrorStorageTruncated
}
v := binary.LittleEndian.Uint32(buf[:4])
return Value{Type: TypeUint32, uintVal: uint64(v)}, 4, nil
case TypeInt32:
if len(buf) < 4 {
return Value{}, 0, ErrorStorageTruncated
}
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, ErrorStorageTruncated
}
v := binary.LittleEndian.Uint16(buf[:2])
return Value{Type: TypeUint16, uintVal: uint64(v)}, 2, nil
case TypeInt16:
if len(buf) < 2 {
return Value{}, 0, ErrorStorageTruncated
}
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, ErrorStorageTruncated
}
return Value{Type: TypeUint8, uintVal: uint64(buf[0])}, 1, nil
case TypeInt8:
if len(buf) < 1 {
return Value{}, 0, ErrorStorageTruncated
}
return Value{Type: TypeInt8, intVal: int64(int8(buf[0]))}, 1, nil
case TypeBool:
if len(buf) < 1 {
return Value{}, 0, ErrorStorageTruncated
}
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, ErrorStorageTruncated
}
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, core.E("levin.decodeValue", core.Sprintf("unknown type tag: 0x%02x", tag), ErrorStorageUnknownType)
}
}
// 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 := range count {
if off+8 > len(buf) {
return Value{}, 0, ErrorStorageTruncated
}
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 := range count {
if off+4 > len(buf) {
return Value{}, 0, ErrorStorageTruncated
}
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 := range count {
strLen, sn, err := UnpackVarint(buf[off:])
if err != nil {
return Value{}, 0, err
}
off += sn
if uint64(len(buf)-off) < strLen {
return Value{}, 0, ErrorStorageTruncated
}
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 := range count {
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, core.E("levin.decodeArray", core.Sprintf("unknown type tag: array of 0x%02x", elemType), ErrorStorageUnknownType)
}
}