DrinkMeAlice. Json
(drink_me_alice v0.1.0)
Copy Markdown
Bounded decoder/encoder boundary for SurrealDB JSON-RPC payloads.
Surreal replies are
ordinary JSON (floats may appear, integer tokens may exceed i64, object
key order is unspecified), so this decoder runs OTP :json.decode/3
behind explicit structural and decoded-term limits:
- input bytes bounded by the caller (the WebSocket frame guard already
caps a message at
max_bytes, default 1048576); checked again here so the bound does not depend on transport state; - container depth <= 16 (RPC envelopes plus one level of record content stay far below this);
- object keys <= 128 per object, duplicate decoded keys rejected;
- aggregate document elements <= 65536;
- each decoded string <= 65536 bytes and valid UTF-8;
- integer tokens <= 40 digits excluding the optional sign (Erlang integers are exact at any width; the bound stops digit-string amplification);
- float tokens <= 64 bytes, decoded with
Floatsemantics; the dialect may legally carry floats, so they are accepted and counted rather than rejected by shape.
Two enforcement layers, honestly labeled:
preflight/1— a single linear, allocation-free scan over the raw bytes before:json.decoderuns. It counts every string literal's DECODED UTF-8 size (escape-aware:\uXXXXpairs count at their recombined width, so a 40 KiB value of\"escapes — whose raw span doubles its decoded size — passes while an oversized decoded value is rejected) and container nesting, rejecting violations before OTP has allocated anything for them. This is the pre-allocation bound — the string callback below is only a post-construction backstop, and is documented as such.- Decode callbacks — container start/push and every scalar enforce
the bounds while decoding, aborting before an unbounded term is
built.
term_words/1and process-memory measurements in the tests supply the decode-amplification evidence the card requires.
Encoding (encode/1) accepts only canonical terms under the same
structural bounds, checked before OTP :json.encode/1 runs.
Summary
Functions
The declared structural/decoded-term bounds for this profile.
Decode one Surreal-dialect JSON document under the bounded profile.
Encode an outbound message. Only canonical terms travel: string-keyed
maps, lists, valid UTF-8 binaries, i64 integers, booleans and nil —
no floats, atoms or structs (send those as strings with a SurrealQL
cast). The decode bounds apply (depth, keys, string bytes, elements),
checked before OTP :json.encode/1 runs; output is at most
1048576 bytes.
Configured input byte bound (the 1 MiB message bound).
Lexical pre-pass over raw input before :json.decode allocates.
Returns :ok or {:error, %Error{}}. Enforces, allocation-free
Heap size of a decoded term in machine words
(:erts_debug.flat_size, 8 bytes/word on this target). Honest scope:
this counts heap words only — off-heap binary bytes (>64B
refc binaries) are excluded by flat_size. RSS/binary-allocation
evidence is a separate Q01 measurement; this helper must not be cited
as covering it.
Types
Functions
@spec bounds() :: map()
The declared structural/decoded-term bounds for this profile.
@spec decode(binary()) :: {:ok, json()} | {:error, DrinkMeAlice.Error.t()}
Decode one Surreal-dialect JSON document under the bounded profile.
{:ok, term} | {:error, %Error{}} — malformed_message for syntax
violations, bounded_limit_exceeded for bound breaches, both with a
bounded tag and never an inspected input fragment.
@spec encode(term()) :: {:ok, binary()} | {:error, DrinkMeAlice.Error.t()}
Encode an outbound message. Only canonical terms travel: string-keyed
maps, lists, valid UTF-8 binaries, i64 integers, booleans and nil —
no floats, atoms or structs (send those as strings with a SurrealQL
cast). The decode bounds apply (depth, keys, string bytes, elements),
checked before OTP :json.encode/1 runs; output is at most
1048576 bytes.
@spec max_bytes() :: pos_integer()
Configured input byte bound (the 1 MiB message bound).
@spec preflight(binary()) :: :ok | {:error, DrinkMeAlice.Error.t()}
Lexical pre-pass over raw input before :json.decode allocates.
Returns :ok or {:error, %Error{}}. Enforces, allocation-free:
- every string literal's decoded UTF-8 size <= 65536 bytes —
counted while scanning, so a 40 KiB value of
\"escapes (whose raw span is double its decoded size) passes while an oversized decoded value is rejected before OTP allocates it. Escape widths:\X→ 1 byte;\uXXXX→ the code point's UTF-8 width (1–3), with a high surrogate held pending so a\uD800..\uDBFF+\uDC00..\uDFFFpair counts exactly 4. An unterminated string keeps counting to end of input — oversized decoded content is rejected at the bound even though the malformed literal would fail decode anyway; - container nesting depth <= 16.
This is a bound check, not a validator — malformed JSON still reaches the decoder, which rejects it through the same typed error surface.
@spec term_words(term()) :: non_neg_integer()
Heap size of a decoded term in machine words
(:erts_debug.flat_size, 8 bytes/word on this target). Honest scope:
this counts heap words only — off-heap binary bytes (>64B
refc binaries) are excluded by flat_size. RSS/binary-allocation
evidence is a separate Q01 measurement; this helper must not be cited
as covering it.