Skip to main content

src/packkit@entry.erl

-module(packkit@entry).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/packkit/entry.gleam").
-export([kind/1, is_file/1, is_directory/1, is_symlink/1, is_hardlink/1, path/1, body/1, link_target/1, metadata/1, with_owner_checked/3, with_owner/3, with_modified_at_checked/2, with_modified_at/2, to_string/1, depth/1, mode/1, user_id/1, group_id/1, modified_at_unix/1, path_checked/1, path_unchecked/1, file_checked/2, file/2, directory_checked/1, directory/1, symlink_checked/2, symlink/2, hardlink_checked/2, hardlink/2, with_mode_checked/2, with_mode/2]).
-export_type([entry_path/0, entry_kind/0, metadata/0, entry/0, entry_error/0, metadata_error/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-opaque entry_path() :: {entry_path, binary(), list(binary())}.

-type entry_kind() :: file | directory | symlink | hardlink.

-opaque metadata() :: {metadata, integer(), integer(), integer(), integer()}.

-opaque entry() :: {entry,
        entry_kind(),
        entry_path(),
        bitstring(),
        gleam@option:option(binary()),
        metadata()}.

-type entry_error() :: empty_path |
    {absolute_path, binary()} |
    {path_traversal, binary()} |
    {windows_path, binary()} |
    {empty_segment, binary()} |
    {dot_segment, binary()} |
    {contains_nul, binary()}.

-type metadata_error() :: {mode_out_of_range, integer()} |
    {owner_out_of_range, integer()} |
    {modified_at_out_of_range, integer()}.

-file("src/packkit/entry.gleam", 227).
?DOC(" Read the logical entry kind.\n").
-spec kind(entry()) -> entry_kind().
kind(Entry) ->
    erlang:element(2, Entry).

-file("src/packkit/entry.gleam", 233).
?DOC(
    " Short-circuit predicate for `kind(entry) == File`.  Saves callers\n"
    " from importing `EntryKind` just to filter a list of entries.\n"
).
-spec is_file(entry()) -> boolean().
is_file(Entry) ->
    erlang:element(2, Entry) =:= file.

-file("src/packkit/entry.gleam", 238).
?DOC(" Short-circuit predicate for `kind(entry) == Directory`.\n").
-spec is_directory(entry()) -> boolean().
is_directory(Entry) ->
    erlang:element(2, Entry) =:= directory.

-file("src/packkit/entry.gleam", 243).
?DOC(" Short-circuit predicate for `kind(entry) == Symlink`.\n").
-spec is_symlink(entry()) -> boolean().
is_symlink(Entry) ->
    erlang:element(2, Entry) =:= symlink.

-file("src/packkit/entry.gleam", 248).
?DOC(" Short-circuit predicate for `kind(entry) == Hardlink`.\n").
-spec is_hardlink(entry()) -> boolean().
is_hardlink(Entry) ->
    erlang:element(2, Entry) =:= hardlink.

-file("src/packkit/entry.gleam", 253).
?DOC(" Read the validated entry path.\n").
-spec path(entry()) -> entry_path().
path(Entry) ->
    erlang:element(3, Entry).

-file("src/packkit/entry.gleam", 258).
?DOC(" Read the raw byte body for file-like entries.\n").
-spec body(entry()) -> bitstring().
body(Entry) ->
    erlang:element(4, Entry).

-file("src/packkit/entry.gleam", 263).
?DOC(" Read the optional link target.\n").
-spec link_target(entry()) -> gleam@option:option(binary()).
link_target(Entry) ->
    erlang:element(5, Entry).

-file("src/packkit/entry.gleam", 268).
?DOC(" Read the metadata bundle.\n").
-spec metadata(entry()) -> metadata().
metadata(Entry) ->
    erlang:element(6, Entry).

-file("src/packkit/entry.gleam", 339).
?DOC(
    " Override the entry owner identifiers after validating they are\n"
    " non-negative.  Format-side overflow (e.g. tar's 21-bit field) is\n"
    " still surfaced at encode time as `ArchiveFieldOverflow`.\n"
).
-spec with_owner_checked(entry(), integer(), integer()) -> {ok, entry()} |
    {error, metadata_error()}.
with_owner_checked(Entry, User_id, Group_id) ->
    gleam@bool:guard(
        User_id < 0,
        {error, {owner_out_of_range, User_id}},
        fun() ->
            gleam@bool:guard(
                Group_id < 0,
                {error, {owner_out_of_range, Group_id}},
                fun() ->
                    {metadata, Mode, _, _, Modified_at_unix} = erlang:element(
                        6,
                        Entry
                    ),
                    {ok,
                        {entry,
                            erlang:element(2, Entry),
                            erlang:element(3, Entry),
                            erlang:element(4, Entry),
                            erlang:element(5, Entry),
                            {metadata,
                                Mode,
                                User_id,
                                Group_id,
                                Modified_at_unix}}}
                end
            )
        end
    ).

-file("src/packkit/entry.gleam", 324).
?DOC(
    " Override the entry owner identifiers.  Negative values panic;\n"
    " see [with_owner_checked] for the validated variant.  No upper\n"
    " bound is enforced here — format-specific encoders (tar, ar, cpio\n"
    " newc) each apply their own narrower field-width checks at encode\n"
    " time, so a value that's valid for one format and oversized for\n"
    " another can still be expressed as an `Entry`.\n"
).
-spec with_owner(entry(), integer(), integer()) -> entry().
with_owner(Entry, User_id, Group_id) ->
    case with_owner_checked(Entry, User_id, Group_id) of
        {ok, E} ->
            E;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.with_owner: uid/gid must be non-negative"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"with_owner"/utf8>>,
                    line => 332})
    end.

