Skip to main content

src/packkit@gzip.erl

-module(packkit@gzip).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/packkit/gzip.gleam").
-export([codec/0, default_header/0, with_name_checked/2, with_name/2, with_comment_checked/2, with_comment/2, with_modified_at_checked/2, with_modified_at/2, name/1, comment/1, modified_at_unix/1, extra/1, with_extra_checked/2, with_extra/2, new_decoder/0, new_decoder_with_limits/1, push/2, decode_with_limits/2, decode/1, decode_payload/1, decode_payload_with_limits/2, finish/1, encode_with_header/2, encode/1]).
-export_type([header/0, subfield/0, decoder/0, header_error/0, decoded/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.

?MODULEDOC(
    " RFC 1952 gzip codec.\n"
    "\n"
    " gzip wraps a DEFLATE stream in a member header that may carry an\n"
    " original filename, free-text comment, and modification timestamp.\n"
    " A trailing CRC-32 and ISIZE pair lets readers verify the decoded\n"
    " payload.\n"
).

-opaque header() :: {header,
        gleam@option:option(binary()),
        gleam@option:option(binary()),
        gleam@option:option(integer()),
        list(subfield())}.

-type subfield() :: {subfield, integer(), integer(), bitstring()}.

-opaque decoder() :: {decoder,
        list(bitstring()),
        integer(),
        packkit@limit:limits()}.

-type header_error() :: header_name_contains_nul |
    header_comment_contains_nul |
    {header_modified_at_out_of_range, integer()} |
    {header_extra_subfield_too_long, integer()} |
    {header_extra_total_too_long, integer()} |
    {header_extra_subfield_id_out_of_range, integer(), integer()}.

-type decoded() :: {decoded, header(), bitstring()}.

-file("src/packkit/gzip.gleam", 86).
?DOC(" Gzip codec smart constructor.\n").
-spec codec() -> packkit@codec:codec().
codec() ->
    packkit@codec:gzip().

-file("src/packkit/gzip.gleam", 91).
?DOC(" Default gzip header with no optional fields populated.\n").
-spec default_header() -> header().
default_header() ->
    {header, none, none, none, []}.

-file("src/packkit/gzip.gleam", 150).
?DOC(
    " Attach an optional filename after validating that it does not\n"
    " contain the NUL byte gzip uses as the FNAME terminator.  Use this\n"
    " when the value comes from untrusted input that must round-trip.\n"
).
-spec with_name_checked(header(), binary()) -> {ok, header()} |
    {error, header_error()}.
with_name_checked(Header, Name) ->
    gleam@bool:guard(
        gleam_stdlib:contains_string(Name, <<"\x{0000}"/utf8>>),
        {error, header_name_contains_nul},
        fun() ->
            {ok,
                {header,
                    {some, Name},
                    erlang:element(3, Header),
                    erlang:element(4, Header),
                    erlang:element(5, Header)}}
        end
    ).

-file("src/packkit/gzip.gleam", 106).
?DOC(
    " Attach an optional filename.  Panics if `name` contains the NUL\n"
    " byte gzip uses as the FNAME terminator — see [with_name_checked]\n"
    " when the value comes from untrusted input.\n"
    "\n"
    " The unchecked variant guarantees that the value stored in the\n"
    " header is exactly what the caller passed (lawful round-trip via\n"
    " `name(with_name(h, x)) == Some(x)`).  Earlier revisions silently\n"
    " stripped NULs to \"be helpful\"; that broke the round-trip law and\n"
    " is now a panic, matching the other unchecked setters in this\n"
    " module ([with_modified_at] / [with_extra]) and across the package\n"
    " ([packkit/entry.with_mode] etc.).\n"
).
-spec with_name(header(), binary()) -> header().
with_name(Header, Name) ->
    case with_name_checked(Header, Name) of
        {ok, H} ->
            H;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_name: name must not contain NUL (0x00)"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_name"/utf8>>,
                    line => 110})
    end.

-file("src/packkit/gzip.gleam", 163).
?DOC(
    " Attach an optional comment after validating that it does not\n"
    " contain the NUL byte gzip uses as the FCOMMENT terminator.\n"
).
-spec with_comment_checked(header(), binary()) -> {ok, header()} |
    {error, header_error()}.
with_comment_checked(Header, Comment) ->
    gleam@bool:guard(
        gleam_stdlib:contains_string(Comment, <<"\x{0000}"/utf8>>),
        {error, header_comment_contains_nul},
        fun() ->
            {ok,
                {header,
                    erlang:element(2, Header),
                    {some, Comment},
                    erlang:element(4, Header),
                    erlang:element(5, Header)}}
        end
    ).

