Skip to main content

core.encoding

Layer 4.7 — Data encoding and serialization

Pure-Verum encoders and decoders for every wire format production code typically reaches for. The module covers textual encodings (JSON, Base64/32/58, hex, PEM), binary wire formats (CBOR, MessagePack, DER, varint), and the interoperability-critical "canonical" / "pointer" sub-formats layered on top.

Submodules​

SubmodulePurposeReference
encoding.jsonJSON parsing into JsonValue and serializationRFC 8259
encoding.jcsJSON Canonicalization Scheme (signing-deterministic)RFC 8785
encoding.json_pointerPath syntax for JSON sub-value lookupRFC 6901
encoding.cborConcise Binary Object RepresentationRFC 8949
encoding.msgpackMessagePack binary encoder/decoderspec.md
encoding.base64Base64 (+ URL-safe variant)RFC 4648 §4 / §5
encoding.base32Base32 (case-insensitive, trailing-bits validation)RFC 4648 §6
encoding.base58Bitcoin-style Base58 + Base58CheckSatoshi
encoding.hexLowercase hex encoder + decoderRFC 4648 §8
encoding.pemPEM textual envelope for DER blobsRFC 7468
encoding.varintSQLite-style 1–9 byte varintssqlite.org
encoding.derASN.1 Distinguished Encoding RulesX.690

Two sibling varint encodings live in the stdlib next to the wire formats that need them and are not duplicated here: LEB128 lives at core.protobuf.wire (Protocol Buffers style, little-endian) and QUIC varint lives at core.net.http3.frame (1/2/4/8-byte length-prefixed, RFC 9000 §16).

json​

mount core.encoding.json.*;

public type JsonValue is
JsonNull
| JsonBool(Bool)
| JsonInt(Int)
| JsonFloat(Float)
| JsonString(Text)
| JsonArray(List<JsonValue>)
| JsonObject(Map<Text, JsonValue>);

public type JsonMap is Map<Text, JsonValue>;

public fn parse(source: &Text) -> Result<JsonValue, JsonError>;
public fn parse_strict(source: &Text, max_bytes: Int) -> Result<JsonValue, JsonError>;
public fn parse_value(source: &Text) -> Result<JsonValue, JsonError>;
public fn decode_bytes(source: &[Byte]) -> Result<JsonValue, JsonError>;
public fn stringify(value: &JsonValue) -> Text;
public fn stringify_pretty(value: &JsonValue) -> Text;

parse reads a complete document and rejects trailing content, comments, trailing commas and single-quoted strings. Repeated object keys retain the last value. decode is an alias for parse; encode is an alias for stringify.

parse_strict adds a caller-supplied byte limit and rejects repeated decoded keys with DuplicateKey, including spellings such as "name" and "\u006eame" in the same object. It applies that rule to nested objects; the same key in separate objects is allowed. The limit counts the entire UTF-8 source, including whitespace and escapes, before constructing JSON strings or containers. An oversized document returns DocumentTooLarge; a negative limit returns InvalidLimit. A zero limit admits no nonempty document.

Both full-document parsers retain the parser's nesting, string and collection limits. parse_value reads one value and allows trailing content, but returns neither the remaining input nor an offset. It is not an incremental event parser.

Parsing builds an owned value tree. Strings use Text storage; arrays and objects use List and Map storage. The parser has no zero-allocation guarantee. Its result is always JsonValue: a record annotation or @derive(Deserialize) does not change the parser's return type. See the JSON cookbook for explicit field validation.

decode_bytes constructs text with Text.from_utf8_lossy before calling parse. When invalid UTF-8 must be rejected, validate the bytes with Text.from_utf8 and handle that error before calling parse_strict.

stringify builds compact output in a Text; stringify_pretty uses fixed two-space indentation. Neither accepts a writer sink or formatting options. Object members follow map iteration order, without canonical key sorting. Check floating-point values for finiteness before serialization; the serializer returns Text, without a validation-error result.

JsonError exposes four public diagnostic fields: kind: JsonErrorKind, line: Int, column: Int and message: Text. Callers can construct and update diagnostics; the record alone does not establish that a parser produced it. Errors returned by the parser use one-based positions, with columns counted in bytes. Parser errors describe JSON syntax and limits; application field validation needs its own error policy. The public signatures and parser behavior are defined in core/encoding/json.vr.

base64​

mount core.encoding.base64.*;

public fn encode(input: &[Byte]) -> Text; // RFC 4648 §4
public fn encode_url(input: &[Byte]) -> Text; // §5 URL-safe alphabet
public fn decode(input: &Text) -> Result<List<Byte>, Base64Error>;
public fn decode_url(input: &Text) -> Result<List<Byte>, Base64Error>;

Default encoder emits = padding. URL-safe variant uses -_ instead of +/ and omits padding (web-friendly).

