Generic binary codecs

This commit is contained in:
Tomasz Bekas 2024-05-15 18:13:51 +02:00
parent e208d0b13a
commit e1392c9126
No known key found for this signature in database
GPG Key ID: 4854E04C98824959
4 changed files with 183 additions and 1 deletions

View File

@ -46,9 +46,13 @@ proc `'nb`*(n: string): NBytes = parseInt(n).NBytes
logutils.formatIt(NBytes): $it
const
MiB = 1024.NBytes * 1024.NBytes # ByteSz, 1 mebibyte = 1,048,576 ByteSz
KiB = 1024.NBytes # ByteSz, 1 kibibyte = 1,024 ByteSz
MiB = KiB * 1024 # ByteSz, 1 mebibyte = 1,048,576 ByteSz
GiB = MiB * 1024 # ByteSz, 1 gibibyte = 1,073,741,824 ByteSz
proc KiBs*(v: Natural): NBytes = v.NBytes * KiB
proc MiBs*(v: Natural): NBytes = v.NBytes * MiB
proc GiBs*(v: Natural): NBytes = v.NBytes * GiB
func divUp*[T: NBytes](a, b : T): int =
## Division with result rounded up (rather than truncated as in 'div')

View File

@ -0,0 +1,103 @@
import std/sugar
import pkg/libp2p
import pkg/questionable
import pkg/questionable/results
import pkg/stew/endians2
import pkg/stew/byteutils
import ../units
const MaxBufferSize = 10.MiBs.uint
proc encode*(i: uint64): seq[byte] =
@(i.toBytesBE)
proc decode*(T: type uint64, bytes: seq[byte]): ?!T =
if bytes.len >= sizeof(uint64):
success(uint64.fromBytesBE(bytes))
else:
failure("Not enough bytes to decode `uint64`")
proc encode*(i: int64): seq[byte] = cast[uint64](i).encode
proc decode*(T: type int64, bytes: seq[byte]): ?!T = uint64.decode(bytes).map((ui: uint64) => cast[int64](ui))
proc encode*(i: int): seq[byte] = cast[uint64](i).encode
proc decode*(T: type int, bytes: seq[byte]): ?!T = uint64.decode(bytes).map((ui: uint64) => cast[int](ui))
proc encode*(i: Natural): seq[byte] = cast[int](i).encode
proc decode*(T: type Natural, bytes: seq[byte]): ?!T = int.decode(bytes).map((ui: int) => cast[Natural](ui))
proc encode*(i: NBytes): seq[byte] = cast[int](i).encode
proc decode*(T: type NBytes, bytes: seq[byte]): ?!T = int.decode(bytes).map((ui: int) => cast[NBytes](ui))
proc encode*[T: enum](e: T): seq[byte] = e.ord().encode
proc decode*(T: typedesc[enum], bytes: seq[byte]): ?!T = int.decode(bytes).map((ui: int) => T(ui))
proc encode*(s: string): seq[byte] = s.toBytes
proc decode*(T: type string, bytes: seq[byte]): ?!T = success(string.fromBytes(bytes))
proc encode*(b: bool): seq[byte] = (if b: @[byte 1] else: @[byte 0])
proc decode*(T: type bool, bytes: seq[byte]): ?!T =
if bytes.len >= 1:
success(not (bytes[0] == 0.byte))
else:
failure("Not enought bytes to decode `bool`")
proc encode*[T](ts: seq[T]): seq[byte] =
if ts.len == 0:
return newSeq[byte]()
var pb = initProtoBuffer()
for t in ts:
pb.write(1, t.encode())
pb.finish
pb.buffer
proc decode*[T](_: type seq[T], bytes: seq[byte]): ?!seq[T] =
if bytes.len == 0:
return success(newSeq[T]())
var
pb = initProtoBuffer(bytes, maxSize = MaxBufferSize)
nestedBytes: seq[seq[byte]]
ts: seq[T]
if ? pb.getRepeatedField(1, nestedBytes).mapFailure:
for b in nestedBytes:
let t = ? T.decode(b)
ts.add(t)
success(ts)
proc autoencode*[T: tuple | object](tup: T): seq[byte] =
var
pb = initProtoBuffer(maxSize = MaxBufferSize)
index = 1
for f in fields(tup):
when (compiles do:
let _: seq[byte] = encode(f)):
let fBytes = encode(f)
pb.write(index, fBytes)
index.inc
else:
{.error: "provide `proc encode(a: " & $typeof(f) & "): seq[byte]` to use autoencode".}
pb.finish
pb.buffer
proc autodecode*(T: typedesc[tuple | object], bytes: seq[byte]): ?!T =
var
pb = initProtoBuffer(bytes, maxSize = MaxBufferSize)
res = default(T)
index = 1
for f in fields(res):
var buf = newSeq[byte]()
discard ? pb.getField(index, buf).mapFailure
f = ? decode(typeof(f), buf)
index.inc
success(res)

View File

@ -4,5 +4,6 @@ import ./utils/testasyncstatemachine
import ./utils/testtimer
import ./utils/testthen
import ./utils/testtrackedfutures
import ./utils/testgenericcoders
{.warning[UnusedImport]: off.}

View File

@ -0,0 +1,74 @@
import std/unittest
import pkg/questionable
import pkg/questionable/results
import pkg/codex/utils/genericcoders
import ../helpers
type
MyEnum = enum
MyEnumA = 1
MyEnumB = 2
MyObj = object
a: int
b: string
c: bool
d: MyEnum
e: seq[int]
MyTuple = (int, string, bool, MyEnum, seq[int])
proc `==`*(a, b: MyObj): bool =
(a.a == b.a) and
(a.b == b.b) and
(a.c == b.c) and
(a.d == b.d) and
(a.e == b.e)
proc `$`*(a: MyObj): string =
"a: " & $a.a &
", b: " & $a.b &
", c: " & $a.c &
", d: " & $a.d &
", e: " & $a.e
checksuite "Test encode/decode":
proc coderTest(T: type, a: T) =
let bytes = a.encode
without decoded =? T.decode(bytes), err:
fail
check:
decoded == a
test "Should encode and decode primitive values":
coderTest(int, 123)
coderTest(int, -123)
coderTest(int64, 123.int64)
coderTest(int64, -123.int64)
coderTest(Natural, 123.Natural)
coderTest(NBytes, 123.KiBs)
coderTest(string, "")
coderTest(string, "123")
coderTest(string, "abcdefghij")
coderTest(bool, false)
coderTest(bool, true)
coderTest(MyEnum, MyEnumA)
coderTest(MyEnum, MyEnumB)
coderTest(seq[int], @[1, 2, 3])
coderTest(seq[int], newSeq[int]())
checksuite "Test autoencode/autodecode":
proc autocoderTest(T: type, a: T) =
let bytes = a.autoencode
without decoded =? T.autodecode(bytes), err:
fail
check:
decoded == a
test "Should encode and decode product values":
autocoderTest(MyObj, MyObj(a: 1, b: "abc", c: true, d: MyEnumA, e: @[1, 2, 3]))
autocoderTest(MyTuple, (2, "def", false, MyEnumB, newSeq[int]()))