feat: add wrappers and bimap packages

Add wrappers package with Packer, Errs, and Closer types for
binary serialization and error handling. These are needed by
other Lux packages that were previously importing from vm.

Add bimap package for bidirectional maps.

Update atomic.go to re-export from standalone atomic module.
This commit is contained in:
Zach Kelling
2026-01-26 08:24:41 -08:00
parent c70e17afe4
commit c00fc128cf
8 changed files with 823 additions and 152 deletions
+7 -52
View File
@@ -4,60 +4,15 @@
package utils
import (
"encoding/json"
"sync"
"github.com/luxfi/atomic"
)
var (
_ json.Marshaler = (*Atomic[struct{}])(nil)
_ json.Unmarshaler = (*Atomic[struct{}])(nil)
)
type Atomic[T any] struct {
lock sync.RWMutex
value T
}
// Atomic is a re-export from the standalone atomic module for backward compatibility.
// Use github.com/luxfi/atomic directly for new code.
type Atomic[T any] = atomic.Atomic[T]
// NewAtomic creates a new Atomic with the given initial value.
// Use github.com/luxfi/atomic.NewAtomic directly for new code.
func NewAtomic[T any](value T) *Atomic[T] {
return &Atomic[T]{
value: value,
}
}
func (a *Atomic[T]) Get() T {
a.lock.RLock()
defer a.lock.RUnlock()
return a.value
}
func (a *Atomic[T]) Set(value T) {
a.lock.Lock()
defer a.lock.Unlock()
a.value = value
}
func (a *Atomic[T]) Swap(value T) T {
a.lock.Lock()
defer a.lock.Unlock()
old := a.value
a.value = value
return old
}
func (a *Atomic[T]) MarshalJSON() ([]byte, error) {
a.lock.RLock()
defer a.lock.RUnlock()
return json.Marshal(a.value)
}
func (a *Atomic[T]) UnmarshalJSON(b []byte) error {
a.lock.Lock()
defer a.lock.Unlock()
return json.Unmarshal(b, &a.value)
return atomic.NewAtomic(value)
}
+105
View File
@@ -0,0 +1,105 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
// Package bimap provides a bidirectional map implementation.
package bimap
// BiMap is a bidirectional map that allows lookups in both directions.
type BiMap[K comparable, V comparable] struct {
forward map[K]V
backward map[V]K
}
// New creates a new empty BiMap.
func New[K comparable, V comparable]() *BiMap[K, V] {
return &BiMap[K, V]{
forward: make(map[K]V),
backward: make(map[V]K),
}
}
// Put inserts a key-value pair into the BiMap.
// Returns true if the pair was inserted, false if either key or value already exists.
func (b *BiMap[K, V]) Put(key K, value V) bool {
if _, exists := b.forward[key]; exists {
return false
}
if _, exists := b.backward[value]; exists {
return false
}
b.forward[key] = value
b.backward[value] = key
return true
}
// Get returns the value associated with the key.
func (b *BiMap[K, V]) Get(key K) (V, bool) {
value, ok := b.forward[key]
return value, ok
}
// GetValue is an alias for Get.
func (b *BiMap[K, V]) GetValue(key K) (V, bool) {
return b.Get(key)
}
// GetKey returns the key associated with the value.
func (b *BiMap[K, V]) GetKey(value V) (K, bool) {
key, ok := b.backward[value]
return key, ok
}
// Delete removes the key-value pair by key.
func (b *BiMap[K, V]) Delete(key K) bool {
if value, ok := b.forward[key]; ok {
delete(b.forward, key)
delete(b.backward, value)
return true
}
return false
}
// DeleteValue removes the key-value pair by value.
func (b *BiMap[K, V]) DeleteValue(value V) bool {
if key, ok := b.backward[value]; ok {
delete(b.forward, key)
delete(b.backward, value)
return true
}
return false
}
// Len returns the number of entries in the BiMap.
func (b *BiMap[K, V]) Len() int {
return len(b.forward)
}
// HasKey returns true if the key exists.
func (b *BiMap[K, V]) HasKey(key K) bool {
_, ok := b.forward[key]
return ok
}
// HasValue returns true if the value exists.
func (b *BiMap[K, V]) HasValue(value V) bool {
_, ok := b.backward[value]
return ok
}
// Keys returns all keys in the BiMap.
func (b *BiMap[K, V]) Keys() []K {
keys := make([]K, 0, len(b.forward))
for k := range b.forward {
keys = append(keys, k)
}
return keys
}
// Values returns all values in the BiMap.
func (b *BiMap[K, V]) Values() []V {
values := make([]V, 0, len(b.backward))
for v := range b.backward {
values = append(values, v)
}
return values
}
+4 -30
View File
@@ -3,41 +3,15 @@ module github.com/luxfi/utils
go 1.25.5
require (
github.com/luxfi/constants v1.4.3
github.com/luxfi/crypto v1.17.39
github.com/luxfi/atomic v1.0.0
github.com/stretchr/testify v1.11.1
)
require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/gorilla/rpc v1.2.1 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/luxfi/cache v1.2.0 // indirect
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/geth v1.16.69 // indirect
github.com/luxfi/ids v1.2.9 // indirect
github.com/luxfi/math v1.2.3 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/metric v1.4.10 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
go.uber.org/mock v0.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/sys v0.39.0 // indirect
gonum.org/v1/gonum v0.16.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+8 -70
View File
@@ -1,85 +1,23 @@
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f h1:a5PUgHGinaD6XrLmIDLQmGHocjIjBsBAcR5gALjZvMU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/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/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/luxfi/cache v1.2.0 h1:VEk/ue13oi8w3sKBB/Dsv9fnjmfdXgb0NMibcPLVbss=
github.com/luxfi/cache v1.2.0/go.mod h1:Esn48WMB3JL/+UOdCEV2Chyjr1MG3SSckXWLhB/P080=
github.com/luxfi/constants v1.4.3 h1:bcqUO8twMOhHWCgN/RbjfWulVPbvUQcxDkssaRrCx4g=
github.com/luxfi/constants v1.4.3/go.mod h1:ENkJ121cmDEkwQPDiKK4QhnTnW9u37PGpepbrdVcAmc=
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.17.39 h1:dDmktYOD/sU6WjIpitIfuHp7mRbc3izOsyJrQ1c5eOQ=
github.com/luxfi/crypto v1.17.39/go.mod h1:mChLWmW4CLR1wAN6CeJTveCzUv0DTzGQnYgq01x3W0U=
github.com/luxfi/geth v1.16.69 h1:CHO6xTZ+A+3itk94ts4uyVRJajNVP3RxWTjJp5qGOlk=
github.com/luxfi/geth v1.16.69/go.mod h1:8eEO1hW5sa6OH2VeCMaCPnRz28JBxFvCPBCWPLsU2ck=
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.2.3 h1:BgvIFw/srPXFLbcqtoDhLJOfmBsn86GPA1iWgsoyUb4=
github.com/luxfi/math v1.2.3/go.mod h1:C8STnF2H+D6rqBPt248CiWY2TGuJgdtv/+4UqrT15iM=
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/metric v1.4.10 h1:uWeLupMLxK7YK1c99gY23cT+hornWg7OmZgHNK4HNbw=
github.com/luxfi/metric v1.4.10/go.mod h1:3OIQ+e6mciesm3cik3Q4CSOAvTlBy4JA0ZoKzCB9APo=
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/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
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=
+39
View File
@@ -0,0 +1,39 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wrappers
import (
"io"
"sync"
)
// Closer is a nice utility for closing a group of objects while reporting an
// error if one occurs.
type Closer struct {
lock sync.Mutex
closers []io.Closer
}
// Add a new object to be closed.
func (c *Closer) Add(closer io.Closer) {
c.lock.Lock()
defer c.lock.Unlock()
c.closers = append(c.closers, closer)
}
// Close closes each of the closers add to [c] and returns the first error
// that occurs or nil if no error occurs.
func (c *Closer) Close() error {
c.lock.Lock()
closers := c.closers
c.closers = nil
c.lock.Unlock()
errs := Errs{}
for _, closer := range closers {
errs.Add(closer.Close())
}
return errs.Err
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wrappers
type Errs struct{ Err error }
func (errs *Errs) Errored() bool {
return errs.Err != nil
}
func (errs *Errs) Add(errors ...error) {
if errs.Err == nil {
for _, err := range errors {
if err != nil {
errs.Err = err
break
}
}
}
}
+275
View File
@@ -0,0 +1,275 @@
// 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
// 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
)
func StringLen(str string) int {
// note: there is a max length for string ([MaxStringLen])
// we defer to PackString checking whether str is within limits
return ShortLen + len(str)
}
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")
)
// 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 append 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 unpack 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 append 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 unpack 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 append 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 unpack 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 append 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 unpack 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 append 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 unpack 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 append a byte slice to the byte array
func (p *Packer) PackBytes(bytes []byte) {
p.PackInt(uint32(len(bytes)))
p.PackFixedBytes(bytes)
}
// UnpackBytes unpack 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 append 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
// In order to understand this code, its important to understand the difference
// between a slice's length and its capacity.
func (p *Packer) expand(bytes int) {
neededSize := bytes + p.Offset // Need byte slice's length to be at least [neededSize]
switch {
case neededSize <= len(p.Bytes): // Byte slice has sufficient length already
return
case neededSize > p.MaxSize: // Lengthening the byte slice would cause it to grow too large
p.Err = ErrInsufficientLength
return
case neededSize <= cap(p.Bytes): // Byte slice has sufficient capacity to lengthen it without mem alloc
p.Bytes = p.Bytes[:neededSize]
return
default: // Add capacity/length to byte slice
p.Bytes = append(p.Bytes[:cap(p.Bytes)], make([]byte, neededSize-cap(p.Bytes))...)
}
}
+364
View File
@@ -0,0 +1,364 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package wrappers
import (
"testing"
"github.com/stretchr/testify/require"
)
const (
ByteSentinel = 0
ShortSentinel = 0
IntSentinel = 0
LongSentinel = 0
BoolSentinel = false
)
func TestPackerCheckSpace(t *testing.T) {
require := require.New(t)
p := Packer{Offset: -1}
p.checkSpace(1)
require.True(p.Errored())
require.ErrorIs(p.Err, errNegativeOffset)
p = Packer{}
p.checkSpace(-1)
require.True(p.Errored())
require.ErrorIs(p.Err, errInvalidInput)
p = Packer{Bytes: []byte{0x01}, Offset: 1}
p.checkSpace(1)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: []byte{0x01}, Offset: 2}
p.checkSpace(0)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerExpand(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01}, Offset: 2}
p.expand(1)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: []byte{0x01, 0x02, 0x03}, Offset: 0}
p.expand(1)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02, 0x03}, p.Bytes)
}
func TestPackerPackByte(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 1}
p.PackByte(0x01)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01}, p.Bytes)
p.PackByte(0x02)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackByte(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01}, Offset: 0}
require.Equal(uint8(1), p.UnpackByte())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(ByteLen, p.Offset)
require.Equal(uint8(ByteSentinel), p.UnpackByte())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackShort(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 2}
p.PackShort(0x0102)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02}, p.Bytes)
}
func TestPackerUnpackShort(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01, 0x02}, Offset: 0}
require.Equal(uint16(0x0102), p.UnpackShort())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(ShortLen, p.Offset)
require.Equal(uint16(ShortSentinel), p.UnpackShort())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackInt(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 4}
p.PackInt(0x01020304)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02, 0x03, 0x04}, p.Bytes)
p.PackInt(0x05060708)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackInt(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01, 0x02, 0x03, 0x04}, Offset: 0}
require.Equal(uint32(0x01020304), p.UnpackInt())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(IntLen, p.Offset)
require.Equal(uint32(IntSentinel), p.UnpackInt())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackLong(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 8}
p.PackLong(0x0102030405060708)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, p.Bytes)
p.PackLong(0x090a0b0c0d0e0f00)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackLong(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, Offset: 0}
require.Equal(uint64(0x0102030405060708), p.UnpackLong())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(LongLen, p.Offset)
require.Equal(uint64(LongSentinel), p.UnpackLong())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackFixedBytes(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 4}
p.PackFixedBytes([]byte("Lux"))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte("Lux"), p.Bytes)
p.PackFixedBytes([]byte("Lux"))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackFixedBytes(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("Lux")}
require.Equal([]byte("Lux"), p.UnpackFixedBytes(3))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(3, p.Offset)
require.Nil(p.UnpackFixedBytes(4))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerPackBytes(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 8}
p.PackBytes([]byte("Lux"))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte("\x00\x00\x00\x03Lux"), p.Bytes)
p.PackBytes([]byte("Lux"))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackBytes(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x00\x00\x03Lux")}
require.Equal([]byte("Lux"), p.UnpackBytes())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(7, p.Offset)
require.Nil(p.UnpackBytes())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackLimitedBytes(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x00\x00\x03Lux")}
require.Equal([]byte("Lux"), p.UnpackLimitedBytes(10))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(7, p.Offset)
require.Nil(p.UnpackLimitedBytes(10))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
// Reset and don't allow enough bytes
p = Packer{Bytes: p.Bytes}
require.Nil(p.UnpackLimitedBytes(2))
require.True(p.Errored())
require.ErrorIs(p.Err, errOversized)
}
func TestPackerString(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 6}
p.PackStr("Lux")
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x00, 0x03, 0x4c, 0x75, 0x78}, p.Bytes)
}
func TestPackerUnpackString(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x03Lux")}
require.Equal("Lux", p.UnpackStr())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(5, p.Offset)
require.Empty(p.UnpackStr())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackLimitedString(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte("\x00\x03Lux")}
require.Equal("Lux", p.UnpackLimitedStr(10))
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(5, p.Offset)
require.Empty(p.UnpackLimitedStr(10))
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
// Reset and don't allow enough bytes
p = Packer{Bytes: p.Bytes}
require.Empty(p.UnpackLimitedStr(2))
require.True(p.Errored())
require.ErrorIs(p.Err, errOversized)
}
func TestPacker(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 3}
require.False(p.Errored())
require.NoError(p.Err)
p.PackShort(17)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x0, 0x11}, p.Bytes)
p.PackShort(1)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: p.Bytes}
require.Equal(uint16(17), p.UnpackShort())
require.False(p.Errored())
require.NoError(p.Err)
}
func TestPackBool(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 3}
p.PackBool(false)
p.PackBool(true)
p.PackBool(false)
require.False(p.Errored())
require.NoError(p.Err)
p = Packer{Bytes: p.Bytes}
bool1, bool2, bool3 := p.UnpackBool(), p.UnpackBool(), p.UnpackBool()
require.False(p.Errored())
require.NoError(p.Err)
require.False(bool1)
require.True(bool2)
require.False(bool3)
}
func TestPackerPackBool(t *testing.T) {
require := require.New(t)
p := Packer{MaxSize: 1}
p.PackBool(true)
require.False(p.Errored())
require.NoError(p.Err)
require.Equal([]byte{0x01}, p.Bytes)
p.PackBool(false)
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
}
func TestPackerUnpackBool(t *testing.T) {
require := require.New(t)
p := Packer{Bytes: []byte{0x01}, Offset: 0}
require.True(p.UnpackBool())
require.False(p.Errored())
require.NoError(p.Err)
require.Equal(BoolLen, p.Offset)
require.Equal(BoolSentinel, p.UnpackBool())
require.True(p.Errored())
require.ErrorIs(p.Err, ErrInsufficientLength)
p = Packer{Bytes: []byte{0x42}, Offset: 0}
require.False(p.UnpackBool())
require.True(p.Errored())
require.ErrorIs(p.Err, errBadBool)
}