diff --git a/.gitignore b/.gitignore index 2681317..dcd6561 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ nimble.develop nimble.paths .idea vendor/ -.vscode/ \ No newline at end of file +.vscode/ +nimbledeps \ No newline at end of file diff --git a/serde.nimble b/serde.nimble index 187b395..4d5af94 100644 --- a/serde.nimble +++ b/serde.nimble @@ -9,7 +9,7 @@ skipDirs = @["tests"] # Dependencies requires "nim >= 1.6.14" requires "chronicles >= 0.10.3 & < 0.11.0" -requires "questionable >= 0.10.13 & < 0.11.0" +requires "questionable >= 0.10.15" requires "stint" requires "stew" diff --git a/serde/cbor.nim b/serde/cbor.nim new file mode 100644 index 0000000..de12a78 --- /dev/null +++ b/serde/cbor.nim @@ -0,0 +1,7 @@ +import ./cbor/serializer +import ./cbor/deserializer +import ./cbor/jsonhook +import ./cbor/types as ctypes +import ./utils/types +import ./utils/errors +export serializer, deserializer, ctypes, types, errors, jsonhook diff --git a/serde/cbor/deserializer.nim b/serde/cbor/deserializer.nim new file mode 100644 index 0000000..cdff43b --- /dev/null +++ b/serde/cbor/deserializer.nim @@ -0,0 +1,761 @@ +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 + + +func isIndefinite*(c: CborParser): bool {.inline.} = c.minor == 31 + ## Return true if the parser is positioned on an item of indefinite length. + +{.push raises: [].} +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 + let nextRes = c.next() + if nextRes.isFailure: + return failure(nextRes.error) + 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)) + + let nextRes = c.next() + if nextRes.isFailure: + return failure(nextRes.error) + + 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 + + let nextRes = c.next() + if nextRes.isFailure: + return failure(nextRes.error) + + 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) + +template tryNext(c: var CborParser) = + let nextRes = c.next() + if nextRes.isFailure: + return failure(nextRes.error) + +template trySkip(c: var CborParser) = + let skipRes = c.skipNode() + if skipRes.isFailure: + return failure(skipRes.error) + +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.. 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): bool = + ## 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) + result = true + elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}: + v = parseTime(n).utc + result = true + except ValueError: discard + +proc fromCborHook*(v: var Time; n: CborNode): bool = + ## 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 + result = true + elif n.tag.get == 1 and n.kind in {cborUnsigned, cborNegative, cborFloat}: + v = parseTime(n) + result = true + except ValueError: discard + +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): bool = + ## Return `true` if `v` can be converted from a given `CborNode`. + ## Can be extended and overriden with `fromCborHook(v: var T; n: CborNode)` + ## for specific types of `T`. + when T is CborNode: + v = n + result = true + elif compiles(fromCborHook(v, n)): + result = fromCborHook(v, n) + elif T is distinct: + result = fromCbor(distinctBase v, n) + elif T is SomeUnsignedInt: + if n.kind == cborUnsigned: + v = T n.uint + result = v.BiggestUInt == n.uint + elif T is SomeSignedInt: + if n.kind == cborUnsigned: + v = T n.uint + result = v.BiggestUInt == n.uint + elif n.kind == cborNegative: + v = T n.int + result = v.BiggestInt == n.int + elif T is bool: + if n.isBool: + v = n.getBool + result = true + elif T is SomeFloat: + if n.kind == cborFloat: + v = T n.float + result = true + elif T is seq[byte]: + if n.kind == cborBytes: + v = n.bytes + result = true + elif T is string: + if n.kind == cborText: + v = n.text + result = true + elif T is seq: + if n.kind == cborArray: + result = true + v.setLen n.seq.len + for i, e in n.seq: + result = result and fromCbor(v[i], e) + if not result: + v.setLen 0 + break + elif T is tuple: + if n.kind == cborArray and n.seq.len == T.tupleLen: + result = true + var i: int + for f in fields(v): + result = result and fromCbor(f, n.seq[i]) + if not result: break + inc i + elif T is ref: + if n.isNull: + v = nil + result = true + else: + if isNil(v): new(v) + result = fromCbor(v[], n) + elif T is object: + if n.kind == cborMap: + result = true + var + i: int + key = CborNode(kind: cborText) + for s, _ in fieldPairs(v): + key.text = s + if not n.map.hasKey key: + result = false + break + else: + result = fromCbor(v.dot(s), n.map[key]) + if not result: break + inc i + result = result and (i == n.map.len) + +proc fromCborQ2*[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 fromCborQ2(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 = fromCborQ2(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 = fromCborQ2(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 fromCborQ2(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 = fromCborQ2(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)) diff --git a/serde/cbor/errors.nim b/serde/cbor/errors.nim new file mode 100644 index 0000000..f375a1a --- /dev/null +++ b/serde/cbor/errors.nim @@ -0,0 +1,37 @@ +import ../utils/types +import ./types +import std/sets + + +proc newUnexpectedKindError*( + expectedType: type, expectedKinds: string, cbor: CborNode +): ref UnexpectedKindError = + newException( + UnexpectedKindError, + "deserialization to " & $expectedType & " failed: expected " & + expectedKinds & + " but got " & $cbor.kind, + ) + +proc newUnexpectedKindError*( + expectedType: type, expectedKinds: set[CborEventKind], cbor: CborNode +): ref UnexpectedKindError = + newUnexpectedKindError(expectedType, $expectedKinds, cbor) + +proc newUnexpectedKindError*( + expectedType: type, expectedKind: CborEventKind, cbor: CborNode +): ref UnexpectedKindError = + newUnexpectedKindError(expectedType, {expectedKind}, cbor) + +proc newUnexpectedKindError*( + expectedType: type, expectedKinds: set[CborNodeKind], cbor: CborNode +): ref UnexpectedKindError = + newUnexpectedKindError(expectedType, $expectedKinds, cbor) + +proc newUnexpectedKindError*( + expectedType: type, expectedKind: CborNodeKind, cbor: CborNode +): ref UnexpectedKindError = + newUnexpectedKindError(expectedType, {expectedKind}, cbor) + +proc newCborError*(msg: string): ref CborParseError = + newException(CborParseError, msg) diff --git a/serde/cbor/helpers.nim b/serde/cbor/helpers.nim new file mode 100644 index 0000000..8450534 --- /dev/null +++ b/serde/cbor/helpers.nim @@ -0,0 +1,42 @@ + +import ./types +import ./errors +from macros import newDotExpr, newIdentNode, strVal + +template exceptCborKind*(expectedType: type, expectedKinds: set[CborNodeKind], + cbor: CborNode) = + if cbor.kind notin expectedKinds: + return failure(newUnexpectedKindError(expectedType, expectedKinds, cbor)) + +template exceptCborKind*(expectedType: type, expectedKind: CborNodeKind, + cbor: CborNode) = + exceptCborKind(expectedType, {expectedKind}, cbor) + +template exceptCborKind*(expectedType: type, expectedKinds: set[CborEventKind], + cbor: CborNode) = + if cbor.kind notin expectedKinds: + return failure(newUnexpectedKindError(expectedType, expectedKinds, cbor)) + +template exceptCborKind*(expectedType: type, expectedKind: CborEventKind, + cbor: CborNode) = + exceptCborKind(expectedType, {expectedKind}, cbor) + +macro dot*(obj: object, fld: string): untyped = + ## Turn ``obj.dot("fld")`` into ``obj.fld``. + newDotExpr(obj, newIdentNode(fld.strVal)) + + +func floatSingle*(half: uint16): float32 = + ## Convert a 16-bit float to 32-bits. + func ldexp(x: float64; exponent: int): float64 {.importc: "ldexp", + header: "".} + let + exp = (half shr 10) and 0x1f + mant = float64(half and 0x3ff) + val = if exp == 0: + ldexp(mant, -24) + elif exp != 31: + ldexp(mant + 1024, exp.int - 25) + else: + if mant == 0: Inf else: NaN + if (half and 0x8000) == 0: val else: -val diff --git a/serde/cbor/jsonhook.nim b/serde/cbor/jsonhook.nim new file mode 100644 index 0000000..21e7a7a --- /dev/null +++ b/serde/cbor/jsonhook.nim @@ -0,0 +1,42 @@ +import std/[base64, tables] +import ../json/stdjson +import ./types +import ./errors +import ./deserializer + +proc toJsonHook*(n: CborNode): JsonNode = + case n.kind: + of cborUnsigned: + newJInt n.uint.BiggestInt + of cborNegative: + newJInt n.int.BiggestInt + of cborBytes: + newJString base64.encode(cast[string](n.bytes), safe = true) + of cborText: + newJString n.text + of cborArray: + let a = newJArray() + for e in n.seq.items: + a.add(e.toJsonHook) + a + of cborMap: + let o = newJObject() + for k, v in n.map.pairs: + if k.kind == cborText: + o[k.text] = v.toJsonHook + else: + o[$k] = v.toJsonHook + o + of cborTag: nil + of cborSimple: + if n.isBool: + newJBool(n.getBool()) + elif n.isNull: + newJNull() + else: nil + of cborFloat: + newJFloat n.float + of cborRaw: + without parsed =? parseCbor(n.raw), error: + raise newCborError(error.msg) + toJsonHook(parsed) diff --git a/serde/cbor/serializer.nim b/serde/cbor/serializer.nim index 9b39e60..4422958 100644 --- a/serde/cbor/serializer.nim +++ b/serde/cbor/serializer.nim @@ -1,12 +1,42 @@ {.push checks: off.} -import ../utils/cbor +import std/[streams, options, tables, typetraits, math, endians, times, base64] +import ./types + +func isHalfPrecise(single: float32): bool = + # TODO: check for subnormal false-positives + let val = cast[uint32](single) + if val == 0 or val == (1'u32 shl 31): + result = true + else: + let + exp = int32((val and (0xff'u32 shl 23)) shr 23) - 127 + mant = val and 0x7fffff'u32 + if -25 < exp and exp < 16 and (mant and 0x1fff) == 0: + result = true + +func floatHalf(single: float32): uint16 = + ## Convert a 32-bit float to 16-bits. + let + val = cast[uint32](single) + exp = val and 0x7f800000 + mant = val and 0x7fffff + sign = uint16(val shr 16) and (1 shl 15) + let + unbiasedExp = int32(exp shr 23) - 127 + halfExp = unbiasedExp + 15 + if halfExp < 1: + if 14 - halfExp < 25: + result = sign or uint16((mant or 0x800000) shr uint16(14 - halfExp)) + else: + result = sign or uint16(halfExp shl 10) or uint16(mant shr 13) func initialByte(major, minor: Natural): uint8 {.inline.} = uint8((major shl 5) or (minor and 0b11111)) proc writeInitial[T: SomeInteger](str: Stream; m: uint8; n: T) = ## Write the initial integer of a CBOR item. + let m = m shl 5 when T is byte: if n < 24: @@ -36,7 +66,7 @@ proc writeInitial[T: SomeInteger](str: Stream; m: uint8; n: T) = {.unroll.} str.write((uint8)n shr i) str.write((uint8)n) -{.pop.} +# {.pop.} proc writeCborArrayLen*(str: Stream; len: Natural) = ## Write a marker to the stream that initiates an array of ``len`` items. @@ -71,6 +101,8 @@ proc writeCbor*(str: Stream; buf: pointer; len: int) = str.writeInitial(BytesMajor, len) if len > 0: str.writeData(buf, len) +proc isSorted*(n: CborNode): bool {.gcsafe.} + proc writeCbor*[T](str: Stream; v: T) = ## Write the CBOR binary representation of a `T` to a `Stream`. ## The behavior of this procedure can be extended or overriden @@ -219,3 +251,119 @@ proc encode*[T](v: T): string = let s = newStringStream() s.writeCbor(v) s.data + +proc toRaw*(n: CborNode): CborNode = + ## Reduce a CborNode to a string of bytes. + if n.kind == cborRaw: n + else: CborNode(kind: cborRaw, raw: encode(n)) + +proc isSorted(n: CborNode): bool = + ## Check if the item is sorted correctly. + var lastRaw = "" + for key in n.map.keys: + let thisRaw = key.toRaw.raw + if lastRaw != "": + if cmp(lastRaw, thisRaw) > 0: return false + lastRaw = thisRaw + true + +proc sort*(n: var CborNode) = + ## Sort a CBOR map object. + var tmp = initOrderedTable[CborNode, CborNode](n.map.len.nextPowerOfTwo) + for key, val in n.map.mpairs: + tmp[key.toRaw] = move(val) + sort(tmp) do (x, y: tuple[k: CborNode; v: CborNode]) -> int: + result = cmp(x.k.raw, y.k.raw) + n.map = move tmp + +proc writeCborHook*(str: Stream; dt: DateTime) = + ## Write a `DateTime` using the tagged string representation + ## defined in RCF7049 section 2.4.1. + writeCborTag(str, 0) + writeCbor(str, format(dt, timeFormat)) + +proc writeCborHook*(str: Stream; t: Time) = + ## Write a `Time` using the tagged numerical representation + ## defined in RCF7049 section 2.4.1. + writeCborTag(str, 1) + writeCbor(str, t.toUnix) + +func toCbor*(x: CborNode): CborNode = x + +func toCbor*(x: SomeInteger): CborNode = + if x > 0: + CborNode(kind: cborUnsigned, uint: x.uint64) + else: + CborNode(kind: cborNegative, int: x.int64) + +func toCbor*(x: openArray[byte]): CborNode = + CborNode(kind: cborBytes, bytes: @x) + +func toCbor*(x: string): CborNode = + CborNode(kind: cborText, text: x) + +func toCbor*(x: openArray[CborNode]): CborNode = + CborNode(kind: cborArray, seq: @x) + +func toCbor*(pairs: openArray[(CborNode, CborNode)]): CborNode = + CborNode(kind: cborMap, map: pairs.toOrderedTable) + +func toCbor*(tag: uint64; val: CborNode): CborNode = + result = toCbor(val) + result.tag = some(tag) + +func toCbor*(x: bool): CborNode = + case x + of false: + CborNode(kind: cborSimple, simple: 20) + of true: + CborNode(kind: cborSimple, simple: 21) + +func toCbor*(x: SomeFloat): CborNode = + CborNode(kind: cborFloat, float: x.float64) + +func toCbor*(x: pointer): CborNode = + ## A hack to produce a CBOR null item. + assert(x.isNil) + CborNode(kind: cborSimple, simple: 22) + +func initCborBytes*[T: char|byte](buf: openArray[T]): CborNode = + ## Create a CBOR byte string from `buf`. + result = CborNode(kind: cborBytes, bytes: newSeq[byte](buf.len)) + for i in 0..= 0.10.13 & < 0.11.0" task test, "Run the test suite": exec "nimble install -d -y" exec "nim c -r test" + \ No newline at end of file