Canonicality​

Both decoders enforce the RFC 4648 §3.5 trailing-bits rule: unused low bits of the final character must be zero. Both also reject non-canonical padding shapes — multiple distinct inputs decoding to the same bytes is a malleability surface for JWT / capability-token equality checks, so the decoders treat non-canonical padding as Base64Error.InvalidPadding:

n % 4Standard decodeURL-safe decode_url
0accepted (full block)accepted
1InvalidLengthInvalidLength (dangling tail)
2requires == (AABB==); rejects AAaccepts AA (no pad) or AA==; rejects AA= (mid-shape)
3requires = (AAA=); rejects AAAaccepts AAA (no pad) or AAA=; rejects AAA==

URL-safe input that mixes shapes (e.g. AA, AA=, AA== all decoding to the same byte) is now rejected with InvalidPadding whenever any padding is present — pre-fix, the URL-safe decoder accepted all three.

hex​

mount core.encoding.hex.*;

public fn encode(input: &[Byte]) -> Text; // lowercase
public fn decode(input: &Text) -> Result<List<Byte>, HexError>;

Case-insensitive decoder, lowercase encoder (matches RFC 4648 §8).

varint — SQLite-style​

mount core.encoding.varint.*;

public fn sqlite_encoded_len(value: Int64) -> Int;
public fn sqlite_encode_into(out: &mut List<Byte>, value: Int64) -> Int;
public fn sqlite_encode(value: Int64) -> List<Byte>;
public fn sqlite_decode(buf: &[Byte], start: Int) -> Result<(Int64, Int), VarintError>;
public fn sqlite_decode_first(buf: &[Byte]) -> Result<(Int64, Int), VarintError>;
public fn sqlite_skip(buf: &[Byte], start: Int) -> Result<Int, VarintError>;

Big-endian 1–9 byte form used throughout the SQLite file format (see sqlite.org/fileformat2.html under "Varint"). Bytes 1–8 carry 7 data bits + continuation bit in the MSB; byte 9 (if present) holds the remaining 8 bits with no continuation. The full signed i64 range is covered — negative values use the 9-byte form because their top bit is always set, unlike LEB128 which zig-zags first.

Errors (VarintErrorKind):

KindWhen
TruncatedInput buffer ends mid-varint or before byte 9
OverflowReserved for future extensions; no i64 input triggers it

jcs — JSON Canonicalization Scheme (RFC 8785)​

mount core.encoding.jcs.{canonicalize_value, canonicalize_str};

public fn canonicalize_value(v: &JsonValue) -> Result<Text, JcsError>;
public fn canonicalize_str(input: &Text) -> Result<Text, JcsError>;

The deterministic JSON serialisation signing workflows require — byte-identical output regardless of map-key insertion order so signer and verifier produce the same input.

Rule§Effect
Keys sorted by UTF-16 code unit3.2.3{"b":1,"a":2} → {"a":2,"b":1}
No whitespace3.2.4compact form
Integer-valued floats collapse3.2.2.11.0 → 1
Minimal string escapes3.2.1only ", \, U+0000..U+001F

Used by JWS-on-JSON, W3C Verifiable Credentials, DIDs, Matrix room events, signed build manifests.

let canonical = jcs.canonicalize_str(&raw_json)?;
let digest = sha256(canonical.as_bytes());

Astral-plane characters (U+10000+) are decoded into UTF-16 surrogate pairs before comparison so the sort order matches the JSON.stringify(sorted) form used by other JCS implementations. NaN / ±Inf values return UnsupportedValue — JSON cannot represent them.

json_pointer — JSON Pointer (RFC 6901)​

mount core.encoding.json_pointer.{
JsonPointer, parse, format_json_pointer, resolve,
};

public fn parse(s: &Text) -> Result<JsonPointer, JsonPointerError>;
public fn format_json_pointer(p: &JsonPointer) -> Text;
public fn resolve(p: &JsonPointer, doc: &JsonValue) -> Maybe<JsonValue>;

Path syntax for referencing a specific sub-value inside a JSON document. The building block behind JSON Patch (RFC 6902), OpenAPI $ref, JSON Schema traversal, and CRDT operational transforms.

TokenExampleMeaning
""rootwhole document
"/foo"document["foo"]property by name
"/foo/0"document["foo"][0]array index
"/a~1b"document["a/b"]/ escaped
"/m~0n"document["m~n"]~ escaped

Builder API for programmatic construction:

let p = JsonPointer.root()
.push(Text.from("users"))
.push(Text.from("0"))
.push(Text.from("name"));
let s = json_pointer.format_json_pointer(&p); // "/users/0/name"

cbor — Concise Binary Object Representation (RFC 8949)​

mount core.encoding.cbor.{CborValue, encode, decode, encode_canonical};

