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