Skip to content

Commit

Permalink
Add general serialization for natural numbers (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
pantrif authored Aug 14, 2024
1 parent 09e101f commit 1c8284e
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/serialization/codec/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ package codec

type Codec interface {
Marshal(v interface{}) ([]byte, error)
MarshalGeneral(x uint64) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
UnmarshalGeneral(data []byte, v *uint64) error
}
8 changes: 8 additions & 0 deletions pkg/serialization/codec/jam/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package jam

import "errors"

var (
// errFirstByteNineByteSerialization is returned when the first byte has wrong value in 9-byte serialization
errFirstByteNineByteSerialization = errors.New("expected first byte to be 255 for 9-byte serialization")
)
65 changes: 65 additions & 0 deletions pkg/serialization/codec/jam/general_natural.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package jam

import (
"encoding/binary"
"math"
"math/bits"
)

// GeneralNatural implements the formula (275: able to encode naturals of up to 2^64)
type GeneralNatural struct{}

func (j *GeneralNatural) SerializeUint64(x uint64) []byte {
var l uint8
// Determine the length needed to represent the value
for l = 0; l < 8; l++ {
if x < (1 << (7 * (l + 1))) {
break
}
}
bytes := make([]byte, 0)
if l < 8 {
// Calculate the prefix byte, ensure it stays within uint8 range
prefix := uint8((256 - (1 << (8 - l))) + (x>>(8*l))&math.MaxUint8)
bytes = append(bytes, prefix)
} else {
bytes = append(bytes, math.MaxUint8)
}
// Serialize the integer in little-endian order
for i := 0; i < int(l); i++ {
byteVal := uint8((x >> (8 * i)) & math.MaxUint8)
bytes = append(bytes, byteVal)
}
return bytes
}

// DeserializeUint64 deserializes a byte slice into a uint64 value.
func (j *GeneralNatural) DeserializeUint64(serialized []byte, u *uint64) error {
*u = 0

n := len(serialized)
if n == 0 {
return nil
}

if n > 8 {
if serialized[0] != math.MaxUint8 {
return errFirstByteNineByteSerialization
}
*u = binary.LittleEndian.Uint64(serialized[1:9])
return nil
}

prefix := serialized[0]
l := uint8(bits.LeadingZeros8(^prefix))

// Deserialize the first `l` bytes
for i := uint8(0); i < l; i++ {
*u |= uint64(serialized[i+1]) << (8 * i)
}

// Combine the remaining part of the prefix
*u |= uint64(prefix&(math.MaxUint8>>l)) << (8 * l)

return nil
}
71 changes: 71 additions & 0 deletions pkg/serialization/codec/jam/general_natural_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package jam

import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"math"
"testing"
)

func TestEncodeDecodeUint64(t *testing.T) {
testCases := []struct {
input uint64
expected []byte
}{
// l = 0
{0, []byte{0}},
{1, []byte{1}},
{math.MaxInt8, []byte{127}}, // 127
// l = 1
{1 << 7, []byte{128, 128}}, // 128
{math.MaxUint8, []byte{128, 255}}, // 255
{1 << 8, []byte{129, 0}}, // 256
{(1 << 10) - 1, []byte{131, 255}}, // 1023
{1 << 10, []byte{132, 0}}, // 1024
{(1 << 14) - 1, []byte{191, 255}}, // 16383
//l = 2
{1 << 14, []byte{192, 0, 64}}, // 16384
{math.MaxUint16, []byte{192, 255, 255}}, // 65535
{1 << 16, []byte{193, 0, 0}}, // 65536
{(1 << 21) - 1, []byte{223, 255, 255}}, // 2097151
//l = 3
{1 << 21, []byte{224, 0, 0, 32}}, // 2097152
{(1 << 28) - 1, []byte{239, 255, 255, 255}}, // 268435455
//l = 4
{1 << 28, []byte{240, 0, 0, 0, 16}}, // 268435456
{(1 << 35) - 1, []byte{247, 255, 255, 255, 255}}, // 34359738367
//l = 5
{1 << 35, []byte{248, 0, 0, 0, 0, 8}}, // 34359738368
{(1 << 42) - 1, []byte{251, 255, 255, 255, 255, 255}}, // 4398046511103
//l = 6
{1 << 42, []byte{252, 0, 0, 0, 0, 0, 4}}, // 4398046511104
{(1 << 49) - 1, []byte{253, 255, 255, 255, 255, 255, 255}}, // 562949953421311
//l = 7
{1 << 49, []byte{254, 0, 0, 0, 0, 0, 0, 2}}, // 562949953421312
{(1 << 56) - 1, []byte{254, 255, 255, 255, 255, 255, 255, 255}}, // 72057594037927935
// l = 8
{1 << 56, []byte{255, 0, 0, 0, 0, 0, 0, 0, 1}}, // 72057594037927936
{1 << 63, []byte{255, 0, 0, 0, 0, 0, 0, 0, 128}}, // 9223372036854775808
}

gn := GeneralNatural{}

for _, tc := range testCases {
t.Run(fmt.Sprintf("uint64(%d)", tc.input), func(t *testing.T) {
// Marshal the input value
serialized := gn.SerializeUint64(tc.input)

// Check if the serialized output matches the expected output
assert.Equal(t, tc.expected, serialized, "serialized output mismatch for input %d", tc.input)

// Unmarshal the serialized data back into a uint64
var deserialized uint64
err := gn.DeserializeUint64(serialized, &deserialized)
require.NoError(t, err, "unmarshal(%v) returned an unexpected error", serialized)

// Check if the deserialized value matches the original input
assert.Equal(t, tc.input, deserialized, "deserialized value mismatch for input %d", tc.input)
})
}
}
34 changes: 34 additions & 0 deletions pkg/serialization/codec/jam_codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package codec

