Skip to main content

src/aws@internal@sigv4_canonical.erl

-module(aws@internal@sigv4_canonical).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/aws/internal/sigv4_canonical.gleam").
-export([canonical_headers/1, signed_headers/1, canonical_query_string/1, encode_path/1, build_canonical_uri/2]).

-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(
    " Canonical-request helpers shared by SigV4 and SigV4a.\n"
    "\n"
    " Both algorithms compute the same canonical headers block, the\n"
    " same signed-headers line, the same canonical query string, and\n"
    " the same canonical URI (with RFC 3986 dot-segment removal when\n"
    " requested). The algorithm-specific differences live entirely in\n"
    " `sigv4.gleam` / `sigv4a.gleam`:\n"
    "\n"
    "   - the algorithm string (`AWS4-HMAC-SHA256` vs\n"
    "     `AWS4-ECDSA-P256-SHA256`)\n"
    "   - the credential scope (region-bound vs region-less)\n"
    "   - the per-algorithm header set (`X-Amz-Region-Set` vs none)\n"
    "   - the signing key derivation (HMAC chain vs HMAC-DRBG to an\n"
    "     EC scalar)\n"
    "   - the signature step (HMAC-SHA256 vs ECDSA P-256)\n"
    "\n"
    " The functions in this module are pure — no IO, no clock — and\n"
    " take `List(Header)` / `String` arguments so callers can compose\n"
    " them with their own pre-/post-processing.\n"
).

-file("src/aws/internal/sigv4_canonical.gleam", 101).
-spec do_group_by_name(
    list({binary(), binary()}),
    list({binary(), list(binary())})
) -> list({binary(), list(binary())}).
do_group_by_name(Pairs, Acc) ->
    case Pairs of
        [] ->
            lists:reverse(
                gleam@list:map(
                    Acc,
                    fun(P) ->
                        {erlang:element(1, P),
                            lists:reverse(erlang:element(2, P))}
                    end
                )
            );

        [{Name, Value} | Rest] ->
            Updated = case gleam@list:key_find(Acc, Name) of
                {ok, Existing} ->
                    New_values = [Value | Existing],
                    gleam@list:key_set(Acc, Name, New_values);

                {error, _} ->
                    [{Name, [Value]} | Acc]
            end,
            do_group_by_name(Rest, Updated)
    end.

-file("src/aws/internal/sigv4_canonical.gleam", 126).
-spec do_collapse(list(binary()), boolean(), list(binary())) -> list(binary()).
do_collapse(Chars, Last_was_space, Acc) ->
    case Chars of
        [] ->
            Acc;

        [C | Rest] ->
            case (C =:= <<" "/utf8>>) orelse (C =:= <<"\t"/utf8>>) of
                true ->
                    case Last_was_space of
                        true ->
                            do_collapse(Rest, true, Acc);

                        false ->
                            do_collapse(Rest, true, [<<" "/utf8>> | Acc])
                    end;

                false ->
                    do_collapse(Rest, false, [C | Acc])
            end
    end.

-file("src/aws/internal/sigv4_canonical.gleam", 120).
-spec collapse_spaces(binary()) -> binary().
collapse_spaces(S) ->
    _pipe = do_collapse(gleam@string:to_graphemes(S), false, []),
    _pipe@1 = lists:reverse(_pipe),
    erlang:list_to_binary(_pipe@1).

-file("src/aws/internal/sigv4_canonical.gleam", 31).
?DOC(
    " Build the canonical headers block: lowercase names, trim +\n"
    " collapse internal runs of ASCII whitespace in values, group\n"
    " duplicate header names with comma-joined values, sort by name,\n"
    " emit one `name:value\\n` line each.\n"
).
-spec canonical_headers(list(aws@internal@http_request:header())) -> binary().
canonical_headers(Headers) ->
    Prepared = begin
        _pipe = Headers,
        _pipe@1 = gleam@list:map(
            _pipe,
            fun(H) ->
                {string:lowercase(erlang:element(2, H)),
                    collapse_spaces(gleam@string:trim(erlang:element(3, H)))}
            end
        ),
        _pipe@2 = do_group_by_name(_pipe@1, []),
        gleam@list:sort(
            _pipe@2,
            fun(A, B) ->
                gleam@string:compare(erlang:element(1, A), erlang:element(1, B))
            end
        )
    end,
    _pipe@3 = Prepared,
    _pipe@4 = gleam@list:map(
        _pipe@3,
        fun(P) ->
            <<<<<<(erlang:element(1, P))/binary, ":"/utf8>>/binary,
                    (gleam@string:join(erlang:element(2, P), <<","/utf8>>))/binary>>/binary,
                "\n"/utf8>>
        end
    ),
    erlang:list_to_binary(_pipe@4).

