zap: absorb luxfi/codec → zap/codec subpackage (canonical ZAP wire codec home)

Move the entire luxfi/codec module content into luxfi/zap/codec as the
canonical home for the ZAP wire codec. luxfi/codec module is now
redundant and queued for archival; consumers should import
github.com/luxfi/zap/codec directly.

This decomplects the alias chain proto/zap_codec → codec/zapcodec into
one canonical home. The 'codec' name lives because the package IS the
wire codec; 'zap' is its namespace because ZAP is the wire it speaks.

Contents:
  - codec.go, errors.go, packing.go (root codec.Manager + helpers)
  - codecmock/  (go.uber.org/mock generated Manager mock)
  - linearcodec/ (big-endian linear codec, legacy compat)
  - reflectcodec/ (struct fielder + reflection type codec)
  - wrappers/ (packer/closer wrappers)
  - zapcodec/ (little-endian ZAP-native codec, sourced from the
    cleaned luxfi/zapcodec@v1.0.1 extraction)

All import paths rewritten github.com/luxfi/codec/* and
github.com/luxfi/zapcodec → github.com/luxfi/zap/codec/*.

Build: GOWORK=off go build ./codec/...       — PASS
Tests: GOWORK=off go test ./codec/...        — PASS (wrappers, zapcodec)
This commit is contained in:
Hanzo AI
2026-06-06 15:42:29 -07:00
parent 62ec03f8a3
commit 3fa1fd2dd6
20 changed files with 3937 additions and 10 deletions
+146
View File
@@ -0,0 +1,146 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package codec
import (
"errors"
"github.com/luxfi/zap/codec/wrappers"
)
// Common codec errors
var (
ErrUnsupportedType = errors.New("unsupported type")
ErrMaxSliceLenExceeded = errors.New("max slice length exceeded")
ErrDoesNotImplementInterface = errors.New("does not implement interface")
ErrUnexportedField = errors.New("unexported field")
ErrMarshalNil = errors.New("can't marshal nil pointer")
ErrMarshalZeroLength = errors.New("can't marshal zero length value")
ErrUnmarshalNil = errors.New("can't unmarshal into nil")
ErrUnmarshalZeroLength = errors.New("can't unmarshal zero length value")
ErrCantPackVersion = errors.New("couldn't pack codec version")
ErrCantUnpackVersion = errors.New("couldn't unpack codec version")
ErrUnknownVersion = errors.New("unknown codec version")
ErrDuplicateType = errors.New("duplicate type registration")
ErrExtraSpace = errors.New("trailing buffer space")
)
// Codec marshals and unmarshals
type Codec interface {
MarshalInto(interface{}, *wrappers.Packer) error
UnmarshalFrom(*wrappers.Packer, interface{}) error
Size(value interface{}) (int, error)
}
// Registry handles type registration for codec
type Registry interface {
RegisterType(interface{}) error
}
// GeneralCodec combines Codec and Registry interfaces
type GeneralCodec interface {
Codec
Registry
}
// Manager manages multiple codec versions
type Manager interface {
RegisterCodec(version uint16, codec Codec) error
Marshal(version uint16, source interface{}) ([]byte, error)
Unmarshal(bytes []byte, dest interface{}) (uint16, error)
Size(version uint16, value interface{}) (int, error)
}
// DefaultMaxSize is the default maximum size for codec manager (1MB)
const DefaultMaxSize = 1024 * 1024
// NewManager returns a new codec manager
func NewManager(maxSize uint64) Manager {
return &manager{
maxSize: int(maxSize),
codecs: make(map[uint16]Codec),
}
}
// NewDefaultManager returns a codec manager with default max size
func NewDefaultManager() Manager {
return NewManager(DefaultMaxSize)
}
type manager struct {
maxSize int
codecs map[uint16]Codec
}
func (m *manager) RegisterCodec(version uint16, codec Codec) error {
if _, exists := m.codecs[version]; exists {
return ErrDuplicateType
}
m.codecs[version] = codec
return nil
}
func (m *manager) Marshal(version uint16, source interface{}) ([]byte, error) {
codec, exists := m.codecs[version]
if !exists {
return nil, ErrUnknownVersion
}
p := &wrappers.Packer{MaxSize: m.maxSize}
p.PackShort(version)
if p.Err != nil {
return nil, ErrCantPackVersion
}
if err := codec.MarshalInto(source, p); err != nil {
return nil, err
}
return p.Bytes[:p.Offset], p.Err
}
func (m *manager) Unmarshal(bytes []byte, dest interface{}) (uint16, error) {
if len(bytes) < 2 {
return 0, ErrCantUnpackVersion
}
// Enforce maxSize during unmarshalling - reject oversized inputs
if len(bytes) > m.maxSize {
return 0, ErrMaxSliceLenExceeded
}
p := &wrappers.Packer{Bytes: bytes, MaxSize: m.maxSize}
version := p.UnpackShort()
if p.Err != nil {
return 0, ErrCantUnpackVersion
}
codec, exists := m.codecs[version]
if !exists {
return version, ErrUnknownVersion
}
if err := codec.UnmarshalFrom(p, dest); err != nil {
return version, err
}
// Check for trailing bytes
if p.Offset != len(bytes) {
return version, ErrExtraSpace
}
return version, nil
}
func (m *manager) Size(version uint16, value interface{}) (int, error) {
codec, exists := m.codecs[version]
if !exists {
return 0, ErrUnknownVersion
}
size, err := codec.Size(value)
if err != nil {
return 0, err
}
return 2 + size, nil // +2 for version
}
+100
View File
@@ -0,0 +1,100 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/luxfi/codec (interfaces: Manager)
//
// Generated by this command:
//
// mockgen -package=codecmock -destination=codecmock/manager.go -mock_names=Manager=Manager . Manager
//
// Package codecmock is a generated GoMock package.
package codecmock
import (
reflect "reflect"
codec "github.com/luxfi/zap/codec"
gomock "go.uber.org/mock/gomock"
)
// Manager is a mock of Manager interface.
type Manager struct {
ctrl *gomock.Controller
recorder *ManagerMockRecorder
isgomock struct{}
}
// ManagerMockRecorder is the mock recorder for Manager.
type ManagerMockRecorder struct {
mock *Manager
}
// NewManager creates a new mock instance.
func NewManager(ctrl *gomock.Controller) *Manager {
mock := &Manager{ctrl: ctrl}
mock.recorder = &ManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Manager) EXPECT() *ManagerMockRecorder {
return m.recorder
}
// Marshal mocks base method.
func (m *Manager) Marshal(version uint16, source any) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Marshal", version, source)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Marshal indicates an expected call of Marshal.
func (mr *ManagerMockRecorder) Marshal(version, source any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Marshal", reflect.TypeOf((*Manager)(nil).Marshal), version, source)
}
// RegisterCodec mocks base method.
func (m *Manager) RegisterCodec(version uint16, arg1 codec.Codec) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RegisterCodec", version, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// RegisterCodec indicates an expected call of RegisterCodec.
func (mr *ManagerMockRecorder) RegisterCodec(version, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterCodec", reflect.TypeOf((*Manager)(nil).RegisterCodec), version, arg1)
}
// Size mocks base method.
func (m *Manager) Size(version uint16, value any) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Size", version, value)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Size indicates an expected call of Size.
func (mr *ManagerMockRecorder) Size(version, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Size", reflect.TypeOf((*Manager)(nil).Size), version, value)
}
// Unmarshal mocks base method.
func (m *Manager) Unmarshal(source []byte, destination any) (uint16, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unmarshal", source, destination)
ret0, _ := ret[0].(uint16)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Unmarshal indicates an expected call of Unmarshal.
func (mr *ManagerMockRecorder) Unmarshal(source, destination any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unmarshal", reflect.TypeOf((*Manager)(nil).Unmarshal), source, destination)
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package codec
// Size constants for binary packing
const (
// ByteLen is the number of bytes per byte
ByteLen = 1
// ShortLen is the number of bytes per short
ShortLen = 2
// VersionSize is the number of bytes used for codec version
VersionSize = ShortLen
// IntLen is the number of bytes per int
IntLen = 4
// LongLen is the number of bytes per long
LongLen = 8
// BoolLen is the number of bytes per bool
BoolLen = 1
// IPLen is the number of bytes per IP (16 bytes + 2 for port)
IPLen = 16 + ShortLen
)
// StringLen returns the packed length of a string (2-byte length prefix + string bytes)
func StringLen(str string) int {
return ShortLen + len(str)
}
// Errs collects errors during a series of operations.
// It stores only the first error encountered.
type Errs struct {
Err error
}
// Errored returns true if an error has been recorded.
func (errs *Errs) Errored() bool {
return errs.Err != nil
}
// Add records the first non-nil error.
func (errs *Errs) Add(errors ...error) {
if errs.Err == nil {
for _, err := range errors {
if err != nil {
errs.Err = err
break
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package linearcodec
import (
"fmt"
"reflect"
"sync"
"github.com/luxfi/zap/codec"
"github.com/luxfi/zap/codec/reflectcodec"
"github.com/luxfi/zap/codec/wrappers"
"github.com/luxfi/container/bimap"
)
var (
_ Codec = (*linearCodec)(nil)
_ codec.Codec = (*linearCodec)(nil)
_ codec.Registry = (*linearCodec)(nil)
_ codec.GeneralCodec = (*linearCodec)(nil)
)
// Codec marshals and unmarshals
type Codec interface {
codec.Registry
codec.Codec
SkipRegistrations(int)
}
// linearCodec handles marshaling and unmarshaling of structs
type linearCodec struct {
codec.Codec
lock sync.RWMutex
nextTypeID uint32
registeredTypes *bimap.BiMap[uint32, reflect.Type]
}
// New returns a new, concurrency-safe codec; it allow to specify tagNames.
func New(tagNames []string) Codec {
hCodec := &linearCodec{
nextTypeID: 0,
registeredTypes: bimap.New[uint32, reflect.Type](),
}
hCodec.Codec = reflectcodec.New(hCodec, tagNames)
return hCodec
}
// NewDefault is a convenience constructor; it returns a new codec with default
// tagNames.
func NewDefault() Codec {
return New([]string{reflectcodec.DefaultTagName})
}
// Skip some number of type IDs
func (c *linearCodec) SkipRegistrations(num int) {
c.lock.Lock()
c.nextTypeID += uint32(num)
c.lock.Unlock()
}
// RegisterType is used to register types that may be unmarshaled into an interface
// [val] is a value of the type being registered
func (c *linearCodec) RegisterType(val interface{}) error {
c.lock.Lock()
defer c.lock.Unlock()
valType := reflect.TypeOf(val)
if c.registeredTypes.HasValue(valType) {
return fmt.Errorf("%w: %v", codec.ErrDuplicateType, valType)
}
c.registeredTypes.Put(c.nextTypeID, valType)
c.nextTypeID++
return nil
}
func (*linearCodec) PrefixSize(reflect.Type) int {
// see PackPrefix implementation
return wrappers.IntLen
}
func (c *linearCodec) PackPrefix(p *wrappers.Packer, valueType reflect.Type) error {
c.lock.RLock()
defer c.lock.RUnlock()
typeID, ok := c.registeredTypes.GetKey(valueType) // Get the type ID of the value being marshaled
if !ok {
return fmt.Errorf("can't marshal unregistered type %q", valueType)
}
p.PackInt(typeID) // Pack type ID so we know what to unmarshal this into
return p.Err
}
func (c *linearCodec) UnpackPrefix(p *wrappers.Packer, valueType reflect.Type) (reflect.Value, error) {
c.lock.RLock()
defer c.lock.RUnlock()
typeID := p.UnpackInt() // Get the type ID
if p.Err != nil {
return reflect.Value{}, fmt.Errorf("couldn't unmarshal interface: %w", p.Err)
}
// Get a type that implements the interface
implementingType, ok := c.registeredTypes.GetValue(typeID)
if !ok {
return reflect.Value{}, fmt.Errorf("couldn't unmarshal interface: unknown type ID %d", typeID)
}
// Ensure type actually does implement the interface
if !implementingType.Implements(valueType) {
return reflect.Value{}, fmt.Errorf("couldn't unmarshal interface: %s %w %s",
implementingType,
codec.ErrDoesNotImplementInterface,
valueType,
)
}
return reflect.New(implementingType).Elem(), nil // instance of the proper type
}
+271
View File
@@ -0,0 +1,271 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package codec
import (
"encoding/binary"
"errors"
"math"
"github.com/luxfi/ids"
)
// Common errors
var (
ErrInsufficientLength = errors.New("packing: insufficient length")
ErrNegativeLength = errors.New("packing: negative length")
ErrBadLength = errors.New("packing: bad length")
ErrOverflow = errors.New("packing: overflow")
)
// MaxStringLen is the maximum string length that can be packed
const MaxStringLen = math.MaxUint16
// Packer provides methods to pack data into bytes
type Packer struct {
Bytes []byte
Offset int
Err error
}
// NewPacker returns a new Packer with the given max size
func NewPacker(maxSize int) *Packer {
if maxSize < 0 {
maxSize = 0
}
// Avoid huge upfront allocations; capacity will grow as needed.
if maxSize > DefaultMaxSize {
maxSize = DefaultMaxSize
}
return &Packer{
Bytes: make([]byte, 0, maxSize),
Offset: 0,
}
}
// PackerFromBytes returns a Packer initialized with the given bytes
func PackerFromBytes(b []byte) *Packer {
return &Packer{
Bytes: b,
Offset: 0,
}
}
// Remaining returns the number of bytes remaining to read
func (p *Packer) Remaining() int {
return len(p.Bytes) - p.Offset
}
// Errored returns true if there's been an error
func (p *Packer) Errored() bool {
return p.Err != nil
}
// expand ensures capacity for n more bytes
func (p *Packer) expand(n int) {
if p.Err != nil {
return
}
needed := p.Offset + n
if needed > cap(p.Bytes) {
newCap := max(cap(p.Bytes)*2, needed)
newBytes := make([]byte, len(p.Bytes), newCap)
copy(newBytes, p.Bytes)
p.Bytes = newBytes
}
if needed > len(p.Bytes) {
p.Bytes = p.Bytes[:needed]
}
}
// PackByte packs a byte
func (p *Packer) PackByte(val byte) {
p.expand(1)
if p.Err != nil {
return
}
p.Bytes[p.Offset] = val
p.Offset++
}
// UnpackByte unpacks a byte
func (p *Packer) UnpackByte() byte {
if p.Err != nil {
return 0
}
if p.Offset >= len(p.Bytes) {
p.Err = ErrInsufficientLength
return 0
}
val := p.Bytes[p.Offset]
p.Offset++
return val
}
// PackShort packs a uint16
func (p *Packer) PackShort(val uint16) {
p.expand(2)
if p.Err != nil {
return
}
binary.BigEndian.PutUint16(p.Bytes[p.Offset:], val)
p.Offset += 2
}
// UnpackShort unpacks a uint16
func (p *Packer) UnpackShort() uint16 {
if p.Err != nil {
return 0
}
if p.Offset+2 > len(p.Bytes) {
p.Err = ErrInsufficientLength
return 0
}
val := binary.BigEndian.Uint16(p.Bytes[p.Offset:])
p.Offset += 2
return val
}
// PackInt packs a uint32
func (p *Packer) PackInt(val uint32) {
p.expand(4)
if p.Err != nil {
return
}
binary.BigEndian.PutUint32(p.Bytes[p.Offset:], val)
p.Offset += 4
}
// UnpackInt unpacks a uint32
func (p *Packer) UnpackInt() uint32 {
if p.Err != nil {
return 0
}
if p.Offset+4 > len(p.Bytes) {
p.Err = ErrInsufficientLength
return 0
}
val := binary.BigEndian.Uint32(p.Bytes[p.Offset:])
p.Offset += 4
return val
}
// PackLong packs a uint64
func (p *Packer) PackLong(val uint64) {
p.expand(8)
if p.Err != nil {
return
}
binary.BigEndian.PutUint64(p.Bytes[p.Offset:], val)
p.Offset += 8
}
// UnpackLong unpacks a uint64
func (p *Packer) UnpackLong() uint64 {
if p.Err != nil {
return 0
}
if p.Offset+8 > len(p.Bytes) {
p.Err = ErrInsufficientLength
return 0
}
val := binary.BigEndian.Uint64(p.Bytes[p.Offset:])
p.Offset += 8
return val
}
// PackBool packs a bool
func (p *Packer) PackBool(val bool) {
if val {
p.PackByte(1)
} else {
p.PackByte(0)
}
}
// UnpackBool unpacks a bool
func (p *Packer) UnpackBool() bool {
return p.UnpackByte() != 0
}
// PackBytes packs a byte slice with length prefix
func (p *Packer) PackBytes(val []byte) {
if len(val) > math.MaxInt32 {
p.Err = ErrOverflow
return
}
p.PackInt(uint32(len(val)))
p.PackFixedBytes(val)
}
// UnpackBytes unpacks a byte slice with length prefix
func (p *Packer) UnpackBytes() []byte {
length := p.UnpackInt()
return p.UnpackFixedBytes(int(length))
}
// PackFixedBytes packs a fixed-length byte slice
func (p *Packer) PackFixedBytes(val []byte) {
p.expand(len(val))
if p.Err != nil {
return
}
copy(p.Bytes[p.Offset:], val)
p.Offset += len(val)
}
// UnpackFixedBytes unpacks a fixed-length byte slice
func (p *Packer) UnpackFixedBytes(n int) []byte {
if p.Err != nil {
return nil
}
if n < 0 {
p.Err = ErrNegativeLength
return nil
}
if p.Offset+n > len(p.Bytes) {
p.Err = ErrInsufficientLength
return nil
}
val := make([]byte, n)
copy(val, p.Bytes[p.Offset:p.Offset+n])
p.Offset += n
return val
}
// PackStr packs a string with length prefix
func (p *Packer) PackStr(val string) {
strLen := len(val)
if strLen > MaxStringLen {
p.Err = ErrBadLength
return
}
p.PackShort(uint16(strLen))
p.PackFixedBytes([]byte(val))
}
// UnpackStr unpacks a string with length prefix
func (p *Packer) UnpackStr() string {
strLen := p.UnpackShort()
return string(p.UnpackFixedBytes(int(strLen)))
}
// PackID packs an ID
func (p *Packer) PackID(id ids.ID) {
p.PackFixedBytes(id[:])
}
// UnpackID unpacks an ID
func (p *Packer) UnpackID() ids.ID {
bytes := p.UnpackFixedBytes(ids.IDLen)
if p.Err != nil {
return ids.Empty
}
id, err := ids.ToID(bytes)
if err != nil {
p.Err = err
return ids.Empty
}
return id
}
+96
View File
@@ -0,0 +1,96 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reflectcodec
import (
"fmt"
"reflect"
"sync"
"github.com/luxfi/zap/codec"
)
// TagValue is the value the tag must have to be serialized.
const TagValue = "true"
var _ StructFielder = (*structFielder)(nil)
// StructFielder handles discovery of serializable fields in a struct.
type StructFielder interface {
// Returns the fields that have been marked as serializable in [t], which is
// a struct type.
// Returns an error if a field has tag "[tagName]: [TagValue]" but the field
// is un-exported.
// GetSerializedField(Foo) --> [1,5,8] means Foo.Field(1), Foo.Field(5),
// Foo.Field(8) are to be serialized/deserialized.
GetSerializedFields(t reflect.Type) ([]int, error)
}
func NewStructFielder(tagNames []string) StructFielder {
return &structFielder{
tags: tagNames,
serializedFieldIndices: make(map[reflect.Type][]int),
}
}
type structFielder struct {
lock sync.RWMutex
// multiple tags per field can be specified. A field is serialized/deserialized
// if it has at least one of the specified tags.
tags []string
// Key: a struct type
// Value: Slice where each element is index in the struct type of a field
// that is serialized/deserialized e.g. Foo --> [1,5,8] means Foo.Field(1),
// etc. are to be serialized/deserialized. We assume this cache is pretty
// small (a few hundred keys at most) and doesn't take up much memory.
serializedFieldIndices map[reflect.Type][]int
}
func (s *structFielder) GetSerializedFields(t reflect.Type) ([]int, error) {
if serializedFields, ok := s.getCachedSerializedFields(t); ok { // use pre-computed result
return serializedFields, nil
}
s.lock.Lock()
defer s.lock.Unlock()
numFields := t.NumField()
serializedFields := make([]int, 0, numFields)
for i := 0; i < numFields; i++ { // Go through all fields of this struct
field := t.Field(i)
// Multiple tags per fields can be specified.
// Serialize/Deserialize field if it has
// any tag with the right value
var captureField bool
for _, tag := range s.tags {
if field.Tag.Get(tag) == TagValue {
captureField = true
break
}
}
if !captureField {
continue
}
if !field.IsExported() { // Can only marshal exported fields
return nil, fmt.Errorf("can not marshal %w: %s",
codec.ErrUnexportedField,
field.Name,
)
}
serializedFields = append(serializedFields, i)
}
s.serializedFieldIndices[t] = serializedFields // cache result
return serializedFields, nil
}
func (s *structFielder) getCachedSerializedFields(t reflect.Type) ([]int, bool) {
s.lock.RLock()
defer s.lock.RUnlock()
cachedFields, ok := s.serializedFieldIndices[t]
return cachedFields, ok
}
+746
View File
@@ -0,0 +1,746 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package reflectcodec
import (
"bytes"
"errors"
"fmt"
"math"
"reflect"
"slices"
"github.com/luxfi/zap/codec"
"github.com/luxfi/zap/codec/wrappers"
"github.com/luxfi/math/set"
)
const (
// DefaultTagName that enables serialization.
DefaultTagName = "serialize"
initialSliceLen = 16
)
var (
_ codec.Codec = (*genericCodec)(nil)
errNeedPointer = errors.New("argument to unmarshal must be a pointer")
errRecursiveInterfaceTypes = errors.New("recursive interface types")
)
type TypeCodec interface {
// UnpackPrefix unpacks the prefix of an interface from the given packer.
// The prefix specifies the concrete type that the interface should be
// deserialized into. This function returns a new instance of that concrete
// type. The concrete type must implement the given type.
UnpackPrefix(*wrappers.Packer, reflect.Type) (reflect.Value, error)
// PackPrefix packs the prefix for the given type into the given packer.
// This identifies the bytes that follow, which are the byte representation
// of an interface, as having the given concrete type.
// When deserializing the bytes, the prefix specifies which concrete type
// to deserialize into.
PackPrefix(*wrappers.Packer, reflect.Type) error
// PrefixSize returns prefix length for the given type into the given
// packer.
PrefixSize(reflect.Type) int
}
// genericCodec handles marshaling and unmarshaling of structs with a generic
// implementation for interface encoding.
//
// A few notes:
//
// 1. We use "marshal" and "serialize" interchangeably, and "unmarshal" and
// "deserialize" interchangeably
// 2. To include a field of a struct in the serialized form, add the tag
// `{tagName}:"true"` to it. `{tagName}` defaults to `serialize`.
// 3. These typed members of a struct may be serialized:
// bool, string, uint[8,16,32,64], int[8,16,32,64],
// structs, slices, arrays, maps, interface.
// structs, slices, maps and arrays can only be serialized if their constituent
// values can be.
// 4. To marshal an interface, you must pass a pointer to the value
// 5. To unmarshal an interface, you must call
// codec.RegisterType([instance of the type that fulfills the interface]).
// 6. Serialized fields must be exported
// 7. nil slices are marshaled as empty slices
type genericCodec struct {
typer TypeCodec
fielder StructFielder
}
// New returns a new, concurrency-safe codec
func New(typer TypeCodec, tagNames []string) codec.Codec {
return &genericCodec{
typer: typer,
fielder: NewStructFielder(tagNames),
}
}
func (c *genericCodec) Size(value interface{}) (int, error) {
if value == nil {
return 0, codec.ErrMarshalNil
}
typeStack := make(set.Set[reflect.Type])
size, _, err := c.size(reflect.ValueOf(value), typeStack)
return size, err
}
// size returns the size of the value along with whether the value is constant
// sized.
func (c *genericCodec) size(
value reflect.Value,
typeStack set.Set[reflect.Type],
) (int, bool, error) {
switch valueKind := value.Kind(); valueKind {
case reflect.Uint8:
return wrappers.ByteLen, true, nil
case reflect.Int8:
return wrappers.ByteLen, true, nil
case reflect.Uint16:
return wrappers.ShortLen, true, nil
case reflect.Int16:
return wrappers.ShortLen, true, nil
case reflect.Uint32:
return wrappers.IntLen, true, nil
case reflect.Int32:
return wrappers.IntLen, true, nil
case reflect.Uint64:
return wrappers.LongLen, true, nil
case reflect.Int64:
return wrappers.LongLen, true, nil
case reflect.Bool:
return wrappers.BoolLen, true, nil
case reflect.String:
return wrappers.StringLen(value.String()), false, nil
case reflect.Ptr:
if value.IsNil() {
return 0, false, codec.ErrMarshalNil
}
return c.size(value.Elem(), typeStack)
case reflect.Interface:
if value.IsNil() {
return 0, false, codec.ErrMarshalNil
}
underlyingValue := value.Interface()
underlyingType := reflect.TypeOf(underlyingValue)
if typeStack.Contains(underlyingType) {
return 0, false, fmt.Errorf("%w: %s", errRecursiveInterfaceTypes, underlyingType)
}
typeStack.Add(underlyingType)
prefixSize := c.typer.PrefixSize(underlyingType)
valueSize, _, err := c.size(value.Elem(), typeStack)
typeStack.Remove(underlyingType)
return prefixSize + valueSize, false, err
case reflect.Slice:
numElts := value.Len()
if numElts == 0 {
return wrappers.IntLen, false, nil
}
size, constSize, err := c.size(value.Index(0), typeStack)
if err != nil {
return 0, false, err
}
if size == 0 {
return 0, false, fmt.Errorf("can't marshal slice of zero length values: %w", codec.ErrMarshalZeroLength)
}
// For fixed-size types we manually calculate lengths rather than
// processing each element separately to improve performance.
if constSize {
return wrappers.IntLen + numElts*size, false, nil
}
for i := 1; i < numElts; i++ {
innerSize, _, err := c.size(value.Index(i), typeStack)
if err != nil {
return 0, false, err
}
size += innerSize
}
return wrappers.IntLen + size, false, nil
case reflect.Array:
numElts := value.Len()
if numElts == 0 {
return 0, true, nil
}
size, constSize, err := c.size(value.Index(0), typeStack)
if err != nil {
return 0, false, err
}
// For fixed-size types we manually calculate lengths rather than
// processing each element separately to improve performance.
if constSize {
return numElts * size, true, nil
}
for i := 1; i < numElts; i++ {
innerSize, _, err := c.size(value.Index(i), typeStack)
if err != nil {
return 0, false, err
}
size += innerSize
}
return size, false, nil
case reflect.Struct:
serializedFields, err := c.fielder.GetSerializedFields(value.Type())
if err != nil {
return 0, false, err
}
var (
size int
constSize = true
)
for _, fieldIndex := range serializedFields {
innerSize, innerConstSize, err := c.size(value.Field(fieldIndex), typeStack)
if err != nil {
return 0, false, err
}
size += innerSize
constSize = constSize && innerConstSize
}
return size, constSize, nil
case reflect.Map:
iter := value.MapRange()
if !iter.Next() {
return wrappers.IntLen, false, nil
}
keySize, keyConstSize, err := c.size(iter.Key(), typeStack)
if err != nil {
return 0, false, err
}
valueSize, valueConstSize, err := c.size(iter.Value(), typeStack)
if err != nil {
return 0, false, err
}
if keySize == 0 && valueSize == 0 {
return 0, false, fmt.Errorf("can't marshal map with zero length entries: %w", codec.ErrMarshalZeroLength)
}
switch {
case keyConstSize && valueConstSize:
numElts := value.Len()
return wrappers.IntLen + numElts*(keySize+valueSize), false, nil
case keyConstSize:
var (
numElts = 1
totalValueSize = valueSize
)
for iter.Next() {
valueSize, _, err := c.size(iter.Value(), typeStack)
if err != nil {
return 0, false, err
}
totalValueSize += valueSize
numElts++
}
return wrappers.IntLen + numElts*keySize + totalValueSize, false, nil
case valueConstSize:
var (
numElts = 1
totalKeySize = keySize
)
for iter.Next() {
keySize, _, err := c.size(iter.Key(), typeStack)
if err != nil {
return 0, false, err
}
totalKeySize += keySize
numElts++
}
return wrappers.IntLen + totalKeySize + numElts*valueSize, false, nil
default:
totalSize := wrappers.IntLen + keySize + valueSize
for iter.Next() {
keySize, _, err := c.size(iter.Key(), typeStack)
if err != nil {
return 0, false, err
}
valueSize, _, err := c.size(iter.Value(), typeStack)
if err != nil {
return 0, false, err
}
totalSize += keySize + valueSize
}
return totalSize, false, nil
}
default:
return 0, false, fmt.Errorf("can't evaluate marshal length of unknown kind %s", valueKind)
}
}
// To marshal an interface, [value] must be a pointer to the interface
func (c *genericCodec) MarshalInto(value interface{}, p *wrappers.Packer) error {
if value == nil {
return codec.ErrMarshalNil
}
typeStack := make(set.Set[reflect.Type])
return c.marshal(reflect.ValueOf(value), p, typeStack)
}
// marshal writes the byte representation of [value] to [p]
//
// c.lock should be held for the duration of this function
func (c *genericCodec) marshal(
value reflect.Value,
p *wrappers.Packer,
typeStack set.Set[reflect.Type],
) error {
switch valueKind := value.Kind(); valueKind {
case reflect.Uint8:
p.PackByte(uint8(value.Uint()))
return p.Err
case reflect.Int8:
p.PackByte(uint8(value.Int()))
return p.Err
case reflect.Uint16:
p.PackShort(uint16(value.Uint()))
return p.Err
case reflect.Int16:
p.PackShort(uint16(value.Int()))
return p.Err
case reflect.Uint32:
p.PackInt(uint32(value.Uint()))
return p.Err
case reflect.Int32:
p.PackInt(uint32(value.Int()))
return p.Err
case reflect.Uint64:
p.PackLong(value.Uint())
return p.Err
case reflect.Int64:
p.PackLong(uint64(value.Int()))
return p.Err
case reflect.String:
p.PackStr(value.String())
return p.Err
case reflect.Bool:
p.PackBool(value.Bool())
return p.Err
case reflect.Ptr:
if value.IsNil() {
return codec.ErrMarshalNil
}
return c.marshal(value.Elem(), p, typeStack)
case reflect.Interface:
if value.IsNil() {
return codec.ErrMarshalNil
}
underlyingValue := value.Interface()
underlyingType := reflect.TypeOf(underlyingValue)
if typeStack.Contains(underlyingType) {
return fmt.Errorf("%w: %s", errRecursiveInterfaceTypes, underlyingType)
}
typeStack.Add(underlyingType)
if err := c.typer.PackPrefix(p, underlyingType); err != nil {
return err
}
if err := c.marshal(value.Elem(), p, typeStack); err != nil {
return err
}
typeStack.Remove(underlyingType)
return p.Err
case reflect.Slice:
numElts := value.Len() // # elements in the slice/array. 0 if this slice is nil.
if numElts > math.MaxInt32 {
return fmt.Errorf("%w; slice length, %d, exceeds maximum length, %d",
codec.ErrMaxSliceLenExceeded,
numElts,
math.MaxInt32,
)
}
p.PackInt(uint32(numElts)) // pack # elements
if p.Err != nil {
return p.Err
}
if numElts == 0 {
// Returning here prevents execution of the (expensive) reflect
// calls below which check if the slice is []byte and, if it is,
// the call of value.Bytes()
return nil
}
// If this is a slice of bytes, manually pack the bytes rather
// than calling marshal on each byte. This improves performance.
if elemKind := value.Type().Elem().Kind(); elemKind == reflect.Uint8 {
p.PackFixedBytes(value.Bytes())
return p.Err
}
for i := 0; i < numElts; i++ { // Process each element in the slice
startOffset := p.Offset
if err := c.marshal(value.Index(i), p, typeStack); err != nil {
return err
}
if startOffset == p.Offset {
return fmt.Errorf("couldn't marshal slice of zero length values: %w", codec.ErrMarshalZeroLength)
}
}
return nil
case reflect.Array:
if value.CanAddr() && value.Type().Elem().Kind() == reflect.Uint8 {
p.PackFixedBytes(value.Bytes())
return p.Err
}
numElts := value.Len()
for i := 0; i < numElts; i++ { // Process each element in the array
if err := c.marshal(value.Index(i), p, typeStack); err != nil {
return err
}
}
return nil
case reflect.Struct:
serializedFields, err := c.fielder.GetSerializedFields(value.Type())
if err != nil {
return err
}
for _, fieldIndex := range serializedFields { // Go through all fields of this struct that are serialized
if err := c.marshal(value.Field(fieldIndex), p, typeStack); err != nil { // Serialize the field and write to byte array
return err
}
}
return nil
case reflect.Map:
keys := value.MapKeys()
numElts := len(keys)
if numElts > math.MaxInt32 {
return fmt.Errorf("%w; slice length, %d, exceeds maximum length, %d",
codec.ErrMaxSliceLenExceeded,
numElts,
math.MaxInt32,
)
}
p.PackInt(uint32(numElts)) // pack # elements
if p.Err != nil {
return p.Err
}
// pack key-value pairs sorted by increasing key
type keyTuple struct {
key reflect.Value
startIndex int
endIndex int
}
sortedKeys := make([]keyTuple, len(keys))
startOffset := p.Offset
endOffset := p.Offset
for i, key := range keys {
if err := c.marshal(key, p, typeStack); err != nil {
return err
}
if p.Err != nil {
return fmt.Errorf("couldn't marshal map key %+v: %w ", key, p.Err)
}
sortedKeys[i] = keyTuple{
key: key,
startIndex: endOffset,
endIndex: p.Offset,
}
endOffset = p.Offset
}
slices.SortFunc(sortedKeys, func(a, b keyTuple) int {
aBytes := p.Bytes[a.startIndex:a.endIndex]
bBytes := p.Bytes[b.startIndex:b.endIndex]
return bytes.Compare(aBytes, bBytes)
})
allKeyBytes := slices.Clone(p.Bytes[startOffset:p.Offset])
p.Offset = startOffset
for _, key := range sortedKeys {
keyStartOffset := p.Offset
// pack key
startIndex := key.startIndex - startOffset
endIndex := key.endIndex - startOffset
keyBytes := allKeyBytes[startIndex:endIndex]
p.PackFixedBytes(keyBytes)
if p.Err != nil {
return p.Err
}
// serialize and pack value
if err := c.marshal(value.MapIndex(key.key), p, typeStack); err != nil {
return err
}
if keyStartOffset == p.Offset {
return fmt.Errorf("couldn't marshal map with zero length entries: %w", codec.ErrMarshalZeroLength)
}
}
return nil
default:
return fmt.Errorf("%w: %s", codec.ErrUnsupportedType, valueKind)
}
}
// UnmarshalFrom unmarshals [p.Bytes] into [dest], where [dest] must be a pointer or
// interface
func (c *genericCodec) UnmarshalFrom(p *wrappers.Packer, dest interface{}) error {
if dest == nil {
return codec.ErrUnmarshalNil
}
destPtr := reflect.ValueOf(dest)
if destPtr.Kind() != reflect.Ptr {
return errNeedPointer
}
typeStack := make(set.Set[reflect.Type])
return c.unmarshal(p, destPtr.Elem(), typeStack)
}
// Unmarshal from p.Bytes into [value]. [value] must be addressable.
//
// c.lock should be held for the duration of this function
func (c *genericCodec) unmarshal(
p *wrappers.Packer,
value reflect.Value,
typeStack set.Set[reflect.Type],
) error {
switch value.Kind() {
case reflect.Uint8:
value.SetUint(uint64(p.UnpackByte()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal uint8: %w", p.Err)
}
return nil
case reflect.Int8:
value.SetInt(int64(p.UnpackByte()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal int8: %w", p.Err)
}
return nil
case reflect.Uint16:
value.SetUint(uint64(p.UnpackShort()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal uint16: %w", p.Err)
}
return nil
case reflect.Int16:
value.SetInt(int64(p.UnpackShort()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal int16: %w", p.Err)
}
return nil
case reflect.Uint32:
value.SetUint(uint64(p.UnpackInt()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal uint32: %w", p.Err)
}
return nil
case reflect.Int32:
value.SetInt(int64(p.UnpackInt()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal int32: %w", p.Err)
}
return nil
case reflect.Uint64:
value.SetUint(p.UnpackLong())
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal uint64: %w", p.Err)
}
return nil
case reflect.Int64:
value.SetInt(int64(p.UnpackLong()))
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal int64: %w", p.Err)
}
return nil
case reflect.Bool:
value.SetBool(p.UnpackBool())
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal bool: %w", p.Err)
}
return nil
case reflect.Slice:
numElts32 := p.UnpackInt()
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal slice: %w", p.Err)
}
if numElts32 > math.MaxInt32 {
return fmt.Errorf("%w; array length, %d, exceeds maximum length, %d",
codec.ErrMaxSliceLenExceeded,
numElts32,
math.MaxInt32,
)
}
numElts := int(numElts32)
sliceType := value.Type()
innerType := sliceType.Elem()
// If this is a slice of bytes, manually unpack the bytes rather
// than calling unmarshal on each byte. This improves performance.
if elemKind := innerType.Kind(); elemKind == reflect.Uint8 {
value.SetBytes(p.UnpackFixedBytes(numElts))
return p.Err
}
// Unmarshal each element and append it into the slice.
value.Set(reflect.MakeSlice(sliceType, 0, initialSliceLen))
zeroValue := reflect.Zero(innerType)
for i := 0; i < numElts; i++ {
value.Set(reflect.Append(value, zeroValue))
startOffset := p.Offset
if err := c.unmarshal(p, value.Index(i), typeStack); err != nil {
return err
}
if startOffset == p.Offset {
return fmt.Errorf("couldn't unmarshal slice of zero length values: %w", codec.ErrUnmarshalZeroLength)
}
}
return nil
case reflect.Array:
numElts := value.Len()
if elemKind := value.Type().Elem().Kind(); elemKind == reflect.Uint8 {
unpackedBytes := p.UnpackFixedBytes(numElts)
if p.Errored() {
return p.Err
}
// Get a slice to the underlying array value
underlyingSlice := value.Slice(0, numElts).Interface().([]byte)
copy(underlyingSlice, unpackedBytes)
return nil
}
for i := 0; i < numElts; i++ {
if err := c.unmarshal(p, value.Index(i), typeStack); err != nil {
return err
}
}
return nil
case reflect.String:
value.SetString(p.UnpackStr())
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal string: %w", p.Err)
}
return nil
case reflect.Interface:
intfImplementor, err := c.typer.UnpackPrefix(p, value.Type())
if err != nil {
return err
}
intfImplementorType := intfImplementor.Type()
if typeStack.Contains(intfImplementorType) {
return fmt.Errorf("%w: %s", errRecursiveInterfaceTypes, intfImplementorType)
}
typeStack.Add(intfImplementorType)
// Unmarshal into the struct
if err := c.unmarshal(p, intfImplementor, typeStack); err != nil {
return err
}
typeStack.Remove(intfImplementorType)
value.Set(intfImplementor)
return nil
case reflect.Struct:
// Get indices of fields that will be unmarshaled into
serializedFieldIndices, err := c.fielder.GetSerializedFields(value.Type())
if err != nil {
return fmt.Errorf("couldn't unmarshal struct: %w", err)
}
// Go through the fields and unmarshal into them
for _, fieldIndex := range serializedFieldIndices {
if err := c.unmarshal(p, value.Field(fieldIndex), typeStack); err != nil {
return err
}
}
return nil
case reflect.Ptr:
// Get the type this pointer points to
t := value.Type().Elem()
// Create a new pointer to a new value of the underlying type
v := reflect.New(t)
// Fill the value
if err := c.unmarshal(p, v.Elem(), typeStack); err != nil {
return err
}
// Assign to the top-level struct's member
value.Set(v)
return nil
case reflect.Map:
numElts32 := p.UnpackInt()
if p.Err != nil {
return fmt.Errorf("couldn't unmarshal map: %w", p.Err)
}
if numElts32 > math.MaxInt32 {
return fmt.Errorf("%w; map length, %d, exceeds maximum length, %d",
codec.ErrMaxSliceLenExceeded,
numElts32,
math.MaxInt32,
)
}
var (
numElts = int(numElts32)
mapType = value.Type()
mapKeyType = mapType.Key()
mapValueType = mapType.Elem()
prevKey []byte
)
// Set [value] to be a new map of the appropriate type.
value.Set(reflect.MakeMap(mapType))
for i := 0; i < numElts; i++ {
mapKey := reflect.New(mapKeyType).Elem()
keyStartOffset := p.Offset
if err := c.unmarshal(p, mapKey, typeStack); err != nil {
return err
}
// Get the key's byte representation and check that the new key is
// actually bigger (according to bytes.Compare) than the previous
// key.
//
// We do this to enforce that key-value pairs are sorted by
// increasing key.
keyBytes := p.Bytes[keyStartOffset:p.Offset]
if i != 0 && bytes.Compare(keyBytes, prevKey) <= 0 {
return fmt.Errorf("keys aren't sorted: (%s, %s)", prevKey, mapKey)
}
prevKey = keyBytes
// Get the value
mapValue := reflect.New(mapValueType).Elem()
if err := c.unmarshal(p, mapValue, typeStack); err != nil {
return err
}
if keyStartOffset == p.Offset {
return fmt.Errorf("couldn't unmarshal map with zero length entries: %w", codec.ErrUnmarshalZeroLength)
}
// Assign the key-value pair in the map
value.SetMapIndex(mapKey, mapValue)
}
return nil
default:
return fmt.Errorf("can't unmarshal unknown type %s", value.Kind().String())
}
}
+39
View File
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wrappers
import (
"io"
"sync"
)
// Closer is a nice utility for closing a group of objects while reporting an
// error if one occurs.
type Closer struct {
lock sync.Mutex
closers []io.Closer
}
// Add a new object to be closed.
func (c *Closer) Add(closer io.Closer) {
c.lock.Lock()
defer c.lock.Unlock()
c.closers = append(c.closers, closer)
}
// Close closes each of the closers add to [c] and returns the first error
// that occurs or nil if no error occurs.
func (c *Closer) Close() error {
c.lock.Lock()
closers := c.closers
c.closers = nil
c.lock.Unlock()
errs := Errs{}
for _, closer := range closers {
errs.Add(closer.Close())
}
return errs.Err
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package wrappers provides common wrapper types and utilities.
package wrappers
const (
// ByteLen is the number of bytes per byte
ByteLen = 1
// ShortLen is the number of bytes per short
ShortLen = 2
// IntLen is the number of bytes per int
IntLen = 4
// LongLen is the number of bytes per long
LongLen = 8
// BoolLen is the number of bytes per bool
BoolLen = 1
// IPLen is the number of bytes per IP
IPLen = 16 + ShortLen
)
// Errs collects errors during a series of operations.
type Errs struct {
Err error
}
// Errored returns true if an error has been recorded.
func (errs *Errs) Errored() bool {
return errs.Err != nil
}
// Add records the first non-nil error.
func (errs *Errs) Add(errors ...error) {
if errs.Err == nil {
for _, err := range errors {
if err != nil {
errs.Err = err
break
}
}
}
}
+259
View File
@@ -0,0 +1,259 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wrappers
import (
"encoding/binary"
"errors"
"math"
)
const (
MaxStringLen = math.MaxUint16
)
var (
ErrInsufficientLength = errors.New("packer has insufficient length for input")
errNegativeOffset = errors.New("negative offset")
errInvalidInput = errors.New("input does not match expected format")
errBadBool = errors.New("unexpected value when unpacking bool")
errOversized = errors.New("size is larger than limit")
)
// StringLen returns the packed length of a string
func StringLen(str string) int {
return ShortLen + len(str)
}
// Packer packs and unpacks a byte array from/to standard values
type Packer struct {
Errs
// The largest allowed size of expanding the byte array
MaxSize int
// The current byte array
Bytes []byte
// The offset that is being written to in the byte array
Offset int
}
// PackByte appends a byte to the byte array
func (p *Packer) PackByte(val byte) {
p.expand(ByteLen)
if p.Errored() {
return
}
p.Bytes[p.Offset] = val
p.Offset++
}
// UnpackByte unpacks a byte from the byte array
func (p *Packer) UnpackByte() byte {
p.checkSpace(ByteLen)
if p.Errored() {
return 0
}
val := p.Bytes[p.Offset]
p.Offset += ByteLen
return val
}
// PackShort appends a short to the byte array
func (p *Packer) PackShort(val uint16) {
p.expand(ShortLen)
if p.Errored() {
return
}
binary.BigEndian.PutUint16(p.Bytes[p.Offset:], val)
p.Offset += ShortLen
}
// UnpackShort unpacks a short from the byte array
func (p *Packer) UnpackShort() uint16 {
p.checkSpace(ShortLen)
if p.Errored() {
return 0
}
val := binary.BigEndian.Uint16(p.Bytes[p.Offset:])
p.Offset += ShortLen
return val
}
// PackInt appends an int to the byte array
func (p *Packer) PackInt(val uint32) {
p.expand(IntLen)
if p.Errored() {
return
}
binary.BigEndian.PutUint32(p.Bytes[p.Offset:], val)
p.Offset += IntLen
}
// UnpackInt unpacks an int from the byte array
func (p *Packer) UnpackInt() uint32 {
p.checkSpace(IntLen)
if p.Errored() {
return 0
}
val := binary.BigEndian.Uint32(p.Bytes[p.Offset:])
p.Offset += IntLen
return val
}
// PackLong appends a long to the byte array
func (p *Packer) PackLong(val uint64) {
p.expand(LongLen)
if p.Errored() {
return
}
binary.BigEndian.PutUint64(p.Bytes[p.Offset:], val)
p.Offset += LongLen
}
// UnpackLong unpacks a long from the byte array
func (p *Packer) UnpackLong() uint64 {
p.checkSpace(LongLen)
if p.Errored() {
return 0
}
val := binary.BigEndian.Uint64(p.Bytes[p.Offset:])
p.Offset += LongLen
return val
}
// PackBool packs a bool into the byte array
func (p *Packer) PackBool(b bool) {
if b {
p.PackByte(1)
} else {
p.PackByte(0)
}
}
// UnpackBool unpacks a bool from the byte array
func (p *Packer) UnpackBool() bool {
b := p.UnpackByte()
switch b {
case 0:
return false
case 1:
return true
default:
p.Add(errBadBool)
return false
}
}
// PackFixedBytes appends a byte slice with no length descriptor to the byte array
func (p *Packer) PackFixedBytes(bytes []byte) {
p.expand(len(bytes))
if p.Errored() {
return
}
copy(p.Bytes[p.Offset:], bytes)
p.Offset += len(bytes)
}
// UnpackFixedBytes unpacks a byte slice with no length descriptor from the byte array
func (p *Packer) UnpackFixedBytes(size int) []byte {
p.checkSpace(size)
if p.Errored() {
return nil
}
bytes := p.Bytes[p.Offset : p.Offset+size]
p.Offset += size
return bytes
}
// PackBytes appends a byte slice to the byte array
func (p *Packer) PackBytes(bytes []byte) {
p.PackInt(uint32(len(bytes)))
p.PackFixedBytes(bytes)
}
// UnpackBytes unpacks a byte slice from the byte array
func (p *Packer) UnpackBytes() []byte {
size := p.UnpackInt()
return p.UnpackFixedBytes(int(size))
}
// UnpackLimitedBytes unpacks a byte slice. If the size of the slice is greater
// than limit, adds errOversized to the packer and returns nil.
func (p *Packer) UnpackLimitedBytes(limit uint32) []byte {
size := p.UnpackInt()
if size > limit {
p.Add(errOversized)
return nil
}
return p.UnpackFixedBytes(int(size))
}
// PackStr appends a string to the byte array
func (p *Packer) PackStr(str string) {
strSize := len(str)
if strSize > MaxStringLen {
p.Add(errInvalidInput)
return
}
p.PackShort(uint16(strSize))
p.PackFixedBytes([]byte(str))
}
// UnpackStr unpacks a string from the byte array
func (p *Packer) UnpackStr() string {
strSize := p.UnpackShort()
return string(p.UnpackFixedBytes(int(strSize)))
}
// UnpackLimitedStr unpacks a string. If the size of the string is greater than
// limit, adds errOversized to the packer and returns the empty string.
func (p *Packer) UnpackLimitedStr(limit uint16) string {
strSize := p.UnpackShort()
if strSize > limit {
p.Add(errOversized)
return ""
}
return string(p.UnpackFixedBytes(int(strSize)))
}
// checkSpace requires that there is at least bytes of write space left in the
// byte array. If this is not true, an error is added to the packer.
func (p *Packer) checkSpace(bytes int) {
switch {
case p.Offset < 0:
p.Add(errNegativeOffset)
case bytes < 0:
p.Add(errInvalidInput)
case len(p.Bytes)-p.Offset < bytes:
p.Add(ErrInsufficientLength)
}
}
// expand ensures that there is bytes bytes left of space in the byte slice.
// If this is not allowed due to the maximum size, an error is added to the packer.
func (p *Packer) expand(bytes int) {
neededSize := bytes + p.Offset
switch {
case neededSize <= len(p.Bytes):
return
case neededSize > p.MaxSize:
p.Err = ErrInsufficientLength
return
case neededSize <= cap(p.Bytes):
p.Bytes = p.Bytes[:neededSize]
return
default:
p.Bytes = append(p.Bytes[:cap(p.Bytes)], make([]byte, neededSize-cap(p.Bytes))...)
}
}
+364
View File
@@ -0,0 +1,364 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wrappers
import (
"testing"
"github.com/stretchr/testify/require"
)
const (
ByteSentinel = 0
ShortSentinel = 0
IntSentinel = 0
LongSentinel = 0
BoolSentinel = false
)
func TestPackerCheckSpace(t *testing.T) {
require := require.New(t)
p := Packer{Offset: -1}
p.checkSpace(1)
require.True(p.Errored())
require.ErrorIs(p.Err, errNegativeOffset)
p = Packer{}
p.checkSpace(-1)
require.True(p.Errored())
require.ErrorIs(p.Err, errInvalidInput)
p = Packer{Bytes: []byte{0x01}, Offset: 1}
p.checkSpace(1)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: []byte{0x01}, Offset: 2}
p.checkSpace(0)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerExpand(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01}, Offset: 2}
p.expand(1)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: []byte{0x01, 0x02, 0x03}, Offset: 0}
p.expand(1)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02, 0x03}, p.Bytes)
}
func TestPackerPackByte(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 1}
p.PackByte(0x01)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01}, p.Bytes)
p.PackByte(0x02)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackByte(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01}, Offset: 0}
require.Equal(uint8(1), p.UnpackByte())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(ByteLen, p.Offset)
require.Equal(uint8(ByteSentinel), p.UnpackByte())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackShort(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 2}
p.PackShort(0x0102)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02}, p.Bytes)
}
func TestPackerUnpackShort(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01, 0x02}, Offset: 0}
require.Equal(uint16(0x0102), p.UnpackShort())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(ShortLen, p.Offset)
require.Equal(uint16(ShortSentinel), p.UnpackShort())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackInt(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 4}
p.PackInt(0x01020304)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02, 0x03, 0x04}, p.Bytes)
p.PackInt(0x05060708)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackInt(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01, 0x02, 0x03, 0x04}, Offset: 0}
require.Equal(uint32(0x01020304), p.UnpackInt())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(IntLen, p.Offset)
require.Equal(uint32(IntSentinel), p.UnpackInt())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackLong(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 8}
p.PackLong(0x0102030405060708)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, p.Bytes)
p.PackLong(0x090a0b0c0d0e0f00)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackLong(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, Offset: 0}
require.Equal(uint64(0x0102030405060708), p.UnpackLong())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(LongLen, p.Offset)
require.Equal(uint64(LongSentinel), p.UnpackLong())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackFixedBytes(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 4}
p.PackFixedBytes([]byte("Lux"))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte("Lux"), p.Bytes)
p.PackFixedBytes([]byte("Lux"))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackFixedBytes(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("Lux")}
require.Equal([]byte("Lux"), p.UnpackFixedBytes(3))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(3, p.Offset)
require.Nil(p.UnpackFixedBytes(4))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackBytes(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 8}
p.PackBytes([]byte("Lux"))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte("\x00\x00\x00\x03Lux"), p.Bytes)
p.PackBytes([]byte("Lux"))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackBytes(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x00\x00\x03Lux")}
require.Equal([]byte("Lux"), p.UnpackBytes())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(7, p.Offset)
require.Nil(p.UnpackBytes())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackLimitedBytes(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x00\x00\x03Lux")}
require.Equal([]byte("Lux"), p.UnpackLimitedBytes(10))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(7, p.Offset)
require.Nil(p.UnpackLimitedBytes(10))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
// Reset and don't allow enough bytes
p = Packer{Bytes: p.Bytes}
require.Nil(p.UnpackLimitedBytes(2))
require.True(p.Errored())
require.ErrorIs(p.Err, errOversized)
}
func TestPackerString(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 6}
p.PackStr("Lux")
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x00, 0x03, 0x4c, 0x75, 0x78}, p.Bytes)
}
func TestPackerUnpackString(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x03Lux")}
require.Equal("Lux", p.UnpackStr())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(5, p.Offset)
require.Empty(p.UnpackStr())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackLimitedString(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x03Lux")}
require.Equal("Lux", p.UnpackLimitedStr(10))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(5, p.Offset)
require.Empty(p.UnpackLimitedStr(10))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
// Reset and don't allow enough bytes
p = Packer{Bytes: p.Bytes}
require.Empty(p.UnpackLimitedStr(2))
require.True(p.Errored())
require.ErrorIs(p.Err, errOversized)
}
func TestPacker(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 3}
require.False(p.Errored())
require.NoError(p.Err)
p.PackShort(17)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x0, 0x11}, p.Bytes)
p.PackShort(1)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: p.Bytes}
require.Equal(uint16(17), p.UnpackShort())
require.False(p.Errored())
require.NoError(p.Err)
}
func TestPackBool(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 3}
p.PackBool(false)
p.PackBool(true)
p.PackBool(false)
require.False(p.Errored())
require.NoError(p.Err)
p = Packer{Bytes: p.Bytes}
bool1, bool2, bool3 := p.UnpackBool(), p.UnpackBool(), p.UnpackBool()
require.False(p.Errored())
require.NoError(p.Err)
require.False(bool1)
require.True(bool2)
require.False(bool3)
}
func TestPackerPackBool(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 1}
p.PackBool(true)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01}, p.Bytes)
p.PackBool(false)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackBool(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01}, Offset: 0}
require.True(p.UnpackBool())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(BoolLen, p.Offset)
require.Equal(BoolSentinel, p.UnpackBool())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: []byte{0x42}, Offset: 0}
require.False(p.UnpackBool())
require.True(p.Errored())
require.ErrorIs(p.Err, errBadBool)
}
+23
View File
@@ -0,0 +1,23 @@
# zapcodec
ZAP-native little-endian reflection codec for the Lux platform.
See [LLM.md](./LLM.md) for the full module spec, wire format, and history.
```go
import (
"github.com/luxfi/zapcodec"
"github.com/luxfi/utils/wrappers"
)
c := zapcodec.NewDefault()
_ = c.RegisterType(&MyConcrete{})
p := &wrappers.Packer{MaxSize: 1 << 20}
_ = c.MarshalInto(value, p)
```
For the canonical version-prefix outer layer used by the SDK wallet,
see `github.com/luxfi/proto/zap_codec.NewVersionedManager`.
Extracted from `github.com/luxfi/codec/zapcodec` in Wave 2G-Archive.
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zapcodec
import "errors"
// Sentinel errors — same surface as the historical luxfi/codec
// sentinels so consumers asserting via errors.Is on the post-rename
// values get equivalent behaviour after the module move.
var (
ErrUnsupportedType = errors.New("zapcodec: unsupported type")
ErrMaxSliceLenExceeded = errors.New("zapcodec: max slice length exceeded")
ErrDoesNotImplementInterface = errors.New("zapcodec: does not implement interface")
ErrUnexportedField = errors.New("zapcodec: unexported field")
ErrMarshalZeroLength = errors.New("zapcodec: can't marshal zero length value")
ErrUnmarshalZeroLength = errors.New("zapcodec: can't unmarshal zero length value")
ErrMarshalNil = errors.New("zapcodec: can't marshal nil pointer")
ErrUnmarshalNil = errors.New("zapcodec: can't unmarshal into nil")
ErrDuplicateType = errors.New("zapcodec: duplicate type registration")
)
+259
View File
@@ -0,0 +1,259 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zapcodec
import (
"encoding/binary"
"errors"
"math"
)
// Packing constants (mirror wrappers.ByteLen et al., kept local so this
// package has no runtime dependency on wrappers — the codec.Manager that
// owns this codec passes raw byte slices in and out).
const (
byteLen = 1
shortLen = 2
intLen = 4
longLen = 8
boolLen = 1
// maxStringLen is the upper bound for a single string value. Mirrors
// the linearcodec MaxStringLen so cross-codec data carrying strings
// keeps the same envelope.
maxStringLen = math.MaxUint16
)
var (
// ErrInsufficientLength indicates the packer ran out of room (either
// growing past MaxSize on write, or reading past the buffer on read).
ErrInsufficientLength = errors.New("zapcodec: packer has insufficient length")
// errNegativeOffset indicates the caller drove the packer offset
// negative — defensive guard, should never fire on honest input.
errNegativeOffset = errors.New("zapcodec: negative offset")
// errInvalidInput indicates a structurally malformed input (e.g. a
// boolean byte that's neither 0 nor 1).
errInvalidInput = errors.New("zapcodec: input does not match expected format")
// errBadBool indicates a malformed bool tag byte.
errBadBool = errors.New("zapcodec: unexpected value when unpacking bool")
// errOversized indicates a length-prefixed value exceeded the
// caller-supplied limit.
errOversized = errors.New("zapcodec: size is larger than limit")
)
// packer is the zapcodec little-endian byte packer. Same surface as
// wrappers.Packer but emits/parses LE instead of BE. The point of going
// LE is that x86_64 and arm64 are little-endian by hardware, so the LE
// path emits raw MOV instructions where the BE path emits MOV+BSWAP.
// On the marshal hot path this is the dominant per-field cost.
//
// packer is intentionally NOT exported. Callers that want to marshal a
// type at the zapcodec wire format go through Codec.Marshal /
// Codec.Unmarshal on a codec.Manager registered via NewDefault().
type packer struct {
err error
maxSize int
bytes []byte
offset int
}
func (p *packer) addErr(e error) {
if p.err == nil {
p.err = e
}
}
func (p *packer) errored() bool { return p.err != nil }
// PackByte appends a byte.
func (p *packer) PackByte(v byte) {
p.expand(byteLen)
if p.errored() {
return
}
p.bytes[p.offset] = v
p.offset++
}
// UnpackByte reads a byte.
func (p *packer) UnpackByte() byte {
p.checkSpace(byteLen)
if p.errored() {
return 0
}
v := p.bytes[p.offset]
p.offset++
return v
}
// PackShort appends a uint16 in little-endian.
func (p *packer) PackShort(v uint16) {
p.expand(shortLen)
if p.errored() {
return
}
binary.LittleEndian.PutUint16(p.bytes[p.offset:], v)
p.offset += shortLen
}
// UnpackShort reads a uint16 in little-endian.
func (p *packer) UnpackShort() uint16 {
p.checkSpace(shortLen)
if p.errored() {
return 0
}
v := binary.LittleEndian.Uint16(p.bytes[p.offset:])
p.offset += shortLen
return v
}
// PackInt appends a uint32 in little-endian.
func (p *packer) PackInt(v uint32) {
p.expand(intLen)
if p.errored() {
return
}
binary.LittleEndian.PutUint32(p.bytes[p.offset:], v)
p.offset += intLen
}
// UnpackInt reads a uint32 in little-endian.
func (p *packer) UnpackInt() uint32 {
p.checkSpace(intLen)
if p.errored() {
return 0
}
v := binary.LittleEndian.Uint32(p.bytes[p.offset:])
p.offset += intLen
return v
}
// PackLong appends a uint64 in little-endian.
func (p *packer) PackLong(v uint64) {
p.expand(longLen)
if p.errored() {
return
}
binary.LittleEndian.PutUint64(p.bytes[p.offset:], v)
p.offset += longLen
}
// UnpackLong reads a uint64 in little-endian.
func (p *packer) UnpackLong() uint64 {
p.checkSpace(longLen)
if p.errored() {
return 0
}
v := binary.LittleEndian.Uint64(p.bytes[p.offset:])
p.offset += longLen
return v
}
// PackBool appends a bool as a single byte.
func (p *packer) PackBool(b bool) {
if b {
p.PackByte(1)
} else {
p.PackByte(0)
}
}
// UnpackBool reads a bool. Rejects any tag byte not in {0,1}.
func (p *packer) UnpackBool() bool {
b := p.UnpackByte()
switch b {
case 0:
return false
case 1:
return true
default:
p.addErr(errBadBool)
return false
}
}
// PackFixedBytes appends raw bytes (no length prefix).
func (p *packer) PackFixedBytes(b []byte) {
p.expand(len(b))
if p.errored() {
return
}
copy(p.bytes[p.offset:], b)
p.offset += len(b)
}
// UnpackFixedBytes reads n raw bytes (no length prefix).
func (p *packer) UnpackFixedBytes(n int) []byte {
p.checkSpace(n)
if p.errored() {
return nil
}
b := p.bytes[p.offset : p.offset+n]
p.offset += n
return b
}
// PackBytes appends a uint32-length-prefixed byte slice.
func (p *packer) PackBytes(b []byte) {
p.PackInt(uint32(len(b)))
p.PackFixedBytes(b)
}
// UnpackBytes reads a uint32-length-prefixed byte slice.
func (p *packer) UnpackBytes() []byte {
n := p.UnpackInt()
return p.UnpackFixedBytes(int(n))
}
// PackStr appends a string with a uint16 length prefix (mirrors
// wrappers.PackStr — same envelope shape, just LE).
func (p *packer) PackStr(s string) {
if len(s) > maxStringLen {
p.addErr(errInvalidInput)
return
}
p.PackShort(uint16(len(s)))
p.PackFixedBytes([]byte(s))
}
// UnpackStr reads a uint16-length-prefixed string.
func (p *packer) UnpackStr() string {
n := p.UnpackShort()
return string(p.UnpackFixedBytes(int(n)))
}
// checkSpace asserts that at least n bytes remain to read.
func (p *packer) checkSpace(n int) {
switch {
case p.offset < 0:
p.addErr(errNegativeOffset)
case n < 0:
p.addErr(errInvalidInput)
case len(p.bytes)-p.offset < n:
p.addErr(ErrInsufficientLength)
}
}
// expand grows the underlying buffer to accommodate n more write bytes.
// Refuses to grow past maxSize.
func (p *packer) expand(n int) {
needed := p.offset + n
switch {
case needed <= len(p.bytes):
return
case needed > p.maxSize:
p.err = ErrInsufficientLength
return
case needed <= cap(p.bytes):
p.bytes = p.bytes[:needed]
return
default:
p.bytes = append(p.bytes[:cap(p.bytes)], make([]byte, needed-cap(p.bytes))...)
}
}
+590
View File
@@ -0,0 +1,590 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zapcodec
import (
"bytes"
"errors"
"fmt"
"math"
"reflect"
"slices"
"github.com/luxfi/math/set"
)
const initialSliceLen = 16
var (
errNeedPointer = errors.New("zapcodec: argument to unmarshal must be a pointer")
errRecursiveInterfaceTypes = errors.New("zapcodec: recursive interface types")
)
// reflectiveCodec is the reflection-driven (un)marshaller. It walks
// struct fields via the embedded structFielder and emits/parses
// little-endian bytes via the local packer.
//
// The reflective walker is intentionally separate from the typeCoder
// (the registry-driven interface prefix codec): the codecImpl on
// zapcodec.go satisfies typeCoder, this walker calls into it through
// the typer field.
type reflectiveCodec struct {
typer typeCoder
fielder *structFielder
}
// typeCoder is the interface/registry-driven prefix codec. codecImpl
// satisfies this — UnpackPrefix reads a uint32 type-id and returns a
// fresh zero value of the registered type; PackPrefix writes the
// type-id; PrefixSize returns the constant 4-byte prefix width.
type typeCoder interface {
UnpackPrefix(*packer, reflect.Type) (reflect.Value, error)
PackPrefix(*packer, reflect.Type) error
PrefixSize(reflect.Type) int
}
func newReflective(t typeCoder, tagNames []string) *reflectiveCodec {
return &reflectiveCodec{
typer: t,
fielder: newStructFielder(tagNames),
}
}
// Size returns the byte size of marshalling value, before any version
// prefix the manager adds.
func (c *reflectiveCodec) Size(value interface{}) (int, error) {
if value == nil {
return 0, ErrMarshalNil
}
stack := make(set.Set[reflect.Type])
size, _, err := c.size(reflect.ValueOf(value), stack)
return size, err
}
func (c *reflectiveCodec) size(value reflect.Value, stack set.Set[reflect.Type]) (int, bool, error) {
switch kind := value.Kind(); kind {
case reflect.Uint8, reflect.Int8, reflect.Bool:
return byteLen, true, nil
case reflect.Uint16, reflect.Int16:
return shortLen, true, nil
case reflect.Uint32, reflect.Int32:
return intLen, true, nil
case reflect.Uint64, reflect.Int64:
return longLen, true, nil
case reflect.String:
// uint16 length prefix + body
return shortLen + len(value.String()), false, nil
case reflect.Ptr:
if value.IsNil() {
return 0, false, ErrMarshalNil
}
return c.size(value.Elem(), stack)
case reflect.Interface:
if value.IsNil() {
return 0, false, ErrMarshalNil
}
under := value.Interface()
underT := reflect.TypeOf(under)
if stack.Contains(underT) {
return 0, false, fmt.Errorf("%w: %s", errRecursiveInterfaceTypes, underT)
}
stack.Add(underT)
prefix := c.typer.PrefixSize(underT)
body, _, err := c.size(value.Elem(), stack)
stack.Remove(underT)
return prefix + body, false, err
case reflect.Slice:
n := value.Len()
if n == 0 {
return intLen, false, nil
}
elemSize, constSize, err := c.size(value.Index(0), stack)
if err != nil {
return 0, false, err
}
if elemSize == 0 {
return 0, false, fmt.Errorf("can't marshal slice of zero length values: %w", ErrMarshalZeroLength)
}
if constSize {
return intLen + n*elemSize, false, nil
}
total := elemSize
for i := 1; i < n; i++ {
inner, _, err := c.size(value.Index(i), stack)
if err != nil {
return 0, false, err
}
total += inner
}
return intLen + total, false, nil
case reflect.Array:
n := value.Len()
if n == 0 {
return 0, true, nil
}
elemSize, constSize, err := c.size(value.Index(0), stack)
if err != nil {
return 0, false, err
}
if constSize {
return n * elemSize, true, nil
}
total := elemSize
for i := 1; i < n; i++ {
inner, _, err := c.size(value.Index(i), stack)
if err != nil {
return 0, false, err
}
total += inner
}
return total, false, nil
case reflect.Struct:
fields, err := c.fielder.GetSerializedFields(value.Type())
if err != nil {
return 0, false, err
}
var (
total int
constSize = true
)
for _, fi := range fields {
inner, innerConst, err := c.size(value.Field(fi), stack)
if err != nil {
return 0, false, err
}
total += inner
constSize = constSize && innerConst
}
return total, constSize, nil
case reflect.Map:
iter := value.MapRange()
if !iter.Next() {
return intLen, false, nil
}
keySize, keyConst, err := c.size(iter.Key(), stack)
if err != nil {
return 0, false, err
}
valSize, valConst, err := c.size(iter.Value(), stack)
if err != nil {
return 0, false, err
}
if keySize == 0 && valSize == 0 {
return 0, false, fmt.Errorf("can't marshal map with zero length entries: %w", ErrMarshalZeroLength)
}
switch {
case keyConst && valConst:
n := value.Len()
return intLen + n*(keySize+valSize), false, nil
case keyConst:
n := 1
tot := valSize
for iter.Next() {
vs, _, err := c.size(iter.Value(), stack)
if err != nil {
return 0, false, err
}
tot += vs
n++
}
return intLen + n*keySize + tot, false, nil
case valConst:
n := 1
tot := keySize
for iter.Next() {
ks, _, err := c.size(iter.Key(), stack)
if err != nil {
return 0, false, err
}
tot += ks
n++
}
return intLen + tot + n*valSize, false, nil
default:
tot := intLen + keySize + valSize
for iter.Next() {
ks, _, err := c.size(iter.Key(), stack)
if err != nil {
return 0, false, err
}
vs, _, err := c.size(iter.Value(), stack)
if err != nil {
return 0, false, err
}
tot += ks + vs
}
return tot, false, nil
}
default:
return 0, false, fmt.Errorf("can't size unknown kind %s", kind)
}
}
// Marshal writes value into p. value MAY be a pointer-to-interface (in
// which case the interface prefix is written before the underlying).
func (c *reflectiveCodec) Marshal(value interface{}, p *packer) error {
if value == nil {
return ErrMarshalNil
}
stack := make(set.Set[reflect.Type])
return c.marshal(reflect.ValueOf(value), p, stack)
}
func (c *reflectiveCodec) marshal(value reflect.Value, p *packer, stack set.Set[reflect.Type]) error {
switch kind := value.Kind(); kind {
case reflect.Uint8:
p.PackByte(uint8(value.Uint()))
return p.err
case reflect.Int8:
p.PackByte(uint8(value.Int()))
return p.err
case reflect.Uint16:
p.PackShort(uint16(value.Uint()))
return p.err
case reflect.Int16:
p.PackShort(uint16(value.Int()))
return p.err
case reflect.Uint32:
p.PackInt(uint32(value.Uint()))
return p.err
case reflect.Int32:
p.PackInt(uint32(value.Int()))
return p.err
case reflect.Uint64:
p.PackLong(value.Uint())
return p.err
case reflect.Int64:
p.PackLong(uint64(value.Int()))
return p.err
case reflect.String:
p.PackStr(value.String())
return p.err
case reflect.Bool:
p.PackBool(value.Bool())
return p.err
case reflect.Ptr:
if value.IsNil() {
return ErrMarshalNil
}
return c.marshal(value.Elem(), p, stack)
case reflect.Interface:
if value.IsNil() {
return ErrMarshalNil
}
under := value.Interface()
underT := reflect.TypeOf(under)
if stack.Contains(underT) {
return fmt.Errorf("%w: %s", errRecursiveInterfaceTypes, underT)
}
stack.Add(underT)
if err := c.typer.PackPrefix(p, underT); err != nil {
return err
}
if err := c.marshal(value.Elem(), p, stack); err != nil {
return err
}
stack.Remove(underT)
return p.err
case reflect.Slice:
n := value.Len()
if n > math.MaxInt32 {
return fmt.Errorf("%w; slice length %d exceeds %d",
ErrMaxSliceLenExceeded, n, math.MaxInt32)
}
p.PackInt(uint32(n))
if p.err != nil {
return p.err
}
if n == 0 {
return nil
}
if value.Type().Elem().Kind() == reflect.Uint8 {
p.PackFixedBytes(value.Bytes())
return p.err
}
for i := 0; i < n; i++ {
start := p.offset
if err := c.marshal(value.Index(i), p, stack); err != nil {
return err
}
if start == p.offset {
return fmt.Errorf("couldn't marshal slice of zero length values: %w", ErrMarshalZeroLength)
}
}
return nil
case reflect.Array:
if value.CanAddr() && value.Type().Elem().Kind() == reflect.Uint8 {
p.PackFixedBytes(value.Bytes())
return p.err
}
n := value.Len()
for i := 0; i < n; i++ {
if err := c.marshal(value.Index(i), p, stack); err != nil {
return err
}
}
return nil
case reflect.Struct:
fields, err := c.fielder.GetSerializedFields(value.Type())
if err != nil {
return err
}
for _, fi := range fields {
if err := c.marshal(value.Field(fi), p, stack); err != nil {
return err
}
}
return nil
case reflect.Map:
keys := value.MapKeys()
n := len(keys)
if n > math.MaxInt32 {
return fmt.Errorf("%w; map length %d exceeds %d",
ErrMaxSliceLenExceeded, n, math.MaxInt32)
}
p.PackInt(uint32(n))
if p.err != nil {
return p.err
}
type keyTuple struct {
key reflect.Value
startIdx, endIdx int
}
sortedKeys := make([]keyTuple, len(keys))
startOff := p.offset
endOff := p.offset
for i, k := range keys {
if err := c.marshal(k, p, stack); err != nil {
return err
}
if p.err != nil {
return fmt.Errorf("couldn't marshal map key %+v: %w", k, p.err)
}
sortedKeys[i] = keyTuple{key: k, startIdx: endOff, endIdx: p.offset}
endOff = p.offset
}
slices.SortFunc(sortedKeys, func(a, b keyTuple) int {
return bytes.Compare(p.bytes[a.startIdx:a.endIdx], p.bytes[b.startIdx:b.endIdx])
})
allKeys := slices.Clone(p.bytes[startOff:p.offset])
p.offset = startOff
for _, k := range sortedKeys {
keyStart := p.offset
si := k.startIdx - startOff
ei := k.endIdx - startOff
p.PackFixedBytes(allKeys[si:ei])
if p.err != nil {
return p.err
}
if err := c.marshal(value.MapIndex(k.key), p, stack); err != nil {
return err
}
if keyStart == p.offset {
return fmt.Errorf("couldn't marshal map with zero length entries: %w", ErrMarshalZeroLength)
}
}
return nil
default:
return fmt.Errorf("%w: %s", ErrUnsupportedType, kind)
}
}
// Unmarshal reads dest (pointer-typed) from p.
func (c *reflectiveCodec) Unmarshal(p *packer, dest interface{}) error {
if dest == nil {
return ErrUnmarshalNil
}
dv := reflect.ValueOf(dest)
if dv.Kind() != reflect.Ptr {
return errNeedPointer
}
stack := make(set.Set[reflect.Type])
return c.unmarshal(p, dv.Elem(), stack)
}
func (c *reflectiveCodec) unmarshal(p *packer, value reflect.Value, stack set.Set[reflect.Type]) error {
switch value.Kind() {
case reflect.Uint8:
value.SetUint(uint64(p.UnpackByte()))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal uint8: %w", p.err)
}
return nil
case reflect.Int8:
value.SetInt(int64(int8(p.UnpackByte())))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal int8: %w", p.err)
}
return nil
case reflect.Uint16:
value.SetUint(uint64(p.UnpackShort()))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal uint16: %w", p.err)
}
return nil
case reflect.Int16:
value.SetInt(int64(int16(p.UnpackShort())))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal int16: %w", p.err)
}
return nil
case reflect.Uint32:
value.SetUint(uint64(p.UnpackInt()))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal uint32: %w", p.err)
}
return nil
case reflect.Int32:
value.SetInt(int64(int32(p.UnpackInt())))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal int32: %w", p.err)
}
return nil
case reflect.Uint64:
value.SetUint(p.UnpackLong())
if p.err != nil {
return fmt.Errorf("couldn't unmarshal uint64: %w", p.err)
}
return nil
case reflect.Int64:
value.SetInt(int64(p.UnpackLong()))
if p.err != nil {
return fmt.Errorf("couldn't unmarshal int64: %w", p.err)
}
return nil
case reflect.Bool:
value.SetBool(p.UnpackBool())
if p.err != nil {
return fmt.Errorf("couldn't unmarshal bool: %w", p.err)
}
return nil
case reflect.Slice:
n32 := p.UnpackInt()
if p.err != nil {
return fmt.Errorf("couldn't unmarshal slice: %w", p.err)
}
if n32 > math.MaxInt32 {
return fmt.Errorf("%w; array length %d exceeds %d",
ErrMaxSliceLenExceeded, n32, math.MaxInt32)
}
n := int(n32)
sliceT := value.Type()
innerT := sliceT.Elem()
if innerT.Kind() == reflect.Uint8 {
value.SetBytes(p.UnpackFixedBytes(n))
return p.err
}
value.Set(reflect.MakeSlice(sliceT, 0, initialSliceLen))
zero := reflect.Zero(innerT)
for i := 0; i < n; i++ {
value.Set(reflect.Append(value, zero))
start := p.offset
if err := c.unmarshal(p, value.Index(i), stack); err != nil {
return err
}
if start == p.offset {
return fmt.Errorf("couldn't unmarshal slice of zero length values: %w", ErrUnmarshalZeroLength)
}
}
return nil
case reflect.Array:
n := value.Len()
if value.Type().Elem().Kind() == reflect.Uint8 {
unpacked := p.UnpackFixedBytes(n)
if p.errored() {
return p.err
}
under := value.Slice(0, n).Interface().([]byte)
copy(under, unpacked)
return nil
}
for i := 0; i < n; i++ {
if err := c.unmarshal(p, value.Index(i), stack); err != nil {
return err
}
}
return nil
case reflect.String:
value.SetString(p.UnpackStr())
if p.err != nil {
return fmt.Errorf("couldn't unmarshal string: %w", p.err)
}
return nil
case reflect.Interface:
impl, err := c.typer.UnpackPrefix(p, value.Type())
if err != nil {
return err
}
implT := impl.Type()
if stack.Contains(implT) {
return fmt.Errorf("%w: %s", errRecursiveInterfaceTypes, implT)
}
stack.Add(implT)
if err := c.unmarshal(p, impl, stack); err != nil {
return err
}
stack.Remove(implT)
value.Set(impl)
return nil
case reflect.Struct:
fields, err := c.fielder.GetSerializedFields(value.Type())
if err != nil {
return fmt.Errorf("couldn't unmarshal struct: %w", err)
}
for _, fi := range fields {
if err := c.unmarshal(p, value.Field(fi), stack); err != nil {
return err
}
}
return nil
case reflect.Ptr:
t := value.Type().Elem()
v := reflect.New(t)
if err := c.unmarshal(p, v.Elem(), stack); err != nil {
return err
}
value.Set(v)
return nil
case reflect.Map:
n32 := p.UnpackInt()
if p.err != nil {
return fmt.Errorf("couldn't unmarshal map: %w", p.err)
}
if n32 > math.MaxInt32 {
return fmt.Errorf("%w; map length %d exceeds %d",
ErrMaxSliceLenExceeded, n32, math.MaxInt32)
}
var (
n = int(n32)
mapT = value.Type()
keyT = mapT.Key()
valT = mapT.Elem()
prevKey []byte
)
value.Set(reflect.MakeMap(mapT))
for i := 0; i < n; i++ {
k := reflect.New(keyT).Elem()
keyStart := p.offset
if err := c.unmarshal(p, k, stack); err != nil {
return err
}
keyBytes := p.bytes[keyStart:p.offset]
if i != 0 && bytes.Compare(keyBytes, prevKey) <= 0 {
return fmt.Errorf("keys aren't sorted: (%s, %s)", prevKey, k)
}
prevKey = keyBytes
v := reflect.New(valT).Elem()
if err := c.unmarshal(p, v, stack); err != nil {
return err
}
if keyStart == p.offset {
return fmt.Errorf("couldn't unmarshal map with zero length entries: %w", ErrUnmarshalZeroLength)
}
value.SetMapIndex(k, v)
}
return nil
default:
return fmt.Errorf("can't unmarshal unknown type %s", value.Kind())
}
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zapcodec
import (
"fmt"
"reflect"
"sync"
)
// DefaultTagName is the struct tag this codec honours. Same value as
// the legacy luxfi/codec linearcodec used ("serialize") — the post-
// extraction wire format is byte-identical to what consumers expect.
const DefaultTagName = "serialize"
// tagValue is the value the struct tag must have for a field to be
// serialised — e.g. `serialize:"true"`.
const tagValue = "true"
// structFielder discovers serializable fields in a struct via tag
// inspection. Cached per-type — the cache is keyed by reflect.Type
// pointer so it's stable across calls.
type structFielder struct {
lock sync.RWMutex
// tags are the struct-tag names this fielder honours. A field is
// serialised if ANY of the listed tags has value "true".
tags []string
// serializedFieldIndices caches the per-type field-index slice.
serializedFieldIndices map[reflect.Type][]int
}
// newStructFielder constructs a fresh structFielder over the supplied
// tag names. Concurrency-safe after construction.
func newStructFielder(tagNames []string) *structFielder {
return &structFielder{
tags: tagNames,
serializedFieldIndices: make(map[reflect.Type][]int),
}
}
// GetSerializedFields returns the indices of fields in t that have
// any of the structFielder's tags set to "true". Caches per-type.
// Returns ErrUnexportedField if a tagged field is un-exported (which
// would prevent reflect-based marshalling).
func (s *structFielder) GetSerializedFields(t reflect.Type) ([]int, error) {
if cached, ok := s.getCached(t); ok {
return cached, nil
}
s.lock.Lock()
defer s.lock.Unlock()
numFields := t.NumField()
serializedFields := make([]int, 0, numFields)
for i := 0; i < numFields; i++ {
field := t.Field(i)
var capture bool
for _, tag := range s.tags {
if field.Tag.Get(tag) == tagValue {
capture = true
break
}
}
if !capture {
continue
}
if !field.IsExported() {
return nil, fmt.Errorf("can not marshal %w: %s", ErrUnexportedField, field.Name)
}
serializedFields = append(serializedFields, i)
}
s.serializedFieldIndices[t] = serializedFields
return serializedFields, nil
}
// getCached returns the cached field-index slice for t. The bool
// distinguishes "cached as empty" from "not cached".
func (s *structFielder) getCached(t reflect.Type) ([]int, bool) {
s.lock.RLock()
defer s.lock.RUnlock()
cached, ok := s.serializedFieldIndices[t]
return cached, ok
}
+259
View File
@@ -0,0 +1,259 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package zapcodec is the ZAP-native little-endian reflection codec.
//
// Module history: this package originated inside github.com/luxfi/codec
// as codec/zapcodec, a drop-in replacement for codec/linearcodec that
// emits little-endian bytes. After the Wave 2G codec rip moved every
// production caller off github.com/luxfi/codec, zapcodec was extracted
// into its own top-level module so consumers (proto/zap_codec, the
// canonical wallet codec construction site) can depend on it without
// pulling in the archived codec module.
//
// Wire-format delta vs the historical linearcodec:
//
// - All multi-byte integers are little-endian. x86_64 and arm64
// hardware is LE-native; LE writes map to single MOV instructions
// where BE writes need BSWAP.
// - Interface type-id prefixes are uint32 LE (linearcodec used uint32
// BE).
// - String length prefix is uint16 LE (linearcodec used uint16 BE).
// - Slice/map length prefixes are uint32 (same width as linearcodec,
// LE byte order).
// - Bool is a single byte, struct fields are emitted in
// serialize-tag order — same as linearcodec.
//
// Self-contained design (Hickey decomplection):
//
// - VALUE: the wire codec choice. Today: little-endian reflection
// codec. The value lives here, in its own module, qualified by
// namespace.
// - COMPOSITION: the public Codec interface (MarshalInto /
// UnmarshalFrom / Size) is satisfied by *codecImpl. Callers compose
// this with their own version-prefix outer layer (see
// proto/zap_codec.Manager for the canonical wiring).
// - ORTHOGONAL: this package has no knowledge of any specific wire
// payload type (PVM/XVM/warp/...) — it's a generic reflection-
// driven (un)marshaller. Per-type registration happens at the
// caller via Codec.RegisterType.
//
// Dependencies: the only external dependency is luxfi/utils/wrappers
// for the Packer type that crosses module boundaries on
// MarshalInto/UnmarshalFrom. The reflection-driven encoder body uses
// a local little-endian packer (zapcodec/packer.go) that aliases the
// wrappers.Packer's underlying buffer — no per-Marshal copy.
package zapcodec
import (
"errors"
"fmt"
"reflect"
"sync"
"github.com/luxfi/container/bimap"
"github.com/luxfi/utils/wrappers"
)
// Codec is the zapcodec public surface — the reflection-driven
// (un)marshaller plus a sequential-type-id registry.
//
// Implementations are concurrency-safe: RegisterType / SkipRegistrations
// take an internal write lock; Marshal/Unmarshal/Size take a read lock
// only when inspecting the registry.
type Codec interface {
// RegisterType assigns val the next sequential type-id so it can be
// (un)marshalled into an interface field at the same codec
// instance.
RegisterType(val interface{}) error
// SkipRegistrations bumps the next-type-id counter by n. Lets
// callers preserve historical type-id slot layouts during a
// migration window.
SkipRegistrations(n int)
// MarshalInto serialises value into p. value MAY be a pointer-to-
// interface (in which case the interface type-id prefix is written
// before the underlying value).
MarshalInto(value interface{}, p *wrappers.Packer) error
// UnmarshalFrom deserialises p into dest. dest MUST be a pointer.
UnmarshalFrom(p *wrappers.Packer, dest interface{}) error
// Size returns the on-wire size of value before any outer
// version-prefix layer the manager applies.
Size(value interface{}) (int, error)
}
// Registry is the type-registration sub-surface of Codec. Useful for
// callers that only need to register types onto a codec they hold by
// the narrower contract.
type Registry interface {
RegisterType(val interface{}) error
}
// GeneralCodec is the union of Codec and Registry — provided for
// structural symmetry with the historical luxfi/codec.GeneralCodec.
type GeneralCodec interface {
Codec
Registry
}
// codecImpl is the concrete implementation behind New / NewDefault.
type codecImpl struct {
reflective *reflectiveCodec
lock sync.RWMutex
nextTypeID uint32
registeredTypes *bimap.BiMap[uint32, reflect.Type]
}
// New returns a fresh zapcodec instance that honours the supplied
// struct-tag names. Concurrency-safe.
func New(tagNames []string) Codec {
c := &codecImpl{
nextTypeID: 0,
registeredTypes: bimap.New[uint32, reflect.Type](),
}
c.reflective = newReflective(c, tagNames)
return c
}
// NewDefault returns a zapcodec instance honouring the "serialize"
// struct tag — the canonical configuration.
func NewDefault() Codec {
return New([]string{DefaultTagName})
}
// SkipRegistrations bumps the next-type-id counter by n.
func (c *codecImpl) SkipRegistrations(n int) {
c.lock.Lock()
c.nextTypeID += uint32(n)
c.lock.Unlock()
}
// RegisterType registers val under the next sequential type-id.
func (c *codecImpl) RegisterType(val interface{}) error {
c.lock.Lock()
defer c.lock.Unlock()
t := reflect.TypeOf(val)
if c.registeredTypes.HasValue(t) {
return fmt.Errorf("%w: %v", ErrDuplicateType, t)
}
c.registeredTypes.Put(c.nextTypeID, t)
c.nextTypeID++
return nil
}
// PrefixSize is the size of an interface type-id prefix — uint32, 4
// bytes. The reflective codec calls this when computing Size for
// values that contain interface fields.
func (*codecImpl) PrefixSize(reflect.Type) int { return intLen }
// PackPrefix writes the type-id prefix for valueType into p.
func (c *codecImpl) PackPrefix(p *packer, valueType reflect.Type) error {
c.lock.RLock()
defer c.lock.RUnlock()
id, ok := c.registeredTypes.GetKey(valueType)
if !ok {
return fmt.Errorf("can't marshal unregistered type %q", valueType)
}
p.PackInt(id)
return p.err
}
// UnpackPrefix reads a type-id prefix and returns a new zero value of
// the registered concrete type implementing valueType.
func (c *codecImpl) UnpackPrefix(p *packer, valueType reflect.Type) (reflect.Value, error) {
c.lock.RLock()
defer c.lock.RUnlock()
id := p.UnpackInt()
if p.err != nil {
return reflect.Value{}, fmt.Errorf("couldn't unmarshal interface: %w", p.err)
}
implT, ok := c.registeredTypes.GetValue(id)
if !ok {
return reflect.Value{}, fmt.Errorf("couldn't unmarshal interface: unknown type ID %d", id)
}
if !implT.Implements(valueType) {
return reflect.Value{}, fmt.Errorf("couldn't unmarshal interface: %s %w %s",
implT, ErrDoesNotImplementInterface, valueType)
}
return reflect.New(implT).Elem(), nil
}
// MarshalInto serialises value into the supplied wrappers.Packer. The
// local zapcodec packer aliases p's underlying buffer directly — no
// per-Marshal heap alloc for the packer itself.
func (c *codecImpl) MarshalInto(value interface{}, p *wrappers.Packer) error {
if value == nil {
return ErrMarshalNil
}
zp := packer{
err: p.Err,
maxSize: p.MaxSize,
bytes: p.Bytes,
offset: p.Offset,
}
err := c.reflective.Marshal(value, &zp)
p.Bytes = zp.bytes
p.Offset = zp.offset
if p.Err == nil {
p.Err = chainWrappersErr(zp.err)
}
if err != nil {
return chainWrappersErr(err)
}
return p.Err
}
// UnmarshalFrom deserialises dest from p.
func (c *codecImpl) UnmarshalFrom(p *wrappers.Packer, dest interface{}) error {
zp := packer{
err: p.Err,
maxSize: p.MaxSize,
bytes: p.Bytes,
offset: p.Offset,
}
err := c.reflective.Unmarshal(&zp, dest)
p.Bytes = zp.bytes
p.Offset = zp.offset
if p.Err == nil {
p.Err = chainWrappersErr(zp.err)
}
if err != nil {
return chainWrappersErr(err)
}
return p.Err
}
// chainWrappersErr wraps zapcodec sentinel errors so they chain into
// their luxfi/utils/wrappers equivalents for errors.Is compatibility.
//
// Callers that historically asserted on wrappers.ErrInsufficientLength
// (e.g. proto/p/state's TestGetFeeStateErrors,
// vms/proposervm/block's TestParseBytes, vms/platformvm/state's
// TestParseValidator/DelegatorMetadata) continue to match without
// edit. Callers asserting on zapcodec.ErrInsufficientLength directly
// also match — both names are in the chain.
//
// Wrapping is an O(1) errors.Join — no allocation when err is nil.
func chainWrappersErr(err error) error {
if err == nil {
return nil
}
switch {
case errors.Is(err, ErrInsufficientLength):
return errors.Join(wrappers.ErrInsufficientLength, err)
}
return err
}
// Size returns the on-wire size of value, excluding any outer
// version-prefix layer the manager applies.
func (c *codecImpl) Size(value interface{}) (int, error) {
return c.reflective.Size(value)
}
+382
View File
@@ -0,0 +1,382 @@
// Copyright (C) 2025-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package zapcodec
import (
"encoding/binary"
"errors"
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/utils/wrappers"
)
// ---------------------------------------------------------------------
// Test fixtures
//
// Each fixture covers one structural shape. Together they exercise every
// code path in reflectiveCodec.{size,marshal,unmarshal}: integers of all
// widths, bool, string, slice (byte and non-byte), array, struct,
// nested struct, interface dispatch, and map.
// ---------------------------------------------------------------------
type prims struct {
U8 uint8 `serialize:"true"`
U16 uint16 `serialize:"true"`
U32 uint32 `serialize:"true"`
U64 uint64 `serialize:"true"`
I8 int8 `serialize:"true"`
I16 int16 `serialize:"true"`
I32 int32 `serialize:"true"`
I64 int64 `serialize:"true"`
B bool `serialize:"true"`
S string `serialize:"true"`
}
type bytesBag struct {
Fixed [4]byte `serialize:"true"`
Slice []byte `serialize:"true"`
}
type sliceOfStructs struct {
Items []prims `serialize:"true"`
}
type mapWrapper struct {
M map[uint32]uint32 `serialize:"true"`
}
// shape is the test interface used for interface-prefix dispatch.
type shape interface {
tag() string
}
type circle struct {
R uint32 `serialize:"true"`
}
func (*circle) tag() string { return "circle" }
type square struct {
Side uint32 `serialize:"true"`
}
func (*square) tag() string { return "square" }
type shapeHolder struct {
S shape `serialize:"true"`
}
// ---------------------------------------------------------------------
// Test-local Manager
//
// The historical luxfi/codec.Manager prepended a 2-byte BE version
// prefix before calling Codec.MarshalInto. This test file embeds a
// tiny equivalent so the zapcodec module is independently testable
// without depending on the legacy codec module.
//
// The version prefix is BE on purpose — that mirrors the historical
// codec.Manager.PackShort behaviour the original tests asserted on
// (bytes 0-1 are the version, BE-encoded). Production callers using
// proto/zap_codec.Manager get an LE version prefix — see that package
// for the canonical wiring.
// ---------------------------------------------------------------------
type testManager struct {
codec Codec
version uint16
}
func newTestManager(c Codec, version uint16) *testManager {
return &testManager{codec: c, version: version}
}
func (m *testManager) Marshal(version uint16, src interface{}) ([]byte, error) {
if version != m.version {
return nil, errors.New("unknown codec version")
}
p := &wrappers.Packer{MaxSize: 1 << 20}
p.PackShort(version) // BE per codec.Manager historical contract
if p.Errored() {
return nil, p.Err
}
if err := m.codec.MarshalInto(src, p); err != nil {
return nil, err
}
if p.Err != nil {
return nil, p.Err
}
return p.Bytes[:p.Offset], nil
}
func (m *testManager) Unmarshal(buf []byte, dst interface{}) (uint16, error) {
if len(buf) < 2 {
return 0, errors.New("can't unpack version")
}
p := &wrappers.Packer{Bytes: buf, MaxSize: 1 << 20}
v := p.UnpackShort()
if p.Errored() {
return 0, p.Err
}
if v != m.version {
return v, errors.New("unknown codec version")
}
if err := m.codec.UnmarshalFrom(p, dst); err != nil {
return v, err
}
if p.Offset != len(buf) {
return v, errors.New("trailing buffer space")
}
return v, nil
}
func (m *testManager) Size(version uint16, value interface{}) (int, error) {
if version != m.version {
return 0, errors.New("unknown codec version")
}
sz, err := m.codec.Size(value)
if err != nil {
return 0, err
}
return 2 + sz, nil // 2-byte version prefix
}
// ---------------------------------------------------------------------
// Setup helpers
// ---------------------------------------------------------------------
func newManager(t *testing.T) *testManager {
t.Helper()
c := NewDefault()
require.NoError(t, c.RegisterType(&circle{}))
require.NoError(t, c.RegisterType(&square{}))
return newTestManager(c, 0)
}
// ---------------------------------------------------------------------
// Round-trip tests
// ---------------------------------------------------------------------
func TestRoundTripPrims(t *testing.T) {
m := newManager(t)
in := &prims{
U8: 0xAA, U16: 0xBEEF, U32: 0xDEADBEEF, U64: 0xCAFEBABEDEADBEEF,
I8: -1, I16: -2, I32: -3, I64: -4,
B: true, S: "ZAP rules",
}
b, err := m.Marshal(0, in)
require.NoError(t, err)
var out prims
v, err := m.Unmarshal(b, &out)
require.NoError(t, err)
require.Equal(t, uint16(0), v)
require.Equal(t, *in, out)
}
func TestRoundTripBytes(t *testing.T) {
m := newManager(t)
in := &bytesBag{Fixed: [4]byte{1, 2, 3, 4}, Slice: []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE}}
b, err := m.Marshal(0, in)
require.NoError(t, err)
var out bytesBag
_, err = m.Unmarshal(b, &out)
require.NoError(t, err)
require.Equal(t, *in, out)
}
func TestRoundTripSliceOfStructs(t *testing.T) {
m := newManager(t)
in := &sliceOfStructs{
Items: []prims{
{U8: 1, U32: 100, S: "first"},
{U8: 2, U32: 200, S: "second"},
{U8: 3, U32: 300, S: "third"},
},
}
b, err := m.Marshal(0, in)
require.NoError(t, err)
var out sliceOfStructs
_, err = m.Unmarshal(b, &out)
require.NoError(t, err)
require.Equal(t, *in, out)
}
func TestRoundTripMap(t *testing.T) {
m := newManager(t)
in := &mapWrapper{M: map[uint32]uint32{1: 100, 2: 200, 3: 300}}
b, err := m.Marshal(0, in)
require.NoError(t, err)
var out mapWrapper
_, err = m.Unmarshal(b, &out)
require.NoError(t, err)
require.Equal(t, *in, out)
}
func TestRoundTripInterface(t *testing.T) {
m := newManager(t)
in1 := &shapeHolder{S: &circle{R: 42}}
b, err := m.Marshal(0, in1)
require.NoError(t, err)
var out1 shapeHolder
_, err = m.Unmarshal(b, &out1)
require.NoError(t, err)
require.Equal(t, uint32(42), out1.S.(*circle).R)
in2 := &shapeHolder{S: &square{Side: 7}}
b, err = m.Marshal(0, in2)
require.NoError(t, err)
var out2 shapeHolder
_, err = m.Unmarshal(b, &out2)
require.NoError(t, err)
require.Equal(t, uint32(7), out2.S.(*square).Side)
}
// ---------------------------------------------------------------------
// Wire-format assertions
// ---------------------------------------------------------------------
// TestWireIsLittleEndian asserts the codec emits little-endian
// multi-byte integers. The testManager's 2-byte version prefix is
// BigEndian (matching the historical codec.Manager contract);
// everything past byte 2 is the codec's output and must be LE.
func TestWireIsLittleEndian(t *testing.T) {
m := newManager(t)
// Pick a value whose LE and BE encodings are byte-distinct:
// 0xDEADBEEF → LE: EF BE AD DE, BE: DE AD BE EF.
in := &struct {
X uint32 `serialize:"true"`
}{X: 0xDEADBEEF}
b, err := m.Marshal(0, in)
require.NoError(t, err)
// Bytes 0-1: codec version (BE 0x0000).
require.Equal(t, byte(0x00), b[0])
require.Equal(t, byte(0x00), b[1])
// Bytes 2-5: the uint32 payload in LE.
require.Equal(t, byte(0xEF), b[2])
require.Equal(t, byte(0xBE), b[3])
require.Equal(t, byte(0xAD), b[4])
require.Equal(t, byte(0xDE), b[5])
// Cross-check via binary.LittleEndian:
require.Equal(t, uint32(0xDEADBEEF), binary.LittleEndian.Uint32(b[2:]))
}
// TestWireSliceLenIsLE asserts that slice length prefixes are also LE.
func TestWireSliceLenIsLE(t *testing.T) {
m := newManager(t)
in := &struct {
B []byte `serialize:"true"`
}{B: []byte{1, 2, 3}}
b, err := m.Marshal(0, in)
require.NoError(t, err)
// Bytes 0-1: version. Bytes 2-5: LE uint32 length = 3.
require.Equal(t, uint32(3), binary.LittleEndian.Uint32(b[2:]))
require.Equal(t, byte(1), b[6])
require.Equal(t, byte(2), b[7])
require.Equal(t, byte(3), b[8])
}
// ---------------------------------------------------------------------
// Error paths
// ---------------------------------------------------------------------
func TestMarshalNil(t *testing.T) {
m := newManager(t)
_, err := m.Marshal(0, nil)
require.ErrorIs(t, err, ErrMarshalNil)
}
func TestUnmarshalNeedsPointer(t *testing.T) {
m := newManager(t)
in := &prims{U32: 99}
b, err := m.Marshal(0, in)
require.NoError(t, err)
// Pass a non-pointer destination; the codec layer should reject.
var out prims
_, err = m.Unmarshal(b, out)
require.Error(t, err)
// Specific sentinel: errNeedPointer is internal; the manager
// wraps it. errors.Is on the public sentinel ErrUnmarshalNil is
// NOT what fires here — this is "not a pointer", a distinct
// error. Just assert the error contains the expected phrase.
require.Contains(t, err.Error(), "must be a pointer")
}
func TestUnmarshalShortBuffer(t *testing.T) {
m := newManager(t)
// Buffer carries only the codec version (2 bytes). A prims has
// 8 + 2 + 4 + 8 + ... bytes of fixed-size content; reading any
// integer should produce ErrInsufficientLength wrapped into the
// codec error chain.
short := []byte{0x00, 0x00}
var out prims
_, err := m.Unmarshal(short, &out)
require.Error(t, err)
// Underlying cause is our ErrInsufficientLength.
require.True(t, errors.Is(err, ErrInsufficientLength),
"expected ErrInsufficientLength chain, got %v", err)
}
// TestSizeMatchesMarshalLen is a structural invariant: Size(v) MUST
// equal len(Marshal(v)) - len(version prefix). If they diverge, either
// the size calculation is wrong or the marshal emits extra bytes — both
// are wire-format bugs.
func TestSizeMatchesMarshalLen(t *testing.T) {
m := newManager(t)
cases := []interface{}{
&prims{U64: 99, S: "abc"},
&bytesBag{Slice: []byte{1, 2, 3, 4, 5, 6, 7}},
&sliceOfStructs{Items: []prims{{U8: 1}, {U8: 2}}},
&mapWrapper{M: map[uint32]uint32{1: 10, 2: 20}},
&shapeHolder{S: &circle{R: 7}},
}
for _, c := range cases {
b, err := m.Marshal(0, c)
require.NoError(t, err)
sz, err := m.Size(0, c)
require.NoError(t, err)
require.Equal(t, len(b), sz, "Size and Marshal disagree for %T", c)
}
}
// TestDuplicateRegistration asserts the Registry contract — the
// same type registered twice returns ErrDuplicateType.
func TestDuplicateRegistration(t *testing.T) {
c := NewDefault()
require.NoError(t, c.RegisterType(&circle{}))
err := c.RegisterType(&circle{})
require.ErrorIs(t, err, ErrDuplicateType)
}
// TestSkipRegistrationsKeepsSlotIDs asserts that SkipRegistrations
// does what its name says: bumps the next-type-id counter without
// registering, so subsequent registrations land at the expected ID.
// The slot-ID effect is observed externally: after Skip(5), a
// registered type unmarshalled from a wire with id=5 must decode.
func TestSkipRegistrationsKeepsSlotIDs(t *testing.T) {
c := NewDefault()
c.SkipRegistrations(5)
require.NoError(t, c.RegisterType(&circle{}))
m := newTestManager(c, 0)
in := &shapeHolder{S: &circle{R: 99}}
b, err := m.Marshal(0, in)
require.NoError(t, err)
// The interface-type-id is at byte offset (2 version + struct
// prelude). For shapeHolder the only field is the interface, so
// the type-id is the first 4 bytes after the version prefix.
require.Equal(t, uint32(5), binary.LittleEndian.Uint32(b[2:6]))
var out shapeHolder
_, err = m.Unmarshal(b, &out)
require.NoError(t, err)
require.Equal(t, uint32(99), out.S.(*circle).R)
}
+24 -1
View File
@@ -3,19 +3,42 @@ module github.com/luxfi/zap
go 1.26.3
require (
github.com/luxfi/container v0.0.4
github.com/luxfi/ids v1.2.9
github.com/luxfi/math v1.4.0
github.com/luxfi/mdns v0.1.1
github.com/luxfi/pq v1.0.3
github.com/luxfi/utils v1.1.4
github.com/quic-go/quic-go v0.59.1
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
golang.org/x/crypto v0.52.0
)
require (
github.com/cloudflare/circl v1.6.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/luxfi/accel v1.1.9 // indirect
github.com/luxfi/atomic v1.0.0 // indirect
github.com/luxfi/cache v1.2.1 // indirect
github.com/luxfi/constants v1.4.7 // indirect
github.com/luxfi/geth v1.16.98 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/metric v1.5.1 // indirect
github.com/luxfi/mock v0.1.1 // indirect
github.com/luxfi/sampler v1.0.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/grandcat/zeroconf v1.0.0 // indirect
github.com/luxfi/crypto v1.19.17
+62 -9
View File
@@ -1,33 +1,80 @@
capnproto.org/go/capnp/v3 v3.0.1-alpha.2/go.mod h1:2vT5D2dtG8sJGEoEKU17e+j7shdaYp1Myl8X03B3hmc=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/luxfi/accel v1.1.9 h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
github.com/luxfi/accel v1.1.9/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg=
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
github.com/luxfi/ids v1.2.9 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI=
github.com/luxfi/ids v1.2.9/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc=
github.com/luxfi/math v1.4.0 h1:/sb7Grw3hfO+5INWAWdB95jTvCeXg8fSQxsxDzcFtd4=
github.com/luxfi/math v1.4.0/go.mod h1:iW0FOCC8qF2mPE+MakG780CAHA83848lb1L04thA1Pg=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.1 h1:6tVarXMnNR3Xzud8FYUHjtXabTll3HI/OEqG0tgLAcI=
github.com/luxfi/metric v1.5.1/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/zap-proto/http v0.0.0-20260507033350-ccd81fd7275d h1:CI5bkhixkBbYWgbn0sqn1ugZei36sHwMkZpKP8f7eGw=
github.com/zap-proto/http v0.0.0-20260507033350-ccd81fd7275d/go.mod h1:xySyVTIwjknmVE+p+6rukkX86rFZtXeAu/QY7KXMYqw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
@@ -38,10 +85,7 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -53,3 +97,12 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=