import (
"errors"
"github.com/eigerco/strawberry/pkg/serialization/codec/jam"
)

// JAMCodec implements the Codec interface for JSON encoding and decoding.
type JAMCodec struct {
gn jam.GeneralNatural
}

// NewJamCodec initializes an instance of Jam codec
func NewJamCodec() *JAMCodec {
return &JAMCodec{gn: jam.GeneralNatural{}}
}

func (j *JAMCodec) Marshal(v interface{}) ([]byte, error) {
// TODO
return nil, errors.New("not implemented")
}

func (j *JAMCodec) MarshalGeneral(v uint64) ([]byte, error) {
return j.gn.SerializeUint64(v), nil
}

func (j *JAMCodec) Unmarshal(data []byte, v interface{}) error {
// TODO
return errors.New("not implemented")
}

func (j *JAMCodec) UnmarshalGeneral(data []byte, v *uint64) error {
return j.gn.DeserializeUint64(data, v)
}
8 changes: 8 additions & 0 deletions pkg/serialization/codec/json_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ func (j *JSONCodec) Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}

func (j *JSONCodec) MarshalGeneral(v uint64) ([]byte, error) {
return json.Marshal(v)
}

func (j *JSONCodec) Unmarshal(data []byte, v interface{}) error {
return json.Unmarshal(data, v)
}

func (j *JSONCodec) UnmarshalGeneral(data []byte, v *uint64) error {
return json.Unmarshal(data, v)
}
8 changes: 8 additions & 0 deletions pkg/serialization/codec/scale_codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ func (s *SCALECodec) Marshal(v interface{}) ([]byte, error) {
return scale.Marshal(v)
}

func (j *SCALECodec) MarshalGeneral(v uint64) ([]byte, error) {
return scale.Marshal(v)
}

func (s *SCALECodec) Unmarshal(data []byte, v interface{}) error {
return scale.Unmarshal(data, v)
}

func (s *SCALECodec) UnmarshalGeneral(data []byte, v *uint64) error {
return scale.Unmarshal(data, v)
}
10 changes: 10 additions & 0 deletions pkg/serialization/serializer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,17 @@ func (s *Serializer) Encode(v interface{}) ([]byte, error) {
return s.codec.Marshal(v)
}

// EncodeGeneral is specific encoding for natural numbers up to 2^64
func (s *Serializer) EncodeGeneral(v uint64) ([]byte, error) {
return s.codec.MarshalGeneral(v)
}

// Decode deserializes the given data into the specified value using the codec.
func (s *Serializer) Decode(data []byte, v interface{}) error {
return s.codec.Unmarshal(data, v)
}

// DecodeGeneral is specific decoding for natural numbers up to 2^64
func (s *Serializer) DecodeGeneral(data []byte, v *uint64) error {
return s.codec.UnmarshalGeneral(data, v)
}
17 changes: 17 additions & 0 deletions pkg/serialization/serializer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,20 @@ func TestSCALESerializer(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, example, decoded)
}

func TestGeneralSerializer(t *testing.T) {
jamCodec := codec.NewJamCodec()
serializer := serialization.NewSerializer(jamCodec)

// Test Encoding
v := uint64(127)
encoded, err := serializer.EncodeGeneral(v)
require.NoError(t, err)
require.Equal(t, []byte{127}, encoded)

// Test Decoding
var decoded uint64
err = serializer.DecodeGeneral(encoded, &decoded)
require.NoError(t, err)
assert.Equal(t, v, decoded)
}

0 comments on commit 1c8284e

Please sign in to comment.