Skip to main content

src/packkit.erl

-module(packkit).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/packkit.gleam").
-export([package_version/0, decompress_with_limits/3, decompress/2, compress/2, read_with_limits/3, read/2, write/2, detect_filename/1, detect_bytes/1, detect_path_or_bytes/2, pack/2, unpack_with_limits/3, unpack/2]).

-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(
    " Facade for the `packkit` package.  This module wires the codec,\n"
    " archive, and recipe primitives so callers can read, write, pack,\n"
    " and unpack data without selecting the underlying engine by hand.\n"
).

-file("src/packkit.gleam", 60).
?DOC(" The package version.\n").
-spec package_version() -> binary().
package_version() ->
    <<"0.1.0"/utf8>>.

-file("src/packkit.gleam", 165).
-spec enforce_input_limit(bitstring(), packkit@limit:limits()) -> {ok, nil} |
    {error, packkit@error:codec_error()}.
enforce_input_limit(Bytes, Limits) ->
    Size = erlang:byte_size(Bytes),
    case Size > packkit@limit:max_input_bytes(Limits) of
        true ->
            {error, {codec_limit_exceeded, <<"max_input_bytes"/utf8>>, Size}};

        false ->
            {ok, nil}
    end.

-file("src/packkit.gleam", 219).
-spec decompress_zlib(
    bitstring(),
    packkit@codec:codec(),
    packkit@limit:limits()
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
decompress_zlib(Bytes, Codec_value, Limits) ->
    case packkit@codec:dictionary_of(Codec_value) of
        none ->
            packkit@zlib:decode_with_limits(Bytes, Limits);

        {some, Dict} ->
            packkit@zlib:decode_with_dictionary_and_limits(
                Bytes,
                packkit@codec:dictionary_bytes(Dict),
                Limits
            )
    end.

-file("src/packkit.gleam", 294).
-spec reject_dictionary(packkit@codec:codec()) -> {ok, nil} |
    {error, packkit@error:codec_error()}.
reject_dictionary(Codec_value) ->
    case packkit@codec:dictionary_of(Codec_value) of
        none ->
            {ok, nil};

        {some, _} ->
            case packkit@codec:name(Codec_value) of
                <<"zlib"/utf8>> ->
                    {ok, nil};

                Name ->
                    {error,
                        {codec_option_unsupported, <<"dictionary"/utf8>>, Name}}
            end
    end.

-file("src/packkit.gleam", 309).
-spec reject_level(packkit@codec:codec(), binary()) -> {ok, nil} |
    {error, packkit@error:codec_error()}.
reject_level(Codec_value, Codec_name) ->
    case packkit@codec:level(Codec_value) of
        none ->
            {ok, nil};

        {some, _} ->
            {error, {codec_option_unsupported, <<"level"/utf8>>, Codec_name}}
    end.

-file("src/packkit.gleam", 266).
?DOC(
    " Codecs whose encoders genuinely cannot consume a level knob — any\n"
    " non-`None` level (other than the implicit default) is reported as\n"
    " `CodecOptionUnsupported` so callers see the mismatch instead of\n"
    " the codec silently doing the same thing for every level.\n"
).
-spec compress_levelless(
    bitstring(),
    packkit@codec:codec(),
    binary(),
    fun((bitstring()) -> {ok, bitstring()} |
        {error, packkit@error:codec_error()})
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
compress_levelless(Bytes, Codec_value, Codec_name, Run) ->
    gleam@result:'try'(
        reject_dictionary(Codec_value),
        fun(_) ->
            gleam@result:'try'(
                reject_level(Codec_value, Codec_name),
                fun(_) -> Run(Bytes) end
            )
        end
    ).

-file("src/packkit.gleam", 345).
-spec default_level_value() -> integer().
default_level_value() ->
    packkit@level:value(packkit@level:default()).

-file("src/packkit.gleam", 327).
?DOC(
    " Accepts the implicit default level, rejects anything else with\n"
    " `CodecOptionUnsupported`.  Used by codecs whose encoders share a\n"
    " single fixed strategy, so non-default levels would be silently\n"
    " dropped if accepted.\n"
).
-spec reject_non_default_level(packkit@codec:codec(), binary()) -> {ok, nil} |
    {error, packkit@error:codec_error()}.
reject_non_default_level(Codec_value, Codec_name) ->
    case packkit@codec:level(Codec_value) of
        none ->
            {ok, nil};

        {some, L} ->
            case packkit@level:value(L) =:= default_level_value() of
                true ->
                    {ok, nil};

                false ->
                    {error,
                        {codec_option_unsupported, <<"level"/utf8>>, Codec_name}}
            end
    end.

-file("src/packkit.gleam", 112).
?DOC(
    " Decompress `bytes` with `codec`, threading the supplied `Limits`\n"
    " value through to the underlying codec's `decode_with_limits`\n"
    " entrypoint.  Codecs without an explicit limits hook receive their\n"
    " own family-specific defaults; today every byte-to-byte codec we\n"
    " support honours `Limits`.\n"
).
-spec decompress_with_limits(
    bitstring(),
    packkit@codec:codec(),
    packkit@limit:limits()
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
decompress_with_limits(Bytes, Codec_value, Limits) ->
    case packkit@codec:kind(Codec_value) of
        identity ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) ->
                    gleam@result:'try'(
                        reject_non_default_level(
                            Codec_value,
                            <<"identity"/utf8>>
                        ),
                        fun(_) -> _pipe = enforce_input_limit(Bytes, Limits),
                            gleam@result:map(_pipe, fun(_) -> Bytes end) end
                    )
                end
            );

        deflate ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@deflate:decode_with_limits(Bytes, Limits) end
            );

        zlib ->
            decompress_zlib(Bytes, Codec_value, Limits);

        gzip ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) ->
                    _pipe@1 = packkit@gzip:decode_with_limits(Bytes, Limits),
                    gleam@result:map(
                        _pipe@1,
                        fun(Decoded) -> erlang:element(3, Decoded) end
                    )
                end
            );

        lz4 ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@lz4:decode_with_limits(Bytes, Limits) end
            );

        snappy ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@snappy:decode_with_limits(Bytes, Limits) end
            );

        bzip2 ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@bzip2:decode_with_limits(Bytes, Limits) end
            );

        lzw ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@lzw:decode_with_limits(Bytes, Limits) end
            );

        xz ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@xz:decode_with_limits(Bytes, Limits) end
            );

        zstd ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@zstd:decode_with_limits(Bytes, Limits) end
            );

        brotli ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) -> packkit@brotli:decode_with_limits(Bytes, Limits) end
            )
    end.