-file("src/packkit/entry.gleam", 383).
?DOC(
    " Override the last-modified timestamp after validating it is\n"
    " non-negative.  The format-specific upper bound is enforced at\n"
    " encode time as `ArchiveFieldOverflow`.\n"
).
-spec with_modified_at_checked(entry(), integer()) -> {ok, entry()} |
    {error, metadata_error()}.
with_modified_at_checked(Entry, Unix_seconds) ->
    gleam@bool:guard(
        Unix_seconds < 0,
        {error, {modified_at_out_of_range, Unix_seconds}},
        fun() ->
            {metadata, Mode, User_id, Group_id, _} = erlang:element(6, Entry),
            {ok,
                {entry,
                    erlang:element(2, Entry),
                    erlang:element(3, Entry),
                    erlang:element(4, Entry),
                    erlang:element(5, Entry),
                    {metadata, Mode, User_id, Group_id, Unix_seconds}}}
        end
    ).

-file("src/packkit/entry.gleam", 372).
?DOC(
    " Override the last-modified timestamp.  Negative values panic;\n"
    " see [with_modified_at_checked] for the validated variant.  As\n"
    " with [with_owner], the format-specific upper bound (gzip 32-bit,\n"
    " tar 11-octal-digit, …) is enforced at encode time.\n"
).
-spec with_modified_at(entry(), integer()) -> entry().
with_modified_at(Entry, Unix_seconds) ->
    case with_modified_at_checked(Entry, Unix_seconds) of
        {ok, E} ->
            E;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.with_modified_at: unix_seconds must be non-negative"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"with_modified_at"/utf8>>,
                    line => 376})
    end.

-file("src/packkit/entry.gleam", 408).
?DOC(" Convert an `EntryPath` back to its canonical string form.\n").
-spec to_string(entry_path()) -> binary().
to_string(Path) ->
    erlang:element(2, Path).

-file("src/packkit/entry.gleam", 413).
?DOC(" Number of segments in the path.\n").
-spec depth(entry_path()) -> integer().
depth(Path) ->
    erlang:length(erlang:element(3, Path)).

-file("src/packkit/entry.gleam", 418).
?DOC(" File mode stored in metadata.\n").
-spec mode(metadata()) -> integer().
mode(Metadata) ->
    erlang:element(2, Metadata).

-file("src/packkit/entry.gleam", 423).
?DOC(" User identifier stored in metadata.\n").
-spec user_id(metadata()) -> integer().
user_id(Metadata) ->
    erlang:element(3, Metadata).

-file("src/packkit/entry.gleam", 428).
?DOC(" Group identifier stored in metadata.\n").
-spec group_id(metadata()) -> integer().
group_id(Metadata) ->
    erlang:element(4, Metadata).

-file("src/packkit/entry.gleam", 433).
?DOC(" Last-modified Unix timestamp stored in metadata.\n").
-spec modified_at_unix(metadata()) -> integer().
modified_at_unix(Metadata) ->
    erlang:element(5, Metadata).

-file("src/packkit/entry.gleam", 437).
-spec file_metadata() -> metadata().
file_metadata() ->
    {metadata, 420, 0, 0, 0}.

-file("src/packkit/entry.gleam", 441).
-spec directory_metadata() -> metadata().
directory_metadata() ->
    {metadata, 493, 0, 0, 0}.