public fn encode(v: &CborValue) -> List<Byte>;
public fn decode(bytes: &[Byte]) -> Result<CborValue, CborError>;
public fn encode_canonical(v: &CborValue) -> List<Byte>; // §4.2

Compact self-describing binary format used by COSE (RFC 9052), CWT (RFC 8392), WebAuthn attestation, IoT / CoAP payloads, and Verum's own stdlib disk-cache header.

Type coverage: unsigned/negative ints, byte strings, text strings, arrays, maps, tagged values, bool/null/undefined, half/single/double-precision floats. Decoder accepts definite and indefinite-length forms; encoder emits only definite (per §4.1 recommendation). f16/f32 payloads widen to f64 via bit- exact manual conversion — no float-cast intrinsic dependency.

encode_canonical sorts map keys by encoded byte-lex order for deterministic signing (the form required by COSE).

msgpack — MessagePack​

mount core.encoding.msgpack.{MsgPackValue, encode, decode};

Binary alternative to JSON used by Redis RESP3, Pinterest MySQL adapter, PyPy, Erlang / Ruby / Python msgpack libraries. Full type coverage: nil / bool / int (u64 / i64 range with smallest- container encoding) / float32 / float64 / str / bin / array / map / ext. Encoder auto-selects the most compact wire form for each value; decoder handles every fixed + 8/16/32 length variant.

Nesting guarded at MAX_MSGPACK_NESTING = 128; pathological inputs return NestingTooDeep rather than overflowing the stack. Trailing bytes after a complete value return TrailingBytes — no silent partial-parse.

base32 — RFC 4648 §6 / §7​

mount core.encoding.base32.{
encode, decode, decode_no_pad, // §6 standard alphabet
encode_hex, decode_hex, decode_hex_no_pad, // §7 ExtendedHex alphabet
};

Standard alphabet A-Z + 2-7, case-insensitive on decode, = padded to a multiple of 8 chars. ExtendedHex alphabet 0-9 + A-V (used by NSEC3 hashed-owner names — preserves sort order with the underlying bytes).

Canonicality​

Trailing-bits validation : unused low bits of the last quintet MUST be zero (non-zero → TrailingBits). On top of that, the decoders reject non-canonical pad counts. RFC 4648 §6 fixes pad ∈ {0, 1, 3, 4, 6} (encoding 5 / 4 / 3 / 2 / 1 bytes); other counts would let an attacker forge multiple distinct encodings of the same TOTP secret / onion address by toggling the unused real chars. The decoders surface those as Base32Error.InvalidLength(pad):

padValidBytes encoded
0yes5 (full block)
1yes4
2nonon-canonical
3yes3
4yes2
5nonon-canonical
6yes1
7, 8nonon-canonical

decode_no_pad / decode_hex_no_pad accept QR-code-style inputs that drop padding — used for TOTP secret sharing (Google Authenticator). The synthesised pad always lands in the canonical set, so the same gate applies.

base58 — Bitcoin-style​

mount core.encoding.base58.{
encode, decode, encode_check, decode_check, BASE58_ALPHABET,
};

Alphabet 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz (omits 0, O, I, l to avoid human-confusion during copy- paste). Zero-prefix bytes map to leading 1s (the classic Bitcoin rule). encode_check / decode_check append a 4-byte double-SHA256 suffix — a one-character flip has probability 2^-32 of passing.

Used by Bitcoin legacy addresses (P2PKH / P2SH), Solana public keys, Stellar StrKey, IPFS v0 CIDs, Monero subaddresses.

pem — RFC 7468​

mount core.encoding.pem.{
PemBlock, encode_block, encode_bundle, decode_one, decode_all,
};

Textual envelope for DER blobs. Label-agnostic — callers dispatch on block.label to pick the parser ("CERTIFICATE", "PRIVATE KEY", "RSA PRIVATE KEY", "CERTIFICATE REQUEST", "X509 CRL", etc.).

let text = pem.encode_block("CERTIFICATE", der.as_slice());
let blocks = pem.decode_all(&text)?; // concatenated-bundle aware

Line-wraps at 64 base64 chars per §2. Preamble text before the first -----BEGIN is ignored per §2 (the openssl x509 -text headers pass through). Label-mismatch on begin/end returns MismatchedLabels — the RFC-required rejection.

Example — JSON round-trip​

mount core.encoding.json.{parse, stringify, JsonValue, JsonError};

fn round_trip() -> Result<Text, JsonError> {
let raw = "{\"id\": 42, \"tags\": [\"a\", \"b\"]}";
let value = parse(&raw)?;
Result.Ok(stringify(&value))
}

Example — bearer token encoding​

mount core.encoding.base64.encode_url;
mount core.hash.crypto.sha256.Sha256;

fn bearer_from_secret(secret: &[Byte]) -> Text {
let digest = Sha256.digest(secret);
encode_url(&digest)
}