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