-file("src/aws/internal/sigv4_canonical.gleam", 47).
?DOC(
    " Semicolon-joined, sorted, lowercased header names — the\n"
    " `SignedHeaders=` value on the `Authorization` line.\n"
).
-spec signed_headers(list(aws@internal@http_request:header())) -> binary().
signed_headers(Headers) ->
    _pipe = Headers,
    _pipe@1 = gleam@list:map(
        _pipe,
        fun(H) -> string:lowercase(erlang:element(2, H)) end
    ),
    _pipe@2 = gleam@list:unique(_pipe@1),
    _pipe@3 = gleam@list:sort(_pipe@2, fun gleam@string:compare/2),
    gleam@string:join(_pipe@3, <<";"/utf8>>).

-file("src/aws/internal/sigv4_canonical.gleam", 57).
?DOC(
    " Canonical query string: split on `&`, URI-encode names + values,\n"
    " sort first by name then by value. Empty input → empty output.\n"
).
-spec canonical_query_string(binary()) -> binary().
canonical_query_string(Query) ->
    case Query of
        <<""/utf8>> ->
            <<""/utf8>>;

        _ ->
            _pipe = gleam@string:split(Query, <<"&"/utf8>>),
            _pipe@1 = gleam@list:map(
                _pipe,
                fun(Pair) -> case gleam@string:split_once(Pair, <<"="/utf8>>) of
                        {ok, {Name, Value}} ->
                            {aws@internal@uri:encode_component(Name),
                                aws@internal@uri:encode_component(Value)};

                        {error, _} ->
                            {aws@internal@uri:encode_component(Pair),
                                <<""/utf8>>}
                    end end
            ),
            _pipe@2 = gleam@list:sort(
                _pipe@1,
                fun(A, B) ->
                    case gleam@string:compare(
                        erlang:element(1, A),
                        erlang:element(1, B)
                    ) of
                        eq ->
                            gleam@string:compare(
                                erlang:element(2, A),
                                erlang:element(2, B)
                            );

                        Other ->
                            Other
                    end
                end
            ),
            _pipe@3 = gleam@list:map(
                _pipe@2,
                fun(P) ->
                    <<<<(erlang:element(1, P))/binary, "="/utf8>>/binary,
                        (erlang:element(2, P))/binary>>
                end
            ),
            gleam@string:join(_pipe@3, <<"&"/utf8>>)
    end.

-file("src/aws/internal/sigv4_canonical.gleam", 95).
?DOC(
    " Percent-encode each path segment — the URI representation used\n"
    " in the canonical request line.\n"
).
-spec encode_path(binary()) -> binary().
encode_path(Path) ->
    _pipe = gleam@string:split(Path, <<"/"/utf8>>),
    _pipe@1 = gleam@list:map(_pipe, fun aws@internal@uri:encode_segment/1),
    gleam@string:join(_pipe@1, <<"/"/utf8>>).

-file("src/aws/internal/sigv4_canonical.gleam", 159).
-spec process_segments(list(binary()), list(binary())) -> list(binary()).
process_segments(Segments, Stack) ->
    case Segments of
        [] ->
            lists:reverse(Stack);

        [<<""/utf8>> | Rest] ->
            process_segments(Rest, Stack);

        [<<"."/utf8>> | Rest@1] ->
            process_segments(Rest@1, Stack);

        [<<".."/utf8>> | Rest@2] ->
            case Stack of
                [_ | Tail] ->
                    process_segments(Rest@2, Tail);

                [] ->
                    process_segments(Rest@2, Stack)
            end;

        [Seg | Rest@3] ->
            process_segments(Rest@3, [Seg | Stack])
    end.

-file("src/aws/internal/sigv4_canonical.gleam", 145).
-spec normalize_path(binary()) -> binary().
normalize_path(Path) ->
    Trailing_slash = gleam_stdlib:string_ends_with(Path, <<"/"/utf8>>) andalso (Path
    /= <<"/"/utf8>>),
    Segments = case gleam_stdlib:string_starts_with(Path, <<"/"/utf8>>) of
        true ->
            _pipe = gleam@string:split(Path, <<"/"/utf8>>),
            gleam@list:drop(_pipe, 1);

        false ->
            gleam@string:split(Path, <<"/"/utf8>>)
    end,
    Processed = process_segments(Segments, []),
    case {Processed, Trailing_slash} of
        {[], _} ->
            <<"/"/utf8>>;

        {Parts, true} ->
            <<<<"/"/utf8, (gleam@string:join(Parts, <<"/"/utf8>>))/binary>>/binary,
                "/"/utf8>>;

        {Parts@1, false} ->
            <<"/"/utf8, (gleam@string:join(Parts@1, <<"/"/utf8>>))/binary>>
    end.

-file("src/aws/internal/sigv4_canonical.gleam", 86).
?DOC(
    " Compose RFC 3986 dot-segment removal (when requested) with\n"
    " percent encoding. S3 callers want `normalize: False` so object\n"
    " keys with `.` / `..` survive; every other AWS service wants\n"
    " `True`.\n"
).
-spec build_canonical_uri(binary(), boolean()) -> binary().
build_canonical_uri(Path, Normalize) ->
    case Normalize of
        true ->
            encode_path(normalize_path(Path));

        false ->
            encode_path(Path)
    end.