Packages
macula
4.3.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
Current section
Files
src/record/macula_record_cbor.erl
%% @doc Deterministic CBOR encoder/decoder.
%%
%% Implements the subset of RFC 8949 needed by Macula records:
%% unsigned ints, 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>`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>
%% </ul>
-module(macula_record_cbor).
-export([encode/1, decode/1]).
-export_type([value/0]).
-type value() ::
non_neg_integer()
| binary()
| {text, binary()}
| [value()]
| #{value() => value()}
| null
| atom().
-define(MAX_UINT64, 16#FFFFFFFFFFFFFFFF).
%%------------------------------------------------------------------
%% Encode
%%------------------------------------------------------------------
-spec encode(value()) -> binary().
encode(N) when is_integer(N), N >= 0, N =< ?MAX_UINT64 ->
head(0, N);
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. Anything else with major 7 is unsupported.
decode_one(<<7:3, 22:5, R/binary>>) ->
{null, 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};
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}).