Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions cbor_serialization/edn.nim
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion cbor_serialization/parser.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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*(
Expand Down
25 changes: 25 additions & 0 deletions docs/examples/debugging0.nim
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- [Getting started](./getting_started.md)
- [Streaming](./streaming.md)
- [Debugging](./debugging.md)
- [Reference](./reference.md)

# Developer guide
Expand Down
23 changes: 23 additions & 0 deletions docs/src/debugging.md
Original file line number Diff line number Diff line change
@@ -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]`
11 changes: 0 additions & 11 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/test_all.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
128 changes: 128 additions & 0 deletions tests/test_edn.nim
Original file line number Diff line number Diff line change
@@ -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
Loading