-file("src/packkit/gzip.gleam", 120).
?DOC(
    " Attach an optional comment.  Panics if `comment` contains the NUL\n"
    " byte gzip uses as the FCOMMENT terminator — see\n"
    " [with_comment_checked] when the value comes from untrusted input.\n"
    "\n"
    " Like [with_name], the stored value is exactly what the caller\n"
    " passed; earlier revisions silently stripped NULs.\n"
).
-spec with_comment(header(), binary()) -> header().
with_comment(Header, Comment) ->
    case with_comment_checked(Header, Comment) of
        {ok, H} ->
            H;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_comment: comment must not contain NUL (0x00)"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_comment"/utf8>>,
                    line => 124})
    end.

-file("src/packkit/gzip.gleam", 191).
?DOC(
    " Attach an optional Unix mtime after validating it fits gzip's\n"
    " 32-bit MTIME field.\n"
).
-spec with_modified_at_checked(header(), integer()) -> {ok, header()} |
    {error, header_error()}.
with_modified_at_checked(Header, Unix_seconds) ->
    gleam@bool:guard(
        (Unix_seconds < 0) orelse (Unix_seconds > 16#FFFFFFFF),
        {error, {header_modified_at_out_of_range, Unix_seconds}},
        fun() ->
            {ok,
                {header,
                    erlang:element(2, Header),
                    erlang:element(3, Header),
                    {some, Unix_seconds},
                    erlang:element(5, Header)}}
        end
    ).

-file("src/packkit/gzip.gleam", 178).
?DOC(
    " Attach an optional Unix mtime.  Out-of-range values panic at\n"
    " construction time so a `Header` value cannot quietly carry a\n"
    " timestamp gzip's 32-bit MTIME field cannot represent.  Use\n"
    " [with_modified_at_checked] when the input is untrusted.\n"
).
-spec with_modified_at(header(), integer()) -> header().
with_modified_at(Header, Unix_seconds) ->
    case with_modified_at_checked(Header, Unix_seconds) of
        {ok, H} ->
            H;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_modified_at: unix_seconds must be in the inclusive range 0..0xFFFFFFFF"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_modified_at"/utf8>>,
                    line => 185})
    end.

-file("src/packkit/gzip.gleam", 203).
?DOC(" Read the optional filename field.\n").
-spec name(header()) -> gleam@option:option(binary()).
name(Header) ->
    erlang:element(2, Header).

-file("src/packkit/gzip.gleam", 208).
?DOC(" Read the optional comment field.\n").
-spec comment(header()) -> gleam@option:option(binary()).
comment(Header) ->
    erlang:element(3, Header).

-file("src/packkit/gzip.gleam", 213).
?DOC(" Read the optional mtime field.\n").
-spec modified_at_unix(header()) -> gleam@option:option(integer()).
modified_at_unix(Header) ->
    erlang:element(4, Header).

-file("src/packkit/gzip.gleam", 219).
?DOC(
    " Read the FEXTRA subfields.  Empty when the gzip header carries\n"
    " no FEXTRA region.\n"
).
-spec extra(header()) -> list(subfield()).
extra(Header) ->
    erlang:element(5, Header).

-file("src/packkit/gzip.gleam", 256).
-spec validate_extra_subfield_ids(list(subfield())) -> {ok, nil} |
    {error, header_error()}.
validate_extra_subfield_ids(Subfields) ->
    case Subfields of
        [] ->
            {ok, nil};

        [{subfield, Id_1, Id_2, _} | Rest] ->
            case (((Id_1 < 0) orelse (Id_1 > 16#FF)) orelse (Id_2 < 0)) orelse (Id_2
            > 16#FF) of
                true ->
                    {error, {header_extra_subfield_id_out_of_range, Id_1, Id_2}};

                false ->
                    validate_extra_subfield_ids(Rest)
            end
    end.

-file("src/packkit/gzip.gleam", 269).
-spec measure_extra_subfields(list(subfield()), integer()) -> {ok, integer()} |
    {error, header_error()}.
measure_extra_subfields(Subfields, Acc) ->
    case Subfields of
        [] ->
            {ok, Acc};

        [{subfield, _, _, Data} | Rest] ->
            Len = erlang:byte_size(Data),
            case Len > 16#FFFF of
                true ->
                    {error, {header_extra_subfield_too_long, Len}};

                false ->
                    measure_extra_subfields(Rest, (Acc + 4) + Len)
            end
    end.

-file("src/packkit/gzip.gleam", 243).
?DOC(
    " Attach a list of FEXTRA subfields after validating that every\n"
    " subfield ID byte fits the 8-bit slot, every subfield body fits\n"
    " gzip's 16-bit LEN, and the catenated total fits the 16-bit\n"
    " XLEN.  Returns a typed `HeaderError` on any of those overflows.\n"
).
-spec with_extra_checked(header(), list(subfield())) -> {ok, header()} |
    {error, header_error()}.
with_extra_checked(Header, Subfields) ->
    gleam@result:'try'(
        validate_extra_subfield_ids(Subfields),
        fun(_) ->
            gleam@result:'try'(
                measure_extra_subfields(Subfields, 0),
                fun(Total) ->
                    gleam@bool:guard(
                        Total > 16#FFFF,
                        {error, {header_extra_total_too_long, Total}},
                        fun() ->
                            {ok,
                                {header,
                                    erlang:element(2, Header),
                                    erlang:element(3, Header),
                                    erlang:element(4, Header),
                                    Subfields}}
                        end
                    )
                end
            )
        end
    ).

-file("src/packkit/gzip.gleam", 226).
?DOC(
    " Attach a list of FEXTRA subfields.  Out-of-range IDs or\n"
    " overlong bodies panic; use [with_extra_checked] when the caller\n"
    " has not pre-validated the values.\n"
).
-spec with_extra(header(), list(subfield())) -> header().
with_extra(Header, Subfields) ->
    case with_extra_checked(Header, Subfields) of
        {ok, H} ->
            H;

        {error, {header_extra_subfield_id_out_of_range, _, _}} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_extra: subfield id bytes must each be in 0..255"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_extra"/utf8>>,
                    line => 230});

        {error, {header_extra_subfield_too_long, _}} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_extra: each subfield body must be at most 65535 bytes"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_extra"/utf8>>,
                    line => 232});

        {error, {header_extra_total_too_long, _}} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_extra: total FEXTRA region must be at most 65535 bytes"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_extra"/utf8>>,
                    line => 234});

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/gzip.with_extra: unexpected validation error"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/gzip"/utf8>>,
                    function => <<"with_extra"/utf8>>,
                    line => 235})
    end.

