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.
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
| Type | CBOR | Payload |
|---|---|---|
| nil, boolean | simple values 22, 20, 21 | |
| integer | major 0/1; tag 2/3 beyond 64 bits | bignum bytes |
| float | major 7 | IEEE 754 double, NaN and infinities included |
| decimal | tag 4 | [exponent, mantissa] |
| rational | tag 30 | [numerator, denominator] |
| string | major 3 | UTF-8 |
| bytes | major 2 | raw bytes, no base64 |
| list | major 4 | items |
| map | major 5 | pairs |
| timestamp | tag 1, integer only | microseconds since 1970 |
| uri | tag 32 | text |
| uuid | tag 37 | 16 raw bytes |
| everything else | private tags 200–214 | see 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
| Tag | Type | Wraps |
|---|---|---|
| 200 | char | the code point, as an integer |
| 201 | symbol | text |
| 202 | keyword | text |
| 203 | tuple | array |
| 204 | array | array |
| 205 | ordered map | map |
| 206 | set | array |
| 207 | sorted set | array, in sorted order |
| 208 | struct | [name, field, …] |
| 209 | date | days since 1970-01-01 |
| 210 | time | microseconds since midnight |
| 211 | datetime | [epoch microseconds, offset minutes] |
| 212 | duration | a field bitmask, then the fields present |
| 213 | regex | [pattern, flags] |
| 214 | custom 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.
| Bit | Duration field | Regex flag |
|---|---|---|
| 0 | years | i |
| 1 | months | m |
| 2 | weeks | s |
| 3 | days | u |
| 4 | hours | x |
| 5 | minutes | f |
| 6 | microseconds (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.
%{
"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