Current section
Files
Jump to
Current section
Files
src/multipartkit@form.erl
-module(multipartkit@form).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/multipartkit/form.gleam").
-export([new/0, parts/1, add_field/3, add_field_strict/3, unsafe_add_part/2, add_file/5, add_file_auto_with/5, add_file_auto/4, add_file_strict/5]).
-export_type([form/0, form_error/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 form() :: {form, list(multipartkit@part:part())}.
-type form_error() :: {name_contains_control_bytes, binary()} |
{filename_contains_control_bytes, binary()} |
{content_type_contains_control_bytes, binary()}.
-file("src/multipartkit/form.gleam", 43).
?DOC(" A new empty form.\n").
-spec new() -> form().
new() ->
{form, []}.
-file("src/multipartkit/form.gleam", 211).
?DOC(" Read the parts in insertion order.\n").
-spec parts(form()) -> list(multipartkit@part:part()).
parts(Form) ->
lists:reverse(erlang:element(2, Form)).
-file("src/multipartkit/form.gleam", 215).
-spec push(form(), multipartkit@part:part()) -> form().
push(Form, The_part) ->
{form, [The_part | erlang:element(2, Form)]}.
-file("src/multipartkit/form.gleam", 60).
?DOC(
" Append a text field. `value` is encoded as UTF-8 in the part body. No\n"
" filename is set.\n"
"\n"
" Carriage returns, line feeds, and NUL bytes in `name` are silently\n"
" stripped to prevent header injection. The cached `name` on the resulting\n"
" `Part` reflects the sanitized value, matching what a parse-after-encode\n"
" round-trip would produce. The strip is data loss the caller cannot\n"
" observe — `add_field(\"name\\n\", _)` produces a part with `name=\"\"` —\n"
" so callers passing user-typed or upstream data into `name` should\n"
" prefer `add_field_strict`, which surfaces the bad input as\n"
" `Error(NameContainsControlBytes(value:))` instead. Use\n"
" `unsafe_add_part` if byte-exact preservation of arbitrary header\n"
" values is required.\n"
).
-spec add_field(form(), binary(), binary()) -> form().
add_field(Form, Name, Value) ->
Safe_name = multipartkit@internal@disposition:sanitize_value(Name),
Header = {<<"Content-Disposition"/utf8>>,
multipartkit@internal@disposition:build_form_data_value(Safe_name, none)},
New_part = multipartkit@part:unchecked_new(
[Header],
{some, Safe_name},
none,
none,
<<Value/binary>>
),
push(Form, New_part).
-file("src/multipartkit/form.gleam", 156).
?DOC(
" Strict counterpart of `add_field`: rejects names containing CR /\n"
" LF / NUL bytes with `Error(NameContainsControlBytes(value:))`.\n"
"\n"
" The non-strict `add_field` silently strips these bytes (so\n"
" `add_field(\"name\\n\", _)` produces a part with `name=\"\"`). For\n"
" callers that pass user-typed or upstream data into `name` and\n"
" want to surface bad inputs as a typed error rather than silent\n"
" data loss, use this variant. The `value` payload carries the\n"
" caller's original input so the error renders as\n"
" \"field name `foo\\\\nbar` contains forbidden control bytes\". (#40)\n"
).
-spec add_field_strict(form(), binary(), binary()) -> {ok, form()} |
{error, form_error()}.
add_field_strict(Form, Name, Value) ->
gleam@bool:guard(
multipartkit@internal@disposition:has_header_breaking_bytes(Name),
{error, {name_contains_control_bytes, Name}},
fun() -> {ok, add_field(Form, Name, Value)} end
).
-file("src/multipartkit/form.gleam", 206).
?DOC(
" Append a pre-built `Part` without validation or normalisation.\n"
"\n"
" The caller is responsible for keeping `headers`, `name`, `filename`, and\n"
" `content_type` mutually consistent. Prefer `add_field` / `add_file` /\n"
" `add_file_auto` for library-maintained consistency.\n"
).
-spec unsafe_add_part(form(), multipartkit@part:part()) -> form().
unsafe_add_part(Form, The_part) ->
push(Form, The_part).
-file("src/multipartkit/form.gleam", 219).
-spec build_file_part(binary(), binary(), binary(), bitstring()) -> multipartkit@part:part().
build_file_part(Name, Filename, Content_type, Body) ->
Safe_name = multipartkit@internal@disposition:sanitize_value(Name),
Safe_filename = multipartkit@internal@disposition:sanitize_value(Filename),
Safe_content_type = multipartkit@internal@disposition:sanitize_value(
Content_type
),
Disposition_header = {<<"Content-Disposition"/utf8>>,
multipartkit@internal@disposition:build_form_data_value(
Safe_name,
{some, Safe_filename}
)},
Content_type_header = {<<"Content-Type"/utf8>>, Safe_content_type},
multipartkit@part:unchecked_new(
[Disposition_header, Content_type_header],
{some, Safe_name},
{some, Safe_filename},
{some, Safe_content_type},
Body
).
-file("src/multipartkit/form.gleam", 92).
?DOC(
" Append a file part with an explicit content type.\n"
"\n"
" Carriage returns, line feeds, and NUL bytes in `name`, `filename`, and\n"
" `content_type` are silently stripped to prevent header injection. The\n"
" cached `name`, `filename`, and `content_type` on the resulting `Part`\n"
" reflect the sanitized values. The strip on `filename` is especially\n"
" dangerous — `add_file(_, \"fi\\nle.png\", _, _)` concatenates the two\n"
" halves into the *different valid filename* `\"file.png\"`, which can\n"
" change authorisation-relevant identifiers. Callers passing user-typed\n"
" or upstream data should prefer `add_file_strict`, which surfaces the\n"
" bad input as `Error(NameContainsControlBytes(value:))` /\n"
" `Error(FilenameContainsControlBytes(value:))` /\n"
" `Error(ContentTypeContainsControlBytes(value:))`. Use\n"
" `unsafe_add_part` if byte-exact preservation of arbitrary header\n"
" values is required.\n"
).
-spec add_file(form(), binary(), binary(), binary(), bitstring()) -> form().
add_file(Form, Name, Filename, Content_type, Body) ->
push(Form, build_file_part(Name, Filename, Content_type, Body)).
-file("src/multipartkit/form.gleam", 128).
?DOC(
" Append a file part, inferring the content type via the supplied\n"
" `Inferer`.\n"
"\n"
" Inference precedence:\n"
"\n"
" 1. `inferer.from_filename(filename)`\n"
" 2. `inferer.from_bytes(body)`\n"
" 3. `application/octet-stream`\n"
"\n"
" The inferred content type is sanitized (CR / LF / NUL stripped) before\n"
" being written to the header.\n"
).
-spec add_file_auto_with(
form(),
binary(),
binary(),
bitstring(),
multipartkit@infer:inferer()
) -> form().
add_file_auto_with(Form, Name, Filename, Body, Inferer) ->
Content_type = case (erlang:element(2, Inferer))(Filename) of
{some, Value} ->
Value;
none ->
case (erlang:element(3, Inferer))(Body) of
{some, Value@1} ->
Value@1;
none ->
<<"application/octet-stream"/utf8>>
end
end,
add_file(Form, Name, Filename, Content_type, Body).
-file("src/multipartkit/form.gleam", 108).
?DOC(
" Append a file part using the default (no-op) inferer.\n"
"\n"
" Equivalent to `add_file_auto_with(form, name, filename, body,\n"
" infer.default_inferer())`. The default inferer returns `None` from both\n"
" helpers in v0.1.0, so this falls through to `application/octet-stream`\n"
" unless you call `add_file_auto_with` with a real inferer.\n"
).
-spec add_file_auto(form(), binary(), binary(), bitstring()) -> form().
add_file_auto(Form, Name, Filename, Body) ->
add_file_auto_with(
Form,
Name,
Filename,
Body,
multipartkit@infer:default_inferer()
).
-file("src/multipartkit/form.gleam", 179).
?DOC(
" Strict counterpart of `add_file`: rejects names, filenames, and\n"
" content types containing CR / LF / NUL bytes with the matching\n"
" `FormError` variant.\n"
"\n"
" The non-strict `add_file` silently strips these bytes. For\n"
" `name` the strip leaves an empty string (loud round-trip\n"
" failure); for `filename` it concatenates the two halves into a\n"
" *different valid filename*, which can change\n"
" authorisation-relevant identifiers (the attacker shape\n"
" described in #41). The strict variant catches both at the\n"
" builder boundary so the wrong wire never gets produced. (#41)\n"
).
-spec add_file_strict(form(), binary(), binary(), binary(), bitstring()) -> {ok,
form()} |
{error, form_error()}.
add_file_strict(Form, Name, Filename, Content_type, Body) ->
gleam@bool:guard(
multipartkit@internal@disposition:has_header_breaking_bytes(Name),
{error, {name_contains_control_bytes, Name}},
fun() ->
gleam@bool:guard(
multipartkit@internal@disposition:has_header_breaking_bytes(
Filename
),
{error, {filename_contains_control_bytes, Filename}},
fun() ->
gleam@bool:guard(
multipartkit@internal@disposition:has_header_breaking_bytes(
Content_type
),
{error,
{content_type_contains_control_bytes, Content_type}},
fun() ->
{ok,
add_file(
Form,
Name,
Filename,
Content_type,
Body
)}
end
)
end
)
end
).