-file("src/packkit.gleam", 96).
?DOC(
    " Decompress `bytes` with `codec` using the default limits.  Honours\n"
    " the codec's optional preset dictionary (currently only zlib) and\n"
    " otherwise rejects dictionary use with a typed error.\n"
).
-spec decompress(bitstring(), packkit@codec:codec()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
decompress(Bytes, Codec_value) ->
    decompress_with_limits(Bytes, Codec_value, packkit@limit:default()).

-file("src/packkit.gleam", 200).
-spec compress_zlib(bitstring(), packkit@codec:codec()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
compress_zlib(Bytes, Codec_value) ->
    gleam@result:'try'(
        reject_non_default_level(Codec_value, <<"zlib"/utf8>>),
        fun(_) -> case packkit@codec:dictionary_of(Codec_value) of
                none ->
                    packkit@zlib:encode(Bytes);

                {some, Dict} ->
                    packkit@zlib:encode_with_dictionary(
                        Bytes,
                        packkit@codec:dictionary_bytes(Dict)
                    )
            end end
    ).

-file("src/packkit.gleam", 235).
-spec compress_gzip(bitstring(), packkit@codec:codec()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
compress_gzip(Bytes, Codec_value) ->
    gleam@result:'try'(
        reject_dictionary(Codec_value),
        fun(_) ->
            gleam@result:'try'(
                reject_non_default_level(Codec_value, <<"gzip"/utf8>>),
                fun(_) -> packkit@gzip:encode(Bytes) end
            )
        end
    ).

-file("src/packkit.gleam", 283).
?DOC(
    " Codecs whose encoders accept a level conceptually but currently\n"
    " always emit the simplest representation (xz LZMA2 uncompressed,\n"
    " zstd raw frames, brotli uncompressed metablocks).  We accept the\n"
    " implicit default level (so the smart constructor still works) but\n"
    " reject any caller-supplied non-default level so it's never\n"
    " silently dropped.  Dictionaries are still rejected.\n"
).
-spec compress_fixed_level(
    bitstring(),
    packkit@codec:codec(),
    binary(),
    fun((bitstring()) -> {ok, bitstring()} |
        {error, packkit@error:codec_error()})
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
compress_fixed_level(Bytes, Codec_value, Codec_name, Run) ->
    gleam@result:'try'(
        reject_dictionary(Codec_value),
        fun(_) ->
            gleam@result:'try'(
                reject_non_default_level(Codec_value, Codec_name),
                fun(_) -> Run(Bytes) end
            )
        end
    ).

-file("src/packkit.gleam", 349).
-spec effective_level(packkit@codec:codec()) -> gleam@option:option(integer()).
effective_level(Codec_value) ->
    case packkit@codec:level(Codec_value) of
        {some, L} ->
            {some, packkit@level:value(L)};

        none ->
            none
    end.

-file("src/packkit.gleam", 177).
-spec compress_deflate(bitstring(), packkit@codec:codec()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
compress_deflate(Bytes, Codec_value) ->
    gleam@result:'try'(
        reject_dictionary(Codec_value),
        fun(_) -> case effective_level(Codec_value) of
                none ->
                    packkit@deflate:encode(Bytes);

                {some, 0} ->
                    packkit@deflate:encode_stored_only(Bytes);

                {some, N} ->
                    case N =:= default_level_value() of
                        true ->
                            packkit@deflate:encode(Bytes);

                        false ->
                            {error,
                                {codec_option_unsupported,
                                    <<"level"/utf8>>,
                                    <<"deflate"/utf8>>}}
                    end
            end end
    ).

-file("src/packkit.gleam", 356).
-spec int_clamp(integer(), integer(), integer()) -> integer().
int_clamp(Value, Low, High) ->
    case {Value < Low, Value > High} of
        {true, _} ->
            Low;

        {_, true} ->
            High;

        {_, _} ->
            Value
    end.

-file("src/packkit.gleam", 246).
-spec compress_bzip2(bitstring(), packkit@codec:codec()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
compress_bzip2(Bytes, Codec_value) ->
    gleam@result:'try'(
        reject_dictionary(Codec_value),
        fun(_) ->
            Level_value = case effective_level(Codec_value) of
                {some, 0} ->
                    1;

                {some, N} ->
                    int_clamp(N, 1, 9);

                none ->
                    9
            end,
            packkit@bzip2:encode_with_level(Bytes, Level_value)
        end
    ).

-file("src/packkit.gleam", 68).
?DOC(
    " Compress `bytes` with `codec`.  The codec's optional level and\n"
    " preset dictionary are honoured where the family supports them; if\n"
    " the family cannot honour an option a typed `CodecOptionUnsupported`\n"
    " error is returned instead of silently dropping the request.\n"
).
-spec compress(bitstring(), packkit@codec:codec()) -> {ok, bitstring()} |
    {error, packkit@error:codec_error()}.
compress(Bytes, Codec_value) ->
    case packkit@codec:kind(Codec_value) of
        identity ->
            gleam@result:'try'(
                reject_dictionary(Codec_value),
                fun(_) ->
                    gleam@result:'try'(
                        reject_non_default_level(
                            Codec_value,
                            <<"identity"/utf8>>
                        ),
                        fun(_) -> {ok, Bytes} end
                    )
                end
            );

        deflate ->
            compress_deflate(Bytes, Codec_value);

        zlib ->
            compress_zlib(Bytes, Codec_value);

        gzip ->
            compress_gzip(Bytes, Codec_value);

        lz4 ->
            compress_levelless(
                Bytes,
                Codec_value,
                <<"lz4"/utf8>>,
                fun packkit@lz4:encode/1
            );

        snappy ->
            compress_levelless(
                Bytes,
                Codec_value,
                <<"snappy"/utf8>>,
                fun packkit@snappy:encode/1
            );

        bzip2 ->
            compress_bzip2(Bytes, Codec_value);

        lzw ->
            compress_levelless(
                Bytes,
                Codec_value,
                <<"lzw"/utf8>>,
                fun packkit@lzw:encode/1
            );

        xz ->
            compress_fixed_level(
                Bytes,
                Codec_value,
                <<"xz"/utf8>>,
                fun packkit@xz:encode/1
            );

        zstd ->
            compress_fixed_level(
                Bytes,
                Codec_value,
                <<"zstd"/utf8>>,
                fun packkit@zstd:encode/1
            );

        brotli ->
            compress_fixed_level(
                Bytes,
                Codec_value,
                <<"brotli"/utf8>>,
                fun packkit@brotli:encode/1
            )
    end.

-file("src/packkit.gleam", 377).
?DOC(
    " Read an archive while threading the supplied `Limits` through to\n"
    " the underlying archive decoder.  Each family enforces the subset of\n"
    " limits that applies to it (input size, member count, name length,\n"
    " entry depth).\n"
).
-spec read_with_limits(
    bitstring(),
    packkit@archive:archive_format(),
    packkit@limit:limits()
) -> {ok, packkit@archive:archive()} | {error, packkit@error:archive_error()}.
read_with_limits(Bytes, Format, Limits) ->
    case packkit@archive:kind(Format) of
        tar ->
            packkit@tar:decode_with_limits(Bytes, Limits);

        zip ->
            packkit@zip:decode_with_limits(Bytes, Limits);

        cpio_newc ->
            packkit@cpio:decode_with_limits(Bytes, Limits);

        ar ->
            packkit@ar:decode_with_limits(Bytes, Limits);

        seven_z ->
            packkit@seven_z:decode_with_limits(Bytes, Limits)
    end.

-file("src/packkit.gleam", 366).
?DOC(
    " Read an archive from `bytes` interpreted as `format` using the\n"
    " default resource limits.\n"
).
-spec read(bitstring(), packkit@archive:archive_format()) -> {ok,
        packkit@archive:archive()} |
    {error, packkit@error:archive_error()}.
read(Bytes, Format) ->
    read_with_limits(Bytes, Format, packkit@limit:default()).

-file("src/packkit.gleam", 410).
-spec ensure_archive_format_matches(
    packkit@archive:archive(),
    packkit@archive:archive_format()
) -> {ok, nil} | {error, packkit@error:archive_error()}.
ensure_archive_format_matches(Archive_value, Requested) ->
    Archive_kind = packkit@archive:kind(packkit@archive:format(Archive_value)),
    Requested_kind = packkit@archive:kind(Requested),
    case Archive_kind =:= Requested_kind of
        true ->
            {ok, nil};

        false ->
            {error,
                {archive_format_mismatch,
                    packkit@archive:name(packkit@archive:format(Archive_value)),
                    packkit@archive:name(Requested)}}
    end.

-file("src/packkit.gleam", 396).
?DOC(
    " Serialise an archive to bytes using `format`.  The supplied\n"
    " `format` must match the format tag the `Archive` was constructed\n"
    " with — `Archive` is bound to one format at construction time, and\n"
    " pretending it is a different format would silently corrupt the\n"
    " output.  Mismatches surface as `ArchiveFormatMismatch`.\n"
).
-spec write(packkit@archive:archive(), packkit@archive:archive_format()) -> {ok,
        bitstring()} |
    {error, packkit@error:archive_error()}.
write(Archive_value, Format) ->
    gleam@result:'try'(
        ensure_archive_format_matches(Archive_value, Format),
        fun(_) -> case packkit@archive:kind(Format) of
                tar ->
                    packkit@tar:encode(Archive_value);

                zip ->
                    packkit@zip:encode(Archive_value);

                cpio_newc ->
                    packkit@cpio:encode(Archive_value);

                ar ->
                    packkit@ar:encode(Archive_value);

                seven_z ->
                    packkit@seven_z:encode(Archive_value)
            end end
    ).

-file("src/packkit.gleam", 474).
?DOC(" Detect from a filename or path suffix.\n").
-spec detect_filename(binary()) -> {ok, packkit@detect:detected()} |
    {error, packkit@error:detect_error()}.
detect_filename(Path) ->
    packkit@detect:from_filename(Path).

-file("src/packkit.gleam", 479).
?DOC(" Detect from byte signatures.\n").
-spec detect_bytes(bitstring()) -> {ok, packkit@detect:detected()} |
    {error, packkit@error:detect_error()}.
detect_bytes(Bytes) ->
    packkit@detect:from_bytes(Bytes).

-file("src/packkit.gleam", 487).
?DOC(
    " Detect via filename first, then fall back to magic-byte detection\n"
    " on the supplied content.  Re-exports\n"
    " `packkit/detect.from_path_or_bytes` from the top-level facade so\n"
    " most CLI integrations only need to import `packkit`.\n"
).
-spec detect_path_or_bytes(binary(), bitstring()) -> {ok,
        packkit@detect:detected()} |
    {error, packkit@error:detect_error()}.
detect_path_or_bytes(Path, Bytes) ->
    packkit@detect:from_path_or_bytes(Path, Bytes).

-file("src/packkit.gleam", 494).
-spec apply_codec_chain_forward(bitstring(), list(packkit@codec:codec())) -> {ok,
        bitstring()} |
    {error, packkit@error:codec_error()}.
apply_codec_chain_forward(Bytes, Codecs) ->
    case Codecs of
        [] ->
            {ok, Bytes};

        [Head | Rest] ->
            gleam@result:'try'(
                compress(Bytes, Head),
                fun(Compressed) ->
                    apply_codec_chain_forward(Compressed, Rest)
                end
            )
    end.

-file("src/packkit.gleam", 507).
-spec apply_codec_chain_reverse(
    bitstring(),
    list(packkit@codec:codec()),
    packkit@limit:limits()
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
apply_codec_chain_reverse(Bytes, Codecs, Limits) ->
    case Codecs of
        [] ->
            {ok, Bytes};

        [Head | Rest] ->
            gleam@result:'try'(
                decompress_with_limits(Bytes, Head, Limits),
                fun(Plain) -> apply_codec_chain_reverse(Plain, Rest, Limits) end
            )
    end.

-file("src/packkit.gleam", 525).
-spec codec_to_archive_error(
    {ok, WIG} | {error, packkit@error:codec_error()},
    binary()
) -> {ok, WIG} | {error, packkit@error:archive_error()}.
codec_to_archive_error(Value, Step) ->
    case Value of
        {ok, V} ->
            {ok, V};

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

-file("src/packkit.gleam", 427).
?DOC(" Pack an archive with the codec chain described by `recipe`.\n").
-spec pack(packkit@archive:archive(), packkit@recipe:recipe()) -> {ok,
        bitstring()} |
    {error, packkit@error:archive_error()}.
pack(Archive_value, Recipe_value) ->
    gleam@result:'try'(
        write(Archive_value, packkit@recipe:archive_format(Recipe_value)),
        fun(Archive_bytes) ->
            _pipe = apply_codec_chain_forward(
                Archive_bytes,
                packkit@recipe:codecs(Recipe_value)
            ),
            codec_to_archive_error(_pipe, <<"encode"/utf8>>)
        end
    ).

-file("src/packkit.gleam", 452).
?DOC(
    " Unpack a byte stream produced by `recipe`, threading the supplied\n"
    " `Limits` through both the codec chain and the underlying archive\n"
    " decoder.\n"
).
-spec unpack_with_limits(
    bitstring(),
    packkit@recipe:recipe(),
    packkit@limit:limits()
) -> {ok, packkit@archive:archive()} | {error, packkit@error:archive_error()}.
unpack_with_limits(Bytes, Recipe_value, Limits) ->
    gleam@result:'try'(
        begin
            _pipe = apply_codec_chain_reverse(
                Bytes,
                lists:reverse(packkit@recipe:codecs(Recipe_value)),
                Limits
            ),
            codec_to_archive_error(_pipe, <<"decode"/utf8>>)
        end,
        fun(Raw_bytes) ->
            read_with_limits(
                Raw_bytes,
                packkit@recipe:archive_format(Recipe_value),
                Limits
            )
        end
    ).

-file("src/packkit.gleam", 442).
?DOC(
    " Unpack a byte stream produced by `recipe` using the default\n"
    " resource limits.\n"
).
-spec unpack(bitstring(), packkit@recipe:recipe()) -> {ok,
        packkit@archive:archive()} |
    {error, packkit@error:archive_error()}.
unpack(Bytes, Recipe_value) ->
    unpack_with_limits(Bytes, Recipe_value, packkit@limit:default()).