joetjen.net
EN DE
Data format

The binary format

.dxnb is DXN encoded as CBOR (RFC 8949): a three-byte envelope, then one self-describing CBOR item. Types CBOR already has use its major types or registered tags; the rest use a small block of private tags.

Format 1.0

The envelope

Every .dxnb file starts with the bytes 44 58 — the letters DX — and a version byte, currently 01. The envelope is mandatory, unlike the text header: it costs three bytes and lets tools recognise the file by its first bytes. No end marker follows; a CBOR item knows where it ends.

[1 2 3]
44 58 01        "DX", version 1
83 01 02 03     array(3): 1, 2, 3

Comments and discarded values have no binary form: they are gone before there is a value to encode.

Type mapping

TypeCBORPayload
nil, booleansimple values 22, 20, 21
integermajor 0/1; tag 2/3 beyond 64 bitsbignum bytes
floatmajor 7IEEE 754 double, NaN and infinities included
decimaltag 4[exponent, mantissa]
rationaltag 30[numerator, denominator]
stringmajor 3UTF-8
bytesmajor 2raw bytes, no base64
listmajor 4items
mapmajor 5pairs
timestamptag 1, integer onlymicroseconds since 1970
uritag 32text
uuidtag 3716 raw bytes
everything elseprivate tags 200–214see below

Two choices here exist to keep values exact. Integers past 64 bits must use the bignum tags, never a float. Timestamps must use the integer form of tag 1, never the float form, which loses microseconds at today’s dates.

Private tags

TagTypeWraps
200charthe code point, as an integer
201symboltext
202keywordtext
203tuplearray
204arrayarray
205ordered mapmap
206setarray
207sorted setarray, in sorted order
208struct[name, field, …]
209datedays since 1970-01-01
210timemicroseconds since midnight
211datetime[epoch microseconds, offset minutes]
212durationa field bitmask, then the fields present
213regex[pattern, flags]
214custom tag[name, value]

The block sits in CBOR’s one-extra-byte range, clear of the busy low numbers. It is not registered with IANA, so it is collision-free only among DXN documents.

Structs are always positional in binary — [name, field-1, field-2, …] — because field names only help human readers. The keyed text form exists for them alone.

Bit layouts

A duration’s bitmask marks which fields follow, lowest bit first; each present field follows as a signed integer, in bit order. A regex carries its flags as one byte in the same way. Bit 7 is reserved in both and must be 0.

BitDuration fieldRegex flag
0yearsi
1monthsm
2weekss
3daysu
4hoursx
5minutesf
6microseconds (seconds included)r

Byte by byte

Each dump below is what dextrin writes for the text above it.

%{x: 1}
44 58 01        envelope
A1              map(1)
D8 CA 61 78     tag 202 (keyword) "x"
01              1
19.99M
44 58 01        envelope
C4 82           tag 4 (decimal), array(2)
21              exponent -2
19 07 CF        mantissa 1999
%Point[1, 2]
44 58 01                  envelope
D8 D0 83                  tag 208 (struct), array(3)
65 50 6F 69 6E 74         "Point"
01 02                     1, 2
@duration "P1Y2M10D"
44 58 01        envelope
D8 D4 84        tag 212 (duration), array(4)
0B              bitmask 0000 1011: years, months, days
01 02 0A        1, 2, 10
~U[2024-01-01 12:30:00Z]
44 58 01                        envelope
C1                              tag 1 (timestamp)
1B 00 06 0D E1 8A 56 A2 00      1704112200000000 µs
~r/^a+$/i
44 58 01        envelope
D8 D5 82        tag 213 (regex), array(2)
64 5E 61 2B 24  "^a+$"
01              flags 0000 0001: i

Value sharing

Two optional CBOR extensions shrink documents that repeat themselves. Encoders may use them; decoders must accept them.

  • String references — tag 256 opens a scope, tag 25 refers to the n-th string already seen in it. Meant for repeated symbols, keywords and struct names.
  • Shared values — tag 28 marks an item as shareable, tag 29 refers to a marked item by index. Works for any value: a map, a list, a struct.

Sharing only saves bytes; it never creates identity. A decoder hands back an independent copy for every reference, so the result is equal to a document that wrote everything out. A reference to an item not yet seen, or one that would form a cycle, makes the document invalid: .dxnb values are always trees.

One address used three times — 129 bytes, 75 with sharing
%{
  "hq"       => %{"street" => "1 Main St", "city" => "Springfield"}
  "billing"  => %{"street" => "1 Main St", "city" => "Springfield"}
  "shipping" => %{"street" => "1 Main St", "city" => "Springfield"}
}
44 58 01                    envelope
A3                          map(3)
67 "billing"
D8 1C A2 …                  tag 28: the address map, marked (its strings too)
62 "hq"
D8 1D 04                    tag 29: marked item 4 — the map
68 "shipping"
D8 1D 04                    tag 29: marked item 4