src/packkit@zip.erl
-module(packkit@zip).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/packkit/zip.gleam").
-export([format/0, new/0, store/0, deflate/1, bzip2/0, zstd/0, xz/0, lzma/0, name/1, inner_codec/1, encode_with_method/2, encode/1, decode/1, decode_with_limits/2, decode_with_password/2, decode_with_password_and_limits/3]).
-export_type([method/0, encode_fold/0, eocd_record/0, pkware_keys/0, aex_params/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(
" ZIP archive encoder and decoder.\n"
"\n"
" This module implements the PKZIP local-file-header / central\n"
" directory layout for the \"stored\" (uncompressed) and \"deflate\"\n"
" methods, plus the Zip64 extensions (APPNOTE.TXT §4.4) needed to\n"
" carry archives with > 65535 entries, central-directory regions\n"
" > 4 GiB, individual entries > 4 GiB, or local-header offsets\n"
" > 4 GiB. ZIP is modelled as an archive family, not a recipe:\n"
" per-entry compression is selected through `Method` values rather\n"
" than through `packkit/recipe`.\n"
"\n"
" On the decoder side: when the standard EOCD record has any\n"
" sentinel field (`0xFFFF` for the 16-bit slots, `0xFFFFFFFF` for\n"
" the 32-bit slots), the decoder follows the Zip64 EOCD locator at\n"
" `eocd_offset - 20` to the Zip64 EOCD record and reads the real\n"
" 64-bit values from there. Per-entry Zip64 extra fields\n"
" (`header_id = 0x0001`) are parsed in both the central-directory\n"
" entry and the local file header so that compressed / uncompressed\n"
" sizes and local-header offsets above the 4 GiB boundary decode\n"
" correctly.\n"
"\n"
" On the encoder side: each entry transparently switches to the\n"
" Zip64 extra field when any of its `uncompressed_size`,\n"
" `compressed_size`, or `local_header_offset` would not fit in 32\n"
" bits. Archives whose total entry count or central-directory\n"
" region overflow the legacy 16-/32-bit EOCD slots also emit a\n"
" Zip64 EOCD record + locator so the resulting bytes round-trip\n"
" through any conforming Zip64 reader.\n"
).
-opaque method() :: {method,
binary(),
gleam@option:option(packkit@codec:codec())}.
-type encode_fold() :: {encode_fold,
integer(),
list(bitstring()),
list(bitstring()),
gleam@option:option(packkit@error:archive_error())}.
-type eocd_record() :: {eocd_record,
integer(),
integer(),
integer(),
gleam@option:option(binary())}.
-type pkware_keys() :: {pkware_keys, integer(), integer(), integer()}.
-type aex_params() :: {aex_params, integer(), integer(), integer()}.
-file("src/packkit/zip.gleam", 174).
?DOC(" ZIP archive format marker.\n").
-spec format() -> packkit@archive:archive_format().
format() ->
packkit@archive:zip().
-file("src/packkit/zip.gleam", 179).
?DOC(" Create an empty logical ZIP archive value.\n").
-spec new() -> packkit@archive:archive().
new() ->
packkit@archive:new(format()).
-file("src/packkit/zip.gleam", 184).
?DOC(" Stored (uncompressed) ZIP member method.\n").
-spec store() -> method().
store() ->
{method, <<"store"/utf8>>, none}.
-file("src/packkit/zip.gleam", 189).
?DOC(" Deflate-compressed ZIP member method.\n").
-spec deflate(packkit@level:level()) -> method().
deflate(Level) ->
{method,
<<"deflate"/utf8>>,
{some,
begin
_pipe = packkit@codec:deflate(),
packkit@codec:with_level(_pipe, Level)
end}}.
-file("src/packkit/zip.gleam", 197).
?DOC(" Bzip2-compressed ZIP member method (PKZIP method 12).\n").
-spec bzip2() -> method().
bzip2() ->
{method, <<"bzip2"/utf8>>, {some, packkit@codec:bzip2()}}.
-file("src/packkit/zip.gleam", 202).
?DOC(" Zstd-compressed ZIP member method (PKZIP method 93).\n").
-spec zstd() -> method().
zstd() ->
{method, <<"zstd"/utf8>>, {some, packkit@codec:zstd()}}.
-file("src/packkit/zip.gleam", 207).
?DOC(" xz-compressed ZIP member method (PKZIP method 95).\n").
-spec xz() -> method().
xz() ->
{method, <<"xz"/utf8>>, {some, packkit@codec:xz()}}.
-file("src/packkit/zip.gleam", 217).
?DOC(
" PKWARE LZMA ZIP member method (PKZIP method 14). Wraps a raw\n"
" LZMA1 range-coded stream in the 4-byte SDK preamble + 5-byte\n"
" property block (`lc=3 / lp=0 / pb=2`, dict size 64 KiB). The\n"
" encoder emits the stream with general-purpose flag bit 1 set so\n"
" the decoder relies on the central-directory uncompressed size\n"
" instead of an in-stream EOS marker.\n"
).
-spec lzma() -> method().
lzma() ->
{method, <<"lzma"/utf8>>, none}.
-file("src/packkit/zip.gleam", 224).
?DOC(" Stable method name.\n").
-spec name(method()) -> binary().
name(Method) ->
erlang:element(2, Method).
-file("src/packkit/zip.gleam", 229).
?DOC(" Optional inner codec corresponding to the method.\n").
-spec inner_codec(method()) -> gleam@option:option(packkit@codec:codec()).
inner_codec(Method) ->
erlang:element(3, Method).
-file("src/packkit/zip.gleam", 370).
?DOC(
" Run the deflate encoder honoring the requested level on the inner\n"
" codec attached to the method. Level 0 → stored deflate blocks;\n"
" the implicit default level → fixed-Huffman LZ77; any other level\n"
" is rejected with a typed `CodecOptionUnsupported`. Previously this\n"
" step silently fell back to the default encoder regardless of the\n"
" caller's level request.\n"
).
-spec deflate_with_level(
bitstring(),
gleam@option:option(packkit@codec:codec())
) -> {ok, bitstring()} | {error, packkit@error:codec_error()}.
deflate_with_level(Bytes, Inner) ->
Level_value = case Inner of
none ->
none;
{some, C} ->
case packkit@codec:level(C) of
{some, L} ->
{some, packkit@level:value(L)};
none ->
none
end
end,
case Level_value of
none ->
packkit@deflate:encode(Bytes);
{some, 0} ->
packkit@deflate:encode_stored_only(Bytes);
{some, N} ->
case N =:= packkit@level:value(packkit@level:default()) of
true ->
packkit@deflate:encode(Bytes);
false ->
{error,
{codec_option_unsupported,
<<"level"/utf8>>,
<<"zip-deflate"/utf8>>}}
end
end.
-file("src/packkit/zip.gleam", 883).
-spec civil_from_days(integer()) -> {integer(), integer(), integer()}.
civil_from_days(Z) ->
Days_shifted = Z + 719468,
Era = case Days_shifted >= 0 of
true ->
Days_shifted div 146097;
false ->
(Days_shifted - 146096) div 146097
end,
Day_of_era = Days_shifted - (Era * 146097),
Year_of_era = (((Day_of_era - (Day_of_era div 1460)) + (Day_of_era div 36524))
- (Day_of_era div 146096))
div 365,
March_year = Year_of_era + (Era * 400),
Day_of_year = Day_of_era - (((365 * Year_of_era) + (Year_of_era div 4)) - (Year_of_era
div 100)),
Month_prime = ((5 * Day_of_year) + 2) div 153,
Day = (Day_of_year - (((153 * Month_prime) + 2) div 5)) + 1,
Month = case Month_prime < 10 of
true ->
Month_prime + 3;
false ->
Month_prime - 9
end,
Year = case Month =< 2 of
true ->
March_year + 1;
false ->
March_year
end,
{Year, Month, Day}.
-file("src/packkit/zip.gleam", 921).
-spec days_from_civil(integer(), integer(), integer()) -> integer().
days_from_civil(Year, Month, Day) ->
March_year = case Month =< 2 of
true ->
Year - 1;
false ->
Year
end,
Era = case March_year >= 0 of
true ->
March_year div 400;
false ->
(March_year - 399) div 400
end,
Year_of_era = March_year - (Era * 400),
Month_prime = case Month > 2 of
true ->
Month - 3;
false ->
Month + 9
end,
Day_of_year = ((((153 * Month_prime) + 2) div 5) + Day) - 1,
Day_of_era = (((Year_of_era * 365) + (Year_of_era div 4)) - (Year_of_era div 100))
+ Day_of_year,
((Era * 146097) + Day_of_era) - 719468.
-file("src/packkit/zip.gleam", 1065).
-spec option_try(
{ok, UOS} | {error, any()},
fun((UOS) -> gleam@option:option(UOW))
) -> gleam@option:option(UOW).
option_try(Source, Continuation) ->
case Source of
{ok, Value} ->
Continuation(Value);
{error, _} ->
none
end.
-file("src/packkit/zip.gleam", 1075).
-spec option_try_pair(
gleam@option:option(UOZ),
fun((UOZ) -> gleam@option:option(UPB))
) -> gleam@option:option(UPB).
option_try_pair(Source, Continuation) ->
case Source of
{some, Value} ->
Continuation(Value);
none ->
none
end.
-file("src/packkit/zip.gleam", 1103).
-spec decode_unix_uid_gid_value(bitstring(), integer()) -> gleam@option:option({integer(),
bitstring()}).
decode_unix_uid_gid_value(Bytes, Width) ->
case {Width, Bytes} of
{1, <<Value, Rest/binary>>} ->
{some, {Value, Rest}};
{2, <<Value@1:16/little, Rest@1/binary>>} ->
{some, {Value@1, Rest@1}};
{4, <<Value@2:32/little, Rest@2/binary>>} ->
{some, {Value@2, Rest@2}};
{8, <<Value@3:64/little, Rest@3/binary>>} ->
{some, {Value@3, Rest@3}};
{_, _} ->
none
end.
-file("src/packkit/zip.gleam", 1053).
-spec parse_gid_value(bitstring(), integer(), integer()) -> gleam@option:option({integer(),
integer()}).
parse_gid_value(Gid_rest, Gid_size, Uid) ->
option_try_pair(
decode_unix_uid_gid_value(Gid_rest, Gid_size),
fun(_use0) ->
{Gid, _} = _use0,
{some, {Uid, Gid}}
end
).
-file("src/packkit/zip.gleam", 1046).
-spec parse_gid_field(bitstring(), integer()) -> gleam@option:option({integer(),
integer()}).
parse_gid_field(After_uid, Uid) ->
case After_uid of
<<Gid_size, Gid_rest/binary>> ->
parse_gid_value(Gid_rest, Gid_size, Uid);
_ ->
none
end.
-file("src/packkit/zip.gleam", 1038).
-spec parse_uid_then_gid(bitstring(), integer()) -> gleam@option:option({integer(),
integer()}).
parse_uid_then_gid(Rest, Uid_size) ->
option_try_pair(
decode_unix_uid_gid_value(Rest, Uid_size),
fun(_use0) ->
{Uid, After_uid} = _use0,
parse_gid_field(After_uid, Uid)
end
).
-file("src/packkit/zip.gleam", 1031).
-spec parse_unix_uid_gid_new_body(bitstring()) -> gleam@option:option({integer(),
integer()}).
parse_unix_uid_gid_new_body(Body) ->
case Body of
<<_, Uid_size, Rest/binary>> ->
parse_uid_then_gid(Rest, Uid_size);
_ ->
none
end.
-file("src/packkit/zip.gleam", 1145).
-spec ensure_trailing_slash(binary()) -> binary().
ensure_trailing_slash(Value) ->
case gleam_stdlib:string_ends_with(Value, <<"/"/utf8>>) of
true ->
Value;
false ->
<<Value/binary, "/"/utf8>>
end.
-file("src/packkit/zip.gleam", 1733).
-spec pkware_initial_keys() -> pkware_keys().
pkware_initial_keys() ->
{pkware_keys, 16#12345678, 16#23456789, 16#34567890}.
-file("src/packkit/zip.gleam", 1773).
-spec pkware_crc32_fold(integer(), integer()) -> integer().
pkware_crc32_fold(Value, Rounds) ->
case Rounds of
0 ->
Value;
_ ->
Next = case erlang:'band'(Value, 1) of
1 ->
erlang:'bxor'(erlang:'bsr'(Value, 1), 16#EDB88320);
_ ->
erlang:'bsr'(Value, 1)
end,
pkware_crc32_fold(Next, Rounds - 1)
end.
-file("src/packkit/zip.gleam", 1767).
-spec pkware_crc32_byte(integer(), integer()) -> integer().
pkware_crc32_byte(Crc, Byte) ->
Idx = erlang:'band'(erlang:'bxor'(Crc, Byte), 16#FF),
Folded = pkware_crc32_fold(Idx, 8),
erlang:'bxor'(erlang:'bsr'(Crc, 8), Folded).
-file("src/packkit/zip.gleam", 1751).
-spec pkware_update_keys(pkware_keys(), integer()) -> pkware_keys().
pkware_update_keys(Keys, Byte) ->
New_key0 = pkware_crc32_byte(erlang:element(2, Keys), Byte),
Added = erlang:'band'(New_key0, 16#FF) + erlang:element(3, Keys),
Mixed = (Added * 134775813) + 1,
New_key1 = erlang:'band'(Mixed, 16#FFFFFFFF),
Top = erlang:'band'(erlang:'bsr'(New_key1, 24), 16#FF),
New_key2 = pkware_crc32_byte(erlang:element(4, Keys), Top),
{pkware_keys, New_key0, New_key1, New_key2}.
-file("src/packkit/zip.gleam", 1744).
-spec pkware_seed_loop(pkware_keys(), bitstring()) -> pkware_keys().
pkware_seed_loop(Keys, Password) ->
case Password of
<<B, Rest/binary>> ->
pkware_seed_loop(pkware_update_keys(Keys, B), Rest);
_ ->
Keys
end.
-file("src/packkit/zip.gleam", 1740).
-spec pkware_keys_from_password(binary()) -> pkware_keys().
pkware_keys_from_password(Password) ->
pkware_seed_loop(pkware_initial_keys(), gleam_stdlib:identity(Password)).
-file("src/packkit/zip.gleam", 1790).
-spec pkware_decrypt_byte(pkware_keys(), integer()) -> {integer(),
pkware_keys()}.
pkware_decrypt_byte(Keys, Cipher) ->
Temp = erlang:'bor'(erlang:element(4, Keys), 2),
Prod = Temp * erlang:'bxor'(Temp, 1),
Mask = erlang:'band'(erlang:'bsr'(Prod, 8), 16#FF),
Plain = erlang:'bxor'(Cipher, Mask),
Next_keys = pkware_update_keys(Keys, Plain),
{Plain, Next_keys}.
-file("src/packkit/zip.gleam", 1800).
-spec pkware_decrypt_bytes(pkware_keys(), bitstring(), bitstring()) -> {bitstring(),
pkware_keys()}.
pkware_decrypt_bytes(Keys, Cipher, Acc) ->
case Cipher of
<<B, Rest/binary>> ->
{Plain, Keys@1} = pkware_decrypt_byte(Keys, B),
pkware_decrypt_bytes(Keys@1, Rest, <<Acc/bitstring, Plain>>);
_ ->
{Acc, Keys}
end.
-file("src/packkit/zip.gleam", 1869).
-spec split_enc_header(bitstring(), integer()) -> {ok,
{bitstring(), bitstring()}} |
{error, packkit@error:archive_error()}.
split_enc_header(Raw, Total_size) ->
case {gleam_stdlib:bit_array_slice(Raw, 0, 12),
gleam_stdlib:bit_array_slice(Raw, 12, Total_size - 12)} of
{{ok, H}, {ok, P}} ->
{ok, {H, P}};
{{error, _}, _} ->
{error,
{archive_invalid,
<<"ZIP encryption header truncated (need ≥ 12 bytes)"/utf8>>}};
{_, {error, _}} ->
{error,
{archive_invalid,
<<"ZIP encrypted entry payload truncated"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 1890).
-spec pkware_last_byte_loop(bitstring(), integer()) -> integer().
pkware_last_byte_loop(Bytes, Last) ->
case Bytes of
<<B, Rest/binary>> ->
pkware_last_byte_loop(Rest, B);
_ ->
Last
end.
-file("src/packkit/zip.gleam", 1886).
-spec pkware_last_byte(bitstring()) -> integer().
pkware_last_byte(Bytes) ->
pkware_last_byte_loop(Bytes, 0).
-file("src/packkit/zip.gleam", 2035).
-spec aex_key_length(integer(), binary()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
aex_key_length(Strength, Name) ->
case Strength of
16#01 ->
{ok, 16};
16#02 ->
{ok, 24};
16#03 ->
{ok, 32};
_ ->
{error,
{archive_invalid,
<<<<<<<<"ZIP AE-x unknown AES strength byte "/utf8,
(erlang:integer_to_binary(Strength))/binary>>/binary,
" for \""/utf8>>/binary,
Name/binary>>/binary,
"\""/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2103).
-spec xor_bit_arrays_zip(bitstring(), bitstring(), bitstring()) -> bitstring().
xor_bit_arrays_zip(Left, Right, Acc) ->
case {Left, Right} of
{<<L, L_rest/binary>>, <<R, R_rest/binary>>} ->
Mixed = erlang:'bxor'(L, R),
xor_bit_arrays_zip(
L_rest,
R_rest,
gleam_stdlib:bit_array_concat([Acc, <<Mixed>>])
);
{_, _} ->
Acc
end.
-file("src/packkit/zip.gleam", 2166).
-spec parse_pkware_lzma_header(bitstring()) -> {ok,
{integer(), bitstring(), bitstring()}} |
{error, packkit@error:codec_error()}.
parse_pkware_lzma_header(Compressed) ->
case Compressed of
<<_, _, Prop_size:16/little-unsigned, Rest/binary>> ->
Rest_size = erlang:byte_size(Rest),
gleam@bool:guard(
Rest_size < Prop_size,
{error,
{codec_invalid_data,
<<"PKWARE LZMA wrapper truncated property block"/utf8>>}},
fun() ->
Prop_slice = gleam_stdlib:bit_array_slice(
Rest,
0,
Prop_size
),
Payload_slice = gleam_stdlib:bit_array_slice(
Rest,
Prop_size,
Rest_size - Prop_size
),
case {Prop_slice, Payload_slice} of
{{ok, Prop_block}, {ok, Payload}} ->
{ok, {Prop_size, Prop_block, Payload}};
{_, _} ->
{error,
{codec_invalid_data,
<<"PKWARE LZMA wrapper slice out of range"/utf8>>}}
end
end
);
_ ->
{error,
{codec_invalid_data,
<<"PKWARE LZMA wrapper missing header"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2196).
-spec parse_pkware_lzma_props(bitstring()) -> {ok,
packkit@internal@lzma:properties()} |
{error, packkit@error:codec_error()}.
parse_pkware_lzma_props(Prop_block) ->
case Prop_block of
<<Prop_byte, _:32/little-unsigned>> ->
packkit@internal@lzma:properties_of_byte(Prop_byte);
_ ->
{error,
{codec_invalid_data,
<<"PKWARE LZMA wrapper property block has wrong length"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2135).
?DOC(
" Decode a ZIP method 14 PKWARE LZMA payload to its plain bytes.\n"
"\n"
" PKZIP APPNOTE.TXT §5.8 lays the wrapper out as:\n"
" +0 1 byte LZMA SDK major version\n"
" +1 1 byte LZMA SDK minor version\n"
" +2 2 bytes Size of the LZMA property block (LE, normally 5)\n"
" +4 N bytes LZMA1 property block (1 properties byte + 4 little-\n"
" endian dictionary-size bytes when N == 5)\n"
" +4+N Raw LZMA1 range-coded payload (the byte stream\n"
" expected by `packkit/internal/lzma.new`)\n"
"\n"
" We forward the property byte to `lzma.properties_of_byte`, the\n"
" remaining range-coded bytes to `lzma.new`, and ask the decoder\n"
" for exactly the central-directory uncompressed size — the EOS\n"
" marker (when present in payload-bit-1 = 0 streams) terminates\n"
" inside the range coder and the size-limited loop covers the\n"
" payload-bit-1 = 1 case. The dictionary-size field is informational\n"
" for the wrapper; the LZMA decoder allocates its window lazily.\n"
).
-spec decode_pkware_lzma(bitstring(), integer(), packkit@limit:limits()) -> {ok,
bitstring()} |
{error, packkit@error:codec_error()}.
decode_pkware_lzma(Compressed, Uncomp_size, Limits) ->
gleam@result:'try'(
parse_pkware_lzma_header(Compressed),
fun(_use0) ->
{Prop_size, Prop_block, Payload} = _use0,
gleam@bool:guard(
Prop_size /= 5,
{error,
{codec_invalid_data,
<<"PKWARE LZMA wrapper property size must be 5"/utf8>>}},
fun() ->
gleam@result:'try'(
parse_pkware_lzma_props(Prop_block),
fun(Props) ->
gleam@bool:guard(
Uncomp_size > packkit@limit:max_output_bytes(
Limits
),
{error,
{codec_limit_exceeded,
<<"max_output_bytes"/utf8>>,
Uncomp_size}},
fun() ->
gleam@result:'try'(
packkit@internal@lzma:new(
Payload,
Props,
packkit@limit:max_output_bytes(
Limits
)
),
fun(Decoder) ->
gleam@result:'try'(
packkit@internal@lzma:decode_into(
Decoder,
Uncomp_size
),
fun(_use0@1) ->
{Decoded, _} = _use0@1,
{ok, Decoded}
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/packkit/zip.gleam", 2216).
?DOC(
" Build a PKWARE method-14 payload by gluing the 4-byte SDK preamble\n"
" + 5-byte property block in front of an LZMA1 raw range-coded\n"
" stream produced by `packkit/internal/lzma.encode_literal_only`.\n"
" The general-purpose flag bit 1 (set in the entry's local + central\n"
" headers) tells the reader to use the uncompressed size from the\n"
" central directory instead of looking for an in-stream EOS marker —\n"
" which our literal-only encoder never emits.\n"
).
-spec encode_pkware_lzma(bitstring()) -> bitstring().
encode_pkware_lzma(Plain) ->
Props = {properties, 3, 0, 2},
Stream = packkit@internal@lzma:encode_with_lz77(Plain, Props),
Prop_byte = packkit@internal@lzma:properties_to_byte(Props),
Dict_size_bytes = <<16#10000:32/little>>,
Preamble = <<16#14, 16#00, 16#05, 16#00>>,
gleam_stdlib:bit_array_concat(
[Preamble, <<Prop_byte>>, Dict_size_bytes, Stream]
).
-file("src/packkit/zip.gleam", 2236).
-spec strip_trailing_slash(binary()) -> binary().
strip_trailing_slash(Value) ->
case gleam_stdlib:string_ends_with(Value, <<"/"/utf8>>) of
true ->
gleam@string:drop_end(Value, 1);
false ->
Value
end.
-file("src/packkit/zip.gleam", 2243).
-spec path_depth(binary()) -> integer().
path_depth(Name) ->
case Name of
<<""/utf8>> ->
0;
_ ->
erlang:length(gleam@string:split(Name, <<"/"/utf8>>))
end.
-file("src/packkit/zip.gleam", 2250).
-spec codec_to_archive_error(packkit@error:codec_error(), binary()) -> packkit@error:archive_error().
codec_to_archive_error(Err, Path) ->
case Err of
{codec_invalid_data, Message} ->
{archive_entry_rejected,
Path,
<<"deflate decode failed: "/utf8, Message/binary>>};
{codec_limit_exceeded, Limit, Actual} ->
{archive_limit_exceeded, Limit, Actual};
{codec_dictionary_required, _} ->
{archive_entry_rejected,
Path,
<<"deflate decode requires preset dictionary (not supported)"/utf8>>};
{codec_dictionary_mismatch, _} ->
{archive_entry_rejected,
Path,
<<"deflate decode preset dictionary mismatch"/utf8>>};
{codec_option_unsupported, Option, Codec_name} ->
{archive_entry_rejected,
Path,
<<<<<<"codec "/utf8, Codec_name/binary>>/binary,
" does not support the requested option: "/utf8>>/binary,
Option/binary>>};
{codec_not_implemented, Feature} ->
{archive_not_implemented, Feature}
end.
-file("src/packkit/zip.gleam", 2285).
-spec entry_error_to_archive_error(packkit@entry:entry_error(), binary()) -> packkit@error:archive_error().
entry_error_to_archive_error(Err, Path) ->
case Err of
empty_path ->
{archive_entry_rejected, Path, <<"empty path"/utf8>>};
{absolute_path, _} ->
{archive_entry_rejected, Path, <<"absolute path"/utf8>>};
{path_traversal, _} ->
{archive_entry_rejected, Path, <<"path traversal"/utf8>>};
{windows_path, _} ->
{archive_entry_rejected, Path, <<"windows path"/utf8>>};
{empty_segment, _} ->
{archive_entry_rejected, Path, <<"empty segment"/utf8>>};
{dot_segment, _} ->
{archive_entry_rejected, Path, <<"dot segment"/utf8>>};
{contains_nul, _} ->
{archive_entry_rejected, Path, <<"nul byte"/utf8>>}
end.
-file("src/packkit/zip.gleam", 2307).
-spec slice_or_error(bitstring(), integer(), integer()) -> {ok, bitstring()} |
{error, packkit@error:archive_error()}.
slice_or_error(Bytes, Offset, Width) ->
case gleam_stdlib:bit_array_slice(Bytes, Offset, Width) of
{ok, Value} ->
{ok, Value};
{error, _} ->
{error, {archive_invalid, <<"ZIP slice out of bounds"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 1821).
-spec resolve_pkware_decryption(
bitstring(),
integer(),
integer(),
boolean(),
integer(),
gleam@option:option(binary()),
binary()
) -> {ok, {integer(), integer(), bitstring()}} |
{error, packkit@error:archive_error()}.
resolve_pkware_decryption(
Full,
Data_offset,
Total_size,
Encrypted,
Expected_crc,
Password,
Name
) ->
case {Encrypted, Password} of
{false, _} ->
{ok, {Data_offset, Total_size, Full}};
{true, none} ->
{error,
{archive_not_implemented,
<<<<"encrypted ZIP entry \""/utf8, Name/binary>>/binary,
"\" (use decode_with_password)"/utf8>>}};
{true, {some, Pwd}} ->
gleam@result:'try'(
slice_or_error(Full, Data_offset, Total_size),
fun(Raw) ->
gleam@result:'try'(
split_enc_header(Raw, Total_size),
fun(_use0) ->
{Header_cipher, Payload_cipher} = _use0,
Keys = pkware_keys_from_password(Pwd),
{Header_plain, Keys@1} = pkware_decrypt_bytes(
Keys,
Header_cipher,
<<>>
),
Check = pkware_last_byte(Header_plain),
Expected_check = erlang:'band'(
erlang:'bsr'(Expected_crc, 24),
16#FF
),
gleam@bool:guard(
Check /= Expected_check,
{error,
{archive_invalid,
<<<<"ZIP wrong password or corrupt encryption header for \""/utf8,
Name/binary>>/binary,
"\""/utf8>>}},
fun() ->
{Payload_plain, _} = pkware_decrypt_bytes(
Keys@1,
Payload_cipher,
<<>>
),
{ok, {0, Total_size - 12, Payload_plain}}
end
)
end
)
end
)
end.
-file("src/packkit/zip.gleam", 2068).
-spec aes_ctr_loop(
packkit@internal@aes:expanded_key(),
bitstring(),
integer(),
bitstring(),
binary()
) -> {ok, bitstring()} | {error, packkit@error:archive_error()}.
aes_ctr_loop(Key, Ciphertext, Counter, Acc, Name) ->
case Ciphertext of
<<>> ->
{ok, Acc};
_ ->
Counter_block = <<Counter:64/little, 0:64/little>>,
gleam@result:'try'(
case packkit@internal@aes:encrypt_block(Key, Counter_block) of
{ok, Value} ->
{ok, Value};
{error, _} ->
{error,
{archive_invalid,
<<<<"ZIP AE-x CTR block encrypt failed for \""/utf8,
Name/binary>>/binary,
"\""/utf8>>}}
end,
fun(Keystream) ->
Chunk_size = case erlang:byte_size(Ciphertext) of
N when N < 16 ->
N;
_ ->
16
end,
gleam@result:'try'(
slice_or_error(Ciphertext, 0, Chunk_size),
fun(Chunk) ->
gleam@result:'try'(
slice_or_error(Keystream, 0, Chunk_size),
fun(Keystream_chunk) ->
Xored = xor_bit_arrays_zip(
Chunk,
Keystream_chunk,
<<>>
),
gleam@result:'try'(
slice_or_error(
Ciphertext,
Chunk_size,
erlang:byte_size(Ciphertext) - Chunk_size
),
fun(Rest) ->
aes_ctr_loop(
Key,
Rest,
Counter + 1,
gleam_stdlib:bit_array_concat(
[Acc, Xored]
),
Name
)
end
)
end
)
end
)
end
)
end.
-file("src/packkit/zip.gleam", 2060).
-spec aes_ctr_xor(packkit@internal@aes:expanded_key(), bitstring(), binary()) -> {ok,
bitstring()} |
{error, packkit@error:archive_error()}.
aes_ctr_xor(Key, Ciphertext, Name) ->
aes_ctr_loop(Key, Ciphertext, 1, <<>>, Name).
-file("src/packkit/zip.gleam", 2318).
-spec bytes_to_string(bitstring()) -> {ok, binary()} |
{error, packkit@error:archive_error()}.
bytes_to_string(Bytes) ->
case gleam@bit_array:to_string(Bytes) of
{ok, Value} ->
{ok, Value};
{error, _} ->
{error,
{archive_invalid, <<"non-UTF-8 ZIP name (set EFS flag)"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2333).
-spec name_needs_utf8_flag(bitstring()) -> boolean().
name_needs_utf8_flag(Name_bytes) ->
case Name_bytes of
<<B, Rest/binary>> ->
case B >= 16#80 of
true ->
true;
false ->
name_needs_utf8_flag(Rest)
end;
_ ->
false
end.
-file("src/packkit/zip.gleam", 2344).
-spec le16(integer()) -> bitstring().
le16(Value) ->
<<Value:16/little>>.
-file("src/packkit/zip.gleam", 2348).
-spec le32(integer()) -> bitstring().
le32(Value) ->
<<Value:32/little>>.
-file("src/packkit/zip.gleam", 2352).
-spec read_le16_at(bitstring(), integer()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
read_le16_at(Bytes, Offset) ->
case gleam_stdlib:bit_array_slice(Bytes, Offset, 2) of
{ok, <<Value:16/little>>} ->
{ok, Value};
_ ->
{error, {archive_invalid, <<"short read for 16-bit value"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2359).
-spec read_le32_at(bitstring(), integer()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
read_le32_at(Bytes, Offset) ->
case gleam_stdlib:bit_array_slice(Bytes, Offset, 4) of
{ok, <<Value:32/little>>} ->
{ok, Value};
_ ->
{error, {archive_invalid, <<"short read for 32-bit value"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2366).
-spec read_le32(bitstring()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
read_le32(Bytes) ->
case Bytes of
<<Value:32/little>> ->
{ok, Value};
_ ->
{error, {archive_invalid, <<"short read for 32-bit value"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2373).
-spec read_le64_at(bitstring(), integer()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
read_le64_at(Bytes, Offset) ->
case gleam_stdlib:bit_array_slice(Bytes, Offset, 8) of
{ok, <<Value:64/little>>} ->
{ok, Value};
_ ->
{error, {archive_invalid, <<"short read for 64-bit value"/utf8>>}}
end.
-file("src/packkit/zip.gleam", 2380).
-spec le64(integer()) -> bitstring().
le64(Value) ->
<<Value:64/little>>.
-file("src/packkit/zip.gleam", 2455).
-spec maybe_read_le64(bitstring(), boolean()) -> {ok,
{gleam@option:option(integer()), bitstring()}} |
{error, packkit@error:archive_error()}.
maybe_read_le64(Payload, Needed) ->
case Needed of
false ->
{ok, {none, Payload}};
true ->
case Payload of
<<Value:64/little, Rest/binary>> ->
{ok, {{some, Value}, Rest}};
_ ->
{error,
{archive_invalid,
<<"Zip64 extra field truncated"/utf8>>}}
end
end.
-file("src/packkit/zip.gleam", 2437).
-spec decode_zip64_extra_payload(bitstring(), boolean(), boolean(), boolean()) -> {ok,
{gleam@option:option(integer()),
gleam@option:option(integer()),
gleam@option:option(integer())}} |
{error, packkit@error:archive_error()}.
decode_zip64_extra_payload(
Payload,
Uncomp_at_sentinel,
Comp_at_sentinel,
Offset_at_sentinel
) ->
gleam@result:'try'(
maybe_read_le64(Payload, Uncomp_at_sentinel),
fun(_use0) ->
{Uncomp, Payload@1} = _use0,
gleam@result:'try'(
maybe_read_le64(Payload@1, Comp_at_sentinel),
fun(_use0@1) ->
{Comp, Payload@2} = _use0@1,
gleam@result:'try'(
maybe_read_le64(Payload@2, Offset_at_sentinel),
fun(_use0@2) ->
{Offset, _} = _use0@2,
{ok, {Uncomp, Comp, Offset}}
end
)
end
)
end
).
-file("src/packkit/zip.gleam", 1175).
-spec scan_eocd(bitstring(), integer(), integer()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
scan_eocd(Bytes, Position, Floor) ->
case Position < Floor of
true ->
{error, {archive_invalid, <<"missing ZIP EOCD signature"/utf8>>}};
false ->
case gleam_stdlib:bit_array_slice(Bytes, Position, 4) of
{ok, Slice} ->
case read_le32(Slice) of
{ok, Value} when Value =:= 16#06054b50 ->
{ok, Position};
_ ->
scan_eocd(Bytes, Position - 1, Floor)
end;
{error, _} ->
scan_eocd(Bytes, Position - 1, Floor)
end
end.
-file("src/packkit/zip.gleam", 1161).
-spec locate_eocd(bitstring()) -> {ok, integer()} |
{error, packkit@error:archive_error()}.
locate_eocd(Bytes) ->
Size = erlang:byte_size(Bytes),
case Size < 22 of
true ->
{error, {archive_invalid, <<"ZIP stream too short"/utf8>>}};
false ->
Start = case ((Size - 22) - 65535) < 0 of
true ->
0;
false ->
(Size - 22) - 65535
end,
scan_eocd(Bytes, Size - 22, Start)
end.
-file("src/packkit/zip.gleam", 1291).
-spec read_zip64_eocd_record(bitstring(), integer()) -> {ok,
{integer(), integer(), integer()}} |
{error, packkit@error:archive_error()}.
read_zip64_eocd_record(Bytes, Offset) ->
gleam@result:'try'(
read_le32_at(Bytes, Offset),
fun(Signature) ->
gleam@bool:guard(
Signature /= 16#06064b50,
{error,
{archive_invalid,
<<"Zip64 EOCD signature missing at locator-pointed offset"/utf8>>}},
fun() ->
gleam@result:'try'(
read_le64_at(Bytes, Offset + 32),
fun(Total_entries) ->
gleam@result:'try'(
read_le64_at(Bytes, Offset + 40),
fun(Central_size) ->
gleam@result:'try'(
read_le64_at(Bytes, Offset + 48),
fun(Central_offset) ->
{ok,
{Total_entries,
Central_size,
Central_offset}}
end
)
end
)
end
)
end
)
end
).
-file("src/packkit/zip.gleam", 1264).
?DOC(
" Locate and parse the Zip64 EOCD record. The locator sits in the\n"
" 20 bytes immediately preceding the standard EOCD signature; the\n"
" 8-byte little-endian field at offset 8 of the locator points to\n"
" the Zip64 EOCD record itself.\n"
).
-spec read_zip64_eocd_from_locator(bitstring(), integer()) -> {ok,
{integer(), integer(), integer()}} |
{error, packkit@error:archive_error()}.
read_zip64_eocd_from_locator(Bytes, Eocd_position) ->
case Eocd_position < 20 of
true ->
{error,
{archive_invalid,
<<"ZIP EOCD sentinel without Zip64 locator (archive too short)"/utf8>>}};
false ->
Locator_position = Eocd_position - 20,
gleam@result:'try'(
read_le32_at(Bytes, Locator_position),
fun(Locator_sig) ->
gleam@bool:guard(
Locator_sig /= 16#07064b50,
{error,
{archive_invalid,
<<"ZIP EOCD sentinel but no Zip64 EOCD locator present"/utf8>>}},
fun() ->
gleam@result:'try'(
read_le64_at(Bytes, Locator_position + 8),
fun(Zip64_eocd_offset) ->
read_zip64_eocd_record(
Bytes,
Zip64_eocd_offset
)
end
)
end
)
end
)
end.
-file("src/packkit/zip.gleam", 1121).
?DOC(
" Build a Zip64 extended-information extra-field record. Each of\n"
" `uncomp` / `comp` / `local_offset` is `Some(value)` when the\n"
" corresponding 32-bit slot has been replaced with the 0xFFFFFFFF\n"
" sentinel; the function packs the present values in the order\n"
" APPNOTE.TXT §4.5.3 prescribes.\n"
).
-spec build_zip64_extra(
gleam@option:option(integer()),
gleam@option:option(integer()),
gleam@option:option(integer())
) -> bitstring().
build_zip64_extra(Uncomp, Comp, Local_offset) ->
Body = gleam_stdlib:bit_array_concat([case Uncomp of
{some, V} ->
le64(V);
none ->
<<>>
end, case Comp of
{some, V@1} ->
le64(V@1);
none ->
<<>>
end, case Local_offset of
{some, V@2} ->
le64(V@2);
none ->
<<>>
end]),
Body_size = erlang:byte_size(Body),
gleam_stdlib:bit_array_concat([le16(16#0001), le16(Body_size), Body]).
-file("src/packkit/zip.gleam", 953).
-spec build_unix_ts_extra_mtime(integer()) -> bitstring().
build_unix_ts_extra_mtime(Mtime) ->
Body = <<16#01, Mtime:32/little>>,
gleam_stdlib:bit_array_concat(
[le16(16#5455), le16(erlang:byte_size(Body)), Body]
).
-file("src/packkit/zip.gleam", 989).
-spec build_unix_uid_gid_extra(integer(), integer()) -> bitstring().
build_unix_uid_gid_extra(Uid, Gid) ->
Body = <<1, 16#04, Uid:32/little, 16#04, Gid:32/little>>,
gleam_stdlib:bit_array_concat(
[le16(16#7875), le16(erlang:byte_size(Body)), Body]
).
-file("src/packkit/zip.gleam", 113).
-spec is_supported_method(integer()) -> boolean().
is_supported_method(Method) ->
((((((Method =:= 0) orelse (Method =:= 8)) orelse (Method =:= 12)) orelse (Method
=:= 14))
orelse (Method =:= 93))
orelse (Method =:= 95))
orelse (Method =:= 99).
-file("src/packkit/zip.gleam", 320).
?DOC(
" Build the Zip64 EOCD record (56 bytes) followed by the Zip64 EOCD\n"
" locator (20 bytes). The locator's \"Zip64 EOCD record offset\" is\n"
" the byte position of the Zip64 EOCD record relative to the start\n"
" of the file — i.e. `local_bytes_size + central_bytes_size` since\n"
" we always write the Zip64 record immediately after the central\n"
" directory and immediately before the regular EOCD.\n"
).
-spec build_zip64_eocd_and_locator(integer(), integer(), integer()) -> bitstring().
build_zip64_eocd_and_locator(Count, Central_size, Central_offset) ->
Zip64_eocd = gleam_stdlib:bit_array_concat(
[le32(16#06064b50),
le64(44),
le16(16#0314),
le16(45),
le32(0),
le32(0),
le64(Count),
le64(Count),
le64(Central_size),
le64(Central_offset)]
),
Zip64_offset = Central_offset + Central_size,
Zip64_locator = gleam_stdlib:bit_array_concat(
[le32(16#07064b50), le32(0), le64(Zip64_offset), le32(1)]
),
gleam_stdlib:bit_array_concat([Zip64_eocd, Zip64_locator]).
-file("src/packkit/zip.gleam", 855).
-spec dos_pair_to_unix(integer(), integer()) -> integer().
dos_pair_to_unix(Time_code, Date_code) ->
Day = erlang:'band'(Date_code, 16#1F),
Month = erlang:'band'(erlang:'bsr'(Date_code, 5), 16#0F),
Year = erlang:'bsr'(Date_code, 9) + 1980,
Second_pair = erlang:'band'(Time_code, 16#1F),
Minute = erlang:'band'(erlang:'bsr'(Time_code, 5), 16#3F),
Hour = erlang:'bsr'(Time_code, 11),
case ((Day < 1) orelse (Month < 1)) orelse (Month > 12) of
true ->
315532800;
false ->
Days = days_from_civil(Year, Month, Day),
(((Days * 86400) + (Hour * 3600)) + (Minute * 60)) + (Second_pair * 2)
end.
-file("src/packkit/zip.gleam", 824).
-spec unix_to_dos_pair(integer()) -> {integer(), integer()}.
unix_to_dos_pair(Seconds) ->
case (Seconds < 315532800) orelse (Seconds >= 4354819200) of
true ->
{16#0021, 16#0021};
false ->
Days = Seconds div 86400,
Sod = Seconds - (Days * 86400),
{Year, Month, Day} = civil_from_days(Days),
Hour = Sod div 3600,
Minute = (Sod - (Hour * 3600)) div 60,
Second = (Sod - (Hour * 3600)) - (Minute * 60),
Dos_time = erlang:'bor'(
erlang:'bor'(erlang:'bsl'(Hour, 11), erlang:'bsl'(Minute, 5)),
Second div 2
),
Dos_date = erlang:'bor'(
erlang:'bor'(
erlang:'bsl'(Year - 1980, 9),
erlang:'bsl'(Month, 5)
),
Day
),
{Dos_time, Dos_date}
end.
-file("src/packkit/zip.gleam", 401).
-spec check_u16(integer(), binary()) -> {ok, nil} |
{error, packkit@error:archive_error()}.
check_u16(Value, Field) ->
case (Value < 0) orelse (Value > 16#FFFF) of
true ->
{error,
{archive_field_overflow,
<<"zip "/utf8, Field/binary>>,
Value,
16#FFFF}};
false ->
{ok, nil}
end.
-file("src/packkit/zip.gleam", 413).
-spec check_u32(integer(), binary()) -> {ok, nil} |
{error, packkit@error:archive_error()}.
check_u32(Value, Field) ->
case (Value < 0) orelse (Value > 16#FFFFFFFF) of
true ->
{error,
{archive_field_overflow,
<<"zip "/utf8, Field/binary>>,
Value,
16#FFFFFFFF}};
false ->
{ok, nil}
end.
-file("src/packkit/zip.gleam", 559).
-spec encode_entry(packkit@entry:entry(), integer(), method()) -> {ok,
{bitstring(), bitstring(), integer()}} |
{error, packkit@error:archive_error()}.
encode_entry(Value, Offset, Method) ->
Kind = packkit@entry:kind(Value),
Raw_path = packkit@entry:to_string(packkit@entry:path(Value)),
gleam@bool:guard(
(Kind =:= symlink) orelse (Kind =:= hardlink),
{error,
{archive_entry_rejected,
Raw_path,
<<"ZIP encode currently supports files and directories only"/utf8>>}},
fun() ->
Canonical_path = case Kind of
directory ->
ensure_trailing_slash(Raw_path);
_ ->
Raw_path
end,
Name_bytes = gleam_stdlib:identity(Canonical_path),
Name_length = erlang:byte_size(Name_bytes),
gleam@bool:guard(
Name_length > 65535,
{error,
{archive_entry_rejected,
Canonical_path,
<<"ZIP entry name longer than 65535 bytes"/utf8>>}},
fun() ->
Raw_body = case Kind of
directory ->
<<>>;
_ ->
packkit@entry:body(Value)
end,
Uncomp_size = erlang:byte_size(Raw_body),
Crc = case Kind of
directory ->
0;
_ ->
packkit@checksum:crc32(Raw_body)
end,
Entry_method = case Kind of
directory ->
store();
_ ->
Method
end,
gleam@result:'try'(case erlang:element(2, Entry_method) of
<<"store"/utf8>> ->
{ok, {0, Raw_body}};
<<"deflate"/utf8>> ->
_pipe = deflate_with_level(
Raw_body,
erlang:element(3, Entry_method)
),
_pipe@1 = gleam@result:map(
_pipe,
fun(B) -> {8, B} end
),
gleam@result:map_error(
_pipe@1,
fun(_capture) ->
codec_to_archive_error(
_capture,
Canonical_path
)
end
);
<<"bzip2"/utf8>> ->
_pipe@2 = packkit@bzip2:encode(Raw_body),
_pipe@3 = gleam@result:map(
_pipe@2,
fun(B@1) -> {12, B@1} end
),
gleam@result:map_error(
_pipe@3,
fun(_capture@1) ->
codec_to_archive_error(
_capture@1,
Canonical_path
)
end
);
<<"lzma"/utf8>> ->
{ok, {14, encode_pkware_lzma(Raw_body)}};
<<"zstd"/utf8>> ->
_pipe@4 = packkit@zstd:encode(Raw_body),
_pipe@5 = gleam@result:map(
_pipe@4,
fun(B@2) -> {93, B@2} end
),
gleam@result:map_error(
_pipe@5,
fun(_capture@2) ->
codec_to_archive_error(
_capture@2,
Canonical_path
)
end
);
<<"xz"/utf8>> ->
_pipe@6 = packkit@xz:encode(Raw_body),
_pipe@7 = gleam@result:map(
_pipe@6,
fun(B@3) -> {95, B@3} end
),
gleam@result:map_error(
_pipe@7,
fun(_capture@3) ->
codec_to_archive_error(
_capture@3,
Canonical_path
)
end
);
Other ->
{error,
{archive_not_implemented,
<<"ZIP method "/utf8, Other/binary>>}}
end, fun(_use0) ->
{Method_code, Compressed_body} = _use0,
Comp_size = erlang:byte_size(Compressed_body),
gleam@result:'try'(
check_u32(Crc, <<"crc32"/utf8>>),
fun(_) ->
Metadata = packkit@entry:metadata(Value),
Mode = packkit@entry:mode(Metadata),
External_attrs = case Kind of
directory ->
erlang:'bor'(
16#40000000,
erlang:'bsl'(Mode, 16)
);
_ ->
erlang:'bsl'(Mode, 16)
end,
gleam@result:'try'(
check_u32(
External_attrs,
<<"external_attributes"/utf8>>
),
fun(_) ->
Local_needs_zip64 = (Uncomp_size > 16#FFFFFFFF)
orelse (Comp_size > 16#FFFFFFFF),
Central_needs_zip64 = Local_needs_zip64
orelse (Offset > 16#FFFFFFFF),
Local_uncomp_slot = case Uncomp_size
> 16#FFFFFFFF of
true ->
16#FFFFFFFF;
false ->
Uncomp_size
end,
Local_comp_slot = case Comp_size > 16#FFFFFFFF of
true ->
16#FFFFFFFF;
false ->
Comp_size
end,
Central_offset_slot = case Offset > 16#FFFFFFFF of
true ->
16#FFFFFFFF;
false ->
Offset
end,
Mtime_unix = packkit@entry:modified_at_unix(
Metadata
),
{Dos_time, Dos_date} = unix_to_dos_pair(
Mtime_unix
),
Unix_ts_extra = case Mtime_unix > 0 of
true ->
build_unix_ts_extra_mtime(
Mtime_unix
);
false ->
<<>>
end,
Uid = packkit@entry:user_id(
Metadata
),
Gid = packkit@entry:group_id(
Metadata
),
Unix_uid_gid_extra = case (Uid > 0)
orelse (Gid > 0) of
true ->
build_unix_uid_gid_extra(
Uid,
Gid
);
false ->
<<>>
end,
Zip64_local_extra = case Local_needs_zip64 of
false ->
<<>>;
true ->
build_zip64_extra(
{some, Uncomp_size},
{some, Comp_size},
none
)
end,
Zip64_central_extra = case Central_needs_zip64 of
false ->
<<>>;
true ->
build_zip64_extra(
case Uncomp_size > 16#FFFFFFFF of
true ->
{some,
Uncomp_size};
false ->
none
end,
case Comp_size > 16#FFFFFFFF of
true ->
{some,
Comp_size};
false ->
none
end,
case Offset > 16#FFFFFFFF of
true ->
{some, Offset};
false ->
none
end
)
end,
Local_extra = gleam_stdlib:bit_array_concat(
[Zip64_local_extra,
Unix_ts_extra,
Unix_uid_gid_extra]
),
Central_extra = gleam_stdlib:bit_array_concat(
[Zip64_central_extra,
Unix_ts_extra,
Unix_uid_gid_extra]
),
Version_needed = case {Local_needs_zip64
orelse Central_needs_zip64,
Method_code} of
{true, _} ->
45;
{false, M} when M =:= 8 ->
20;
{false, M@1} when M@1 =:= 12 ->
46;
{false, M@2} when M@2 =:= 14 ->
63;
{false, M@3} when M@3 =:= 93 ->
63;
{false, M@4} when M@4 =:= 95 ->
63;
{false, _} ->
10
end,
Lzma_flag_bit = case Method_code of
M@5 when M@5 =:= 14 ->
16#02;
_ ->
0
end,
Efs_flag_bit = case name_needs_utf8_flag(
Name_bytes
) of
true ->
16#0800;
false ->
16#00
end,
Gp_flag = erlang:'bor'(
Lzma_flag_bit,
Efs_flag_bit
),
Local_header = gleam_stdlib:bit_array_concat(
[le32(16#04034b50),
le16(Version_needed),
le16(Gp_flag),
le16(Method_code),
le16(Dos_time),
le16(Dos_date),
le32(Crc),
le32(Local_comp_slot),
le32(Local_uncomp_slot),
le16(Name_length),
le16(
erlang:byte_size(
Local_extra
)
),
Name_bytes,
Local_extra]
),
Local_record = gleam_stdlib:bit_array_concat(
[Local_header, Compressed_body]
),
Local_record_size = erlang:byte_size(
Local_record
),
Central_record = gleam_stdlib:bit_array_concat(
[le32(16#02014b50),
le16(16#0314),
le16(Version_needed),
le16(Gp_flag),
le16(Method_code),
le16(Dos_time),
le16(Dos_date),
le32(Crc),
le32(Local_comp_slot),
le32(Local_uncomp_slot),
le16(Name_length),
le16(
erlang:byte_size(
Central_extra
)
),
le16(0),
le16(0),
le16(0),
le32(External_attrs),
le32(Central_offset_slot),
Name_bytes,
Central_extra]
),
{ok,
{Local_record,
Central_record,
Local_record_size}}
end
)
end
)
end)
end
)
end
).
-file("src/packkit/zip.gleam", 518).
?DOC(
" Walk every entry once with `list.fold`, accumulating the running\n"
" byte offset alongside the local-file + central-directory record\n"
" lists. Using `list.fold` here (instead of an explicit\n"
" tail-recursive helper) keeps the encoder iterative on the\n"
" JavaScript target, which doesn't TCO Gleam recursion and would\n"
" otherwise blow its call stack on archives with tens of thousands\n"
" of entries (and so couldn't ever build a Zip64-sized archive).\n"
).
-spec encode_entries(
list(packkit@entry:entry()),
integer(),
list(bitstring()),
list(bitstring()),
method()
) -> {ok, {list(bitstring()), list(bitstring())}} |
{error, packkit@error:archive_error()}.
encode_entries(Remaining, _, _, _, Method) ->
Init = {encode_fold, 0, [], [], none},
Folded = gleam@list:fold(
Remaining,
Init,
fun(State, Entry_value) -> case erlang:element(5, State) of
{some, _} ->
State;
none ->
case encode_entry(
Entry_value,
erlang:element(2, State),
Method
) of
{ok, {Local_record, Central_record, Advance}} ->
{encode_fold,
erlang:element(2, State) + Advance,
[Local_record | erlang:element(3, State)],
[Central_record | erlang:element(4, State)],
none};
{error, E} ->
{encode_fold,
erlang:element(2, State),
erlang:element(3, State),
erlang:element(4, State),
{some, E}}
end
end end
),
case erlang:element(5, Folded) of
{some, E@1} ->
{error, E@1};
none ->
{ok,
{lists:reverse(erlang:element(3, Folded)),
lists:reverse(erlang:element(4, Folded))}}
end.
-file("src/packkit/zip.gleam", 244).
?DOC(
" Encode a logical archive into a ZIP byte stream using a chosen\n"
" per-entry method. Supports `store`, `deflate`, `bzip2`, `zstd`,\n"
" and `xz`.\n"
).
-spec encode_with_method(packkit@archive:archive(), method()) -> {ok,
bitstring()} |
{error, packkit@error:archive_error()}.
encode_with_method(Archive_value, Method) ->
Entries = packkit@archive:entries(Archive_value),
gleam@result:'try'(
encode_entries(Entries, 0, [], [], Method),
fun(_use0) ->
{Local_blocks, Central_blocks} = _use0,
Local_bytes = gleam_stdlib:bit_array_concat(Local_blocks),
Central_bytes = gleam_stdlib:bit_array_concat(Central_blocks),
Central_offset = erlang:byte_size(Local_bytes),
Central_size = erlang:byte_size(Central_bytes),
Count = erlang:length(Entries),
Comment_bytes = case packkit@archive:comment(Archive_value) of
{some, C} ->
gleam_stdlib:identity(C);
none ->
<<>>
end,
Comment_size = erlang:byte_size(Comment_bytes),
gleam@result:'try'(
check_u16(Comment_size, <<"archive_comment_length"/utf8>>),
fun(_) ->
Needs_zip64_eocd = ((Count > 16#FFFF) orelse (Central_size > 16#FFFFFFFF))
orelse (Central_offset > 16#FFFFFFFF),
Zip64_trailer = case Needs_zip64_eocd of
false ->
<<>>;
true ->
build_zip64_eocd_and_locator(
Count,
Central_size,
Central_offset
)
end,
Eocd_total_entries = case Count > 16#FFFF of
true ->
16#FFFF;
false ->
Count
end,
Eocd_central_size = case Central_size > 16#FFFFFFFF of
true ->
16#FFFFFFFF;
false ->
Central_size
end,
Eocd_central_offset = case Central_offset > 16#FFFFFFFF of
true ->
16#FFFFFFFF;
false ->
Central_offset
end,
Eocd = gleam_stdlib:bit_array_concat(
[le32(16#06054b50),
le16(0),
le16(0),
le16(Eocd_total_entries),
le16(Eocd_total_entries),
le32(Eocd_central_size),
le32(Eocd_central_offset),
le16(Comment_size),
Comment_bytes]
),
{ok,
gleam_stdlib:bit_array_concat(
[Local_bytes, Central_bytes, Zip64_trailer, Eocd]
)}
end
)
end
).
-file("src/packkit/zip.gleam", 235).
?DOC(
" Encode a logical archive into a ZIP byte stream using the stored\n"
" (uncompressed) method for every entry.\n"
).
-spec encode(packkit@archive:archive()) -> {ok, bitstring()} |
{error, packkit@error:archive_error()}.
encode(Archive_value) ->
encode_with_method(Archive_value, store()).
-file("src/packkit/zip.gleam", 1194).
-spec read_eocd(bitstring(), integer()) -> {ok, eocd_record()} |
{error, packkit@error:archive_error()}.
read_eocd(Bytes, Position) ->
gleam@result:'try'(
read_le16_at(Bytes, Position + 10),
fun(Total_entries) ->
gleam@result:'try'(
read_le32_at(Bytes, Position + 12),
fun(Central_size) ->
gleam@result:'try'(
read_le32_at(Bytes, Position + 16),
fun(Central_offset) ->
gleam@result:'try'(
read_le16_at(Bytes, Position + 20),
fun(Comment_length) ->
gleam@result:'try'(case Comment_length of
0 ->
{ok, none};
N ->
gleam@result:'try'(
slice_or_error(
Bytes,
Position + 22,
N
),
fun(Comment_bits) ->
case gleam@bit_array:to_string(
Comment_bits
) of
{ok, Text} ->
{ok,
{some, Text}};
{error, _} ->
{error,
{archive_invalid,
<<"non-UTF-8 archive comment in ZIP EOCD"/utf8>>}}
end
end
)
end, fun(Comment) ->
Needs_zip64 = ((Total_entries =:= 16#FFFF)
orelse (Central_size =:= 16#FFFFFFFF))
orelse (Central_offset =:= 16#FFFFFFFF),
gleam@result:'try'(
case Needs_zip64 of
false ->
{ok,
{Total_entries,
Central_size,
Central_offset}};
true ->
gleam@result:'try'(
read_zip64_eocd_from_locator(
Bytes,
Position
),
fun(_use0) ->
{Zip64_entries,
Zip64_size,
Zip64_offset} = _use0,
{ok,
{case Total_entries
=:= 16#FFFF of
true ->
Zip64_entries;
false ->
Total_entries
end,
case Central_size
=:= 16#FFFFFFFF of
true ->
Zip64_size;
false ->
Central_size
end,
case Central_offset
=:= 16#FFFFFFFF of
true ->
Zip64_offset;
false ->
Central_offset
end}}
end
)
end,
fun(_use0@1) ->
{Total_entries@1,
Central_size@1,
Central_offset@1} = _use0@1,
{ok,
{eocd_record,
Total_entries@1,
Central_offset@1,
Central_size@1,
Comment}}
end
)
end)
end
)
end
)
end
)
end
).
-file("src/packkit/zip.gleam", 2417).
-spec step_extra_field(bitstring(), integer(), integer(), integer()) -> {ok,
bitstring()} |
{error, nil}.
step_extra_field(Rest, Size, Id, Want_id) ->
gleam@result:'try'(case gleam_stdlib:bit_array_slice(Rest, 0, Size) of
{ok, B} ->
{ok, B};
_ ->
{error, nil}
end, fun(Body) -> case Id =:= Want_id of
true ->
{ok, Body};
false ->
case gleam_stdlib:bit_array_slice(
Rest,
Size,
erlang:byte_size(Rest) - Size
) of
{ok, After} ->
find_extra_field(After, Want_id);
_ ->
{error, nil}
end
end end).
-file("src/packkit/zip.gleam", 2409).
-spec find_extra_field(bitstring(), integer()) -> {ok, bitstring()} |
{error, nil}.
find_extra_field(Extra, Want_id) ->
case Extra of
<<Id:16/little, Size:16/little, Rest/binary>> ->
step_extra_field(Rest, Size, Id, Want_id);
_ ->
{error, nil}
end.
-file("src/packkit/zip.gleam", 966).
-spec find_unix_ts_extra_mtime(bitstring()) -> gleam@option:option(integer()).
find_unix_ts_extra_mtime(Extra) ->
case find_extra_field(Extra, 16#5455) of
{error, _} ->
none;
{ok, Body} ->
case Body of
<<Flags, Mtime:32/little, _/binary>> ->
case erlang:'band'(Flags, 16#01) =:= 16#01 of
true ->
{some, Mtime};
false ->
none
end;
_ ->
none
end
end.
-file("src/packkit/zip.gleam", 1020).
-spec find_unix_uid_gid_new(bitstring()) -> gleam@option:option({integer(),
integer()}).
find_unix_uid_gid_new(Extra) ->
option_try(
find_extra_field(Extra, 16#7875),
fun(Body) -> parse_unix_uid_gid_new_body(Body) end
).
-file("src/packkit/zip.gleam", 1085).
-spec find_unix_uid_gid_old(bitstring()) -> gleam@option:option({integer(),
integer()}).
find_unix_uid_gid_old(Extra) ->
case find_extra_field(Extra, 16#7855) of
{error, _} ->
none;
{ok, Body} ->
case Body of
<<Uid:16/little, Gid:16/little, _/binary>> ->
{some, {Uid, Gid}};
_ ->
none
end
end.
-file("src/packkit/zip.gleam", 1013).
-spec find_unix_uid_gid_in_extra(bitstring()) -> gleam@option:option({integer(),
integer()}).
find_unix_uid_gid_in_extra(Extra) ->
case find_unix_uid_gid_new(Extra) of
{some, Pair} ->
{some, Pair};
none ->
find_unix_uid_gid_old(Extra)
end.
-file("src/packkit/zip.gleam", 2002).
-spec parse_aex_extra(bitstring(), binary()) -> {ok, aex_params()} |
{error, packkit@error:archive_error()}.
parse_aex_extra(Extras, Name) ->
case find_extra_field(Extras, 16#9901) of
{error, _} ->
{error,
{archive_invalid,
<<<<"ZIP AE-x marker entry \""/utf8, Name/binary>>/binary,
"\" missing the 0x9901 extra field"/utf8>>}};
{ok, Body} ->
case Body of
<<Version:16/little,
16#41,
16#45,
Strength,
Real_method:16/little>> ->
{ok, {aex_params, Version, Strength, Real_method}};
_ ->
{error,
{archive_invalid,
<<<<"ZIP AE-x 0x9901 extra is malformed for \""/utf8,
Name/binary>>/binary,
"\""/utf8>>}}
end
end.
-file("src/packkit/zip.gleam", 1929).
?DOC(
" Resolve a method-99 AE-x entry to its plain compressed bytes\n"
" (i.e. the bytes the real method's decoder would receive in the\n"
" unencrypted case) plus the real method id. Returns `Error` when\n"
" the 0x9901 extra is missing / malformed, the password fails the\n"
" 2-byte verifier check, or the HMAC tag mismatches the ciphertext.\n"
).
-spec resolve_aex_decryption(
bitstring(),
integer(),
integer(),
bitstring(),
gleam@option:option(binary()),
binary()
) -> {ok, {integer(), integer(), bitstring(), integer()}} |
{error, packkit@error:archive_error()}.
resolve_aex_decryption(Full, Data_offset, Total_size, Extras, Password, Name) ->
gleam@result:'try'(
parse_aex_extra(Extras, Name),
fun(Params) ->
gleam@result:'try'(
aex_key_length(erlang:element(3, Params), Name),
fun(Key_len) ->
Salt_len = Key_len div 2,
gleam@result:'try'(case Password of
{some, Value} ->
{ok, Value};
none ->
{error,
{archive_not_implemented,
<<<<"encrypted ZIP entry \""/utf8,
Name/binary>>/binary,
"\" (use decode_with_password)"/utf8>>}}
end, fun(Pwd) ->
gleam@result:'try'(
slice_or_error(Full, Data_offset, Total_size),
fun(Raw) ->
Ciphertext_len = ((Total_size - Salt_len) - 2)
- 10,
gleam@bool:guard(
Ciphertext_len < 0,
{error,
{archive_invalid,
<<<<"ZIP AE-x entry \""/utf8,
Name/binary>>/binary,
"\" is too short for salt + verifier + HMAC"/utf8>>}},
fun() ->
gleam@result:'try'(
slice_or_error(Raw, 0, Salt_len),
fun(Salt) ->
gleam@result:'try'(
slice_or_error(
Raw,
Salt_len,
2
),
fun(Verifier) ->
gleam@result:'try'(
slice_or_error(
Raw,
Salt_len + 2,
Ciphertext_len
),
fun(Ciphertext) ->
gleam@result:'try'(
slice_or_error(
Raw,
(Salt_len
+ 2)
+ Ciphertext_len,
10
),
fun(
Hmac_tag
) ->
Derived = packkit@checksum:pbkdf2_hmac_sha1(
gleam_stdlib:identity(
Pwd
),
Salt,
1000,
(Key_len
* 2)
+ 2
),
gleam@result:'try'(
slice_or_error(
Derived,
0,
Key_len
),
fun(
Aes_key
) ->
gleam@result:'try'(
slice_or_error(
Derived,
Key_len,
Key_len
),
fun(
Hmac_key
) ->
gleam@result:'try'(
slice_or_error(
Derived,
Key_len
* 2,
2
),
fun(
Expected_verifier
) ->
gleam@bool:guard(
Verifier
/= Expected_verifier,
{error,
{archive_invalid,
<<<<"ZIP AE-x wrong password for entry \""/utf8,
Name/binary>>/binary,
"\""/utf8>>}},
fun(
) ->
Computed_full = packkit@checksum:hmac_sha1(
Hmac_key,
Ciphertext
),
gleam@result:'try'(
slice_or_error(
Computed_full,
0,
10
),
fun(
Computed_tag
) ->
gleam@bool:guard(
Computed_tag
/= Hmac_tag,
{error,
{archive_invalid,
<<<<"ZIP AE-x HMAC mismatch for entry \""/utf8,
Name/binary>>/binary,
"\""/utf8>>}},
fun(
) ->
gleam@result:'try'(
case packkit@internal@aes:expand_key(
Aes_key
) of
{ok,
Value@1} ->
{ok,
Value@1};
{error,
_} ->
{error,
{archive_invalid,
<<<<"ZIP AE-x AES key expansion failed for \""/utf8,
Name/binary>>/binary,
"\""/utf8>>}}
end,
fun(
Expanded
) ->
gleam@result:'try'(
aes_ctr_xor(
Expanded,
Ciphertext,
Name
),
fun(
Plaintext
) ->
{ok,
{0,
Ciphertext_len,
Plaintext,
erlang:element(
4,
Params
)}}
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end)
end
)
end
).
-file("src/packkit/zip.gleam", 2391).
?DOC(
" Parse the Zip64 extended-information extra field (header_id 0x0001)\n"
" out of a packed extra-field block. Only the slots that hit the\n"
" 0xFFFFFFFF sentinel in the surrounding central-directory or local\n"
" file header have 8-byte values present in the extra-field body,\n"
" and they appear in a fixed order (uncomp_size, comp_size,\n"
" local_offset, disk_start). The decoder walks the extra-field\n"
" chain and picks the first record with `header_id = 0x0001`.\n"
).
-spec parse_zip64_extra(bitstring(), boolean(), boolean(), boolean()) -> {ok,
{gleam@option:option(integer()),
gleam@option:option(integer()),
gleam@option:option(integer())}} |
{error, packkit@error:archive_error()}.
parse_zip64_extra(
Extra,
Uncomp_at_sentinel,
Comp_at_sentinel,
Offset_at_sentinel
) ->
case find_extra_field(Extra, 16#0001) of
{ok, Payload} ->
decode_zip64_extra_payload(
Payload,
Uncomp_at_sentinel,
Comp_at_sentinel,
Offset_at_sentinel
);
_ ->
{ok, {none, none, none}}
end.
-file("src/packkit/zip.gleam", 1506).
-spec read_local_entry(
bitstring(),
integer(),
binary(),
integer(),
integer(),
integer(),
integer(),
packkit@limit:limits(),
integer(),
gleam@option:option(binary()),
integer(),
integer(),
integer()
) -> {ok, packkit@entry:entry()} | {error, packkit@error:archive_error()}.
read_local_entry(
Full,
Local_offset,
Name,
Expected_crc,
Uncomp_size,
Comp_size,
External_attrs,
Limits,
Gp_flag,
Password,
Mtime_unix,
Owner_uid,
Owner_gid
) ->
gleam@result:'try'(
read_le32_at(Full, Local_offset),
fun(Signature) ->
gleam@bool:guard(
Signature /= 16#04034b50,
{error,
{archive_invalid,
<<"missing local file header signature"/utf8>>}},
fun() ->
Encrypted = erlang:'band'(Gp_flag, 1) =:= 1,
Strong_encrypted = erlang:'band'(Gp_flag, 16#40) /= 0,
gleam@bool:guard(
Strong_encrypted,
{error,
{archive_not_implemented,
<<"ZIP strong-encryption (gp flag bit 6)"/utf8>>}},
fun() ->
gleam@result:'try'(
read_le16_at(Full, Local_offset + 8),
fun(Method) ->
gleam@bool:guard(
not is_supported_method(Method),
{error,
{archive_not_implemented,
<<"ZIP method "/utf8,
(erlang:integer_to_binary(
Method
))/binary>>}},
fun() ->
gleam@result:'try'(
read_le16_at(
Full,
Local_offset + 26
),
fun(Local_name_length) ->
gleam@result:'try'(
read_le16_at(
Full,
Local_offset + 28
),
fun(Local_extra_length) ->
gleam@result:'try'(
read_le32_at(
Full,
Local_offset
+ 22
),
fun(
Local_uncomp
) ->
gleam@result:'try'(
read_le32_at(
Full,
Local_offset
+ 18
),
fun(
Local_comp
) ->
gleam@result:'try'(
slice_or_error(
Full,
(Local_offset
+ 30)
+ Local_name_length,
Local_extra_length
),
fun(
Local_extra_bits
) ->
gleam@result:'try'(
parse_zip64_extra(
Local_extra_bits,
Local_uncomp
=:= 16#FFFFFFFF,
Local_comp
=:= 16#FFFFFFFF,
false
),
fun(
_use0
) ->
{Local_zip64_uncomp,
Local_zip64_comp,
_} = _use0,
Uncomp_size@1 = case {Uncomp_size,
Local_zip64_uncomp} of
{N,
{some,
V}} when N =:= 16#FFFFFFFF ->
V;
{N@1,
_} ->
N@1
end,
Comp_size@1 = case {Comp_size,
Local_zip64_comp} of
{N@2,
{some,
V@1}} when N@2 =:= 16#FFFFFFFF ->
V@1;
{N@3,
_} ->
N@3
end,
Data_offset = ((Local_offset
+ 30)
+ Local_name_length)
+ Local_extra_length,
Is_aex = Encrypted
andalso (Method
=:= 99),
gleam@result:'try'(
case Is_aex of
true ->
resolve_aex_decryption(
Full,
Data_offset,
Comp_size@1,
Local_extra_bits,
Password,
Name
);
false ->
_pipe = resolve_pkware_decryption(
Full,
Data_offset,
Comp_size@1,
Encrypted,
Expected_crc,
Password,
Name
),
gleam@result:map(
_pipe,
fun(
Triple
) ->
{Offset,
Size,
Source} = Triple,
{Offset,
Size,
Source,
Method}
end
)
end,
fun(
_use0@1
) ->
{Data_offset@1,
Comp_size@2,
Body_source,
Method@1} = _use0@1,
gleam@result:'try'(
case Method@1 of
M when M =:= 0 ->
slice_or_error(
Body_source,
Data_offset@1,
Uncomp_size@1
);
M@1 when M@1 =:= 8 ->
gleam@result:'try'(
slice_or_error(
Body_source,
Data_offset@1,
Comp_size@2
),
fun(
Compressed
) ->
_pipe@1 = packkit@deflate:decode_with_limits(
Compressed,
Limits
),
gleam@result:map_error(
_pipe@1,
fun(
_capture
) ->
codec_to_archive_error(
_capture,
Name
)
end
)
end
);
M@2 when M@2 =:= 12 ->
gleam@result:'try'(
slice_or_error(
Body_source,
Data_offset@1,
Comp_size@2
),
fun(
Compressed@1
) ->
_pipe@2 = packkit@bzip2:decode_with_limits(
Compressed@1,
Limits
),
gleam@result:map_error(
_pipe@2,
fun(
_capture@1
) ->
codec_to_archive_error(
_capture@1,
Name
)
end
)
end
);
M@3 when M@3 =:= 14 ->
gleam@result:'try'(
slice_or_error(
Body_source,
Data_offset@1,
Comp_size@2
),
fun(
Compressed@2
) ->
_pipe@3 = decode_pkware_lzma(
Compressed@2,
Uncomp_size@1,
Limits
),
gleam@result:map_error(
_pipe@3,
fun(
_capture@2
) ->
codec_to_archive_error(
_capture@2,
Name
)
end
)
end
);
M@4 when M@4 =:= 93 ->
gleam@result:'try'(
slice_or_error(
Body_source,
Data_offset@1,
Comp_size@2
),
fun(
Compressed@3
) ->
_pipe@4 = packkit@zstd:decode_with_limits(
Compressed@3,
Limits
),
gleam@result:map_error(
_pipe@4,
fun(
_capture@3
) ->
codec_to_archive_error(
_capture@3,
Name
)
end
)
end
);
M@5 when M@5 =:= 95 ->
gleam@result:'try'(
slice_or_error(
Body_source,
Data_offset@1,
Comp_size@2
),
fun(
Compressed@4
) ->
_pipe@5 = packkit@xz:decode_with_limits(
Compressed@4,
Limits
),
gleam@result:map_error(
_pipe@5,
fun(
_capture@4
) ->
codec_to_archive_error(
_capture@4,
Name
)
end
)
end
);
Other ->
{error,
{archive_not_implemented,
<<"ZIP method "/utf8,
(erlang:integer_to_binary(
Other
))/binary>>}}
end,
fun(
Body
) ->
Skip_crc = Is_aex
andalso (Expected_crc
=:= 0),
gleam@bool:guard(
not Skip_crc
andalso (packkit@checksum:crc32(
Body
)
/= Expected_crc),
{error,
{archive_invalid,
<<"ZIP CRC32 mismatch"/utf8>>}},
fun(
) ->
Is_directory = gleam_stdlib:string_ends_with(
Name,
<<"/"/utf8>>
),
Mode = erlang:'band'(
erlang:'bsr'(
External_attrs,
16
),
16#FFFF
),
Apply_metadata = fun(
E
) ->
With_mode = case Mode of
0 ->
E;
_ ->
packkit@entry:with_mode(
E,
Mode
)
end,
With_mtime = case Mtime_unix
> 0 of
true ->
packkit@entry:with_modified_at(
With_mode,
Mtime_unix
);
false ->
With_mode
end,
case (Owner_uid
> 0)
orelse (Owner_gid
> 0) of
true ->
packkit@entry:with_owner(
With_mtime,
Owner_uid,
Owner_gid
);
false ->
With_mtime
end
end,
case Is_directory of
true ->
_pipe@6 = packkit@entry:directory_checked(
strip_trailing_slash(
Name
)
),
_pipe@7 = gleam@result:map_error(
_pipe@6,
fun(
_capture@5
) ->
entry_error_to_archive_error(
_capture@5,
Name
)
end
),
gleam@result:map(
_pipe@7,
Apply_metadata
);
false ->
_pipe@8 = packkit@entry:file_checked(
Name,
Body
),
_pipe@9 = gleam@result:map_error(
_pipe@8,
fun(
_capture@6
) ->
entry_error_to_archive_error(
_capture@6,
Name
)
end
),
gleam@result:map(
_pipe@9,
Apply_metadata
)
end
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/packkit/zip.gleam", 1318).
-spec parse_central_directory(
bitstring(),
list(packkit@entry:entry()),
integer(),
bitstring(),
integer(),
packkit@limit:limits(),
gleam@option:option(binary())
) -> {ok, list(packkit@entry:entry())} | {error, packkit@error:archive_error()}.
parse_central_directory(
Bytes,
Acc,
Remaining,
Full,
Accumulated_body_bytes,
Limits,
Password
) ->
case Remaining of
0 ->
{ok, Acc};
_ ->
gleam@result:'try'(
read_le32_at(Bytes, 0),
fun(Signature) ->
gleam@bool:guard(
Signature /= 16#02014b50,
{error,
{archive_invalid,
<<"missing ZIP central directory signature"/utf8>>}},
fun() ->
gleam@result:'try'(
read_le16_at(Bytes, 8),
fun(Gp_flag) ->
gleam@result:'try'(
read_le16_at(Bytes, 10),
fun(Method) ->
gleam@result:'try'(
read_le16_at(Bytes, 12),
fun(Dos_time) ->
gleam@result:'try'(
read_le16_at(Bytes, 14),
fun(Dos_date) ->
gleam@result:'try'(
read_le32_at(
Bytes,
16
),
fun(Crc) ->
gleam@result:'try'(
read_le32_at(
Bytes,
20
),
fun(
Comp_size
) ->
gleam@result:'try'(
read_le32_at(
Bytes,
24
),
fun(
Uncomp_size
) ->
gleam@result:'try'(
read_le16_at(
Bytes,
28
),
fun(
Name_length
) ->
gleam@result:'try'(
read_le16_at(
Bytes,
30
),
fun(
Extra_length
) ->
gleam@result:'try'(
read_le16_at(
Bytes,
32
),
fun(
Comment_length
) ->
gleam@result:'try'(
read_le32_at(
Bytes,
38
),
fun(
External_attrs
) ->
gleam@result:'try'(
read_le32_at(
Bytes,
42
),
fun(
Local_offset
) ->
Name_offset = 46,
gleam@result:'try'(
slice_or_error(
Bytes,
Name_offset,
Name_length
),
fun(
Name_bits
) ->
gleam@result:'try'(
bytes_to_string(
Name_bits
),
fun(
Name
) ->
gleam@result:'try'(
slice_or_error(
Bytes,
Name_offset
+ Name_length,
Extra_length
),
fun(
Extra_bits
) ->
Uncomp_at_sentinel = Uncomp_size
=:= 16#FFFFFFFF,
Comp_at_sentinel = Comp_size
=:= 16#FFFFFFFF,
Offset_at_sentinel = Local_offset
=:= 16#FFFFFFFF,
gleam@result:'try'(
parse_zip64_extra(
Extra_bits,
Uncomp_at_sentinel,
Comp_at_sentinel,
Offset_at_sentinel
),
fun(
_use0
) ->
{Zip64_uncomp,
Zip64_comp,
Zip64_offset} = _use0,
Uncomp_size@1 = case Zip64_uncomp of
{some,
V} ->
V;
none ->
Uncomp_size
end,
Comp_size@1 = case Zip64_comp of
{some,
V@1} ->
V@1;
none ->
Comp_size
end,
Local_offset@1 = case Zip64_offset of
{some,
V@2} ->
V@2;
none ->
Local_offset
end,
Mtime_resolved = case find_unix_ts_extra_mtime(
Extra_bits
) of
{some,
V@3} when V@3 >= 0 ->
V@3;
_ ->
case (Dos_time
=:= 16#0021)
andalso (Dos_date
=:= 16#0021) of
true ->
0;
false ->
dos_pair_to_unix(
Dos_time,
Dos_date
)
end
end,
{Owner_uid,
Owner_gid} = case find_unix_uid_gid_in_extra(
Extra_bits
) of
{some,
{Uid,
Gid}} when (Uid >= 0) andalso (Gid >= 0) ->
{Uid,
Gid};
_ ->
{0,
0}
end,
gleam@bool:guard(
erlang:byte_size(
Name
)
> packkit@limit:max_entry_name_bytes(
Limits
),
{error,
{archive_limit_exceeded,
<<"max_entry_name_bytes"/utf8>>,
erlang:byte_size(
Name
)}},
fun(
) ->
Cleaned_name = strip_trailing_slash(
Name
),
Depth = path_depth(
Cleaned_name
),
gleam@bool:guard(
Depth
> packkit@limit:max_entry_depth(
Limits
),
{error,
{archive_limit_exceeded,
<<"max_entry_depth"/utf8>>,
Depth}},
fun(
) ->
gleam@bool:guard(
not is_supported_method(
Method
),
{error,
{archive_not_implemented,
<<"ZIP method "/utf8,
(erlang:integer_to_binary(
Method
))/binary>>}},
fun(
) ->
gleam@result:'try'(
read_local_entry(
Full,
Local_offset@1,
Name,
Crc,
Uncomp_size@1,
Comp_size@1,
External_attrs,
Limits,
Gp_flag,
Password,
Mtime_resolved,
Owner_uid,
Owner_gid
),
fun(
Entry_value
) ->
Next_total = Accumulated_body_bytes
+ erlang:byte_size(
packkit@entry:body(
Entry_value
)
),
gleam@bool:guard(
Next_total
> packkit@limit:max_output_bytes(
Limits
),
{error,
{archive_limit_exceeded,
<<"max_output_bytes"/utf8>>,
Next_total}},
fun(
) ->
Record_size = ((46
+ Name_length)
+ Extra_length)
+ Comment_length,
Next_bits = case gleam_stdlib:bit_array_slice(
Bytes,
Record_size,
erlang:byte_size(
Bytes
)
- Record_size
) of
{ok,
Value} ->
Value;
{error,
_} ->
<<>>
end,
parse_central_directory(
Next_bits,
[Entry_value |
Acc],
Remaining
- 1,
Full,
Next_total,
Limits,
Password
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end
)
end.
-file("src/packkit/zip.gleam", 463).
-spec decode_internal(
bitstring(),
gleam@option:option(binary()),
packkit@limit:limits()
) -> {ok, packkit@archive:archive()} | {error, packkit@error:archive_error()}.
decode_internal(Bytes, Password, Limits) ->
gleam@bool:guard(
erlang:byte_size(Bytes) > packkit@limit:max_input_bytes(Limits),
{error,
{archive_limit_exceeded,
<<"max_input_bytes"/utf8>>,
erlang:byte_size(Bytes)}},
fun() ->
gleam@result:'try'(
locate_eocd(Bytes),
fun(Eocd_offset) ->
gleam@result:'try'(
read_eocd(Bytes, Eocd_offset),
fun(Eocd) ->
gleam@bool:guard(
erlang:element(2, Eocd) > packkit@limit:max_members(
Limits
),
{error,
{archive_limit_exceeded,
<<"max_members"/utf8>>,
erlang:element(2, Eocd)}},
fun() ->
gleam@result:'try'(
slice_or_error(
Bytes,
erlang:element(3, Eocd),
erlang:element(4, Eocd)
),
fun(Central_bits) ->
gleam@result:'try'(
parse_central_directory(
Central_bits,
[],
erlang:element(2, Eocd),
Bytes,
0,
Limits,
Password
),
fun(Entries) ->
Base = packkit@archive:from_entries(
format(),
lists:reverse(Entries)
),
case erlang:element(5, Eocd) of
{some, C} ->
{ok,
packkit@archive:with_comment(
Base,
C
)};
none ->
{ok, Base}
end
end
)
end
)
end
)
end
)
end
)
end
).
-file("src/packkit/zip.gleam", 426).
?DOC(" Decode a ZIP archive using default limits.\n").
-spec decode(bitstring()) -> {ok, packkit@archive:archive()} |
{error, packkit@error:archive_error()}.
decode(Bytes) ->
decode_internal(Bytes, none, packkit@limit:default()).
-file("src/packkit/zip.gleam", 433).
?DOC(" Decode a ZIP archive using explicit limits.\n").
-spec decode_with_limits(bitstring(), packkit@limit:limits()) -> {ok,
packkit@archive:archive()} |
{error, packkit@error:archive_error()}.
decode_with_limits(Bytes, Limits) ->
decode_internal(Bytes, none, Limits).
-file("src/packkit/zip.gleam", 447).
?DOC(
" Decode a ZIP archive whose entries may be protected by the\n"
" PKWARE traditional (\"ZipCrypto\") encryption scheme. The\n"
" password is applied to every encrypted entry; entries without\n"
" the gp-flag encryption bit decode unchanged. Wrong-password\n"
" detection relies on the 12-byte encryption header check byte\n"
" (the high byte of the entry's CRC-32), so a wrong password\n"
" surfaces as `ArchiveInvalid`.\n"
).
-spec decode_with_password(bitstring(), binary()) -> {ok,
packkit@archive:archive()} |
{error, packkit@error:archive_error()}.
decode_with_password(Bytes, Password) ->
decode_internal(Bytes, {some, Password}, packkit@limit:default()).
-file("src/packkit/zip.gleam", 455).
?DOC(" Same as `decode_with_password` but with explicit limits.\n").
-spec decode_with_password_and_limits(
bitstring(),
binary(),
packkit@limit:limits()
) -> {ok, packkit@archive:archive()} | {error, packkit@error:archive_error()}.
decode_with_password_and_limits(Bytes, Password, Limits) ->
decode_internal(Bytes, {some, Password}, Limits).