nim-eth/eth/rlp/writer.nim

259 lines
8.0 KiB
Nim

import
std/macros,
./object_serialization, ./priv/defs
type
RlpWriter* = object
pendingLists: seq[tuple[remainingItems, outBytes: int]]
output: seq[byte]
IntLike* = concept x, y
type T = type(x)
# arithmetic ops
x + y is T
x * y is T
x - y is T
x div y is T
x mod y is T
# some int compatibility required for big endian encoding:
x shr int is T
x shl int is T
x and 0xff is int
x < 128 is bool
Integer* = SomeInteger # or IntLike
const
wrapObjsInList* = true
proc bytesNeeded(num: Integer): int =
type IntType = type(num)
var n = num
while n != IntType(0):
inc result
n = n shr 8
proc writeBigEndian(outStream: var seq[byte], number: Integer,
lastByteIdx: int, numberOfBytes: int) =
mixin `and`, `shr`
var n = number
for i in countdown(lastByteIdx, lastByteIdx - int(numberOfBytes) + 1):
outStream[i] = byte(n and 0xff)
n = n shr 8
proc writeBigEndian(outStream: var seq[byte], number: Integer,
numberOfBytes: int) {.inline.} =
outStream.setLen(outStream.len + numberOfBytes)
outStream.writeBigEndian(number, outStream.len - 1, numberOfBytes)
proc writeCount(bytes: var seq[byte], count: int, baseMarker: byte) =
if count < THRESHOLD_LIST_LEN:
bytes.add(baseMarker + byte(count))
else:
let
origLen = bytes.len
lenPrefixBytes = count.bytesNeeded
bytes.setLen(origLen + int(lenPrefixBytes) + 1)
bytes[origLen] = baseMarker + (THRESHOLD_LIST_LEN - 1) + byte(lenPrefixBytes)
bytes.writeBigEndian(count, bytes.len - 1, lenPrefixBytes)
proc initRlpWriter*: RlpWriter =
newSeq(result.pendingLists, 0)
newSeq(result.output, 0)
proc decRet(n: var int, delta: int): int =
n -= delta
return n
proc maybeClosePendingLists(self: var RlpWriter) =
while self.pendingLists.len > 0:
let lastListIdx = self.pendingLists.len - 1
doAssert self.pendingLists[lastListIdx].remainingItems >= 1
if decRet(self.pendingLists[lastListIdx].remainingItems, 1) == 0:
# A list have been just finished. It was started in `startList`.
let listStartPos = self.pendingLists[lastListIdx].outBytes
self.pendingLists.setLen lastListIdx
# How many bytes were written since the start?
let listLen = self.output.len - listStartPos
# Compute the number of bytes required to write down the list length
let totalPrefixBytes = if listLen < int(THRESHOLD_LIST_LEN): 1
else: int(listLen.bytesNeeded) + 1
# Shift the written data to make room for the prefix length
self.output.setLen(self.output.len + totalPrefixBytes)
moveMem(addr self.output[listStartPos + totalPrefixBytes],
unsafeAddr self.output[listStartPos],
listLen)
# Write out the prefix length
if listLen < THRESHOLD_LIST_LEN:
self.output[listStartPos] = LIST_START_MARKER + byte(listLen)
else:
let listLenBytes = totalPrefixBytes - 1
self.output[listStartPos] = LEN_PREFIXED_LIST_MARKER + byte(listLenBytes)
self.output.writeBigEndian(listLen, listStartPos + listLenBytes, listLenBytes)
else:
# The currently open list is not finished yet. Nothing to do.
return
proc appendRawList(self: var RlpWriter, bytes: openArray[byte]) =
self.output.writeCount(bytes.len, LIST_START_MARKER)
self.output.add(bytes)
self.maybeClosePendingLists()
proc appendRawBytes*(self: var RlpWriter, bytes: openArray[byte]) =
self.output.add(bytes)
self.maybeClosePendingLists()
proc startList*(self: var RlpWriter, listSize: int) =
if listSize == 0:
self.appendRawList([])
else:
self.pendingLists.add((listSize, self.output.len))
proc appendBlob(self: var RlpWriter, data: openArray[byte], startMarker: byte) =
if data.len == 1 and byte(data[0]) < BLOB_START_MARKER:
self.output.add byte(data[0])
else:
self.output.writeCount(data.len, startMarker)
self.output.add data
self.maybeClosePendingLists()
proc appendImpl(self: var RlpWriter, data: string) =
appendBlob(self, data.toOpenArrayByte(0, data.high), BLOB_START_MARKER)
proc appendBlob(self: var RlpWriter, data: openArray[byte]) =
appendBlob(self, data, BLOB_START_MARKER)
proc appendBlob(self: var RlpWriter, data: openArray[char]) =
appendBlob(self, data.toOpenArrayByte(0, data.high), BLOB_START_MARKER)
proc appendInt(self: var RlpWriter, i: Integer) =
# this is created as a separate proc as an extra precaution against
# any overloading resolution problems when matching the IntLike concept.
type IntType = type(i)
if i == IntType(0):
self.output.add BLOB_START_MARKER
elif i < BLOB_START_MARKER.Integer:
self.output.add byte(i)
else:
let bytesNeeded = i.bytesNeeded
self.output.writeCount(bytesNeeded, BLOB_START_MARKER)
self.output.writeBigEndian(i, bytesNeeded)
self.maybeClosePendingLists()
proc appendFloat(self: var RlpWriter, data: float64) =
# This is not covered in the RLP spec, but Geth uses Go's
# `math.Float64bits`, which is defined here:
# https://github.com/gopherjs/gopherjs/blob/master/compiler/natives/src/math/math.go
let uintWords = cast[ptr UncheckedArray[uint32]](unsafeAddr data)
let uint64bits = (uint64(uintWords[1]) shl 32) or uint64(uintWords[0])
self.appendInt(uint64bits)
template appendImpl(self: var RlpWriter, i: Integer) =
appendInt(self, i)
template appendImpl(self: var RlpWriter, e: enum) =
appendImpl(self, int(e))
template appendImpl(self: var RlpWriter, b: bool) =
appendImpl(self, int(b))
proc appendImpl[T](self: var RlpWriter, listOrBlob: openArray[T]) =
mixin append
# TODO: This append proc should be overloaded by `openArray[byte]` after
# nim bug #7416 is fixed.
when T is (byte or char):
self.appendBlob(listOrBlob)
else:
self.startList listOrBlob.len
for i in 0 ..< listOrBlob.len:
self.append listOrBlob[i]
proc appendRecordType*(self: var RlpWriter, obj: object|tuple, wrapInList = wrapObjsInList) =
mixin enumerateRlpFields, append
if wrapInList:
self.startList(static obj.type.rlpFieldsCount)
template op(field) =
when hasCustomPragma(field, rlpCustomSerialization):
append(self, obj, field)
else:
append(self, field)
enumerateRlpFields(obj, op)
proc appendImpl(self: var RlpWriter, data: object) {.inline.} =
self.appendRecordType(data)
proc appendImpl(self: var RlpWriter, data: tuple) {.inline.} =
self.appendRecordType(data)
# We define a single `append` template with a pretty low specificity
# score in order to facilitate easier overloading with user types:
template append*[T](w: var RlpWriter; data: T) =
when data is float64:
# XXX: This works around an overloading bug.
# Apparently, integer literals will be converted to `float64`
# values with higher precedence than the generic match to Integer
appendFloat(w, data)
else:
appendImpl(w, data)
proc initRlpList*(listSize: int): RlpWriter =
result = initRlpWriter()
startList(result, listSize)
# TODO: This should return a lent value
template finish*(self: RlpWriter): seq[byte] =
doAssert self.pendingLists.len == 0, "Insufficient number of elements written to a started list"
self.output
proc encode*[T](v: T): seq[byte] =
mixin append
var writer = initRlpWriter()
writer.append(v)
return writer.finish
proc encodeInt*(i: Integer): seq[byte] =
var writer = initRlpWriter()
writer.appendInt(i)
return writer.finish
macro encodeList*(args: varargs[untyped]): seq[byte] =
var
listLen = args.len
writer = genSym(nskVar, "rlpWriter")
body = newStmtList()
append = bindSym("append", brForceOpen)
for arg in args:
body.add quote do:
`append`(`writer`, `arg`)
result = quote do:
var `writer` = initRlpList(`listLen`)
`body`
finish(`writer`)
when false:
# XXX: Currently fails with a malformed AST error on the args.len expression
template encodeList*(args: varargs[untyped]): seq[byte] =
mixin append
var writer = initRlpList(args.len)
for arg in args:
writer.append(arg)
writer.finish