Skip to main content

src/packkit@stream.erl

-module(packkit@stream).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/packkit/stream.gleam").
-export([new_deflate_decoder/0, new_zlib_decoder/0, new_gzip_decoder/0, new_lz4_decoder/0, new_snappy_decoder/0, new_bzip2_decoder/0, new_lzw_decoder/0, new_xz_decoder/0, new_zstd_decoder/0, new_brotli_decoder/0, with_limits/2, push/2, finish/1, decode_chunks/2, new_deflate_encoder/0, new_zlib_encoder/0, new_gzip_encoder/0, new_lz4_encoder/0, new_snappy_encoder/0, new_bzip2_encoder/0, new_lzw_encoder/0, new_xz_encoder/0, new_zstd_encoder/0, new_brotli_encoder/0, encoder_with_limits/2, push_encoder/2, finish_encoder/1, encode_chunks/2]).
-export_type([decoder/0, decoder_kind/0, encoder/0, encoder_kind/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(
    " Streaming helpers for codec families.\n"
    "\n"
    " `packkit/stream` exposes opaque decoder states that buffer input\n"
    " chunks and run a one-shot decode at `finish` time.  The API matches\n"
    " the streaming shape the spec calls for - `new_*_decoder`, `push`,\n"
    " `finish` - so callers can already wire incremental pipelines even\n"
    " while the underlying codec decoders are still eager.\n"
    "\n"
    " **Relationship to `packkit/codec.Codec`.**  The streaming\n"
    " constructors (`new_gzip_encoder`, `new_zstd_decoder`, ...) do NOT\n"
    " accept a `packkit/codec.Codec` value, so the per-codec `Level` /\n"
    " preset `Dictionary` settings carried by a `Codec` are not threaded\n"
    " through this API; every stream uses the codec's default encode\n"
    " strategy.  This is intentional: a `Codec` is the config object\n"
    " `packkit.compress` / `packkit.decompress` (the one-shot facade)\n"
    " consumes, while `packkit/stream` only ever invokes the codec\n"
    " module's bare `encode/1` / `decode/1` entry points.  Callers that\n"
    " need a non-default level or a preset dictionary should call the\n"
    " per-codec module directly (`gzip.encode_with_header`,\n"
    " `zlib.encode_with_dictionary`, `deflate.encode_dynamic`, …) and\n"
    " drive the chunking themselves until the streaming API grows\n"
    " codec-typed constructors.  The `Limits` value, by contrast, IS\n"
    " honoured incrementally — see [push] / [push_encoder].\n"
).

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

-type decoder_kind() :: deflate |
    zlib |
    gzip |
    lz4 |
    snappy |
    bzip2 |
    lzw |
    xz |
    zstd |
    brotli.

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

-type encoder_kind() :: enc_deflate |
    enc_zlib |
    enc_gzip |
    enc_lz4 |
    enc_snappy |
    enc_bzip2 |
    enc_lzw |
    enc_xz |
    enc_zstd |
    enc_brotli.

-file("src/packkit/stream.gleam", 123).
-spec new_decoder(decoder_kind(), packkit@limit:limits()) -> decoder().
new_decoder(Kind, Limits) ->
    {decoder, Kind, [], 0, Limits}.

-file("src/packkit/stream.gleam", 72).
?DOC(" Start a new incremental DEFLATE decoder using the default limits.\n").
-spec new_deflate_decoder() -> decoder().
new_deflate_decoder() ->
    new_decoder(deflate, packkit@limit:default()).

-file("src/packkit/stream.gleam", 77).
?DOC(" Start a new incremental zlib decoder using the default limits.\n").
-spec new_zlib_decoder() -> decoder().
new_zlib_decoder() ->
    new_decoder(zlib, packkit@limit:default()).

-file("src/packkit/stream.gleam", 82).
?DOC(" Start a new incremental gzip decoder using the default limits.\n").
-spec new_gzip_decoder() -> decoder().
new_gzip_decoder() ->
    new_decoder(gzip, packkit@limit:default()).

-file("src/packkit/stream.gleam", 87).
?DOC(" Start a new incremental LZ4 frame decoder using the default limits.\n").
-spec new_lz4_decoder() -> decoder().
new_lz4_decoder() ->
    new_decoder(lz4, packkit@limit:default()).

-file("src/packkit/stream.gleam", 93).
?DOC(
    " Start a new incremental Snappy (framed) decoder using the default\n"
    " limits.  See [packkit/snappy] for the raw-block variants.\n"
).
-spec new_snappy_decoder() -> decoder().
new_snappy_decoder() ->
    new_decoder(snappy, packkit@limit:default()).

-file("src/packkit/stream.gleam", 98).
?DOC(" Start a new incremental bzip2 decoder using the default limits.\n").
-spec new_bzip2_decoder() -> decoder().
new_bzip2_decoder() ->
    new_decoder(bzip2, packkit@limit:default()).

-file("src/packkit/stream.gleam", 104).
?DOC(
    " Start a new incremental Unix `.Z` (LZW) decoder using the default\n"
    " limits.\n"
).
-spec new_lzw_decoder() -> decoder().
new_lzw_decoder() ->
    new_decoder(lzw, packkit@limit:default()).

-file("src/packkit/stream.gleam", 109).
?DOC(" Start a new incremental xz decoder using the default limits.\n").
-spec new_xz_decoder() -> decoder().
new_xz_decoder() ->
    new_decoder(xz, packkit@limit:default()).

-file("src/packkit/stream.gleam", 114).
?DOC(" Start a new incremental zstd decoder using the default limits.\n").
-spec new_zstd_decoder() -> decoder().
new_zstd_decoder() ->
    new_decoder(zstd, packkit@limit:default()).

-file("src/packkit/stream.gleam", 119).
?DOC(" Start a new incremental brotli decoder using the default limits.\n").
-spec new_brotli_decoder() -> decoder().
new_brotli_decoder() ->
    new_decoder(brotli, packkit@limit:default()).

-file("src/packkit/stream.gleam", 128).
?DOC(" Replace the limits used by an incremental decoder.\n").
-spec with_limits(decoder(), packkit@limit:limits()) -> decoder().
with_limits(Decoder, Limits) ->
    {decoder,
        erlang:element(2, Decoder),
        erlang:element(3, Decoder),
        erlang:element(4, Decoder),
        Limits}.

-file("src/packkit/stream.gleam", 136).
?DOC(
    " Append a chunk of input bytes to the decoder, enforcing\n"
    " `max_input_bytes` incrementally.  Returns a typed\n"
    " `CodecLimitExceeded` if the running buffered byte count would\n"
    " exceed the configured limit.\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(4, Decoder) + Chunk_size,
    case New_total > packkit@limit:max_input_bytes(erlang:element(5, Decoder)) of
        true ->
            {error,
                {codec_limit_exceeded, <<"max_input_bytes"/utf8>>, New_total}};

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

-file("src/packkit/stream.gleam", 160).
?DOC(" Finalize the decoder and return the full decoded payload.\n").
-spec finish(decoder()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
finish(Decoder) ->
    Bytes = gleam_stdlib:bit_array_concat(
        lists:reverse(erlang:element(3, Decoder))
    ),
    case erlang:element(2, Decoder) of
        deflate ->
            packkit@deflate:decode_with_limits(
                Bytes,
                erlang:element(5, Decoder)
            );

        zlib ->
            packkit@zlib:decode_with_limits(Bytes, erlang:element(5, Decoder));

        gzip ->
            _pipe = packkit@gzip:decode_with_limits(
                Bytes,
                erlang:element(5, Decoder)
            ),
            gleam@result:map(
                _pipe,
                fun(Decoded) -> erlang:element(3, Decoded) end
            );

        lz4 ->
            packkit@lz4:decode_with_limits(Bytes, erlang:element(5, Decoder));

        snappy ->
            packkit@snappy:decode_with_limits(Bytes, erlang:element(5, Decoder));

        bzip2 ->
            packkit@bzip2:decode_with_limits(Bytes, erlang:element(5, Decoder));

        lzw ->
            packkit@lzw:decode_with_limits(Bytes, erlang:element(5, Decoder));

        xz ->
            packkit@xz:decode_with_limits(Bytes, erlang:element(5, Decoder));

        zstd ->
            packkit@zstd:decode_with_limits(Bytes, erlang:element(5, Decoder));

        brotli ->
            packkit@brotli:decode_with_limits(Bytes, erlang:element(5, Decoder))
    end.

-file("src/packkit/stream.gleam", 193).
-spec feed_all(decoder(), list(bitstring())) -> {ok, decoder()} |
    {error, packkit@error:codec_error()}.
feed_all(Decoder, Chunks) ->
    case Chunks of
        [] ->
            {ok, Decoder};

        [Head | Rest] ->
            gleam@result:'try'(
                push(Decoder, Head),
                fun(Next) -> feed_all(Next, Rest) end
            )
    end.

-file("src/packkit/stream.gleam", 185).
?DOC(
    " Convenience helper that pushes every chunk through the decoder in\n"
    " order and returns the final decoded payload.  Surfaces the same\n"
    " typed `CodecLimitExceeded` `push` would, so a long sequence of\n"
    " chunks cannot silently overrun the input budget.\n"
).
-spec decode_chunks(decoder(), list(bitstring())) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
decode_chunks(Decoder, Chunks) ->
    gleam@result:'try'(feed_all(Decoder, Chunks), fun(Fed) -> finish(Fed) end).

-file("src/packkit/stream.gleam", 293).
-spec new_encoder(encoder_kind(), packkit@limit:limits()) -> encoder().
new_encoder(Kind, Limits) ->
    {encoder, Kind, [], 0, Limits}.

-file("src/packkit/stream.gleam", 239).
?DOC(" Start a new incremental DEFLATE encoder using the default limits.\n").
-spec new_deflate_encoder() -> encoder().
new_deflate_encoder() ->
    new_encoder(enc_deflate, packkit@limit:default()).

-file("src/packkit/stream.gleam", 244).
?DOC(" Start a new incremental zlib encoder using the default limits.\n").
-spec new_zlib_encoder() -> encoder().
new_zlib_encoder() ->
    new_encoder(enc_zlib, packkit@limit:default()).

-file("src/packkit/stream.gleam", 252).
?DOC(
    " Start a new incremental gzip encoder using the default limits.\n"
    " The output uses `gzip.default_header()` — callers that need\n"
    " per-stream metadata should keep using `gzip.encode` directly\n"
    " until the streaming API grows an explicit header constructor.\n"
).
-spec new_gzip_encoder() -> encoder().
new_gzip_encoder() ->
    new_encoder(enc_gzip, packkit@limit:default()).

-file("src/packkit/stream.gleam", 257).
?DOC(" Start a new incremental LZ4 frame encoder using the default limits.\n").
-spec new_lz4_encoder() -> encoder().
new_lz4_encoder() ->
    new_encoder(enc_lz4, packkit@limit:default()).

-file("src/packkit/stream.gleam", 263).
?DOC(
    " Start a new incremental Snappy (framed) encoder using the default\n"
    " limits.\n"
).
-spec new_snappy_encoder() -> encoder().
new_snappy_encoder() ->
    new_encoder(enc_snappy, packkit@limit:default()).

-file("src/packkit/stream.gleam", 268).
?DOC(" Start a new incremental bzip2 encoder using the default limits.\n").
-spec new_bzip2_encoder() -> encoder().
new_bzip2_encoder() ->
    new_encoder(enc_bzip2, packkit@limit:default()).

-file("src/packkit/stream.gleam", 274).
?DOC(
    " Start a new incremental Unix `.Z` (LZW) encoder using the default\n"
    " limits.\n"
).
-spec new_lzw_encoder() -> encoder().
new_lzw_encoder() ->
    new_encoder(enc_lzw, packkit@limit:default()).

-file("src/packkit/stream.gleam", 279).
?DOC(" Start a new incremental xz encoder using the default limits.\n").
-spec new_xz_encoder() -> encoder().
new_xz_encoder() ->
    new_encoder(enc_xz, packkit@limit:default()).

-file("src/packkit/stream.gleam", 284).
?DOC(" Start a new incremental zstd encoder using the default limits.\n").
-spec new_zstd_encoder() -> encoder().
new_zstd_encoder() ->
    new_encoder(enc_zstd, packkit@limit:default()).

-file("src/packkit/stream.gleam", 289).
?DOC(" Start a new incremental brotli encoder using the default limits.\n").
-spec new_brotli_encoder() -> encoder().
new_brotli_encoder() ->
    new_encoder(enc_brotli, packkit@limit:default()).

-file("src/packkit/stream.gleam", 298).
?DOC(" Replace the limits used by an incremental encoder.\n").
-spec encoder_with_limits(encoder(), packkit@limit:limits()) -> encoder().
encoder_with_limits(Encoder, Limits) ->
    {encoder,
        erlang:element(2, Encoder),
        erlang:element(3, Encoder),
        erlang:element(4, Encoder),
        Limits}.

-file("src/packkit/stream.gleam", 308).
?DOC(
    " Append a chunk of plaintext bytes to the encoder, enforcing\n"
    " `max_input_bytes` incrementally so an unbounded producer can't\n"
    " trigger an unbounded `encode` allocation at `finish_encoder` time.\n"
).
-spec push_encoder(encoder(), bitstring()) -> {ok, encoder()} |
    {error, packkit@error:codec_error()}.
push_encoder(Encoder, Chunk) ->
    Chunk_size = erlang:byte_size(Chunk),
    New_total = erlang:element(4, Encoder) + Chunk_size,
    case New_total > packkit@limit:max_input_bytes(erlang:element(5, Encoder)) of
        true ->
            {error,
                {codec_limit_exceeded, <<"max_input_bytes"/utf8>>, New_total}};

        false ->
            {ok,
                {encoder,
                    erlang:element(2, Encoder),
                    [Chunk | erlang:element(3, Encoder)],
                    New_total,
                    erlang:element(5, Encoder)}}
    end.

-file("src/packkit/stream.gleam", 336).
?DOC(
    " Finalize the encoder and return the compressed payload.  Each\n"
    " codec is invoked through its default `encode/1` form; advanced\n"
    " per-codec options (gzip header metadata, deflate stored blocks,\n"
    " lz4 content size, bzip2 level) are not exposed through the\n"
    " streaming wrapper and remain accessible via the per-codec module.\n"
).
-spec finish_encoder(encoder()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
finish_encoder(Encoder) ->
    Bytes = gleam_stdlib:bit_array_concat(
        lists:reverse(erlang:element(3, Encoder))
    ),
    case erlang:element(2, Encoder) of
        enc_deflate ->
            packkit@deflate:encode(Bytes);

        enc_zlib ->
            packkit@zlib:encode(Bytes);

        enc_gzip ->
            packkit@gzip:encode(Bytes);

        enc_lz4 ->
            packkit@lz4:encode(Bytes);

        enc_snappy ->
            packkit@snappy:encode(Bytes);

        enc_bzip2 ->
            packkit@bzip2:encode(Bytes);

        enc_lzw ->
            packkit@lzw:encode(Bytes);

        enc_xz ->
            packkit@xz:encode(Bytes);

        enc_zstd ->
            packkit@zstd:encode(Bytes);

        enc_brotli ->
            packkit@brotli:encode(Bytes)
    end.

-file("src/packkit/stream.gleam", 364).
-spec feed_encoder_all(encoder(), list(bitstring())) -> {ok, encoder()} |
    {error, packkit@error:codec_error()}.
feed_encoder_all(Encoder, Chunks) ->
    case Chunks of
        [] ->
            {ok, Encoder};

        [Head | Rest] ->
            gleam@result:'try'(
                push_encoder(Encoder, Head),
                fun(Next) -> feed_encoder_all(Next, Rest) end
            )
    end.

-file("src/packkit/stream.gleam", 356).
?DOC(
    " Convenience helper that pushes every plaintext chunk through the\n"
    " encoder in order and returns the final compressed payload.  Mirrors\n"
    " `decode_chunks` so callers can drive both directions with the same\n"
    " shape (list of input chunks → single output buffer).\n"
).
-spec encode_chunks(encoder(), list(bitstring())) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
encode_chunks(Encoder, Chunks) ->
    gleam@result:'try'(
        feed_encoder_all(Encoder, Chunks),
        fun(Fed) -> finish_encoder(Fed) end
    ).