From 52bc9439482df2a99a07db26107b24ab52a4734d Mon Sep 17 00:00:00 2001 From: munna0908 Date: Thu, 22 May 2025 12:03:22 +0530 Subject: [PATCH] improve error handling for cbor serialization --- serde/cbor/deserializer.nim | 2 +- serde/cbor/serializer.nim | 487 +++++++++++++++++++----------------- tests/cbor/testObjects.nim | 17 +- 3 files changed, 272 insertions(+), 234 deletions(-) diff --git a/serde/cbor/deserializer.nim b/serde/cbor/deserializer.nim index 56a5300..3d406f9 100644 --- a/serde/cbor/deserializer.nim +++ b/serde/cbor/deserializer.nim @@ -9,11 +9,11 @@ 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. -{.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 diff --git a/serde/cbor/serializer.nim b/serde/cbor/serializer.nim index 4422958..4165547 100644 --- a/serde/cbor/serializer.nim +++ b/serde/cbor/serializer.nim @@ -1,8 +1,11 @@ -{.push checks: off.} - import std/[streams, options, tables, typetraits, math, endians, times, base64] +import pkg/questionable +import pkg/questionable/results +import ../utils/errors import ./types +{.push raises: [].} + func isHalfPrecise(single: float32): bool = # TODO: check for subnormal false-positives let val = cast[uint32](single) @@ -34,298 +37,335 @@ func floatHalf(single: float32): uint16 = 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) = +proc writeInitial[T: SomeInteger](str: Stream; m: uint8; n: T): ?!void = ## Write the initial integer of a CBOR item. - - let m = m shl 5 - when T is byte: - if n < 24: - str.write(m or n.uint8) + try: + let m = m shl 5 + when T is byte: + if n < 24: + str.write(m or n.uint8) + else: + str.write(m or 24'u8) + str.write(n) else: - str.write(m or 24'u8) - str.write(n) - else: - if n < 24: - str.write(m or n.uint8) - elif uint64(n) <= uint64(uint8.high): - str.write(m or 24'u8) - str.write(n.uint8) - elif uint64(n) <= uint64(uint16.high): - str.write(m or 25'u8) - str.write((uint8)n shr 8) - str.write((uint8)n) - elif uint64(n) <= uint64(uint32.high): - str.write(m or 26'u8) - for i in countdown(24, 8, 8): - {.unroll.} - str.write((uint8)n shr i) - str.write((uint8)n) - else: - str.write(m or 27'u8) - for i in countdown(56, 8, 8): - {.unroll.} - str.write((uint8)n shr i) - str.write((uint8)n) -# {.pop.} + if n < 24: + str.write(m or n.uint8) + elif uint64(n) <= uint64(uint8.high): + str.write(m or 24'u8) + str.write(n.uint8) + elif uint64(n) <= uint64(uint16.high): + str.write(m or 25'u8) + str.write((uint8)n shr 8) + str.write((uint8)n) + elif uint64(n) <= uint64(uint32.high): + str.write(m or 26'u8) + for i in countdown(24, 8, 8): + {.unroll.} + str.write((uint8)n shr i) + str.write((uint8)n) + else: + str.write(m or 27'u8) + for i in countdown(56, 8, 8): + {.unroll.} + str.write((uint8)n shr i) + str.write((uint8)n) + success() + except IOError as e: + return failure(e.msg) + except OSError as o: + return failure(o.msg) -proc writeCborArrayLen*(str: Stream; len: Natural) = +proc writeCborArrayLen*(str: Stream; len: Natural): ?!void = ## Write a marker to the stream that initiates an array of ``len`` items. str.writeInitial(4, len) -proc writeCborIndefiniteArrayLen*(str: Stream) = +proc writeCborIndefiniteArrayLen*(str: Stream): ?!void = ## Write a marker to the stream that initiates an array of indefinite length. ## Indefinite length arrays are composed of an indefinite amount of arrays ## of definite lengths. - str.write(initialByte(4, 31)) + catch str.write(initialByte(4, 31)) -proc writeCborMapLen*(str: Stream; len: Natural) = +proc writeCborMapLen*(str: Stream; len: Natural): ?!void = ## Write a marker to the stream that initiates an map of ``len`` pairs. str.writeInitial(5, len) -proc writeCborIndefiniteMapLen*(str: Stream) = +proc writeCborIndefiniteMapLen*(str: Stream): ?!void = ## Write a marker to the stream that initiates a map of indefinite length. ## Indefinite length maps are composed of an indefinite amount of maps ## of definite length. - str.write(initialByte(5, 31)) + catch str.write(initialByte(5, 31)) -proc writeCborBreak*(str: Stream) = +proc writeCborBreak*(str: Stream): ?!void = ## Write a marker to the stream that ends an indefinite array or map. - str.write(initialByte(7, 31)) + catch str.write(initialByte(7, 31)) -proc writeCborTag*(str: Stream; tag: Natural) {.inline.} = +proc writeCborTag*(str: Stream; tag: Natural): ?!void {.inline.} = ## Write a tag for the next CBOR item to a binary stream. str.writeInitial(6, tag) -proc writeCbor*(str: Stream; buf: pointer; len: int) = +proc writeCbor*(str: Stream; buf: pointer; len: int): ?!void = ## Write a raw buffer to a CBOR `Stream`. - str.writeInitial(BytesMajor, len) - if len > 0: str.writeData(buf, len) + ?str.writeInitial(BytesMajor, len) + if len > 0: + return catch str.writeData(buf, len) -proc isSorted*(n: CborNode): bool {.gcsafe.} +proc isSorted*(n: CborNode): ?!bool {.gcsafe.} -proc writeCbor*[T](str: Stream; v: T) = +proc writeCbor*[T](str: Stream; v: T): ?!void = ## Write the CBOR binary representation of a `T` to a `Stream`. ## The behavior of this procedure can be extended or overriden ## by defining `proc writeCborHook(str: Stream; v: T)` for specific ## types `T`. - when T is CborNode: - if v.tag.isSome: - str.writeCborTag(v.tag.get) - case v.kind: - of cborUnsigned: - str.writeCbor(v.uint) - of cborNegative: - str.writeCbor(v.int) - of cborBytes: - str.writeInitial(cborBytes.uint8, v.bytes.len) - for b in v.bytes.items: - str.write(b) - of cborText: - str.writeInitial(cborText.uint8, v.text.len) - str.write(v.text) - of cborArray: - str.writeInitial(4, v.seq.len) - for e in v.seq: - str.writeCbor(e) - of cborMap: - assert(v.isSorted, "refusing to write unsorted map to stream") - str.writeInitial(5, v.map.len) - for k, f in v.map.pairs: - str.writeCbor(k) - str.writeCbor(f) - of cborTag: - discard - of cborSimple: - if v.simple > 31'u or v.simple == 24: - str.write(initialByte(cborSimple.uint8, 24)) - str.write(v.simple) - else: - str.write(initialByte(cborSimple.uint8, v.simple)) - of cborFloat: - str.writeCbor(v.float) - of cborRaw: - str.write(v.raw) - elif compiles(writeCborHook(str, v)): - writeCborHook(str, v) - elif T is SomeUnsignedInt: - str.writeInitial(0, v) - elif T is SomeSignedInt: - if v < 0: - # Major type 1 - str.writeInitial(1, -1 - v) - else: - # Major type 0 - str.writeInitial(0, v) - elif T is seq[byte]: - str.writeInitial(BytesMajor, v.len) - if v.len > 0: - str.writeData(unsafeAddr v[0], v.len) - elif T is openArray[char | uint8 | int8]: - str.writeInitial(BytesMajor, v.len) - if v.len > 0: - str.writeData(unsafeAddr v[0], v.len) - elif T is string: - str.writeInitial(TextMajor, v.len) - str.write(v) - elif T is array | seq: - str.writeInitial(4, v.len) - for e in v.items: - writeCbor(str, e) - elif T is tuple: - str.writeInitial(4, T.tupleLen) - for f in v.fields: str.writeCbor(f) - elif T is ptr | ref: - if system.`==`(v, nil): - # Major type 7 - str.write(Null) - else: writeCbor(str, v[]) - elif T is object: - var n: uint - for _, _ in v.fieldPairs: - inc n - str.writeInitial(5, n) - for k, f in v.fieldPairs: - str.writeCbor(k) - str.writeCbor(f) - elif T is bool: - str.write(initialByte(7, (if v: 21 else: 20))) - elif T is SomeFloat: - case v.classify - of fcNormal, fcSubnormal: - let single = v.float32 - if single.float64 == v.float64: - if single.isHalfPrecise: - let half = floatHalf(single) - str.write(initialByte(7, 25)) - when system.cpuEndian == bigEndian: - str.write(half) - else: - var be: uint16 - swapEndian16 be.addr, half.unsafeAddr - str.write(be) + try: + when T is CborNode: + if v.tag.isSome: + return str.writeCborTag(v.tag.get) + case v.kind: + of cborUnsigned: + return str.writeCbor(v.uint) + of cborNegative: + return str.writeCbor(v.int) + of cborBytes: + ?str.writeInitial(cborBytes.uint8, v.bytes.len) + for b in v.bytes.items: + str.write(b) + of cborText: + ?str.writeInitial(cborText.uint8, v.text.len) + str.write(v.text) + of cborArray: + ?str.writeInitial(4, v.seq.len) + for e in v.seq: + ?str.writeCbor(e) + of cborMap: + without isSortedRes =? v.isSorted, error: + return failure(error) + if not isSortedRes: + return failure(newSerdeError("refusing to write unsorted map to stream")) + ?str.writeInitial(5, v.map.len) + for k, f in v.map.pairs: + ?str.writeCbor(k) + ?str.writeCbor(f) + of cborTag: + discard + of cborSimple: + if v.simple > 31'u or v.simple == 24: + str.write(initialByte(cborSimple.uint8, 24)) + str.write(v.simple) else: - str.write initialByte(7, 26) - when system.cpuEndian == bigEndian: - str.write(single) - else: - var be: uint32 - swapEndian32 be.addr, single.unsafeAddr - str.write(be) + str.write(initialByte(cborSimple.uint8, v.simple)) + of cborFloat: + return str.writeCbor(v.float) + of cborRaw: + str.write(v.raw) + elif compiles(writeCborHook(str, v)): + writeCborHook(str, v) + elif T is SomeUnsignedInt: + ?str.writeInitial(0, v) + elif T is SomeSignedInt: + if v < 0: + # Major type 1 + ?str.writeInitial(1, -1 - v) else: - str.write initialByte(7, 27) - when system.cpuEndian == bigEndian: - str.write(v) + # Major type 0 + ?str.writeInitial(0, v) + elif T is seq[byte]: + ?str.writeInitial(BytesMajor, v.len) + if v.len > 0: + str.writeData(unsafeAddr v[0], v.len) + elif T is openArray[char | uint8 | int8]: + ?str.writeInitial(BytesMajor, v.len) + if v.len > 0: + str.writeData(unsafeAddr v[0], v.len) + elif T is string: + ?str.writeInitial(TextMajor, v.len) + str.write(v) + elif T is array | seq: + ?str.writeInitial(4, v.len) + for e in v.items: + ?str.writeCbor(e) + elif T is tuple: + ?str.writeInitial(4, T.tupleLen) + for f in v.fields: ?str.writeCbor(f) + elif T is ptr | ref: + if system.`==`(v, nil): + # Major type 7 + str.write(Null) + else: ?str.writeCbor(v[]) + elif T is object: + var n: uint + for _, _ in v.fieldPairs: + inc n + ?str.writeInitial(5, n) + for k, f in v.fieldPairs: + ?str.writeCbor(k) + ?str.writeCbor(f) + elif T is bool: + str.write(initialByte(7, (if v: 21 else: 20))) + elif T is SomeFloat: + case v.classify + of fcNormal, fcSubnormal: + let single = v.float32 + if single.float64 == v.float64: + if single.isHalfPrecise: + let half = floatHalf(single) + str.write(initialByte(7, 25)) + when system.cpuEndian == bigEndian: + str.write(half) + else: + var be: uint16 + swapEndian16 be.addr, half.unsafeAddr + str.write(be) + else: + str.write initialByte(7, 26) + when system.cpuEndian == bigEndian: + str.write(single) + else: + var be: uint32 + swapEndian32 be.addr, single.unsafeAddr + str.write(be) else: - var be: float64 - swapEndian64 be.addr, v.unsafeAddr - str.write be - return - of fcZero: - str.write initialByte(7, 25) - str.write((char)0x00) - of fcNegZero: - str.write initialByte(7, 25) - str.write((char)0x80) - of fcInf: - str.write initialByte(7, 25) - str.write((char)0x7c) - of fcNan: - str.write initialByte(7, 25) - str.write((char)0x7e) - of fcNegInf: - str.write initialByte(7, 25) - str.write((char)0xfc) - str.write((char)0x00) + str.write initialByte(7, 27) + when system.cpuEndian == bigEndian: + str.write(v) + else: + var be: float64 + swapEndian64 be.addr, v.unsafeAddr + str.write be -proc writeCborArray*(str: Stream; args: varargs[CborNode, toCbor]) = + return success() + of fcZero: + str.write initialByte(7, 25) + str.write((char)0x00) + of fcNegZero: + str.write initialByte(7, 25) + str.write((char)0x80) + of fcInf: + str.write initialByte(7, 25) + str.write((char)0x7c) + of fcNan: + str.write initialByte(7, 25) + str.write((char)0x7e) + of fcNegInf: + str.write initialByte(7, 25) + str.write((char)0xfc) + str.write((char)0x00) + success() + except IOError as io: + return failure(io.msg) + except OSError as os: + return failure(os.msg) + +proc writeCborArray*(str: Stream; args: varargs[CborNode, toCbor]): ?!void = ## Encode to a CBOR array in binary form. This magic doesn't ## always work, some arguments may need to be explicitly ## converted with ``toCbor`` before passing. - str.writeCborArrayLen(args.len) + ?str.writeCborArrayLen(args.len) for x in args: - str.writeCbor(x) + ?str.writeCbor(x) -proc encode*[T](v: T): string = +proc encode*[T](v: T): ?!string = ## Encode an arbitrary value to CBOR binary representation. ## A wrapper over ``writeCbor``. let s = newStringStream() - s.writeCbor(v) - s.data + let res = s.writeCbor(v) + if res.isFailure: + return failure(res.error) + success(s.data) -proc toRaw*(n: CborNode): CborNode = +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)) + if n.kind == cborRaw: + return success(n) + else: + without res =? encode(n), error: + return failure(error) + return success(CborNode(kind: cborRaw, raw: res)) -proc isSorted(n: CborNode): bool = +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 + without res =? key.toRaw, error: + return failure(error.msg) + let thisRaw = res.raw if lastRaw != "": - if cmp(lastRaw, thisRaw) > 0: return false + if cmp(lastRaw, thisRaw) > 0: return success(false) lastRaw = thisRaw - true + success(true) -proc sort*(n: var CborNode) = +proc sort*(n: var CborNode): ?!void = ## 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 + try: + var tmp = initOrderedTable[CborNode, CborNode](n.map.len.nextPowerOfTwo) + for key, val in n.map.mpairs: + without res =? key.toRaw, error: + return failure(error) + tmp[res] = 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 + success() + except Exception as e: + return failure(e.msg) -proc writeCborHook*(str: Stream; dt: DateTime) = +proc writeCborHook*(str: Stream; dt: DateTime): ?!void = ## Write a `DateTime` using the tagged string representation ## defined in RCF7049 section 2.4.1. - writeCborTag(str, 0) - writeCbor(str, format(dt, timeFormat)) + ?writeCborTag(str, 0) + ?writeCbor(str, format(dt, timeFormat)) -proc writeCborHook*(str: Stream; t: Time) = +proc writeCborHook*(str: Stream; t: Time): ?!void = ## Write a `Time` using the tagged numerical representation ## defined in RCF7049 section 2.4.1. - writeCborTag(str, 1) - writeCbor(str, t.toUnix) + ?writeCborTag(str, 1) + ?writeCbor(str, t.toUnix) -func toCbor*(x: CborNode): CborNode = x +func toCbor*(x: CborNode): ?!CborNode = success(x) -func toCbor*(x: SomeInteger): CborNode = +func toCbor*(x: SomeInteger): ?!CborNode = if x > 0: - CborNode(kind: cborUnsigned, uint: x.uint64) + success(CborNode(kind: cborUnsigned, uint: x.uint64)) else: - CborNode(kind: cborNegative, int: x.int64) + success(CborNode(kind: cborNegative, int: x.int64)) -func toCbor*(x: openArray[byte]): CborNode = - CborNode(kind: cborBytes, bytes: @x) +func toCbor*(x: openArray[byte]): ?!CborNode = + success(CborNode(kind: cborBytes, bytes: @x)) -func toCbor*(x: string): CborNode = - CborNode(kind: cborText, text: x) +func toCbor*(x: string): ?!CborNode = + success(CborNode(kind: cborText, text: x)) -func toCbor*(x: openArray[CborNode]): CborNode = - CborNode(kind: cborArray, seq: @x) +func toCbor*(x: openArray[CborNode]): ?!CborNode = + success(CborNode(kind: cborArray, seq: @x)) -func toCbor*(pairs: openArray[(CborNode, CborNode)]): CborNode = - CborNode(kind: cborMap, map: pairs.toOrderedTable) +func toCbor*(pairs: openArray[(CborNode, CborNode)]): ?!CborNode = + try: + return success(CborNode(kind: cborMap, map: pairs.toOrderedTable)) + except Exception as e: + return failure(e.msg) -func toCbor*(tag: uint64; val: CborNode): CborNode = - result = toCbor(val) - result.tag = some(tag) +func toCbor*(tag: uint64; val: CborNode): ?!CborNode = + without res =? toCbor(val), error: + return failure(error.msg) + var cnode = res + cnode.tag = some(tag) + return success(cnode) -func toCbor*(x: bool): CborNode = +func toCbor*(x: bool): ?!CborNode = case x of false: - CborNode(kind: cborSimple, simple: 20) + success(CborNode(kind: cborSimple, simple: 20)) of true: - CborNode(kind: cborSimple, simple: 21) + success(CborNode(kind: cborSimple, simple: 21)) -func toCbor*(x: SomeFloat): CborNode = - CborNode(kind: cborFloat, float: x.float64) +func toCbor*(x: SomeFloat): ?!CborNode = + success(CborNode(kind: cborFloat, float: x.float64)) -func toCbor*(x: pointer): CborNode = +func toCbor*(x: pointer): ?!CborNode = ## A hack to produce a CBOR null item. assert(x.isNil) - CborNode(kind: cborSimple, simple: 22) + if not x.isNil: + return failure("pointer is not nil") + success(CborNode(kind: cborSimple, simple: 22)) func initCborBytes*[T: char|byte](buf: openArray[T]): CborNode = ## Create a CBOR byte string from `buf`. @@ -359,11 +399,4 @@ func initCbor*(items: varargs[CborNode, toCbor]): CborNode = ## Initialize a CBOR arrary. CborNode(kind: cborArray, seq: @items) -template initCborOther*(x: untyped): CborNode = - ## Initialize a ``CborNode`` from a type where ``toCbor`` is not implemented. - ## This encodes ``x`` to binary using ``writeCbor``, so - ## ``$(initCborOther(x))`` will incur an encode and decode roundtrip. - let s = newStringStream() - s.writeCbor(x) - CborNode(kind: cborRaw, raw: s.data) diff --git a/tests/cbor/testObjects.nim b/tests/cbor/testObjects.nim index 9c1bb2e..4b0deed 100644 --- a/tests/cbor/testObjects.nim +++ b/tests/cbor/testObjects.nim @@ -21,11 +21,6 @@ type point: CustomPoint color: CustomColor - Person = object - name: string - age: int - isActive: bool - Inner = object s: string nums: seq[int] @@ -134,12 +129,22 @@ suite "CBOR deserialization": refInner: refObj ) + # Serialize to CBOR with encode + without encodedStr =? encode(original), error: + fail() + # Serialize to CBOR let stream = newStringStream() - stream.writeCbor(original) + if stream.writeCbor(original).isFailure: + fail() + let cborData = stream.data + # Check that both serialized forms are equal + check cborData == encodedStr + # Parse CBOR back to CborNode let parseResult = parseCbor(cborData) + check parseResult.isSuccess let node = parseResult.tryValue