Packages

macula

7.0.0
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK โ€” connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src record macula_record_cbor.erl
Raw

src/record/macula_record_cbor.erl

%% @doc Deterministic CBOR encoder/decoder.
%%
%% Implements the subset of RFC 8949 needed by Macula records:
%% unsigned ints, negative ints, floats, byte strings, text strings,
%% arrays, maps, and `null'.
%%
%% Encoding follows RFC 8949 ยง4.2.1 (deterministic):
%% <ul>
%% <li>Smallest length encoding.</li>
%% <li>Definite lengths only (no indefinite items).</li>
%% <li>Map keys sorted by bytewise lexicographic order of their
%% deterministic encoding.</li>
%% </ul>
%%
%% Internal value representation:
%% <ul>
%% <li>`non_neg_integer()' โ€” uint (major 0)</li>
%% <li>`neg_integer()' โ€” negative integer (major 1)</li>
%% <li>`binary()' โ€” byte string (major 2)</li>
%% <li>`{text, binary()}' โ€” UTF-8 text string (major 3)</li>
%% <li>`atom()' โ€” UTF-8 text string (major 3) via
%% `atom_to_binary/1'. Accepted for round-trip robustness:
%% the frame decoder atomizes binary keys via
%% binary_to_existing_atom/1, so a record decoded from
%% the wire and re-encoded for sig verify carries atom keys
%% inside the payload sub-map. Encoding atoms as text
%% reproduces the original wire bytes byte-for-byte (atom
%% names round-trip exactly through utf8). `null' has a
%% dedicated clause and is NOT routed here.</li>
%% <li>`[value()]' โ€” array (major 4)</li>
%% <li>`#{value() => value()}' โ€” map (major 5)</li>
%% <li>`null' โ€” simple null (major 7, value 22)</li>
%% <li>`float()' โ€” IEEE 754 binary64 (major 7, value 27)</li>
%% </ul>
-module(macula_record_cbor).
-export([encode/1, decode/1, is_encodable_int/1]).
-export_type([value/0]).
-type value() ::
integer()
| float()
| binary()
| {text, binary()}
| [value()]
| #{value() => value()}
| null
| atom().
-define(MAX_UINT64, 16#FFFFFFFFFFFFFFFF).
%%------------------------------------------------------------------
%% Encode
%%------------------------------------------------------------------
%% @doc Can this integer be rendered as major 0 / major 1?
%%
%% Exported so callers that must decide admissibility BEFORE encoding
%% (see `macula_frame:check_payload/1') can ask rather than restate the
%% bound. A bignum past 64 bits matches no `encode/1' clause and would
%% otherwise crash whichever process happens to be encoding.
-spec is_encodable_int(integer()) -> boolean().
is_encodable_int(N) when is_integer(N), N >= 0, N =< ?MAX_UINT64 ->
true;
is_encodable_int(N) when is_integer(N), N < 0, N >= -(?MAX_UINT64 + 1) ->
true;
is_encodable_int(N) when is_integer(N) ->
false.
-spec encode(value()) -> binary().
encode(N) when is_integer(N), N >= 0, N =< ?MAX_UINT64 ->
head(0, N);
%% Negative integers โ€” CBOR major type 1. The encoded count is `-1 - N'
%% (so -1 -> head(1, 0), -11 -> head(1, 10)). Bounded the same as the
%% positive branch but mirrored on the negative side.
encode(N) when is_integer(N), N < 0, N >= -(?MAX_UINT64 + 1) ->
head(1, -1 - N);
%% Floats: major 7, additional info 27, IEEE 754 binary64 (ยง3.3).
%%
%% ALWAYS binary64, never the shorter half or single forms. Determinism
%% requires one canonical encoding per value, not the shortest one, and
%% picking "shortest that round-trips" would make the signed bytes depend on
%% a width-selection rule that every peer must reproduce bit-for-bit. Nine
%% bytes per float is the price of not having that argument.
%%
%% An Erlang float is always finite (arithmetic raises badarith rather than
%% producing NaN or infinity), so there is no NaN canonicalisation question
%% on the encode side.
encode(F) when is_float(F) ->
<<7:3, 27:5, F:64/float>>;
encode({text, B}) when is_binary(B) ->
<<(head(3, byte_size(B)))/binary, B/binary>>;
encode(B) when is_binary(B) ->
<<(head(2, byte_size(B)))/binary, B/binary>>;
encode(L) when is_list(L) ->
encode_array(L);
encode(M) when is_map(M) ->
encode_map(M);
encode(null) ->
<<16#F6>>;
%% Atoms encode as their UTF-8 name as a major-3 text string. This
%% makes the codec self-healing across the wire round-trip: the frame
%% decoder atomizes binary keys via binary_to_existing_atom/1, and
%% records re-encoded for signature verify hit those atoms here. By
%% the symmetry of atom_to_binary/1 / binary_to_existing_atom/1,
%% the resulting wire bytes match the original record exactly.
encode(A) when is_atom(A) ->
Bin = atom_to_binary(A, utf8),
<<(head(3, byte_size(Bin)))/binary, Bin/binary>>.
encode_array(L) ->
Body = << <<(encode(E))/binary>> || E <- L >>,
<<(head(4, length(L)))/binary, Body/binary>>.
encode_map(M) ->
%% Encode each k/v independently, then sort by encoded key bytes
%% (Erlang binary comparison is bytewise โ€” exactly what the spec wants).
Pairs = [ {encode(K), encode(V)} || {K, V} <- maps:to_list(M) ],
Sorted = lists:sort(Pairs),
Body = << <<K/binary, V/binary>> || {K, V} <- Sorted >>,
<<(head(5, maps:size(M)))/binary, Body/binary>>.
%% Type byte + length prefix using the smallest encoding.
head(MT, N) when N =< 23 ->
<<MT:3, N:5>>;
head(MT, N) when N =< 16#FF ->
<<MT:3, 24:5, N:8>>;
head(MT, N) when N =< 16#FFFF ->
<<MT:3, 25:5, N:16>>;
head(MT, N) when N =< 16#FFFFFFFF ->
<<MT:3, 26:5, N:32>>;
head(MT, N) when N =< ?MAX_UINT64 ->
<<MT:3, 27:5, N:64>>.
%%------------------------------------------------------------------
%% Decode
%%------------------------------------------------------------------
-spec decode(binary()) -> value().
decode(Bin) when is_binary(Bin) ->
{V, <<>>} = decode_one(Bin),
V.
%% Major 7, value 22 = null.
decode_one(<<7:3, 22:5, R/binary>>) ->
{null, R};
%% Floats. We only ever EMIT binary64, but a conforming peer may send the
%% shorter forms, so all three are accepted. NaN and the infinities have no
%% Erlang float representation and match no clause here; the frame decoder
%% already turns that into `bad_frame' rather than a crash, which is the
%% right answer for a value this codec cannot faithfully hand to a caller.
decode_one(<<7:3, 25:5, Half:16/bitstring, R/binary>>) ->
{half_to_float(Half), R};
decode_one(<<7:3, 26:5, F:32/float, R/binary>>) ->
{F, R};
decode_one(<<7:3, 27:5, F:64/float, R/binary>>) ->
{F, R};
decode_one(<<MT:3, AI:5, Rest/binary>>) ->
{N, R} = decode_count(AI, Rest),
decode_value(MT, N, R).
decode_count(AI, R) when AI =< 23 -> {AI, R};
decode_count(24, <<N, R/binary>>) -> {N, R};
decode_count(25, <<N:16, R/binary>>) -> {N, R};
decode_count(26, <<N:32, R/binary>>) -> {N, R};
decode_count(27, <<N:64, R/binary>>) -> {N, R}.
decode_value(0, N, R) ->
{N, R};
%% Negative integer (major 1) โ€” the encoded count `N' represents the
%% integer `-1 - N'.
decode_value(1, N, R) ->
{-1 - N, R};
decode_value(2, Len, R) ->
<<B:Len/binary, Rest/binary>> = R,
{B, Rest};
decode_value(3, Len, R) ->
<<B:Len/binary, Rest/binary>> = R,
{{text, B}, Rest};
decode_value(4, Len, R) ->
decode_array(Len, R, []);
decode_value(5, Len, R) ->
decode_map(Len, R, #{}).
decode_array(0, R, Acc) ->
{lists:reverse(Acc), R};
decode_array(N, R, Acc) ->
{V, R1} = decode_one(R),
decode_array(N - 1, R1, [V | Acc]).
decode_map(0, R, Acc) ->
{Acc, R};
decode_map(N, R, Acc) ->
{K, R1} = decode_one(R),
{V, R2} = decode_one(R1),
decode_map(N - 1, R2, Acc#{K => V}).
%% IEEE 754 binary16 -> Erlang float. Subnormals and zero fall out of the
%% same arithmetic; exponent 31 is NaN/infinity, which has no Erlang
%% representation, so it is left to fail the match above.
half_to_float(<<S:1, 0:5, Frac:10>>) ->
sign(S) * math:pow(2, -14) * (Frac / 1024);
half_to_float(<<S:1, Exp:5, Frac:10>>) when Exp < 31 ->
sign(S) * math:pow(2, Exp - 15) * (1 + Frac / 1024).
sign(0) -> 1.0;
sign(1) -> -1.0.