-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, unsafe_add_part/2, add_field/3, add_file/5, add_file_auto_with/5, add_file_auto/4]). -export_type([form/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())}. -file("src/multipartkit/form.gleam", 18). ?DOC(" A new empty form.\n"). -spec new() -> form(). new() -> {form, []}. -file("src/multipartkit/form.gleam", 118). ?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", 122). -spec push(form(), multipartkit@part:part()) -> form(). push(Form, The_part) -> {form, [The_part | erlang:element(2, Form)]}. -file("src/multipartkit/form.gleam", 113). ?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", 155). ?DOC( " Strip CR, LF, and NUL from `value`. Used to neutralise header injection\n" " in any value that ends up on a header line — `name`, `filename`, and\n" " `content_type`.\n" ). -spec sanitize_value(binary()) -> binary(). sanitize_value(Value) -> _pipe = Value, _pipe@1 = gleam@string:replace(_pipe, <<"\r\n"/utf8>>, <<""/utf8>>), _pipe@2 = gleam@string:replace(_pipe@1, <<"\r"/utf8>>, <<""/utf8>>), _pipe@3 = gleam@string:replace(_pipe@2, <<"\n"/utf8>>, <<""/utf8>>), gleam@string:replace(_pipe@3, <<"\x{0000}"/utf8>>, <<""/utf8>>). -file("src/multipartkit/form.gleam", 167). -spec escape_quoted(binary(), binary()) -> binary(). escape_quoted(Remaining, Acc) -> case gleam_stdlib:string_pop_grapheme(Remaining) of {error, nil} -> Acc; {ok, {<<"\\"/utf8>>, Rest}} -> escape_quoted(Rest, <>); {ok, {<<"\""/utf8>>, Rest@1}} -> escape_quoted(Rest@1, <>); {ok, {Other, Rest@2}} -> escape_quoted(Rest@2, <>) end. -file("src/multipartkit/form.gleam", 163). -spec quote(binary()) -> binary(). quote(Value) -> <<<<"\""/utf8, (escape_quoted(Value, <<""/utf8>>))/binary>>/binary, "\""/utf8>>. -file("src/multipartkit/form.gleam", 30). ?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. Use `unsafe_add_part` if byte-exact\n" " preservation is required.\n" ). -spec add_field(form(), binary(), binary()) -> form(). add_field(Form, Name, Value) -> Safe_name = sanitize_value(Name), Disposition = {<<"Content-Disposition"/utf8>>, <<"form-data; name="/utf8, (quote(Safe_name))/binary>>}, New_part = {part, [Disposition], {some, Safe_name}, none, none, <>}, push(Form, New_part). -file("src/multipartkit/form.gleam", 126). -spec build_file_part(binary(), binary(), binary(), bitstring()) -> multipartkit@part:part(). build_file_part(Name, Filename, Content_type, Body) -> Safe_name = sanitize_value(Name), Safe_filename = sanitize_value(Filename), Safe_content_type = sanitize_value(Content_type), Disposition_header = {<<"Content-Disposition"/utf8>>, <<<<<<"form-data; name="/utf8, (quote(Safe_name))/binary>>/binary, "; filename="/utf8>>/binary, (quote(Safe_filename))/binary>>}, Content_type_header = {<<"Content-Type"/utf8>>, Safe_content_type}, {part, [Disposition_header, Content_type_header], {some, Safe_name}, {some, Safe_filename}, {some, Safe_content_type}, Body}. -file("src/multipartkit/form.gleam", 54). ?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. Use `unsafe_add_part` if byte-exact\n" " preservation 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", 90). ?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", 70). ?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() ).