mirror of
https://github.com/luxfi/zap.git
synced 2026-07-27 05:54:26 +00:00
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:
+146
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))...)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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))...)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user