diff --git a/cbor_serialization/edn.nim b/cbor_serialization/edn.nim new file mode 100644 index 0000000..f273df8 --- /dev/null +++ b/cbor_serialization/edn.nim @@ -0,0 +1,154 @@ +# cbor-serialization +# Copyright (c) 2026 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +{.push raises: [], gcsafe.} + +import std/[formatfloat, math], stew/byteutils, serialization, ./reader + +export serialization, reader + +proc addEscaped(res: var string, value: string) = + template addPrefixSlash(c) = + res.add '\\' + res.add c + + const hexChars = "0123456789abcde" + for c in value: + case c + of '\b': + addPrefixSlash 'b' + # \x08 + of '\t': + addPrefixSlash 't' + # \x09 + of '\n': + addPrefixSlash 'n' + # \x0a + of '\f': + addPrefixSlash 'f' + # \x0c + of '\r': + addPrefixSlash 'r' + # \x0d + of '"': + addPrefixSlash '\"' + of '\\': + addPrefixSlash '\\' + of '\x00' .. '\x07', '\x0b', '\x0e' .. '\x1f': + res.add "\\u00" + res.add hexChars[(uint8(c) shr 4) and 0x0f] + res.add hexChars[uint8(c) and 0x0f] + else: + res.add c + +proc hasIndefLen(reader: var CborReader): bool {.raises: [IOError].} = + doAssert reader.parser.stream.readable + let x = reader.parser.stream.peek() + (x and 0b0001_1111) == cborMinorIndef + +# TODO: chunks show as concatenated string/bytes; +# PR https://github.com/vacp2p/nim-cbor-serialization/pull/17 +# gives access to "prelude" which can be used to anotate the chunk split + +# https://www.rfc-editor.org/rfc/rfc8949.html#section-8 +# https://www.rfc-editor.org/rfc/rfc8610#appendix-G +proc toEdnImpl( + reader: var CborReader +): string {.raises: [IOError, SerializationError].} = + mixin readValue + template p(): untyped = + reader.parser + + result = "" + case p.cborKind() + of CborValueKind.Bytes: + result.add "h'" + result.add toHex(reader.readValue(seq[byte])) + result.add '\'' + of CborValueKind.String: + result.add '"' + result.addEscaped reader.readValue(string) + result.add '"' + of CborValueKind.Unsigned: + result.add $reader.readValue(uint64) + of CborValueKind.Negative: + let val = reader.readValue(CborNumber) + if val.integer == uint64.high: + result.add "-18446744073709551616" + else: + result.add '-' + result.add $(val.integer + 1) + of CborValueKind.Float: + let f = reader.readValue(float64) + case f.classify + of fcNan: + result.add "NaN" + of fcInf: + result.add "Infinity" + of fcNegInf: + result.add "-Infinity" + else: + result.addFloatRoundtrip f + of CborValueKind.Object: + result.add '{' + if reader.hasIndefLen(): + result.add "_ " + var i = 0 + parseObjectCustomKey(reader): + if i > 0: + result.add ", " + result.add toEdnImpl(reader) + result.add ": " + do: + result.add toEdnImpl(reader) + inc i + result.add '}' + of CborValueKind.Array: + result.add '[' + if reader.hasIndefLen(): + result.add "_ " + var i = 0 + parseArray(reader): + if i > 0: + result.add ", " + result.add toEdnImpl(reader) + inc i + result.add ']' + of CborValueKind.Tag: + var tag: uint64 + parseTag(reader, tag): + result.add $tag + result.add '(' + result.add toEdnImpl(reader) + result.add ')' + of CborValueKind.Simple, CborValueKind.Bool, CborValueKind.Null, + CborValueKind.Undefined: + result.add $reader.readValue(CborSimpleValue) + +# CBOR Sequence: https://datatracker.ietf.org/doc/html/rfc8742#section-4.2 +proc toEdnSeqImpl( + reader: var CborReader +): string {.raises: [IOError, SerializationError].} = + result = reader.toEdnImpl() + var i = 0 + while reader.parser.stream.readable(): + result.add ", " + result.add reader.toEdnImpl() + inc i + +proc toEdn*( + cbor: CborBytes, Flavor = DefaultFlavor +): string {.raises: [SerializationError].} = + ## Converts `cbor` content into Diagnostic Notation + var stream = unsafeMemoryInput(seq[byte](cbor)) + var reader = CborReader[Flavor].init(stream) + try: + reader.toEdnSeqImpl() + except IOError: + raiseAssert "memoryOutput is exception-free" diff --git a/cbor_serialization/parser.nim b/cbor_serialization/parser.nim index 1e5fffa..6a83f2e 100644 --- a/cbor_serialization/parser.nim +++ b/cbor_serialization/parser.nim @@ -463,7 +463,7 @@ template parseObject*(r: var CborReader, key: untyped, body: untyped) = template parseObjectWithoutSkip*(r: var CborReader, key: untyped, body: untyped) = parseObject(r.parser, false, key, body) -template parseTag*(p: var CborReader, tag: untyped, body: untyped) = +template parseTag*(r: var CborReader, tag: untyped, body: untyped) = parseTag(r.parser, tag, body) proc parseNumber*( diff --git a/docs/examples/debugging0.nim b/docs/examples/debugging0.nim new file mode 100644 index 0000000..d910a7b --- /dev/null +++ b/docs/examples/debugging0.nim @@ -0,0 +1,25 @@ +# ANCHOR: Import +{.push gcsafe, raises: [].} + +import cbor_serialization, cbor_serialization/edn +# ANCHOR_END: Import + +# ANCHOR: Request +type Request = object + cborrpc: string + `method`: string + params: seq[int] + id: int + +# ANCHOR_END: Request + +# ANCHOR: Encode +let encoded = + Cbor.encode(Request(cborrpc: "2.0", `method`: "subtract", params: @[42, 3], id: 1)) +# ANCHOR_END: Encode + +# ANCHOR: Edn +# Decode the CBOR blob into Diagnostic Notation +doAssert CborBytes(encoded).toEdn() == + """{"cborrpc": "2.0", "method": "subtract", "params": [42, 3], "id": 1}""" +# ANCHOR_END: Edn diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 432241e..521039c 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,6 +4,7 @@ - [Getting started](./getting_started.md) - [Streaming](./streaming.md) +- [Debugging](./debugging.md) - [Reference](./reference.md) # Developer guide diff --git a/docs/src/debugging.md b/docs/src/debugging.md new file mode 100644 index 0000000..10cf5f2 --- /dev/null +++ b/docs/src/debugging.md @@ -0,0 +1,23 @@ +# Debugging + +This section provides an overview of the `cbor_serialization` debugging tools. + +## Diagnostic Notation + +The CBOR diagnostic notation is a human-readable format defined in [RFC8949](https://www.rfc-editor.org/rfc/rfc8949.html#section-8), [RFC8610](https://www.rfc-editor.org/rfc/rfc8610#appendix-G) and [RFC8742](https://datatracker.ietf.org/doc/html/rfc8742#section-4.2). It's based on JSON, but it's not compatible with it. + +The `toEdn` API will decode CBOR bytes (including CBOR Sequences) into a string in diagnostic notation format: + +```nim +{{#include ../examples/debugging0.nim:Edn}} +``` + +Deviation from JSON includes: + +- Non-finite floating-point: `Infinity`, `-Infinity`, `NaN` +- Tags: `tagNumer(tagValue)`, ex: `0("2013-03-21T20:04:00Z")` +- Byte strings: `h'base16Value'`, ex: `h'01020304'` +- Simple: `simple(simpleValue)`, ex: `simple(42)` +- Non-string map keys: `{key: value}`, ex: `{1: 2}`, `{[1]: 2}`, `{{1: 2}: 3}` +- Indefinite length: Undercore + space after `{`, `[`; ex: `{_ "a": "b"}`, `[_ 1, 2]` +- Sequences: Each CBOR is separated by `,`; ex: `1, "a", [2, 3]` diff --git a/docs/src/reference.md b/docs/src/reference.md index c475024..dcdd194 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -171,17 +171,6 @@ proc readValue*(r: var Cbor.Reader, value: var Welder) = value.flags.incl r.parseInt(int).WelderFlag ``` -## Custom Iterators - -Custom iterators provide access to sub-token elements: - -```nim -customIntValueIt(r: var CborReader; body: untyped) -customNumberValueIt(r: var CborReader; body: untyped) -customStringValueIt(r: var CborReader; limit: untyped; body: untyped) -customStringValueIt(r: var CborReader; body: untyped) -``` - ## Convenience Iterators ```nim diff --git a/tests/test_all.nim b/tests/test_all.nim index b830ce5..37635a8 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -11,7 +11,8 @@ import test_spec, test_serialization, test_simple_value, test_cbor_flavor, test_parser, - test_reader, test_writer, test_valueref, test_cbor_raw, test_std, test_overloads + test_reader, test_writer, test_valueref, test_cbor_raw, test_std, test_overloads, + test_edn template importBigints() = import bigints diff --git a/tests/test_edn.nim b/tests/test_edn.nim new file mode 100644 index 0000000..f00879d --- /dev/null +++ b/tests/test_edn.nim @@ -0,0 +1,128 @@ +# cbor-serialization +# Copyright (c) 2025 Status Research & Development GmbH +# Licensed under either of +# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) +# * MIT license ([LICENSE-MIT](LICENSE-MIT)) +# at your option. +# This file may not be copied, modified, or distributed except according to +# those terms. + +import unittest2, ./utils, ../cbor_serialization/edn + +# https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.a +const testCases = [ + ("0x00", "0"), + ("0x01", "1"), + ("0x0a", "10"), + ("0x17", "23"), + ("0x1818", "24"), + ("0x1819", "25"), + ("0x1864", "100"), + ("0x1903e8", "1000"), + ("0x1a000f4240", "1000000"), + ("0x1b000000e8d4a51000", "1000000000000"), + ("0x1bffffffffffffffff", "18446744073709551615"), + #("0xc249010000000000000000", "18446744073709551616"), + ("0xc249010000000000000000", "2(h'010000000000000000')"), + ("0x3bffffffffffffffff", "-18446744073709551616"), + #("0xc349010000000000000000", "-18446744073709551617"), + ("0xc349010000000000000000", "3(h'010000000000000000')"), + ("0x20", "-1"), + ("0x29", "-10"), + ("0x3863", "-100"), + ("0x3903e7", "-1000"), + ("0xf90000", "0.0"), + ("0xf98000", "-0.0"), + ("0xf93c00", "1.0"), + ("0xfb3ff199999999999a", "1.1"), + ("0xf93e00", "1.5"), + ("0xf97bff", "65504.0"), + ("0xfa47c35000", "100000.0"), + ("0xfa7f7fffff", "3.4028234663852886e+38"), + ("0xfb7e37e43c8800759c", "1e+300"), # 1.0e+300 + ("0xf90001", "5.960464477539063e-8"), + ("0xf90400", "0.00006103515625"), + ("0xf9c400", "-4.0"), + ("0xfbc010666666666666", "-4.1"), + ("0xf97c00", "Infinity"), + ("0xf97e00", "NaN"), + ("0xf9fc00", "-Infinity"), + ("0xfa7f800000", "Infinity"), + ("0xfa7fc00000", "NaN"), + ("0xfaff800000", "-Infinity"), + ("0xfb7ff0000000000000", "Infinity"), + ("0xfb7ff8000000000000", "NaN"), + ("0xfbfff0000000000000", "-Infinity"), + ("0xf4", "false"), + ("0xf5", "true"), + ("0xf6", "null"), + ("0xf7", "undefined"), + ("0xf0", "simple(16)"), + ("0xf8ff", "simple(255)"), + ("0xc074323031332d30332d32315432303a30343a30305a", """0("2013-03-21T20:04:00Z")"""), + ("0xc11a514b67b0", "1(1363896240)"), + ("0xc1fb41d452d9ec200000", "1(1363896240.5)"), + ("0xd74401020304", "23(h'01020304')"), + ("0xd818456449455446", "24(h'6449455446')"), + ( + "0xd82076687474703a2f2f7777772e6578616d706c652e636f6d", + """32("http://www.example.com")""", + ), + ("0x40", "h''"), + ("0x4401020304", "h'01020304'"), + ("0x60", "\"\""), + ("0x6161", """"a""""), + ("0x6449455446", """"IETF""""), + ("0x62225c", """"\"\\""""), + ("0x62c3bc", """"ü""""), + ("0x63e6b0b4", """"水""""), + ("0x64f0908591", """"𐅑""""), + ("0x80", "[]"), + ("0x83010203", "[1, 2, 3]"), + ("0x8301820203820405", "[1, [2, 3], [4, 5]]"), + ( + "0x98190102030405060708090a0b0c0d0e0f101112131415161718181819", + "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]", + ), + ("0xa0", "{}"), + ("0xa201020304", "{1: 2, 3: 4}"), + ("0xa26161016162820203", """{"a": 1, "b": [2, 3]}"""), + ("0x826161a161626163", """["a", {"b": "c"}]"""), + ( + "0xa56161614161626142616361436164614461656145", + """{"a": "A", "b": "B", "c": "C", "d": "D", "e": "E"}""", + ), + #("0x5f42010243030405ff", "(_ h'0102', h'030405')"), + #("0x7f657374726561646d696e67ff", """(_ "strea", "ming")"""), + ("0x9fff", "[_ ]"), + ("0x9f018202039f0405ffff", "[_ 1, [2, 3], [_ 4, 5]]"), + ("0x9f01820203820405ff", "[_ 1, [2, 3], [4, 5]]"), + ("0x83018202039f0405ff", "[1, [2, 3], [_ 4, 5]]"), + ("0x83019f0203ff820405", "[1, [_ 2, 3], [4, 5]]"), + ( + "0x9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff", + "[_ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]", + ), + ("0xbf61610161629f0203ffff", """{_ "a": 1, "b": [_ 2, 3]}"""), + ("0x826161bf61626163ff", """["a", {_ "b": "c"}]"""), + ("0xbf6346756ef563416d7421ff", """{_ "Fun": true, "Amt": -2}"""), +] + +suite "Test Diagnostic Notation": + test "spec test cases": + for (cbor, diagnostic) in items(testCases): + check toEdn(CborBytes(cbor.unhex)) == diagnostic + + test "CBOR Sequence simple": + let cbor = "0x1818".unhex & "0x1819".unhex + check toEdn(CborBytes(cbor)) == "24, 25" + + test "CBOR Sequence": + var cborSeq = default(seq[byte]) + var expected = "" + for (cbor, diagnostic) in items(testCases): + cborSeq.add cbor.unhex + if expected.len > 0: + expected.add ", " + expected.add diagnostic + check toEdn(CborBytes(cborSeq)) == expected