-file("src/packkit/gzip.gleam", 374).
-spec encode_extra_subfields(list(subfield()), bitstring()) -> bitstring().
encode_extra_subfields(Subfields, Acc) ->
    case Subfields of
        [] ->
            Acc;

        [{subfield, Id_1, Id_2, Data} | Rest] ->
            Len = erlang:byte_size(Data),
            Chunk = gleam_stdlib:bit_array_concat(
                [<<Id_1, Id_2, Len:16/little>>, Data]
            ),
            encode_extra_subfields(
                Rest,
                gleam_stdlib:bit_array_concat([Acc, Chunk])
            )
    end.

-file("src/packkit/gzip.gleam", 363).
-spec encode_extra_block(list(subfield())) -> bitstring().
encode_extra_block(Subfields) ->
    case Subfields of
        [] ->
            <<>>;

        _ ->
            Body = encode_extra_subfields(Subfields, <<>>),
            Xlen = erlang:byte_size(Body),
            gleam_stdlib:bit_array_concat([<<Xlen:16/little>>, Body])
    end.

-file("src/packkit/gzip.gleam", 385).
-spec trailer_bytes(bitstring()) -> bitstring().
trailer_bytes(Plain) ->
    Crc = packkit@checksum:crc32(Plain),
    Isize = erlang:'band'(erlang:byte_size(Plain), 16#FFFFFFFF),
    <<Crc:32/little, Isize:32/little>>.

-file("src/packkit/gzip.gleam", 604).
-spec apply_optional_name(header(), gleam@option:option(binary())) -> header().
apply_optional_name(Header, Value) ->
    case Value of
        {some, V} ->
            with_name(Header, V);

        none ->
            Header
    end.

-file("src/packkit/gzip.gleam", 611).
-spec apply_optional_comment(header(), gleam@option:option(binary())) -> header().
apply_optional_comment(Header, Value) ->
    case Value of
        {some, V} ->
            with_comment(Header, V);

        none ->
            Header
    end.

-file("src/packkit/gzip.gleam", 618).
-spec apply_extra(header(), list(subfield())) -> header().
apply_extra(Header, Subfields) ->
    case Subfields of
        [] ->
            Header;

        _ ->
            {header,
                erlang:element(2, Header),
                erlang:element(3, Header),
                erlang:element(4, Header),
                Subfields}
    end.

-file("src/packkit/gzip.gleam", 655).
-spec parse_extra_subfields(bitstring(), list(subfield())) -> {ok,
        list(subfield())} |
    {error, packkit@error:codec_error()}.
parse_extra_subfields(Bytes, Acc) ->
    case erlang:byte_size(Bytes) of
        0 ->
            {ok, Acc};

        _ ->
            case Bytes of
                <<Id_1, Id_2, Len:16/little, Rest/binary>> ->
                    case erlang:byte_size(Rest) < Len of
                        true ->
                            {error,
                                {codec_invalid_data,
                                    <<"gzip extra subfield body truncated"/utf8>>}};

                        false ->
                            Data@1 = case gleam_stdlib:bit_array_slice(
                                Rest,
                                0,
                                Len
                            ) of
                                {ok, Data} -> Data;
                                _assert_fail ->
                                    erlang:error(#{gleam_error => let_assert,
                                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                                file => <<?FILEPATH/utf8>>,
                                                module => <<"packkit/gzip"/utf8>>,
                                                function => <<"parse_extra_subfields"/utf8>>,
                                                line => 670,
                                                value => _assert_fail,
                                                start => 21538,
                                                'end' => 21589,
                                                pattern_start => 21549,
                                                pattern_end => 21557})
                            end,
                            Remaining@1 = case gleam_stdlib:bit_array_slice(
                                Rest,
                                Len,
                                erlang:byte_size(Rest) - Len
                            ) of
                                {ok, Remaining} -> Remaining;
                                _assert_fail@1 ->
                                    erlang:error(#{gleam_error => let_assert,
                                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                                file => <<?FILEPATH/utf8>>,
                                                module => <<"packkit/gzip"/utf8>>,
                                                function => <<"parse_extra_subfields"/utf8>>,
                                                line => 671,
                                                value => _assert_fail@1,
                                                start => 21604,
                                                'end' => 21706,
                                                pattern_start => 21615,
                                                pattern_end => 21628})
                            end,
                            parse_extra_subfields(
                                Remaining@1,
                                [{subfield, Id_1, Id_2, Data@1} | Acc]
                            )
                    end;

                _ ->
                    {error,
                        {codec_invalid_data,
                            <<"gzip extra subfield header truncated"/utf8>>}}
            end
    end.