-file("src/packkit/entry.gleam", 445).
-spec link_metadata() -> metadata().
link_metadata() ->
    {metadata, 511, 0, 0, 0}.

-file("src/packkit/entry.gleam", 449).
-spec contains_empty_segment(list(binary())) -> boolean().
contains_empty_segment(Segments) ->
    case Segments of
        [] ->
            false;

        [Segment | Rest] ->
            (Segment =:= <<""/utf8>>) orelse contains_empty_segment(Rest)
    end.

-file("src/packkit/entry.gleam", 456).
-spec contains_dot_segment(list(binary())) -> boolean().
contains_dot_segment(Segments) ->
    case Segments of
        [] ->
            false;

        [Segment | Rest] ->
            (Segment =:= <<"."/utf8>>) orelse contains_dot_segment(Rest)
    end.

-file("src/packkit/entry.gleam", 463).
-spec contains_traversal_segment(list(binary())) -> boolean().
contains_traversal_segment(Segments) ->
    case Segments of
        [] ->
            false;

        [Segment | Rest] ->
            (Segment =:= <<".."/utf8>>) orelse contains_traversal_segment(Rest)
    end.

-file("src/packkit/entry.gleam", 66).
?DOC(" Validate a relative archive path.\n").
-spec path_checked(binary()) -> {ok, entry_path()} | {error, entry_error()}.
path_checked(Value) ->
    gleam@bool:guard(
        Value =:= <<""/utf8>>,
        {error, empty_path},
        fun() ->
            gleam@bool:guard(
                gleam_stdlib:contains_string(Value, <<"\x{0000}"/utf8>>),
                {error, {contains_nul, Value}},
                fun() ->
                    gleam@bool:guard(
                        gleam_stdlib:string_starts_with(Value, <<"/"/utf8>>),
                        {error, {absolute_path, Value}},
                        fun() ->
                            gleam@bool:guard(
                                gleam_stdlib:contains_string(
                                    Value,
                                    <<"\\"/utf8>>
                                ),
                                {error, {windows_path, Value}},
                                fun() ->
                                    Segments = gleam@string:split(
                                        Value,
                                        <<"/"/utf8>>
                                    ),
                                    gleam@bool:guard(
                                        contains_empty_segment(Segments),
                                        {error, {empty_segment, Value}},
                                        fun() ->
                                            gleam@bool:guard(
                                                contains_dot_segment(Segments),
                                                {error, {dot_segment, Value}},
                                                fun() ->
                                                    gleam@bool:guard(
                                                        contains_traversal_segment(
                                                            Segments
                                                        ),
                                                        {error,
                                                            {path_traversal,
                                                                Value}},
                                                        fun() ->
                                                            {ok,
                                                                {entry_path,
                                                                    Value,
                                                                    Segments}}
                                                        end
                                                    )
                                                end
                                            )
                                        end
                                    )
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/packkit/entry.gleam", 101).
?DOC(
    " Panicking counterpart of `path_checked`.  The getter on `Entry`\n"
    " claims the short name; this constructor takes the explicit suffix.\n"
).
-spec path_unchecked(binary()) -> entry_path().
path_unchecked(Value) ->
    case path_checked(Value) of
        {ok, Path} ->
            Path;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.path_unchecked: entry path must be a safe relative path"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"path_unchecked"/utf8>>,
                    line => 105})
    end.

-file("src/packkit/entry.gleam", 110).
?DOC(" Build a regular file entry after path validation.\n").
-spec file_checked(binary(), bitstring()) -> {ok, entry()} |
    {error, entry_error()}.
file_checked(Path, Body) ->
    case path_checked(Path) of
        {ok, Safe_path} ->
            {ok, {entry, file, Safe_path, Body, none, file_metadata()}};

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

-file("src/packkit/entry.gleam", 128).
?DOC(" Panicking counterpart of `file_checked`.\n").
-spec file(binary(), bitstring()) -> entry().
file(Path, Body) ->
    case file_checked(Path, Body) of
        {ok, Entry} ->
            Entry;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.file: entry path must be a safe relative path"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"file"/utf8>>,
                    line => 132})
    end.

-file("src/packkit/entry.gleam", 137).
?DOC(" Build a directory entry after path validation.\n").
-spec directory_checked(binary()) -> {ok, entry()} | {error, entry_error()}.
directory_checked(Path) ->
    case path_checked(Path) of
        {ok, Safe_path} ->
            {ok,
                {entry, directory, Safe_path, <<>>, none, directory_metadata()}};

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

