Skip to main content

src/aws@internal@codec@cbor.erl

-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.