-file("src/packkit/gzip.gleam", 717).
-spec latin1_decode(bitstring(), binary()) -> binary().
latin1_decode(Bytes, Acc) ->
    case Bytes of
        <<>> ->
            Acc;

        <<B, Rest/binary>> ->
            Cp = case B < 16#80 of
                true ->
                    <<B>>;

                false ->
                    <<(erlang:'bor'(16#C0, erlang:'bsr'(B, 6))),
                        (erlang:'bor'(16#80, erlang:'band'(B, 16#3F)))>>
            end,
            Ch@1 = case gleam@bit_array:to_string(Cp) of
                {ok, Ch} -> Ch;
                _assert_fail ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"packkit/gzip"/utf8>>,
                                function => <<"latin1_decode"/utf8>>,
                                line => 736,
                                value => _assert_fail,
                                start => 24024,
                                'end' => 24067,
                                pattern_start => 24035,
                                pattern_end => 24041})
            end,
            latin1_decode(Rest, <<Acc/binary, Ch@1/binary>>);

        _ ->
            Acc
    end.

-file("src/packkit/gzip.gleam", 743).
-spec split_at_nul(bitstring(), integer()) -> {ok, {bitstring(), bitstring()}} |
    {error, packkit@error:codec_error()}.
split_at_nul(Bytes, Offset) ->
    case gleam_stdlib:bit_array_slice(Bytes, Offset, 1) of
        {ok, <<0>>} ->
            Prefix@1 = case gleam_stdlib:bit_array_slice(Bytes, 0, Offset) of
                {ok, Prefix} -> Prefix;
                _assert_fail ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"packkit/gzip"/utf8>>,
                                function => <<"split_at_nul"/utf8>>,
                                line => 749,
                                value => _assert_fail,
                                start => 24305,
                                'end' => 24362,
                                pattern_start => 24316,
                                pattern_end => 24326})
            end,
            Total = erlang:byte_size(Bytes),
            Rest@1 = case gleam_stdlib:bit_array_slice(
                Bytes,
                Offset + 1,
                (Total - Offset) - 1
            ) of
                {ok, Rest} -> Rest;
                _assert_fail@1 ->
                    erlang:error(#{gleam_error => let_assert,
                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                file => <<?FILEPATH/utf8>>,
                                module => <<"packkit/gzip"/utf8>>,
                                function => <<"split_at_nul"/utf8>>,
                                line => 751,
                                value => _assert_fail@1,
                                start => 24414,
                                'end' => 24498,
                                pattern_start => 24425,
                                pattern_end => 24433})
            end,
            {ok, {Prefix@1, Rest@1}};

        {ok, _} ->
            split_at_nul(Bytes, Offset + 1);

        {error, _} ->
            {error,
                {codec_invalid_data, <<"gzip header string missing NUL"/utf8>>}}
    end.

-file("src/packkit/gzip.gleam", 696).
-spec maybe_read_string(bitstring(), integer(), integer()) -> {ok,
        {bitstring(), gleam@option:option(binary())}} |
    {error, packkit@error:codec_error()}.
maybe_read_string(Bytes, Flg, Mask) ->
    case erlang:'band'(Flg, Mask) of
        0 ->
            {ok, {Bytes, none}};

        _ ->
            case split_at_nul(Bytes, 0) of
                {ok, {String_bits, After}} ->
                    Decoded = case gleam@bit_array:to_string(String_bits) of
                        {ok, Value} ->
                            Value;

                        {error, _} ->
                            latin1_decode(String_bits, <<""/utf8>>)
                    end,
                    {ok, {After, {some, Decoded}}};

                {error, Err} ->
                    {error, Err}
            end
    end.

-file("src/packkit/gzip.gleam", 797).
-spec read_header_crc16(bitstring()) -> {ok, {integer(), bitstring()}} |
    {error, packkit@error:codec_error()}.