-file("src/packkit/entry.gleam", 152).
?DOC(" Panicking counterpart of `directory_checked`.\n").
-spec directory(binary()) -> entry().
directory(Path) ->
    case directory_checked(Path) of
        {ok, Entry} ->
            Entry;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.directory: entry path must be a safe relative path"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"directory"/utf8>>,
                    line => 156})
    end.

-file("src/packkit/entry.gleam", 162).
?DOC(
    " Build a symbolic-link entry. The entry path is strictly validated.\n"
    " The link target is preserved as metadata but must not contain NUL.\n"
).
-spec symlink_checked(binary(), binary()) -> {ok, entry()} |
    {error, entry_error()}.
symlink_checked(Path, Target) ->
    gleam@bool:guard(
        gleam_stdlib:contains_string(Target, <<"\x{0000}"/utf8>>),
        {error, {contains_nul, Target}},
        fun() -> case path_checked(Path) of
                {ok, Safe_path} ->
                    {ok,
                        {entry,
                            symlink,
                            Safe_path,
                            <<>>,
                            {some, Target},
                            link_metadata()}};

                {error, Error} ->
                    {error, Error}
            end end
    ).

-file("src/packkit/entry.gleam", 185).
?DOC(" Panicking counterpart of `symlink_checked`.\n").
-spec symlink(binary(), binary()) -> entry().
symlink(Path, Target) ->
    case symlink_checked(Path, Target) of
        {ok, Entry} ->
            Entry;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.symlink: invalid archive path or link target"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"symlink"/utf8>>,
                    line => 189})
    end.

-file("src/packkit/entry.gleam", 195).
?DOC(
    " Build a hard-link entry. The entry path is strictly validated.\n"
    " The link target is preserved as metadata but must not contain NUL.\n"
).
-spec hardlink_checked(binary(), binary()) -> {ok, entry()} |
    {error, entry_error()}.
hardlink_checked(Path, Target) ->
    gleam@bool:guard(
        gleam_stdlib:contains_string(Target, <<"\x{0000}"/utf8>>),
        {error, {contains_nul, Target}},
        fun() -> case path_checked(Path) of
                {ok, Safe_path} ->
                    {ok,
                        {entry,
                            hardlink,
                            Safe_path,
                            <<>>,
                            {some, Target},
                            link_metadata()}};

                {error, Error} ->
                    {error, Error}
            end end
    ).

-file("src/packkit/entry.gleam", 218).
?DOC(" Panicking counterpart of `hardlink_checked`.\n").
-spec hardlink(binary(), binary()) -> entry().
hardlink(Path, Target) ->
    case hardlink_checked(Path, Target) of
        {ok, Entry} ->
            Entry;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.hardlink: invalid archive path or link target"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"hardlink"/utf8>>,
                    line => 222})
    end.

-file("src/packkit/entry.gleam", 290).
?DOC(
    " Override the entry mode after validating it fits the widest mode\n"
    " field any of our archive families serialise it through.\n"
).
-spec with_mode_checked(entry(), integer()) -> {ok, entry()} |
    {error, metadata_error()}.
with_mode_checked(Entry, Mode) ->
    gleam@bool:guard(
        (Mode < 0) orelse (Mode > 16#FFFF),
        {error, {mode_out_of_range, Mode}},
        fun() ->
            {metadata, _, User_id, Group_id, Modified_at_unix} = erlang:element(
                6,
                Entry
            ),
            {ok,
                {entry,
                    erlang:element(2, Entry),
                    erlang:element(3, Entry),
                    erlang:element(4, Entry),
                    erlang:element(5, Entry),
                    {metadata, Mode, User_id, Group_id, Modified_at_unix}}}
        end
    ).

-file("src/packkit/entry.gleam", 280).
?DOC(
    " Override the entry mode.  Out-of-range values panic; use\n"
    " [with_mode_checked] for caller-controlled error handling.\n"
).
-spec with_mode(entry(), integer()) -> entry().
with_mode(Entry, Mode) ->
    case with_mode_checked(Entry, Mode) of
        {ok, E} ->
            E;

        {error, _} ->
            erlang:error(#{gleam_error => panic,
                    message => <<"packkit/entry.with_mode: mode must be in the inclusive range 0..0xFFFF"/utf8>>,
                    file => <<?FILEPATH/utf8>>,
                    module => <<"packkit/entry"/utf8>>,
                    function => <<"with_mode"/utf8>>,
                    line => 284})
    end.