nim-serde/serde/cbor/deserializer.nim
2025-05-22 16:39:37 +05:30

672 lines
20 KiB
Nim
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# This file is a modified version of Emery Hemingways CBOR library for Nim,
# originally available at https://github.com/ehmry/cbor-nim and released under The Unlicense.
import std/[math, streams, options, tables, strutils, times, typetraits]
import ./types
import ./helpers
import ../utils/types
import ./errors
import pkg/questionable
import pkg/questionable/results
export results
export types
{.push raises: [].}
func isIndefinite*(c: CborParser): bool {.inline.} = c.minor == 31
## Return true if the parser is positioned on an item of indefinite length.
proc open*(c: var CborParser; s: Stream) =
## Begin parsing a stream of CBOR in binary form.
## The parser will be initialized in an EOF state, call
## ``next`` to advance it before parsing.
c.s = s
c.kind = cborEof
c.intVal = 0
proc next*(c: var CborParser): ?!void =
## Advance the parser to the initial or next event.
try:
if c.s.atEnd:
c.kind = CborEventKind.cborEof
c.intVal = 0
else:
let
ib = c.s.readUint8
mb = ib shr 5
c.minor = ib and 0b11111
case c.minor
of 0..23:
c.intVal = c.minor.uint64
of 24:
c.intVal = c.s.readChar.uint64
of 25:
c.intVal = c.s.readChar.uint64
c.intVal = (c.intVal shl 8) or c.s.readChar.uint64
of 26:
c.intVal = c.s.readChar.uint64
for _ in 1..3:
{.unroll.}
c.intVal = (c.intVal shl 8) or c.s.readChar.uint64
of 27:
c.intVal = c.s.readChar.uint64
for _ in 1..7:
{.unroll.}
c.intVal = (c.intVal shl 8) or c.s.readChar.uint64
else:
c.intVal = 0
case mb
of PositiveMajor:
c.kind = CborEventKind.cborPositive
of NegativeMajor:
c.kind = CborEventKind.cborNegative
of BytesMajor:
c.kind = CborEventKind.cborBytes
of TextMajor:
c.kind = CborEventKind.cborText
of ArrayMajor:
c.kind = CborEventKind.cborArray
of MapMajor:
c.kind = CborEventKind.cborMap
of TagMajor:
c.kind = CborEventKind.cborTag
of SimpleMajor:
if c.minor in {25, 26, 27}:
c.kind = CborEventKind.cborFloat
elif c.isIndefinite:
c.kind = CborEventKind.cborBreak
else:
c.kind = CborEventKind.cborSimple
else:
return failure(newCborError("unhandled major type " & $mb))
success()
except IOError as e:
return failure(e)
except OSError as e:
return failure(e)
proc nextUInt*(c: var CborParser): ?!BiggestUInt =
## Parse the integer value that the parser is positioned on.
if c.kind != CborEventKind.cborPositive:
return failure(newCborError("Expected positive integer, got " & $c.kind))
let val = c.intVal.BiggestUInt
?c.next()
return success(val)
proc nextInt*(c: var CborParser): ?!BiggestInt =
## Parse the integer value that the parser is positioned on.
var val: BiggestInt
case c.kind
of CborEventKind.cborPositive:
val = c.intVal.BiggestInt
of CborEventKind.cborNegative:
val = -1.BiggestInt - c.intVal.BiggestInt
else:
return failure(newCborError("Expected integer, got " & $c.kind))
?c.next()
return success(val)
proc nextFloat*(c: var CborParser): ?!float64 =
## Parse the float value that the parser is positioned on.
var val: float64
if c.kind != CborEventKind.cborFloat:
return failure(newCborError("Expected float, got " & $c.kind))
case c.minor
of 25:
val = floatSingle(c.intVal.uint16).float64
of 26:
val = cast[float32](c.intVal).float64
of 27:
val = cast[float64](c.intVal)
else:
discard
?c.next()
return success(val)
func bytesLen*(c: CborParser): ?!int =
## Return the length of the byte string that the parser is positioned on.
if c.kind != CborEventKind.cborBytes:
return failure(newCborError("Expected bytes, got " & $c.kind))
return success(c.intVal.int)
proc nextBytes*(c: var CborParser; buf: var openArray[byte]): ?!void =
## Read the bytes that the parser is positioned on and advance.
try:
if c.kind != CborEventKind.cborBytes:
return failure(newCborError("Expected bytes, got " & $c.kind))
if buf.len != c.intVal.int:
return failure(newCborError("Buffer length mismatch: expected " &
$c.intVal.int & ", got " & $buf.len))
if buf.len > 0:
let n = c.s.readData(buf[0].addr, buf.len)
if n != buf.len:
return failure(newCborError("truncated read of CBOR data"))
tryNext(c)
success()
except OSError as e:
return failure(e.msg)
except IOError as e:
return failure(e.msg)
proc nextBytes*(c: var CborParser): ?!seq[byte] =
## Read the bytes that the parser is positioned on into a seq and advance.
var val = newSeq[byte](c.intVal.int)
let nextRes = nextBytes(c, val)
if nextRes.isFailure:
return failure(nextRes.error)
return success(val)
func textLen*(c: CborParser): ?!int =
## Return the length of the text that the parser is positioned on.
if c.kind != CborEventKind.cborText:
return failure(newCborError("Expected text, got " & $c.kind))
return success(c.intVal.int)
proc nextText*(c: var CborParser; buf: var string): ?!void =
## Read the text that the parser is positioned on into a string and advance.
try:
if c.kind != CborEventKind.cborText:
return failure(newCborError("Expected text, got " & $c.kind))
buf.setLen c.intVal.int
if buf.len > 0:
let n = c.s.readData(buf[0].addr, buf.len)
if n != buf.len:
return failure(newCborError("truncated read of CBOR data"))
tryNext(c)
success()
except IOError as e:
return failure(e.msg)
except OSError as e:
return failure(e.msg)
proc nextText*(c: var CborParser): ?!string =
## Read the text that the parser is positioned on into a string and advance.
var buf: string
let nextRes = nextText(c, buf)
if nextRes.isFailure:
return failure(nextRes.error)
return success(buf)
func arrayLen*(c: CborParser): int =
## Return the length of the array that the parser is positioned on.
assert(c.kind == CborEventKind.cborArray, $c.kind)
c.intVal.int
func mapLen*(c: CborParser): int =
## Return the length of the map that the parser is positioned on.
assert(c.kind == CborEventKind.cborMap, $c.kind)
c.intVal.int
func tag*(c: CborParser): uint64 =
## Return the tag value the parser is positioned on.
assert(c.kind == CborEventKind.cborTag, $c.kind)
c.intVal
proc skipNode*(c: var CborParser): ?!void =
## Skip the item the parser is positioned on.
try:
case c.kind
of CborEventKind.cborEof:
return failure(newCborError("end of CBOR stream"))
of CborEventKind.cborPositive, CborEventKind.cborNegative,
CborEventKind.cborSimple:
return c.next()
of CborEventKind.cborBytes, CborEventKind.cborText:
if c.isIndefinite:
tryNext(c)
while c.kind != CborEventKind.cborBreak:
if c.kind != CborEventKind.cborBytes:
return failure(newCborError("expected bytes, got " & $c.kind))
for _ in 1..c.intVal.int: discard readChar(c.s)
return c.next()
else:
for _ in 1..c.intVal.int: discard readChar(c.s)
return c.next()
of CborEventKind.cborArray:
if c.isIndefinite:
tryNext(c)
while c.kind != CborEventKind.cborBreak:
trySkip(c)
return c.next()
else:
let len = c.intVal
tryNext(c)
for i in 1..len:
trySkip(c)
of CborEventKind.cborMap:
let mapLen = c.intVal.int
if c.isIndefinite:
tryNext(c)
while c.kind != CborEventKind.cborBreak:
trySkip(c)
return c.next()
else:
tryNext(c)
for _ in 1 .. mapLen:
trySkip(c)
of CborEventKind.cborTag:
tryNext(c)
return c.skipNode()
of CborEventKind.cborFloat:
without f =? c.nextFloat(), error:
return failure(error)
of CborEventKind.cborBreak:
discard
success()
except OSError as e:
return failure(e.msg)
except IOError as e:
return failure(e.msg)
proc nextNode*(c: var CborParser): ?!CborNode =
## Parse the item the parser is positioned on into a ``CborNode``.
## This is cheap for numbers or simple values but expensive
## for nested types.
try:
var next: CborNode
case c.kind
of CborEventKind.cborEof:
return failure(newCborError("end of CBOR stream"))
of CborEventKind.cborPositive:
next = CborNode(kind: cborUnsigned, uint: c.intVal)
tryNext(c)
of CborEventKind.cborNegative:
next = CborNode(kind: cborNegative, int: -1 - c.intVal.int64)
tryNext(c)
of CborEventKind.cborBytes:
if c.isIndefinite:
next = CborNode(kind: cborBytes, bytes: newSeq[byte]())
tryNext(c)
while c.kind != CborEventKind.cborBreak:
if c.kind != CborEventKind.cborBytes:
return failure(newCborError("Expected bytes, got " & $c.kind))
let
chunkLen = c.intVal.int
pos = next.bytes.len
next.bytes.setLen(pos+chunkLen)
let n = c.s.readData(next.bytes[pos].addr, chunkLen)
if n != chunkLen:
return failure(newCborError("truncated read of CBOR data"))
tryNext(c)
else:
without rawBytes =? c.nextBytes(), error:
return failure(error)
next = CborNode(kind: cborBytes, bytes: rawBytes)
of CborEventKind.cborText:
if c.isIndefinite:
next = CborNode(kind: cborText, text: "")
tryNext(c)
while c.kind != CborEventKind.cborBreak:
if c.kind != CborEventKind.cborText:
return failure(newCborError("Expected text, got " & $c.kind))
let
chunkLen = c.intVal.int
pos = next.text.len
next.text.setLen(pos+chunkLen)
let n = c.s.readData(next.text[pos].addr, chunkLen)
if n != chunkLen:
return failure(newCborError("truncated read of CBOR data"))
tryNext(c)
tryNext(c)
else:
without text =? c.nextText(), error:
return failure(error)
next = CborNode(kind: cborText, text: text)
of CborEventKind.cborArray:
next = CborNode(kind: cborArray, seq: newSeq[CborNode](c.intVal))
if c.isIndefinite:
tryNext(c)
while c.kind != CborEventKind.cborBreak:
without node =? c.nextNode(), error:
return failure(error)
next.seq.add(node)
tryNext(c)
else:
tryNext(c)
for i in 0..next.seq.high:
without node =? c.nextNode(), error:
return failure(error)
next.seq[i] = node
of CborEventKind.cborMap:
let mapLen = c.intVal.int
next = CborNode(kind: cborMap, map: initOrderedTable[CborNode, CborNode](
mapLen.nextPowerOfTwo))
if c.isIndefinite:
tryNext(c)
while c.kind != CborEventKind.cborBreak:
without key =? c.nextNode(), error:
return failure(error)
without val =? c.nextNode(), error:
return failure(error)
next.map[key] = val
tryNext(c)
else:
tryNext(c)
for _ in 1 .. mapLen:
without key =? c.nextNode(), error:
return failure(error)
without val =? c.nextNode(), error:
return failure(error)
next.map[key] = val
of CborEventKind.cborTag:
let tag = c.intVal
tryNext(c)
without node =? c.nextNode(), error:
return failure(error)
next = node
next.tag = some tag
of CborEventKind.cborSimple:
case c.minor
of 24:
next = CborNode(kind: cborSimple, simple: c.intVal.uint8)
else:
next = CborNode(kind: cborSimple, simple: c.minor)
tryNext(c)
of CborEventKind.cborFloat:
without f =? c.nextFloat(), error:
return failure(error)
next = CborNode(kind: cborFloat, float: f)
of CborEventKind.cborBreak:
discard
success(next)
except OSError as e:
return failure(e.msg)
except IOError as e:
return failure(e.msg)
except Exception as e:
return failure(e.msg)
proc readCbor*(s: Stream): ?!CborNode =
## Parse a stream into a CBOR object.
var parser: CborParser
parser.open(s)
tryNext(parser)
parser.nextNode()
proc parseCbor*(s: string): ?!CborNode =
## Parse a string into a CBOR object.
## A wrapper over stream parsing.
readCbor(newStringStream s)
proc `$`*(n: CborNode): string =
## Get a ``CborNode`` in diagnostic notation.
result = ""
if n.tag.isSome:
result.add($n.tag.get)
result.add("(")
case n.kind
of cborUnsigned:
result.add $n.uint
of cborNegative:
result.add $n.int
of cborBytes:
result.add "h'"
for c in n.bytes:
result.add(c.toHex)
result.add "'"
of cborText:
result.add escape n.text
of cborArray:
result.add "["
for i in 0..<n.seq.high:
result.add $(n.seq[i])
result.add ", "
if n.seq.len > 0:
result.add $(n.seq[n.seq.high])
result.add "]"
of cborMap:
result.add "{"
let final = n.map.len
var i = 1
for k, v in n.map.pairs:
result.add $k
result.add ": "
result.add $v
if i != final:
result.add ", "
inc i
result.add "}"
of cborTag:
discard
of cborSimple:
case n.simple
of 20: result.add "false"
of 21: result.add "true"
of 22: result.add "null"
of 23: result.add "undefined"
of 31: discard # break code for indefinite-length items
else: result.add "simple(" & $n.simple & ")"
of cborFloat:
case n.float.classify
of fcNan:
result.add "NaN"
of fcInf:
result.add "Infinity"
of fcNegInf:
result.add "-Infinity"
else:
result.add $n.float
of cborRaw:
without val =? parseCbor(n.raw), error:
return error.msg
result.add $val
if n.tag.isSome:
result.add(")")
proc getInt*(n: CborNode; default: int = 0): int =
## Get the numerical value of a ``CborNode`` or a fallback.
case n.kind
of cborUnsigned: n.uint.int
of cborNegative: n.int.int
else: default
proc parseDateText(n: CborNode): DateTime {.raises: [TimeParseError].} =
parse(n.text, timeFormat)
proc parseTime(n: CborNode): Time =
case n.kind
of cborUnsigned, cborNegative:
result = fromUnix n.getInt
of cborFloat:
result = fromUnixFloat n.float
else:
assert false
proc fromCborHook*(v: var DateTime; n: CborNode): ?!void =
## Parse a `DateTime` from the tagged string representation
## defined in RCF7049 section 2.4.1.
if n.tag.isSome:
try:
if n.tag.get == 0 and n.kind == cborText:
v = parseDateText(n)
return success()
elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}:
v = parseTime(n).utc
return success()
except ValueError as e: return failure(e)
proc fromCborHook*(v: var Time; n: CborNode): ?!void =
## Parse a `Time` from the tagged string representation
## defined in RCF7049 section 2.4.1.
if n.tag.isSome:
try:
if n.tag.get == 0 and n.kind == cborText:
v = parseDateText(n).toTime
return success()
elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}:
v = parseTime(n)
return success()
except ValueError as e: return failure(e)
func isTagged*(n: CborNode): bool =
## Check if a CBOR item has a tag.
n.tag.isSome
func hasTag*(n: CborNode; tag: Natural): bool =
## Check if a CBOR item has a tag.
n.tag.isSome and n.tag.get == (uint64)tag
proc `tag=`*(result: var CborNode; tag: Natural) =
## Tag a CBOR item.
result.tag = some(tag.uint64)
func tag*(n: CborNode): uint64 =
## Get a CBOR item tag.
n.tag.get
func isBool*(n: CborNode): bool =
(n.kind == cborSimple) and (n.simple in {20, 21})
func getBool*(n: CborNode; default = false): bool =
## Get the boolean value of a ``CborNode`` or a fallback.
if n.kind == cborSimple:
case n.simple
of 20: false
of 21: true
else: default
else:
default
func isNull*(n: CborNode): bool =
## Return true if ``n`` is a CBOR null.
(n.kind == cborSimple) and (n.simple == 22)
proc getUnsigned*(n: CborNode; default: uint64 = 0): uint64 =
## Get the numerical value of a ``CborNode`` or a fallback.
case n.kind
of cborUnsigned: n.uint
of cborNegative: n.int.uint64
else: default
proc getSigned*(n: CborNode; default: int64 = 0): int64 =
## Get the numerical value of a ``CborNode`` or a fallback.
case n.kind
of cborUnsigned: n.uint.int64
of cborNegative: n.int
else: default
func getFloat*(n: CborNode; default = 0.0): float =
## Get the floating-poing value of a ``CborNode`` or a fallback.
if n.kind == cborFloat:
n.float
else:
default
proc fromCbor*[T](v: var T; n: CborNode): ?!void =
## Return a Result containing the value if `v` can be converted from a given `CborNode`,
## or an error if conversion fails.
## Can be extended and overriden with `fromCborHook(v: var T; n: CborNode)`
## for specific types of `T`.
try:
when T is CborNode:
v = n
result = success()
elif compiles(fromCborHook(v, n)):
return fromCborHook(v, n)
elif T is distinct:
return fromCbor(distinctBase v, n)
elif T is SomeUnsignedInt:
exceptCborKind(T, {cborUnsigned}, n)
v = T n.uint
if v.BiggestUInt == n.uint:
return success()
else:
return failure(newCborError("Value overflow for unsigned integer"))
elif T is SomeSignedInt:
exceptCborKind(T, {cborUnsigned, cborNegative}, n)
if n.kind == cborUnsigned:
v = T n.uint
if v.BiggestUInt == n.uint:
return success()
else:
return failure(newCborError("Value overflow for un signed integer"))
elif n.kind == cborNegative:
v = T n.int
if v.BiggestInt == n.int:
return success()
else:
return failure(newCborError("Value overflow for signed integer"))
elif T is bool:
if not n.isBool:
return failure(newCborError("Expected boolean, got " & $n.kind))
v = n.getBool
return success()
elif T is SomeFloat:
exceptCborKind(T, {cborFloat}, n)
v = T n.float
return success()
elif T is seq[byte]:
exceptCborKind(T, {cborBytes}, n)
v = n.bytes
return success()
elif T is string:
exceptCborKind(T, {cborText}, n)
v = n.text
return success()
elif T is seq:
exceptCborKind(T, {cborArray}, n)
v.setLen n.seq.len
for i, e in n.seq:
let itemResult = fromCbor(v[i], e)
if itemResult.isFailure:
v.setLen 0
return failure(itemResult.error)
return success()
elif T is tuple:
exceptCborKind(T, {cborArray}, n)
if n.seq.len != T.tupleLen:
return failure(newCborError("Expected tuple of length " & $T.tupleLen))
var i: int
for f in fields(v):
let itemResult = fromCbor(f, n.seq[i])
if itemResult.isFailure:
return failure(itemResult.error)
inc i
return success()
elif T is ref:
if n.isNull:
v = nil
return success()
else:
if isNil(v): new(v)
return fromCbor(v[], n)
elif T is object:
exceptCborKind(T, {cborMap}, n)
var
i: int
key = CborNode(kind: cborText)
for s, _ in fieldPairs(v):
key.text = s
if not n.map.hasKey key:
return failure(newCborError("Missing field: " & s))
else:
let fieldResult = fromCbor(v.dot(s), n.map[key])
if fieldResult.isFailure:
return failure(fieldResult.error)
inc i
if i == n.map.len:
return success()
else:
return failure(newCborError("Extra fields in map"))
else:
return failure(newCborError("Unsupported type: " & $T))
except Exception as e:
return failure(newCborError(e.msg))