diff --git a/pkg/serialization/codec/codec.go b/pkg/serialization/codec/codec.go index d994723..11c424a 100644 --- a/pkg/serialization/codec/codec.go +++ b/pkg/serialization/codec/codec.go @@ -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 } diff --git a/pkg/serialization/codec/jam/errors.go b/pkg/serialization/codec/jam/errors.go new file mode 100644 index 0000000..addb023 --- /dev/null +++ b/pkg/serialization/codec/jam/errors.go @@ -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") +) diff --git a/pkg/serialization/codec/jam/general_natural.go b/pkg/serialization/codec/jam/general_natural.go new file mode 100644 index 0000000..8860601 --- /dev/null +++ b/pkg/serialization/codec/jam/general_natural.go @@ -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 +} diff --git a/pkg/serialization/codec/jam/general_natural_test.go b/pkg/serialization/codec/jam/general_natural_test.go new file mode 100644 index 0000000..1ab6df9 --- /dev/null +++ b/pkg/serialization/codec/jam/general_natural_test.go @@ -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) + }) + } +} diff --git a/pkg/serialization/codec/jam_codec.go b/pkg/serialization/codec/jam_codec.go new file mode 100644 index 0000000..b44f017 --- /dev/null +++ b/pkg/serialization/codec/jam_codec.go @@ -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) +} diff --git a/pkg/serialization/codec/json_codec.go b/pkg/serialization/codec/json_codec.go index fb5e78f..e2f9bdd 100644 --- a/pkg/serialization/codec/json_codec.go +++ b/pkg/serialization/codec/json_codec.go @@ -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) +} diff --git a/pkg/serialization/codec/scale_codec.go b/pkg/serialization/codec/scale_codec.go index e9e101a..0a3b2a3 100644 --- a/pkg/serialization/codec/scale_codec.go +++ b/pkg/serialization/codec/scale_codec.go @@ -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) +} diff --git a/pkg/serialization/serializer.go b/pkg/serialization/serializer.go index 41f2982..e0aa7bf 100644 --- a/pkg/serialization/serializer.go +++ b/pkg/serialization/serializer.go @@ -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) +} diff --git a/pkg/serialization/serializer_test.go b/pkg/serialization/serializer_test.go index 6bf1865..b066682 100644 --- a/pkg/serialization/serializer_test.go +++ b/pkg/serialization/serializer_test.go @@ -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) +}