-module(packkit@lzw). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/packkit/lzw.gleam"). -export([codec/0, encode_with_options/3, decode_with_limits/2, decode/1, encode/1]). -export_type([encode_state/0, decode_state/0, writer/0, reader/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( " Unix `compress(1)` LZW codec — `.Z` stream encoder and decoder.\n" "\n" " The stream begins with the two-byte magic `1F 9D` followed by a\n" " flag byte whose low five bits hold the maximum code width\n" " (9..16) and whose top bit toggles block-compress mode. Codes\n" " are packed LSB-first across bytes, the dictionary starts with\n" " the 256 byte literals (plus a clear-table marker when block mode\n" " is on), and each new entry maps the previous code plus the\n" " current character to the next free code. When the dictionary\n" " fills the current width, the encoder pads to the next\n" " `width * 8` bit boundary before promoting to the next width or\n" " emitting a clear-table code.\n" ). -type encode_state() :: {encode_state, gleam@dict:dict(integer(), integer()), integer(), integer(), integer(), integer(), integer(), integer(), boolean(), writer(), integer()}. -type decode_state() :: {decode_state, list(integer()), integer(), gleam@dict:dict(integer(), {integer(), integer()}), integer(), integer(), integer(), integer(), integer(), boolean(), integer(), boolean(), integer()}. -type writer() :: {writer, list(integer()), integer(), integer(), integer()}. -type reader() :: {reader, bitstring(), integer(), integer(), integer(), boolean()}. -file("src/packkit/lzw.gleam", 36). ?DOC(" LZW codec smart constructor (Unix `.Z` family).\n"). -spec codec() -> packkit@codec:codec(). codec() -> packkit@codec:lzw(). -file("src/packkit/lzw.gleam", 200). -spec dictionary_key(integer(), integer()) -> integer(). dictionary_key(Prefix, Byte) -> erlang:'bor'(erlang:'bsl'(Prefix, 8), Byte). -file("src/packkit/lzw.gleam", 222). -spec max_max_code(integer()) -> integer(). max_max_code(Max_bits) -> erlang:'bsl'(1, Max_bits). -file("src/packkit/lzw.gleam", 204). -spec max_code_for(integer(), integer()) -> integer(). max_code_for(N_bits, Max_bits) -> case N_bits >= Max_bits of true -> max_max_code(Max_bits); false -> erlang:'bsl'(1, N_bits) end. -file("src/packkit/lzw.gleam", 425). ?DOC( " The decoder must promote one iteration \"earlier\" than the encoder\n" " because of the classical LZW insertion off-by-one: encoder inserts\n" " the `(prefix, byte)` pair at the iter that writes the OLD prefix's\n" " code, while the decoder inserts `(prev_code, first_of_current_code)`\n" " at the iter that reads the current code — same final entries, but\n" " the decoder's `free_ent` lags one bump behind the encoder's view\n" " of the stream. We check `free_ent >= max_code` so the decoder\n" " promotes between codes 254 and 255 (when the encoder pads), instead\n" " of between codes 255 and 256 (when the decoder's own free_ent would\n" " naturally exceed max_code). Without this, a 256-unique-byte stream\n" " reads the encoder's 9-bit pad as a phantom literal code 0.\n" ). -spec promote_decoder_width(decode_state(), reader()) -> decode_state(). promote_decoder_width(State, _) -> case (erlang:element(5, State) >= erlang:element(7, State)) andalso (erlang:element( 6, State ) < erlang:element(11, State)) of true -> {decode_state, erlang:element(2, State), erlang:element(3, State), erlang:element(4, State), erlang:element(5, State), erlang:element(6, State), erlang:element(7, State), -1, erlang:element(9, State), erlang:element(10, State), erlang:element(11, State), erlang:element(12, State), erlang:element(13, State)}; false -> State end. -file("src/packkit/lzw.gleam", 462). -spec check_output_limit(integer(), packkit@limit:limits()) -> {ok, nil} | {error, packkit@error:codec_error()}. check_output_limit(Size, Limits) -> case Size > packkit@limit:max_output_bytes(Limits) of true -> {error, {codec_limit_exceeded, <<"max_output_bytes"/utf8>>, Size}}; false -> {ok, nil} end. -file("src/packkit/lzw.gleam", 480). -spec expand_loop( integer(), gleam@dict:dict(integer(), {integer(), integer()}), list(integer()), integer() ) -> {ok, {list(integer()), integer()}} | {error, packkit@error:codec_error()}. expand_loop(Code, Table, Acc, Count) -> case Code < 256 of true -> {ok, {[Code | Acc], Count + 1}}; false -> case gleam_stdlib:map_get(Table, Code) of {ok, {Prev, Char}} -> expand_loop(Prev, Table, [Char | Acc], Count + 1); {error, _} -> {error, {codec_invalid_data, <<"lzw code lookup failed"/utf8>>}} end end. -file("src/packkit/lzw.gleam", 473). -spec expand_code(integer(), gleam@dict:dict(integer(), {integer(), integer()})) -> {ok, {list(integer()), integer()}} | {error, packkit@error:codec_error()}. expand_code(Code, Table) -> expand_loop(Code, Table, [], 0). -file("src/packkit/lzw.gleam", 497). -spec first_char(integer(), gleam@dict:dict(integer(), {integer(), integer()})) -> {ok, integer()} | {error, packkit@error:codec_error()}. first_char(Code, Table) -> case Code < 256 of true -> {ok, Code}; false -> case gleam_stdlib:map_get(Table, Code) of {ok, {Prev, _}} -> first_char(Prev, Table); {error, _} -> {error, {codec_invalid_data, <<"lzw first-char lookup failed"/utf8>>}} end end. -file("src/packkit/lzw.gleam", 512). -spec first_char_of_chars(list(integer())) -> {ok, integer()} | {error, packkit@error:codec_error()}. first_char_of_chars(Chars) -> case Chars of [Head | _] -> {ok, Head}; [] -> {error, {codec_invalid_data, <<"lzw expansion produced no bytes"/utf8>>}} end. -file("src/packkit/lzw.gleam", 520). -spec prepend_chars(list(integer()), list(integer())) -> list(integer()). prepend_chars(Chars, Acc) -> case Chars of [] -> Acc; [Head | Rest] -> prepend_chars(Rest, [Head | Acc]) end. -file("src/packkit/lzw.gleam", 432). -spec emit_code( integer(), list(integer()), integer(), gleam@dict:dict(integer(), {integer(), integer()}), integer(), boolean(), integer(), packkit@limit:limits() ) -> {ok, {list(integer()), integer(), gleam@dict:dict(integer(), {integer(), integer()}), integer()}} | {error, packkit@error:codec_error()}. emit_code(Code, Out_rev, Out_len, Table, Prev_code, Has_prev, Free_ent, Limits) -> case (Code =:= Free_ent) andalso Has_prev of true -> gleam@result:'try'( first_char(Prev_code, Table), fun(First) -> Extended_table = gleam@dict:insert( Table, Code, {Prev_code, First} ), gleam@result:'try'( expand_code(Code, Extended_table), fun(_use0) -> {Chars, Count} = _use0, gleam@result:'try'( check_output_limit(Out_len + Count, Limits), fun(_) -> {ok, {prepend_chars(Chars, Out_rev), Out_len + Count, Table, First}} end ) end ) end ); false -> gleam@result:'try'( expand_code(Code, Table), fun(_use0@1) -> {Chars@1, Count@1} = _use0@1, gleam@result:'try'( first_char_of_chars(Chars@1), fun(First@1) -> gleam@result:'try'( check_output_limit(Out_len + Count@1, Limits), fun(_) -> {ok, {prepend_chars(Chars@1, Out_rev), Out_len + Count@1, Table, First@1}} end ) end ) end ) end. -file("src/packkit/lzw.gleam", 527). -spec reverse_to_bit_array(list(integer()), bitstring()) -> bitstring(). reverse_to_bit_array(Values, Acc) -> case Values of [] -> Acc; [Head | Rest] -> reverse_to_bit_array(Rest, <>) end. -file("src/packkit/lzw.gleam", 540). -spec new_writer() -> writer(). new_writer() -> {writer, [], 0, 0, 0}. -file("src/packkit/lzw.gleam", 568). -spec mask_for(integer()) -> integer(). mask_for(Count) -> erlang:'bsl'(1, Count) - 1. -file("src/packkit/lzw.gleam", 572). -spec flush_full_bytes(writer()) -> writer(). flush_full_bytes(Writer) -> case erlang:element(4, Writer) >= 8 of false -> Writer; true -> flush_full_bytes( {writer, [erlang:'band'(erlang:element(3, Writer), 16#FF) | erlang:element(2, Writer)], erlang:'bsr'(erlang:element(3, Writer), 8), erlang:element(4, Writer) - 8, erlang:element(5, Writer)} ) end. -file("src/packkit/lzw.gleam", 548). -spec write_bits(writer(), integer(), integer()) -> writer(). write_bits(Writer, Value, Count) -> case Count of 0 -> Writer; _ -> Masked = erlang:'band'(Value, mask_for(Count)), Buffer = erlang:'bor'( erlang:element(3, Writer), erlang:'bsl'(Masked, erlang:element(4, Writer)) ), flush_full_bytes( {writer, erlang:element(2, Writer), Buffer, erlang:element(4, Writer) + Count, erlang:element(5, Writer) + Count} ) end. -file("src/packkit/lzw.gleam", 275). -spec pad_zero_bits(writer(), integer()) -> writer(). pad_zero_bits(Writer, Count) -> case Count of 0 -> Writer; N when N >= 32 -> pad_zero_bits(write_bits(Writer, 0, 32), N - 32); N@1 -> write_bits(Writer, 0, N@1) end. -file("src/packkit/lzw.gleam", 266). -spec pad_writer_to_block(writer(), integer()) -> writer(). pad_writer_to_block(Writer, N_bits) -> Group_bits = 8 * N_bits, Leftover = case Group_bits of 0 -> 0; Gleam@denominator -> erlang:element(5, Writer) rem Gleam@denominator end, case Leftover of 0 -> Writer; _ -> pad_zero_bits(Writer, Group_bits - Leftover) end. -file("src/packkit/lzw.gleam", 226). -spec promote_width(encode_state()) -> encode_state(). promote_width(State) -> case (erlang:element(4, State) > erlang:element(6, State)) andalso (erlang:element( 5, State ) < erlang:element(8, State)) of true -> Writer = pad_writer_to_block( erlang:element(10, State), erlang:element(5, State) ), New_n_bits = erlang:element(5, State) + 1, {encode_state, erlang:element(2, State), erlang:element(3, State), erlang:element(4, State), New_n_bits, max_code_for(New_n_bits, erlang:element(8, State)), 0, erlang:element(8, State), erlang:element(9, State), Writer, erlang:element(11, State)}; false -> State end. -file("src/packkit/lzw.gleam", 544). -spec write_code(writer(), integer(), integer()) -> writer(). write_code(Writer, Code, N_bits) -> write_bits(Writer, Code, N_bits). -file("src/packkit/lzw.gleam", 262). -spec write_code_with_padding(encode_state(), integer()) -> writer(). write_code_with_padding(State, Code) -> write_code(erlang:element(10, State), Code, erlang:element(5, State)). -file("src/packkit/lzw.gleam", 599). -spec list_reverse_to_bit_array(list(integer()), bitstring()) -> bitstring(). list_reverse_to_bit_array(Values, Acc) -> case Values of [] -> Acc; [Head | Rest] -> list_reverse_to_bit_array(Rest, <>) end. -file("src/packkit/lzw.gleam", 585). -spec flush_writer(writer()) -> bitstring(). flush_writer(Writer) -> Writer@1 = case erlang:element(4, Writer) of 0 -> Writer; _ -> {writer, [erlang:'band'(erlang:element(3, Writer), 16#FF) | erlang:element(2, Writer)], 0, 0, erlang:element(5, Writer)} end, list_reverse_to_bit_array(erlang:element(2, Writer@1), <<>>). -file("src/packkit/lzw.gleam", 618). -spec new_reader(bitstring()) -> reader(). new_reader(Source) -> {reader, Source, 0, 0, 0, false}. -file("src/packkit/lzw.gleam", 622). -spec refill(reader(), integer()) -> reader(). refill(Reader, Needed) -> case (erlang:element(4, Reader) >= Needed) orelse erlang:element(6, Reader) of true -> Reader; false -> case erlang:element(2, Reader) of <> -> refill( {reader, Rest, erlang:'bor'( erlang:element(3, Reader), erlang:'bsl'(B, erlang:element(4, Reader)) ), erlang:element(4, Reader) + 8, erlang:element(5, Reader), false}, Needed ); _ -> {reader, <<>>, erlang:element(3, Reader), erlang:element(4, Reader), erlang:element(5, Reader), true} end end. -file("src/packkit/lzw.gleam", 653). -spec read_code(reader(), integer()) -> {ok, {integer(), reader()}} | {error, nil}. read_code(Reader, N_bits) -> Reader@1 = refill(Reader, N_bits), case erlang:element(4, Reader@1) >= N_bits of false -> {error, nil}; true -> Mask = mask_for(N_bits), Value = erlang:'band'(erlang:element(3, Reader@1), Mask), {ok, {Value, {reader, erlang:element(2, Reader@1), erlang:'bsr'(erlang:element(3, Reader@1), N_bits), erlang:element(4, Reader@1) - N_bits, erlang:element(5, Reader@1) + N_bits, erlang:element(6, Reader@1)}}} end. -file("src/packkit/lzw.gleam", 683). -spec drop_bits(reader(), integer()) -> reader(). drop_bits(Reader, Count) -> case Count of 0 -> Reader; _ -> Reader@1 = refill(Reader, 1), case erlang:element(4, Reader@1) >= 1 of false -> Reader@1; true -> drop_bits( {reader, erlang:element(2, Reader@1), erlang:'bsr'(erlang:element(3, Reader@1), 1), erlang:element(4, Reader@1) - 1, erlang:element(5, Reader@1) + 1, erlang:element(6, Reader@1)}, Count - 1 ) end end. -file("src/packkit/lzw.gleam", 674). -spec skip_to_block_boundary(reader(), integer()) -> reader(). skip_to_block_boundary(Reader, N_bits) -> Group_bits = 8 * N_bits, Leftover = case Group_bits of 0 -> 0; Gleam@denominator -> erlang:element(5, Reader) rem Gleam@denominator end, case Leftover of 0 -> Reader; _ -> drop_bits(Reader, Group_bits - Leftover) end. -file("src/packkit/lzw.gleam", 106). -spec build_header(integer(), boolean()) -> bitstring(). build_header(Max_bits, Block_mode) -> Flag = case Block_mode of true -> erlang:'bor'(Max_bits, 16#80); false -> Max_bits end, <<16#1F, 16#9D, Flag>>. -file("src/packkit/lzw.gleam", 243). -spec maybe_emit_clear(encode_state()) -> encode_state(). maybe_emit_clear(State) -> case erlang:element(9, State) andalso (erlang:element(4, State) > max_max_code( erlang:element(8, State) )) of true -> Writer = write_code( erlang:element(10, State), 256, erlang:element(5, State) ), Writer@1 = pad_writer_to_block(Writer, erlang:element(5, State)), {encode_state, maps:new(), erlang:element(3, State), erlang:element(11, State), 9, max_code_for(9, erlang:element(8, State)), 0, erlang:element(8, State), erlang:element(9, State), Writer@1, erlang:element(11, State)}; false -> State end. -file("src/packkit/lzw.gleam", 160). -spec encode_loop(bitstring(), encode_state()) -> encode_state(). encode_loop(Input, State) -> case Input of <> -> Key = dictionary_key(erlang:element(3, State), Byte), case gleam_stdlib:map_get(erlang:element(2, State), Key) of {ok, Code} -> encode_loop( Rest, {encode_state, erlang:element(2, State), Code, erlang:element(4, State), erlang:element(5, State), erlang:element(6, State), erlang:element(7, State), erlang:element(8, State), erlang:element(9, State), erlang:element(10, State), erlang:element(11, State)} ); {error, _} -> Writer = write_code_with_padding( State, erlang:element(3, State) ), Next_state = case erlang:element(4, State) =< max_max_code( erlang:element(8, State) ) of true -> Table = gleam@dict:insert( erlang:element(2, State), Key, erlang:element(4, State) ), promote_width( {encode_state, Table, Byte, erlang:element(4, State) + 1, erlang:element(5, State), erlang:element(6, State), erlang:element(7, State) + 1, erlang:element(8, State), erlang:element(9, State), Writer, erlang:element(11, State)} ); false -> maybe_emit_clear( {encode_state, erlang:element(2, State), Byte, erlang:element(4, State), erlang:element(5, State), erlang:element(6, State), erlang:element(7, State) + 1, erlang:element(8, State), erlang:element(9, State), Writer, erlang:element(11, State)} ) end, encode_loop(Rest, Next_state) end; _ -> State end. -file("src/packkit/lzw.gleam", 131). -spec encode_payload(bitstring(), integer(), boolean(), integer()) -> bitstring(). encode_payload(Bytes, Max_bits, Block_mode, First_free) -> case Bytes of <> -> State = {encode_state, maps:new(), First, First_free, 9, max_code_for(9, Max_bits), 0, Max_bits, Block_mode, new_writer(), First_free}, State@1 = encode_loop(Rest, State), Writer = write_code_with_padding( State@1, erlang:element(3, State@1) ), flush_writer(Writer); _ -> flush_writer(new_writer()) end. -file("src/packkit/lzw.gleam", 51). ?DOC(" Encode `bytes` as a `.Z` stream with explicit options.\n"). -spec encode_with_options(bitstring(), integer(), boolean()) -> {ok, bitstring()} | {error, packkit@error:codec_error()}. encode_with_options(Bytes, Max_bits, Block_mode) -> gleam@bool:guard( (Max_bits < 9) orelse (Max_bits > 16), {error, {codec_invalid_data, <<"lzw max_bits out of range (9..16)"/utf8>>}}, fun() -> Header = build_header(Max_bits, Block_mode), First_free = case Block_mode of true -> 257; false -> 256 end, Payload = encode_payload(Bytes, Max_bits, Block_mode, First_free), {ok, gleam_stdlib:bit_array_concat([Header, Payload])} end ). -file("src/packkit/lzw.gleam", 333). -spec decode_loop(reader(), decode_state(), packkit@limit:limits()) -> {ok, {list(integer()), integer()}} | {error, packkit@error:codec_error()}. decode_loop(Reader, State, Limits) -> case read_code(Reader, erlang:element(6, State)) of {error, _} -> {ok, {erlang:element(2, State), erlang:element(3, State)}}; {ok, {Code, Reader@1}} -> case erlang:element(12, State) andalso (Code =:= 256) of true -> Reader@2 = skip_to_block_boundary( Reader@1, erlang:element(6, State) ), New_state = {decode_state, erlang:element(2, State), erlang:element(3, State), maps:new(), erlang:element(13, State), 9, max_code_for(9, erlang:element(11, State)), 0, -1, false, erlang:element(11, State), erlang:element(12, State), erlang:element(13, State)}, decode_loop(Reader@2, New_state, Limits); false -> gleam@result:'try'( emit_code( Code, erlang:element(2, State), erlang:element(3, State), erlang:element(4, State), erlang:element(9, State), erlang:element(10, State), erlang:element(5, State), Limits ), fun(_use0) -> {Out_rev, Out_len, Table, Prefix_first} = _use0, {New_table, New_free_ent} = case erlang:element( 10, State ) of true -> {gleam@dict:insert( Table, erlang:element(5, State), {erlang:element(9, State), Prefix_first} ), erlang:element(5, State) + 1}; false -> {Table, erlang:element(5, State)} end, New_state@1 = {decode_state, Out_rev, Out_len, New_table, New_free_ent, erlang:element(6, State), erlang:element(7, State), erlang:element(8, State), Code, true, erlang:element(11, State), erlang:element(12, State), erlang:element(13, State)}, New_state@2 = promote_decoder_width( New_state@1, Reader@1 ), {New_state@3, Reader@4} = case erlang:element( 8, New_state@2 ) of -1 -> Reader@3 = skip_to_block_boundary( Reader@1, erlang:element(6, State) ), {{decode_state, erlang:element(2, New_state@2), erlang:element(3, New_state@2), erlang:element(4, New_state@2), erlang:element(5, New_state@2), erlang:element(6, New_state@2) + 1, max_code_for( erlang:element(6, New_state@2) + 1, erlang:element(11, New_state@2) ), 0, erlang:element(9, New_state@2), erlang:element(10, New_state@2), erlang:element(11, New_state@2), erlang:element(12, New_state@2), erlang:element(13, New_state@2)}, Reader@3}; _ -> {New_state@2, Reader@1} end, decode_loop(Reader@4, New_state@3, Limits) end ) end end. -file("src/packkit/lzw.gleam", 302). -spec decode_payload(bitstring(), integer(), boolean(), packkit@limit:limits()) -> {ok, bitstring()} | {error, packkit@error:codec_error()}. decode_payload(Bytes, Max_bits, Block_mode, Limits) -> First_free = case Block_mode of true -> 257; false -> 256 end, Reader = new_reader(Bytes), State = {decode_state, [], 0, maps:new(), First_free, 9, max_code_for(9, Max_bits), 0, -1, false, Max_bits, Block_mode, First_free}, gleam@result:'try'( decode_loop(Reader, State, Limits), fun(_use0) -> {Out_rev, Out_len} = _use0, gleam@result:'try'( check_output_limit(Out_len, Limits), fun(_) -> {ok, reverse_to_bit_array(Out_rev, <<>>)} end ) end ). -file("src/packkit/lzw.gleam", 77). ?DOC(" Decode a `.Z` stream using explicit `Limits`.\n"). -spec decode_with_limits(bitstring(), packkit@limit:limits()) -> {ok, bitstring()} | {error, packkit@error:codec_error()}. decode_with_limits(Bytes, Limits) -> gleam@bool:guard( erlang:byte_size(Bytes) > packkit@limit:max_input_bytes(Limits), {error, {codec_limit_exceeded, <<"max_input_bytes"/utf8>>, erlang:byte_size(Bytes)}}, fun() -> case Bytes of <> when (M1 =:= 16#1F) andalso (M2 =:= 16#9D) -> Max_bits = erlang:'band'(Flag, 16#1F), Block_mode = erlang:'band'(Flag, 16#80) /= 0, gleam@bool:guard( (Max_bits < 9) orelse (Max_bits > 16), {error, {codec_invalid_data, <<"lzw header advertises invalid max_bits"/utf8>>}}, fun() -> decode_payload(Rest, Max_bits, Block_mode, Limits) end ); _ -> {error, {codec_invalid_data, <<"lzw stream missing 1F 9D magic"/utf8>>}} end end ). -file("src/packkit/lzw.gleam", 72). ?DOC(" Decode a `.Z` stream using the shared default `Limits`.\n"). -spec decode(bitstring()) -> {ok, bitstring()} | {error, packkit@error:codec_error()}. decode(Bytes) -> decode_with_limits(Bytes, packkit@limit:default()). -file("src/packkit/lzw.gleam", 42). ?DOC( " Encode `bytes` as a `.Z` stream using the default 16-bit\n" " block-mode encoder.\n" ). -spec encode(bitstring()) -> {ok, bitstring()} | {error, packkit@error:codec_error()}. encode(Bytes) -> encode_with_options(Bytes, 16, true).