Skip to main content

src/packkit@archive.erl

-module(packkit@archive).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/packkit/archive.gleam").
-export([tar/0, zip/0, seven_z/0, cpio_newc/0, ar/0, new/1, from_entries/2, add/2, add_file_checked/3, add_file/3, add_directory_checked/2, add_directory/2, add_symlink_checked/3, add_symlink/3, add_hardlink_checked/3, add_hardlink/3, with_comment/2, format/1, entries/1, entry_by_path/2, comment/1, entry_count/1, kind/1, name/1]).
-export_type([archive_kind/0, archive_format/0, archive/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.

-type archive_kind() :: tar | zip | seven_z | cpio_newc | ar.

-opaque archive_format() :: {archive_format, archive_kind()}.

-opaque archive() :: {archive,
        archive_format(),
        list(packkit@entry:entry()),
        gleam@option:option(binary())}.

-file("src/packkit/archive.gleam", 35).
?DOC(" Tar archive format.\n").
-spec tar() -> archive_format().
tar() ->
    {archive_format, tar}.

-file("src/packkit/archive.gleam", 40).
?DOC(" ZIP archive format.\n").
-spec zip() -> archive_format().
zip() ->
    {archive_format, zip}.

-file("src/packkit/archive.gleam", 45).
?DOC(" 7z archive format.\n").
-spec seven_z() -> archive_format().
seven_z() ->
    {archive_format, seven_z}.

-file("src/packkit/archive.gleam", 50).
?DOC(" `cpio` newc archive format.\n").
-spec cpio_newc() -> archive_format().
cpio_newc() ->
    {archive_format, cpio_newc}.

-file("src/packkit/archive.gleam", 55).
?DOC(" Unix `ar` archive format.\n").
-spec ar() -> archive_format().
ar() ->
    {archive_format, ar}.

-file("src/packkit/archive.gleam", 60).
?DOC(" Create an empty archive value for the supplied format.\n").
-spec new(archive_format()) -> archive().
new(Format) ->
    {archive, Format, [], none}.

-file("src/packkit/archive.gleam", 65).
?DOC(" Create an archive from a full entry list.\n").
-spec from_entries(archive_format(), list(packkit@entry:entry())) -> archive().
from_entries(Format, Entries) ->
    {archive, Format, lists:reverse(Entries), none}.

-file("src/packkit/archive.gleam", 79).
?DOC(
    " Append an entry to the logical archive.  Runs in O(1) by prepending\n"
    " to a reversed internal list; observable order is preserved through\n"
    " [entries].\n"
).
-spec add(archive(), packkit@entry:entry()) -> archive().
add(Archive, Entry) ->
    {archive,
        erlang:element(2, Archive),
        [Entry | erlang:element(3, Archive)],
        erlang:element(4, Archive)}.

-file("src/packkit/archive.gleam", 88).
?DOC(
    " Add a regular file after checked path validation.  Format-agnostic\n"
    " counterpart to `tar.add_file_checked`; works for any archive\n"
    " produced by `new(format:)`.  Format-side restrictions (e.g. `ar`\n"
    " only carries flat files, `7z`'s encoder is not yet implemented)\n"
    " surface at encode time as `ArchiveError`.\n"
).
-spec add_file_checked(archive(), binary(), bitstring()) -> {ok, archive()} |
    {error, packkit@entry:entry_error()}.
add_file_checked(Archive_value, Path, Body) ->
    case packkit@entry:file_checked(Path, Body) of
        {ok, Value} ->
            {ok, add(Archive_value, Value)};

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

-file("src/packkit/archive.gleam", 100).
?DOC(" Panicking counterpart of `add_file_checked`.\n").
-spec add_file(archive(), binary(), bitstring()) -> archive().
add_file(Archive_value, Path, Body) ->
    add(Archive_value, packkit@entry:file(Path, Body)).

-file("src/packkit/archive.gleam", 109).
?DOC(" Add a directory after checked path validation.\n").
-spec add_directory_checked(archive(), binary()) -> {ok, archive()} |
    {error, packkit@entry:entry_error()}.
add_directory_checked(Archive_value, Path) ->
    case packkit@entry:directory_checked(Path) of
        {ok, Value} ->
            {ok, add(Archive_value, Value)};

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

-file("src/packkit/archive.gleam", 120).
?DOC(" Panicking counterpart of `add_directory_checked`.\n").
-spec add_directory(archive(), binary()) -> archive().
add_directory(Archive_value, Path) ->
    add(Archive_value, packkit@entry:directory(Path)).

-file("src/packkit/archive.gleam", 131).
?DOC(
    " Add a symbolic link after checked path validation.  Formats whose\n"
    " on-disk layout has no symlink slot (e.g. ZIP without the unix\n"
    " extra field, ar) will reject the archive at encode time; the\n"
    " logical archive value can still carry the entry.\n"
).
-spec add_symlink_checked(archive(), binary(), binary()) -> {ok, archive()} |
    {error, packkit@entry:entry_error()}.
add_symlink_checked(Archive_value, Path, Target) ->
    case packkit@entry:symlink_checked(Path, Target) of
        {ok, Value} ->
            {ok, add(Archive_value, Value)};

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

-file("src/packkit/archive.gleam", 143).
?DOC(" Panicking counterpart of `add_symlink_checked`.\n").
-spec add_symlink(archive(), binary(), binary()) -> archive().
add_symlink(Archive_value, Path, Target) ->
    add(Archive_value, packkit@entry:symlink(Path, Target)).

-file("src/packkit/archive.gleam", 153).
?DOC(
    " Add a hard link after checked path validation.  Formats without\n"
    " hard-link support reject the archive at encode time.\n"
).
-spec add_hardlink_checked(archive(), binary(), binary()) -> {ok, archive()} |
    {error, packkit@entry:entry_error()}.
add_hardlink_checked(Archive_value, Path, Target) ->
    case packkit@entry:hardlink_checked(Path, Target) of
        {ok, Value} ->
            {ok, add(Archive_value, Value)};

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

-file("src/packkit/archive.gleam", 165).
?DOC(" Panicking counterpart of `add_hardlink_checked`.\n").
-spec add_hardlink(archive(), binary(), binary()) -> archive().
add_hardlink(Archive_value, Path, Target) ->
    add(Archive_value, packkit@entry:hardlink(Path, Target)).

-file("src/packkit/archive.gleam", 174).
?DOC(" Attach an optional archive comment.\n").
-spec with_comment(archive(), binary()) -> archive().
with_comment(Archive, Comment) ->
    {archive,
        erlang:element(2, Archive),
        erlang:element(3, Archive),
        {some, Comment}}.

-file("src/packkit/archive.gleam", 179).
?DOC(" Read the archive format marker.\n").
-spec format(archive()) -> archive_format().
format(Archive) ->
    erlang:element(2, Archive).

-file("src/packkit/archive.gleam", 184).
?DOC(" Read the logical entries in insertion order.\n").
-spec entries(archive()) -> list(packkit@entry:entry()).
entries(Archive) ->
    lists:reverse(erlang:element(3, Archive)).

-file("src/packkit/archive.gleam", 206).
?DOC(
    " Look up the first entry whose path matches `path`, in observable\n"
    " insertion order.  Returns `Error(Nil)` when no entry matches —\n"
    " keeps the result type close to `list.find` so callers can chain\n"
    " with `result.try`.  Paths are compared by their canonical string\n"
    " form (`entry.to_string`), so trailing-slash normalisation done at\n"
    " insert time is preserved.\n"
    "\n"
    " When an archive carries two entries that compare equal under\n"
    " `entry.path`, this returns the **first** one inserted — the same\n"
    " answer `entries(archive) |> list.find(fn(e) { entry.to_string(...)\n"
    " == path })` would produce.  That is the lawful pairing of `add`\n"
    " with `find` (the function name says \"find first\").  Callers that\n"
    " need \"last wins\" tar-extraction semantics can reverse the entry\n"
    " list first.\n"
    "\n"
    " Use this when you want to extract a known-named entry from a\n"
    " decoded archive — every CLI extractor and \"fetch one file\" use\n"
    " case reinvents the alternative form.\n"
).
-spec entry_by_path(archive(), binary()) -> {ok, packkit@entry:entry()} |
    {error, nil}.
entry_by_path(Archive, Path) ->
    gleam@list:find(
        entries(Archive),
        fun(Ent) ->
            packkit@entry:to_string(packkit@entry:path(Ent)) =:= Path
        end
    ).

-file("src/packkit/archive.gleam", 216).
?DOC(" Read the optional archive comment.\n").
-spec comment(archive()) -> gleam@option:option(binary()).
comment(Archive) ->
    erlang:element(4, Archive).

-file("src/packkit/archive.gleam", 221).
?DOC(" Count the logical entries.\n").
-spec entry_count(archive()) -> integer().
entry_count(Archive) ->
    erlang:length(erlang:element(3, Archive)).

-file("src/packkit/archive.gleam", 226).
?DOC(" Internal tagged kind for the archive format.\n").
-spec kind(archive_format()) -> archive_kind().
kind(Format) ->
    erlang:element(2, Format).

-file("src/packkit/archive.gleam", 232).
?DOC(
    " Stable string name for an archive format.  Kept for diagnostics\n"
    " and `description` output; internal dispatch uses [kind].\n"
).
-spec name(archive_format()) -> binary().
name(Format) ->
    case erlang:element(2, Format) of
        tar ->
            <<"tar"/utf8>>;

        zip ->
            <<"zip"/utf8>>;

        seven_z ->
            <<"7z"/utf8>>;

        cpio_newc ->
            <<"cpio-newc"/utf8>>;

        ar ->
            <<"ar"/utf8>>
    end.