read_header_crc16(Bytes) ->
    case Bytes of
        <<Expected:16/little, Rest/binary>> ->
            {ok, {Expected, Rest}};

        _ ->
            {error, {codec_invalid_data, <<"gzip header CRC missing"/utf8>>}}
    end.

-file("src/packkit/gzip.gleam", 806).
-spec slice_header_for_crc(bitstring(), integer()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
slice_header_for_crc(Original, Header_len) ->
    case gleam_stdlib:bit_array_slice(Original, 0, Header_len) of
        {ok, Header_bytes} ->
            {ok, Header_bytes};

        {error, _} ->
            {error,
                {codec_invalid_data, <<"gzip header CRC bounds error"/utf8>>}}
    end.

-file("src/packkit/gzip.gleam", 782).
-spec verify_header_crc(bitstring(), bitstring()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
verify_header_crc(Bytes, Original) ->
    Header_len = erlang:byte_size(Original) - erlang:byte_size(Bytes),
    gleam@result:'try'(
        read_header_crc16(Bytes),
        fun(_use0) ->
            {Expected, Rest} = _use0,
            gleam@result:'try'(
                slice_header_for_crc(Original, Header_len),
                fun(Header_bytes) ->
                    Actual = erlang:'band'(
                        packkit@checksum:crc32(Header_bytes),
                        16#FFFF
                    ),
                    case Actual =:= Expected of
                        true ->
                            {ok, Rest};

                        false ->
                            {error,
                                {codec_invalid_data,
                                    <<"gzip header CRC16 mismatch"/utf8>>}}
                    end
                end
            )
        end
    ).

-file("src/packkit/gzip.gleam", 818).
?DOC(" Create a new incremental decoder state using the default limits.\n").
-spec new_decoder() -> decoder().
new_decoder() ->
    {decoder, [], 0, packkit@limit:default()}.

-file("src/packkit/gzip.gleam", 823).
?DOC(" Create a new incremental decoder state with explicit limits.\n").
-spec new_decoder_with_limits(packkit@limit:limits()) -> decoder().
new_decoder_with_limits(Limits) ->
    {decoder, [], 0, Limits}.

-file("src/packkit/gzip.gleam", 836).
?DOC(
    " Append a chunk of input bytes to the decoder, enforcing\n"
    " `max_input_bytes` incrementally.  Returns the updated decoder; no\n"
    " output is produced until [finish] runs (the underlying DEFLATE\n"
    " decoder is eager).\n"
    "\n"
    " The shape mirrors [packkit/stream] so callers don't have to remember\n"
    " which streaming module returns which tuple — previously this push\n"
    " returned `(Decoder, List(BitArray))` and the equivalent\n"
    " `stream.push` returned a bare `Decoder`.\n"
).
-spec push(decoder(), bitstring()) -> {ok, decoder()} |
    {error, packkit@error:codec_error()}.
push(Decoder, Chunk) ->
    Chunk_size = erlang:byte_size(Chunk),
    New_total = erlang:element(3, Decoder) + Chunk_size,
    case New_total > packkit@limit:max_input_bytes(erlang:element(4, Decoder)) of
        true ->
            {error,
                {codec_limit_exceeded, <<"max_input_bytes"/utf8>>, New_total}};

        false ->
            {ok,
                {decoder,
                    [Chunk | erlang:element(2, Decoder)],
                    New_total,
                    erlang:element(4, Decoder)}}
    end.

-file("src/packkit/gzip.gleam", 771).
-spec maybe_verify_header_crc(bitstring(), integer(), bitstring()) -> {ok,
        bitstring()} |
    {error, packkit@error:codec_error()}.
maybe_verify_header_crc(Bytes, Flg, Original) ->
    case erlang:'band'(Flg, 16#02) of
        0 ->
            {ok, Bytes};

        _ ->
            verify_header_crc(Bytes, Original)
    end.

-file("src/packkit/gzip.gleam", 625).
-spec maybe_read_extra(bitstring(), integer()) -> {ok,
        {bitstring(), list(subfield())}} |
    {error, packkit@error:codec_error()}.
maybe_read_extra(Bytes, Flg) ->
    case erlang:'band'(Flg, 16#04) of
        0 ->
            {ok, {Bytes, []}};

        _ ->
            case Bytes of
                <<Xlen:16/little, Rest/binary>> ->
                    case erlang:byte_size(Rest) < Xlen of
                        true ->
                            {error,
                                {codec_invalid_data,
                                    <<"gzip extra field truncated"/utf8>>}};

                        false ->
                            Extra_bytes@1 = case gleam_stdlib:bit_array_slice(
                                Rest,
                                0,
                                Xlen
                            ) of
                                {ok, Extra_bytes} -> Extra_bytes;
                                _assert_fail ->
                                    erlang:error(#{gleam_error => let_assert,
                                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                                file => <<?FILEPATH/utf8>>,
                                                module => <<"packkit/gzip"/utf8>>,
                                                function => <<"maybe_read_extra"/utf8>>,
                                                line => 640,
                                                value => _assert_fail,
                                                start => 20576,
                                                'end' => 20635,
                                                pattern_start => 20587,
                                                pattern_end => 20602})
                            end,
                            After@1 = case gleam_stdlib:bit_array_slice(
                                Rest,
                                Xlen,
                                erlang:byte_size(Rest) - Xlen
                            ) of
                                {ok, After} -> After;
                                _assert_fail@1 ->
                                    erlang:error(#{gleam_error => let_assert,
                                                message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
                                                file => <<?FILEPATH/utf8>>,
                                                module => <<"packkit/gzip"/utf8>>,
                                                function => <<"maybe_read_extra"/utf8>>,
                                                line => 641,
                                                value => _assert_fail@1,
                                                start => 20650,
                                                'end' => 20750,
                                                pattern_start => 20661,
                                                pattern_end => 20670})
                            end,
                            gleam@result:'try'(
                                parse_extra_subfields(Extra_bytes@1, []),
                                fun(Subfields) ->
                                    {ok, {After@1, lists:reverse(Subfields)}}
                                end
                            )
                    end;

                _ ->
                    {error,
                        {codec_invalid_data,
                            <<"gzip extra header truncated"/utf8>>}}
            end
    end.

-file("src/packkit/gzip.gleam", 549).
-spec decode_header_and_payload(
    bitstring(),
    integer(),
    header(),
    packkit@limit:limits(),
    bitstring()
) -> {ok, {decoded(), bitstring()}} | {error, packkit@error:codec_error()}.
decode_header_and_payload(Bytes, Flg, Acc, Limits, Original) ->
    gleam@result:'try'(
        maybe_read_extra(Bytes, Flg),
        fun(_use0) ->
            {Bytes@1, Extra_value} = _use0,
            gleam@result:'try'(
                maybe_read_string(Bytes@1, Flg, 16#08),
                fun(_use0@1) ->
                    {Bytes@2, Name_value} = _use0@1,
                    gleam@result:'try'(
                        maybe_read_string(Bytes@2, Flg, 16#10),
                        fun(_use0@2) ->
                            {Bytes@3, Comment_value} = _use0@2,
                            gleam@result:'try'(
                                maybe_verify_header_crc(Bytes@3, Flg, Original),
                                fun(Bytes@4) ->
                                    Header = begin
                                        _pipe = Acc,
                                        _pipe@1 = apply_optional_name(
                                            _pipe,
                                            Name_value
                                        ),
                                        _pipe@2 = apply_optional_comment(
                                            _pipe@1,
                                            Comment_value
                                        ),
                                        apply_extra(_pipe@2, Extra_value)
                                    end,
                                    _ = erlang:'band'(Flg, 16#01),
                                    gleam@result:'try'(
                                        packkit@deflate:decode_with_remainder(
                                            Bytes@4,
                                            Limits
                                        ),
                                        fun(_use0@3) ->
                                            {Plain, After_deflate} = _use0@3,
                                            case After_deflate of
                                                <<Expected_crc:32/little,
                                                    Expected_isize:32/little,
                                                    Rest/binary>> ->
                                                    gleam@bool:guard(
                                                        packkit@checksum:crc32(
                                                            Plain
                                                        )
                                                        /= Expected_crc,
                                                        {error,
                                                            {codec_invalid_data,
                                                                <<"gzip CRC-32 mismatch"/utf8>>}},
                                                        fun() ->
                                                            Isize = erlang:'band'(
                                                                erlang:byte_size(
                                                                    Plain
                                                                ),
                                                                16#FFFFFFFF
                                                            ),
                                                            gleam@bool:guard(
                                                                Isize /= Expected_isize,
                                                                {error,
                                                                    {codec_invalid_data,
                                                                        <<"gzip ISIZE mismatch"/utf8>>}},
                                                                fun() ->
                                                                    {ok,
                                                                        {{decoded,
                                                                                Header,
                                                                                Plain},
                                                                            Rest}}
                                                                end
                                                            )
                                                        end
                                                    );

                                                _ ->
                                                    {error,
                                                        {codec_invalid_data,
                                                            <<"gzip trailer truncated"/utf8>>}}
                                            end
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/packkit/gzip.gleam", 513).
-spec decode_one_member(bitstring(), packkit@limit:limits()) -> {ok,
        {decoded(), bitstring()}} |
    {error, packkit@error:codec_error()}.
decode_one_member(Bytes, Limits) ->
    case Bytes of
        <<M1, M2, Cm, Flg, Mtime:32/little, _, _, Rest/binary>> ->
            gleam@bool:guard(
                (M1 /= 16#1F) orelse (M2 /= 16#8B),
                {error, {codec_invalid_data, <<"gzip magic mismatch"/utf8>>}},
                fun() ->
                    gleam@bool:guard(
                        Cm /= 8,
                        {error,
                            {codec_invalid_data,
                                <<"gzip compression method is not deflate"/utf8>>}},
                        fun() ->
                            gleam@bool:guard(
                                erlang:'band'(Flg, 16#E0) /= 0,
                                {error,
                                    {codec_invalid_data,
                                        <<"gzip reserved FLG bits set"/utf8>>}},
                                fun() ->
                                    Initial = case Mtime of
                                        0 ->
                                            default_header();

                                        N ->
                                            _record = default_header(),
                                            {header,
                                                erlang:element(2, _record),
                                                erlang:element(3, _record),
                                                {some, N},
                                                erlang:element(5, _record)}
                                    end,
                                    decode_header_and_payload(
                                        Rest,
                                        Flg,
                                        Initial,
                                        Limits,
                                        Bytes
                                    )
                                end
                            )
                        end
                    )
                end
            );

        _ ->
            {error, {codec_invalid_data, <<"gzip header truncated"/utf8>>}}
    end.

-file("src/packkit/gzip.gleam", 484).
-spec decode_remaining_members(
    bitstring(),
    bitstring(),
    integer(),
    packkit@limit:limits()
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
decode_remaining_members(Bytes, Acc, Accumulated_size, Limits) ->
    case erlang:byte_size(Bytes) of
        0 ->
            {ok, Acc};

        _ ->
            gleam@result:'try'(
                decode_one_member(Bytes, Limits),
                fun(_use0) ->
                    {Decoded, Rest} = _use0,
                    Next_size = Accumulated_size + erlang:byte_size(
                        erlang:element(3, Decoded)
                    ),
                    case Next_size > packkit@limit:max_output_bytes(Limits) of
                        true ->
                            {error,
                                {codec_limit_exceeded,
                                    <<"max_output_bytes"/utf8>>,
                                    Next_size}};

                        false ->
                            decode_remaining_members(
                                Rest,
                                gleam_stdlib:bit_array_concat(
                                    [Acc, erlang:element(3, Decoded)]
                                ),
                                Next_size,
                                Limits
                            )
                    end
                end
            )
    end.

-file("src/packkit/gzip.gleam", 459).
-spec decode_first_member_and_continue(bitstring(), packkit@limit:limits()) -> {ok,
        decoded()} |
    {error, packkit@error:codec_error()}.
decode_first_member_and_continue(Bytes, Limits) ->
    gleam@result:'try'(
        decode_one_member(Bytes, Limits),
        fun(_use0) ->
            {Decoded, Rest} = _use0,
            case erlang:byte_size(Rest) of
                0 ->
                    {ok, Decoded};

                _ ->
                    Initial_size = erlang:byte_size(erlang:element(3, Decoded)),
                    gleam@result:'try'(
                        decode_remaining_members(
                            Rest,
                            <<>>,
                            Initial_size,
                            Limits
                        ),
                        fun(Additional) ->
                            {ok,
                                {decoded,
                                    erlang:element(2, Decoded),
                                    gleam_stdlib:bit_array_concat(
                                        [erlang:element(3, Decoded), Additional]
                                    )}}
                        end
                    )
            end
        end
    ).

-file("src/packkit/gzip.gleam", 444).
?DOC(
    " Decode a gzip byte stream using explicit limits.\n"
    "\n"
    " Handles multi-member streams (RFC 1952 §2.2 — concatenated gzip\n"
    " files such as those produced by `cat a.gz b.gz`).  The returned\n"
    " `Decoded` carries the header from the FIRST member and the\n"
    " concatenated payload of every member that decoded successfully;\n"
    " no other gzip API exposes per-member headers yet.\n"
).
-spec decode_with_limits(bitstring(), packkit@limit:limits()) -> {ok, decoded()} |
    {error, packkit@error:codec_error()}.
decode_with_limits(Bytes, Limits) ->
    gleam@bool:guard(
        erlang:byte_size(Bytes) > packkit@limit:max_input_bytes(Limits),
        {error,
            {codec_limit_exceeded,
                <<"max_input_bytes"/utf8>>,
                erlang:byte_size(Bytes)}},
        fun() -> decode_first_member_and_continue(Bytes, Limits) end
    ).

-file("src/packkit/gzip.gleam", 407).
?DOC(
    " Decode a gzip byte stream using default limits and return the\n"
    " rich [Decoded] record (header + payload).\n"
    "\n"
    " `decode` is asymmetric with [encode]: `encode` takes payload bytes\n"
    " and emits a stream, while `decode` returns both the payload and the\n"
    " header.  The asymmetry is intentional — gzip is the only codec in\n"
    " the package that carries meaningful per-stream metadata (filename,\n"
    " comment, mtime), and surfacing it on the decode side is what makes\n"
    " `decode |> .header` useful.  When you only care about the payload\n"
    " and want the shape every other codec uses (`BitArray ->\n"
    " Result(BitArray, _)`), use [decode_payload].\n"
).
-spec decode(bitstring()) -> {ok, decoded()} |
    {error, packkit@error:codec_error()}.
decode(Bytes) ->
    decode_with_limits(Bytes, packkit@limit:default()).

-file("src/packkit/gzip.gleam", 417).
?DOC(
    " Decode a gzip byte stream and return only the payload bytes.\n"
    " Parallels every other codec's `decode/1`, which returns\n"
    " `Result(BitArray, _)` — use this when you don't need the gzip\n"
    " header (mtime / filename / comment).\n"
    "\n"
    " Law: `decode_payload(b) == decode(b) |> result.map(fn(d) { d.payload })`.\n"
).
-spec decode_payload(bitstring()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
decode_payload(Bytes) ->
    case decode(Bytes) of
        {ok, Decoded} ->
            {ok, erlang:element(3, Decoded)};

        {error, E} ->
            {error, E}
    end.

-file("src/packkit/gzip.gleam", 427).
?DOC(" Like [decode_payload] but accepts an explicit `Limits` value.\n").
-spec decode_payload_with_limits(bitstring(), packkit@limit:limits()) -> {ok,
        bitstring()} |
    {error, packkit@error:codec_error()}.
decode_payload_with_limits(Bytes, Limits) ->
    case decode_with_limits(Bytes, Limits) of
        {ok, Decoded} ->
            {ok, erlang:element(3, Decoded)};

        {error, E} ->
            {error, E}
    end.

-file("src/packkit/gzip.gleam", 863).
?DOC(
    " Finalize the decoder and return the full decoded payload.\n"
    "\n"
    " Returns a bare `BitArray` (not `List(BitArray)`) so the gzip\n"
    " streaming surface matches `packkit/stream` exactly.\n"
).
-spec finish(decoder()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
finish(Decoder) ->
    Bytes = gleam_stdlib:bit_array_concat(
        lists:reverse(erlang:element(2, Decoder))
    ),
    case decode_with_limits(Bytes, erlang:element(4, Decoder)) of
        {ok, Decoded} ->
            {ok, erlang:element(3, Decoded)};

        {error, E} ->
            {error, E}
    end.

-file("src/packkit/gzip.gleam", 302).
?DOC(
    " Encode `bytes` as a gzip stream using `header`.  The DEFLATE body\n"
    " uses the dynamic-Huffman encoder, which on typical text and\n"
    " structured-data payloads shrinks ~10–30 % more than the fixed-\n"
    " Huffman variant; for pathologically skewed inputs the encoder\n"
    " transparently falls back to fixed Huffman inside\n"
    " `deflate.encode_dynamic` so the stream is always a valid\n"
    " RFC 1951 BTYPE=01 or BTYPE=10 block.\n"
).
-spec encode_with_header(bitstring(), header()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
encode_with_header(Bytes, Header) ->
    gleam@result:'try'(
        packkit@deflate:encode_dynamic(Bytes),
        fun(Deflated) ->
            Mtime = case erlang:element(4, Header) of
                {some, Value} ->
                    Value;

                none ->
                    0
            end,
            Flag_extra = case erlang:element(5, Header) of
                [] ->
                    0;

                _ ->
                    16#04
            end,
            Flag_name = case erlang:element(2, Header) of
                {some, _} ->
                    16#08;

                none ->
                    0
            end,
            Flag_comment = case erlang:element(3, Header) of
                {some, _} ->
                    16#10;

                none ->
                    0
            end,
            Flg = erlang:'bor'(
                erlang:'bor'(Flag_extra, Flag_name),
                Flag_comment
            ),
            Header_bytes = <<16#1F, 16#8B, 8, Flg, Mtime:32/little, 0, 16#FF>>,
            Extra_block = encode_extra_block(erlang:element(5, Header)),
            Name_block = case erlang:element(2, Header) of
                {some, Value@1} ->
                    gleam_stdlib:bit_array_concat(
                        [gleam_stdlib:identity(Value@1), <<0>>]
                    );

                none ->
                    <<>>
            end,
            Comment_block = case erlang:element(3, Header) of
                {some, Value@2} ->
                    gleam_stdlib:bit_array_concat(
                        [gleam_stdlib:identity(Value@2), <<0>>]
                    );

                none ->
                    <<>>
            end,
            Trailer = trailer_bytes(Bytes),
            {ok,
                gleam_stdlib:bit_array_concat(
                    [Header_bytes,
                        Extra_block,
                        Name_block,
                        Comment_block,
                        Deflated,
                        Trailer]
                )}
        end
    ).

-file("src/packkit/gzip.gleam", 291).
?DOC(
    " Encode `bytes` as a gzip stream using the default header (no\n"
    " FNAME / FCOMMENT / FEXTRA / mtime).  Use this when you just want\n"
    " \"compress these bytes\" — the symmetric counterpart of\n"
    " `decode_payload`, mirroring every other codec's `encode/1` shape.\n"
    " Use [encode_with_header] when you need to attach a filename,\n"
    " comment, or mtime to the stream.\n"
).
-spec encode(bitstring()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
encode(Bytes) ->
    encode_with_header(Bytes, default_header()).