Current section

Files

Jump to
multipartkit src multipartkit@part.erl
Raw

src/multipartkit@part.erl

-module(multipartkit@part).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/multipartkit/part.gleam").
-export([unchecked_new/5, new/5, all_headers/1, name/1, filename/1, content_type/1, body/1, equal_on_wire/2, list_equal_on_wire/2, header/2, headers/2]).
-export_type([part/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.
-opaque part() :: {part,
list({binary(), binary()}),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
bitstring()}.
-file("src/multipartkit/part.gleam", 86).
?DOC(false).
-spec unchecked_new(
list({binary(), binary()}),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
bitstring()
) -> part().
unchecked_new(Headers, Name, Filename, Content_type, Body) ->
{part, Headers, Name, Filename, Content_type, Body}.
-file("src/multipartkit/part.gleam", 137).
-spec drop_leading_ows(binary()) -> binary().
drop_leading_ows(Value) ->
case gleam_stdlib:string_pop_grapheme(Value) of
{ok, {<<" "/utf8>>, Rest}} ->
drop_leading_ows(Rest);
{ok, {<<"\t"/utf8>>, Rest}} ->
drop_leading_ows(Rest);
_ ->
Value
end.
-file("src/multipartkit/part.gleam", 144).
-spec drop_trailing_ows(binary()) -> binary().
drop_trailing_ows(Value) ->
gleam@bool:guard(
not (gleam_stdlib:string_ends_with(Value, <<" "/utf8>>) orelse gleam_stdlib:string_ends_with(
Value,
<<"\t"/utf8>>
)),
Value,
fun() -> drop_trailing_ows(gleam@string:drop_end(Value, 1)) end
).
-file("src/multipartkit/part.gleam", 133).
?DOC(
" Strip RFC 7230 §3.2.4 OWS — space (0x20) and horizontal tab (0x09) —\n"
" from both ends of `value`. Mirrors what the parser strips on the wire\n"
" side; using `string.trim` would also strip Unicode whitespace, which\n"
" is over-broad for an HTTP-style header.\n"
).
-spec trim_ows(binary()) -> binary().
trim_ows(Value) ->
_pipe = Value,
_pipe@1 = drop_leading_ows(_pipe),
drop_trailing_ows(_pipe@1).
-file("src/multipartkit/part.gleam", 152).
-spec valid_header_name(binary()) -> boolean().
valid_header_name(Name) ->
not (((gleam_stdlib:contains_string(Name, <<"\r"/utf8>>) orelse gleam_stdlib:contains_string(
Name,
<<"\n"/utf8>>
))
orelse gleam_stdlib:contains_string(Name, <<"\x{0000}"/utf8>>))
orelse gleam_stdlib:contains_string(Name, <<":"/utf8>>)).
-file("src/multipartkit/part.gleam", 161).
-spec valid_header_value(binary()) -> boolean().
valid_header_value(Value) ->
not ((gleam_stdlib:contains_string(Value, <<"\r"/utf8>>) orelse gleam_stdlib:contains_string(
Value,
<<"\n"/utf8>>
))
orelse gleam_stdlib:contains_string(Value, <<"\x{0000}"/utf8>>)).
-file("src/multipartkit/part.gleam", 109).
?DOC(
" Walk the input header list, rejecting CRLF / NUL injection attempts\n"
" and stripping RFC 7230 §3.2.4 OWS (space and horizontal tab) from the\n"
" surrounding edges of each value. The OWS stripping mirrors what the\n"
" parser does on the wire side, so a `Part` constructed with\n"
" `[#(\"X-Foo\", \" spaced \")]` round-trips equal to itself through\n"
" `encode → parse`. The returned list preserves the input order so\n"
" `all_headers` / `headers` keep their documented \"wire order\" property.\n"
).
-spec validate_and_normalise_headers(
list({binary(), binary()}),
list({binary(), binary()})
) -> {ok, list({binary(), binary()})} |
{error, multipartkit@error:multipart_error()}.
validate_and_normalise_headers(Headers, Acc) ->
case Headers of
[] ->
{ok, lists:reverse(Acc)};
[{Name, Value} | Rest] ->
gleam@bool:guard(
not valid_header_name(Name),
{error, {invalid_header_name, Name}},
fun() ->
gleam@bool:guard(
not valid_header_value(Value),
{error, {invalid_header_value, Name, Value}},
fun() ->
validate_and_normalise_headers(
Rest,
[{Name, trim_ows(Value)} | Acc]
)
end
)
end
)
end.
-file("src/multipartkit/part.gleam", 63).
?DOC(
" Construct a `Part`.\n"
"\n"
" The `name`, `filename`, and `content_type` fields are not re-derived\n"
" from the `headers` list — pass the values you expect callers to see,\n"
" or use `parser.parse` for automatic derivation.\n"
"\n"
" Header names and values are validated to prevent CRLF / NUL injection\n"
" into the encoded wire image. A header value that contains `\\r`, `\\n`,\n"
" or NUL would otherwise let an attacker who controls the value smuggle\n"
" additional header lines into the encoded part — the multipart variant\n"
" of CRLF response splitting (RFC 9110 §5.5 disallows these bytes in\n"
" `field-value`). Header names additionally cannot contain `:` (would\n"
" split the header at parse time). The constructor rejects these inputs\n"
" with `Error(InvalidHeaderName(_))` / `Error(InvalidHeaderValue(_, _))`\n"
" rather than silently emitting an off-spec wire image.\n"
).
-spec new(
list({binary(), binary()}),
gleam@option:option(binary()),
gleam@option:option(binary()),
gleam@option:option(binary()),
bitstring()
) -> {ok, part()} | {error, multipartkit@error:multipart_error()}.
new(Headers, Name, Filename, Content_type, Body) ->
gleam@result:'try'(
validate_and_normalise_headers(Headers, []),
fun(Normalised) ->
{ok, {part, Normalised, Name, Filename, Content_type, Body}}
end
).
-file("src/multipartkit/part.gleam", 170).
?DOC(" All headers as `(name, value)` pairs in input order.\n").
-spec all_headers(part()) -> list({binary(), binary()}).
all_headers(Part) ->
erlang:element(2, Part).
-file("src/multipartkit/part.gleam", 177).
?DOC(
" The convenience `name` field derived from `Content-Disposition` for\n"
" `form-data` parts, or `None` for non-form parts and parts without a\n"
" disposition header.\n"
).
-spec name(part()) -> gleam@option:option(binary()).
name(Part) ->
erlang:element(3, Part).
-file("src/multipartkit/part.gleam", 183).
?DOC(
" The convenience `filename` field derived from `Content-Disposition`\n"
" for `form-data` parts, or `None`.\n"
).
-spec filename(part()) -> gleam@option:option(binary()).
filename(Part) ->
erlang:element(4, Part).
-file("src/multipartkit/part.gleam", 189).
?DOC(
" The `Content-Type` header value, or `None` if the part has no\n"
" `Content-Type` header.\n"
).
-spec content_type(part()) -> gleam@option:option(binary()).
content_type(Part) ->
erlang:element(5, Part).
-file("src/multipartkit/part.gleam", 194).
?DOC(" The raw body bytes of the part.\n").
-spec body(part()) -> bitstring().
body(Part) ->
erlang:element(6, Part).
-file("src/multipartkit/part.gleam", 229).
?DOC(
" Structural equality on the wire-level content of two `Part` values.\n"
"\n"
" Compares the headers list (preserving order, with case-sensitive\n"
" name matching that mirrors RFC 7578 §4.2) and the body bytes. The\n"
" convenience cache fields — `name`, `filename`, `content_type` — are\n"
" intentionally ignored because they are derived from the headers\n"
" and may differ between a `Part.new/5`-constructed value (where the\n"
" caller passes the cache) and a parsed `Part` (where the parser\n"
" derives the cache from `Content-Disposition` / `Content-Type`).\n"
" Two `Part`s that `equal_on_wire` returns `True` for will encode\n"
" to the same bytes via `multipartkit.encode/2` (modulo the\n"
" boundary string, which is supplied at encode time).\n"
"\n"
" Use this for property-style round-trip tests where the caller\n"
" passes one `Part` shape into the encoder and gets another\n"
" (cache-derived) shape back from the parser.\n"
).
-spec equal_on_wire(part(), part()) -> boolean().
equal_on_wire(A, B) ->
(erlang:element(2, A) =:= erlang:element(2, B)) andalso (erlang:element(
6,
A
)
=:= erlang:element(6, B)).
-file("src/multipartkit/part.gleam", 236).
?DOC(
" `equal_on_wire` lifted over a pair of `Part` lists. Returns `True`\n"
" when the lists have the same length and every paired element\n"
" satisfies `equal_on_wire`.\n"
).
-spec list_equal_on_wire(list(part()), list(part())) -> boolean().
list_equal_on_wire(A, B) ->
case {A, B} of
{[], []} ->
true;
{[], _} ->
false;
{_, []} ->
false;
{[First_a | Rest_a], [First_b | Rest_b]} ->
case equal_on_wire(First_a, First_b) of
true ->
list_equal_on_wire(Rest_a, Rest_b);
false ->
false
end
end.
-file("src/multipartkit/part.gleam", 249).
-spec find_header(list({binary(), binary()}), binary()) -> list(binary()).
find_header(Headers, Name) ->
gleam@list:filter_map(
Headers,
fun(Entry) ->
case multipartkit@internal@text:equals_ci(
erlang:element(1, Entry),
Name
) of
true ->
{ok, erlang:element(2, Entry)};
false ->
{error, nil}
end
end
).
-file("src/multipartkit/part.gleam", 200).
?DOC(
" Return the first header value whose name matches `name` ASCII\n"
" case-insensitively.\n"
).
-spec header(part(), binary()) -> gleam@option:option(binary()).
header(Part, Name) ->
case find_header(erlang:element(2, Part), Name) of
[First | _] ->
{some, First};
[] ->
none
end.
-file("src/multipartkit/part.gleam", 209).
?DOC(
" Return all header values whose name matches `name` ASCII case-insensitively\n"
" in the order they appear in the part headers.\n"
).
-spec headers(part(), binary()) -> list(binary()).
headers(Part, Name) ->
find_header(erlang:element(2, Part), Name).