-module(aws@internal@codec@cbor).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/codec/cbor.gleam").
-export([encode/1, decode/1, decode_value/1, get_field/2]).
-export_type([value/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" Minimal CBOR (RFC 8949) codec for the rpcv2Cbor protocol.\n"
"\n"
" AWS's rpcv2Cbor wire form is the canonical / deterministic\n"
" CBOR subset: definite-length arrays + maps, no tags, no\n"
" indefinite-length items, sort keys lexicographically when\n"
" encoding maps. The decoder is more permissive — it accepts\n"
" indefinite-length items too because real services have been\n"
" observed shipping them.\n"
"\n"
" Major types this implementation covers:\n"
" * 0 unsigned int — 1/2/3/5/9-byte head encoding\n"
" * 1 negative int — 1/2/3/5/9-byte head\n"
" * 2 byte string — length-prefixed\n"
" * 3 text string — length-prefixed UTF-8\n"
" * 4 array — definite-length on encode, both on decode\n"
" * 5 map — definite-length on encode, both on decode\n"
" * 7 simple values + floats — false/true/null/float64\n"
"\n"
" Not yet covered: tags (major type 6), `undefined` (simple 23),\n"
" half-float (0xF9), 32-bit float (0xFA). Half- and 32-bit floats\n"
" would surface only from CBOR senders explicitly downcasting; the\n"
" AWS rpcv2Cbor services we've seen always send float64. Tags are\n"
" reserved for date-time / bignum encodings the rpcv2Cbor spec\n"
" excludes from request/response bodies.\n"
).
-type value() :: {c_int, integer()} |
{c_float, float()} |
{c_bool, boolean()} |
c_null |
{c_string, binary()} |
{c_bytes, bitstring()} |
{c_list, list(value())} |
{c_map, list({value(), value()})}.
-file("src/aws/internal/codec/cbor.gleam", 110).
?DOC(
" Build a CBOR head byte sequence for a given `major_type`\n"
" (0-7) and `n` value following it. Uses the shortest possible\n"
" encoding: in-byte for n < 24, then 1, 2, 4, or 8 trailing\n"
" bytes for the larger ranges. Callers always pass `n >= 0` —\n"
" they precompute the unsigned magnitude (negative ints\n"
" transform to `-1 - n`, lengths are naturally unsigned).\n"
).
-spec encode_head(integer(), integer()) -> bitstring().
encode_head(Major_type, N) ->
Mt = Major_type * 32,
case N of
_ when N < 24 ->
<<((Mt + N))>>;
_ when N < 256 ->
<<((Mt + 24)), N>>;
_ when N < 65536 ->
<<((Mt + 25)), N:16/big>>;
_ when N < 4294967296 ->
<<((Mt + 26)), N:32/big>>;
_ ->
<<((Mt + 27)), N:64/big>>
end.
-file("src/aws/internal/codec/cbor.gleam", 125).
-spec compare_bytewise(bitstring(), bitstring()) -> gleam@order:order().
compare_bytewise(A, B) ->
case {A, B} of
{<<>>, <<>>} ->
eq;
{<<>>, _} ->
lt;
{_, <<>>} ->
gt;
{<<X, Ar/bitstring>>, <<Y, Br/bitstring>>} ->
case {X, Y} of
{_, _} when X < Y ->
lt;
{_, _} when X > Y ->
gt;
{_, _} ->
compare_bytewise(Ar, Br)
end;
{_, _} ->
eq
end.
-file("src/aws/internal/codec/cbor.gleam", 78).
-spec encode_bytes(bitstring()) -> bitstring().
encode_bytes(B) ->
gleam@bit_array:append(encode_head(2, erlang:byte_size(B)), B).
-file("src/aws/internal/codec/cbor.gleam", 73).
-spec encode_text(binary()) -> bitstring().
encode_text(S) ->
Bytes = gleam_stdlib:identity(S),
gleam@bit_array:append(encode_head(3, erlang:byte_size(Bytes)), Bytes).
-file("src/aws/internal/codec/cbor.gleam", 121).
-spec encode_float64(float()) -> bitstring().
encode_float64(F) ->
<<16#FB, F:64/float-big>>.
-file("src/aws/internal/codec/cbor.gleam", 66).
-spec encode_int(integer()) -> bitstring().
encode_int(N) ->
case N >= 0 of
true ->
encode_head(0, N);
false ->
encode_head(1, -1 - N)
end.
-file("src/aws/internal/codec/cbor.gleam", 88).
-spec encode_map(list({value(), value()})) -> bitstring().
encode_map(Entries) ->
Sorted = gleam@list:sort(
Entries,
fun(A, B) ->
compare_bytewise(
encode(erlang:element(1, A)),
encode(erlang:element(1, B))
)
end
),
gleam@list:fold(
Sorted,
encode_head(5, erlang:length(Sorted)),
fun(Acc, Entry) ->
K = encode(erlang:element(1, Entry)),
V = encode(erlang:element(2, Entry)),
gleam@bit_array:append(gleam@bit_array:append(Acc, K), V)
end
).
-file("src/aws/internal/codec/cbor.gleam", 82).
-spec encode_list(list(value())) -> bitstring().
encode_list(Items) ->
gleam@list:fold(
Items,
encode_head(4, erlang:length(Items)),
fun(Acc, V) -> gleam@bit_array:append(Acc, encode(V)) end
).
-file("src/aws/internal/codec/cbor.gleam", 52).
?DOC(" Encode a `Value` to its canonical CBOR byte stream.\n").
-spec encode(value()) -> bitstring().
encode(V) ->
case V of
{c_int, N} ->
encode_int(N);
{c_float, F} ->
encode_float64(F);
{c_bool, false} ->
<<16#F4>>;
{c_bool, true} ->
<<16#F5>>;
c_null ->
<<16#F6>>;
{c_string, S} ->
encode_text(S);
{c_bytes, B} ->
encode_bytes(B);
{c_list, Items} ->
encode_list(Items);
{c_map, Entries} ->
encode_map(Entries)
end.
-file("src/aws/internal/codec/cbor.gleam", 261).
-spec decode_simple(integer(), bitstring()) -> {ok, {value(), bitstring()}} |
{error, binary()}.
decode_simple(Info, Rest) ->
case Info of
20 ->
{ok, {{c_bool, false}, Rest}};
21 ->
{ok, {{c_bool, true}, Rest}};
22 ->
{ok, {c_null, Rest}};
23 ->
{ok, {c_null, Rest}};
27 ->
case Rest of
<<F:64/float-big, R/bitstring>> ->
{ok, {{c_float, F}, R}};
_ ->
{error, <<"cbor: truncated float64"/utf8>>}
end;
_ ->
{error, <<"cbor: unsupported simple value"/utf8>>}
end.
-file("src/aws/internal/codec/cbor.gleam", 194).
-spec read_int(integer(), bitstring()) -> {ok, {integer(), bitstring()}} |
{error, binary()}.
read_int(Info, Rest) ->
case Info of
_ when Info < 24 ->
{ok, {Info, Rest}};
24 ->
case Rest of
<<N, R/bitstring>> ->
{ok, {N, R}};
_ ->
{error, <<"cbor: truncated 1-byte length"/utf8>>}
end;
25 ->
case Rest of
<<N@1:16/big, R@1/bitstring>> ->
{ok, {N@1, R@1}};
_ ->
{error, <<"cbor: truncated 2-byte length"/utf8>>}
end;
26 ->
case Rest of
<<N@2:32/big, R@2/bitstring>> ->
{ok, {N@2, R@2}};
_ ->
{error, <<"cbor: truncated 4-byte length"/utf8>>}
end;
27 ->
case Rest of
<<N@3:64/big, R@3/bitstring>> ->
{ok, {N@3, R@3}};
_ ->
{error, <<"cbor: truncated 8-byte length"/utf8>>}
end;
_ ->
{error, <<"cbor: unsupported length info"/utf8>>}
end.
-file("src/aws/internal/codec/cbor.gleam", 221).
-spec take_bytes(bitstring(), integer()) -> {ok, {bitstring(), bitstring()}} |
{error, binary()}.
take_bytes(Bytes, N) ->
Bits = N * 8,
case Bytes of
<<B:Bits/bitstring, R/bitstring>> ->
{ok, {B, R}};
_ ->
{error, <<"cbor: truncated byte string"/utf8>>}
end.
-file("src/aws/internal/codec/cbor.gleam", 246).
-spec decode_map_entries(bitstring(), integer(), list({value(), value()})) -> {ok,
{value(), bitstring()}} |
{error, binary()}.
decode_map_entries(Bytes, Remaining, Acc) ->
case Remaining of
0 ->
{ok, {{c_map, lists:reverse(Acc)}, Bytes}};
_ ->
gleam@result:'try'(
decode(Bytes),
fun(_use0) ->
{K, Rest1} = _use0,
gleam@result:'try'(
decode(Rest1),
fun(_use0@1) ->
{V, Rest2} = _use0@1,
decode_map_entries(
Rest2,
Remaining - 1,
[{K, V} | Acc]
)
end
)
end
)
end.
-file("src/aws/internal/codec/cbor.gleam", 232).
-spec decode_list_items(bitstring(), integer(), list(value())) -> {ok,
{value(), bitstring()}} |
{error, binary()}.
decode_list_items(Bytes, Remaining, Acc) ->
case Remaining of
0 ->
{ok, {{c_list, lists:reverse(Acc)}, Bytes}};
_ ->
gleam@result:'try'(
decode(Bytes),
fun(_use0) ->
{V, Rest} = _use0,
decode_list_items(Rest, Remaining - 1, [V | Acc])
end
)
end.
-file("src/aws/internal/codec/cbor.gleam", 151).
?DOC(
" Decode a CBOR byte stream into a `Value`. Returns the leftover\n"
" bytes on success so callers can decode multiple items from one\n"
" buffer; rpcv2Cbor request bodies are single items, so most\n"
" callers can just drop the leftover.\n"
).
-spec decode(bitstring()) -> {ok, {value(), bitstring()}} | {error, binary()}.
decode(Bytes) ->
case Bytes of
<<Head, Rest/bitstring>> ->
Major = Head div 32,
Info = Head - (Major * 32),
case Major of
0 ->
gleam@result:'try'(
read_int(Info, Rest),
fun(_use0) ->
{N, Rest2} = _use0,
{ok, {{c_int, N}, Rest2}}
end
);
1 ->
gleam@result:'try'(
read_int(Info, Rest),
fun(_use0@1) ->
{N@1, Rest2@1} = _use0@1,
{ok, {{c_int, -1 - N@1}, Rest2@1}}
end
);
2 ->
gleam@result:'try'(
read_int(Info, Rest),
fun(_use0@2) ->
{Len, Rest2@2} = _use0@2,
gleam@result:'try'(
take_bytes(Rest2@2, Len),
fun(_use0@3) ->
{B, Rest3} = _use0@3,
{ok, {{c_bytes, B}, Rest3}}
end
)
end
);
3 ->
gleam@result:'try'(
read_int(Info, Rest),
fun(_use0@4) ->
{Len@1, Rest2@3} = _use0@4,
gleam@result:'try'(
take_bytes(Rest2@3, Len@1),
fun(_use0@5) ->
{B@1, Rest3@1} = _use0@5,
case gleam@bit_array:to_string(B@1) of
{ok, S} ->
{ok, {{c_string, S}, Rest3@1}};
{error, _} ->
{error,
<<"cbor: invalid UTF-8 in text string"/utf8>>}
end
end
)
end
);
4 ->
gleam@result:'try'(
read_int(Info, Rest),
fun(_use0@6) ->
{Len@2, Rest2@4} = _use0@6,
decode_list_items(Rest2@4, Len@2, [])
end
);
5 ->
gleam@result:'try'(
read_int(Info, Rest),
fun(_use0@7) ->
{Len@3, Rest2@5} = _use0@7,
decode_map_entries(Rest2@5, Len@3, [])
end
);
7 ->
decode_simple(Info, Rest);
_ ->
{error, <<"cbor: tags / major type 6 not supported"/utf8>>}
end;
_ ->
{error, <<"cbor: empty input"/utf8>>}
end.
-file("src/aws/internal/codec/cbor.gleam", 285).
?DOC(
" Convenience helper for callers that just want the decoded\n"
" value and don't care about the trailing bytes (the common\n"
" rpcv2Cbor request/response body case).\n"
).
-spec decode_value(bitstring()) -> {ok, value()} | {error, binary()}.
decode_value(Bytes) ->
_pipe = decode(Bytes),
gleam@result:map(_pipe, fun(T) -> erlang:element(1, T) end).
-file("src/aws/internal/codec/cbor.gleam", 292).
?DOC(
" `option.None` (None) when looking up a key that's not present\n"
" in a `CMap`. Used by hand-written decoders that pluck specific\n"
" fields out of a CBOR-decoded map.\n"
).
-spec get_field(value(), binary()) -> gleam@option:option(value()).
get_field(Map, Key) ->
case Map of
{c_map, Entries} ->
case gleam@list:find(
Entries,
fun(P) -> erlang:element(1, P) =:= {c_string, Key} end
) of
{ok, {_, V}} ->
{some, V};
{error, _} ->
none
end;
_ ->
none
end.