Advertisement
Alyks

Untitled

Oct 1st, 2020
471
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 7.77 KB | None | 0 0
  1. #!/usr/bin/perl
  2.  
  3. use 5.010;
  4. use strict;
  5. use autodie;
  6. use warnings;
  7.  
  8. use Getopt::Std qw(getopts);
  9. use File::Basename qw(basename);
  10.  
  11. use constant {
  12.     PKGNAME => 'lzt-simple',
  13.     VERSION => '0.02',
  14.     FORMAT  => 'lzt',
  15. };
  16.  
  17. use constant {
  18.     MIN       => 4,
  19.     BUFFER    => 256,
  20.     SIGNATURE => uc(FORMAT) . chr(2),
  21. };
  22.  
  23. sub version {
  24.     printf("%s %s\n", PKGNAME, VERSION);
  25.     exit;
  26. }
  27.  
  28. sub main {
  29.     my %opt;
  30.     getopts('ei:o:vhr', %opt);
  31.  
  32.     $opt{h} && usage(0);
  33.     $opt{v} && version();
  34.  
  35.     my ($input, $output) = @ARGV;
  36.     $input  //= $opt{i} // usage(2);
  37.     $output //= $opt{o};
  38.  
  39.     my $ext = qr{\.${\FORMAT}\z}io;
  40.     if ($opt{e} || $input =~ $ext) {
  41.  
  42.         if (not defined $output) {
  43.             ($output = basename($input)) =~ s{$ext}{}
  44.               || die "$0: no output file specified!\n";
  45.         }
  46.  
  47.         if (not $opt{r} and -e $output) {
  48.             print "'$output' already exists! -- Replace? [y/N] ";
  49.             <STDIN> =~ /^y/i || exit 17;
  50.         }
  51.  
  52.         decompress($input, $output)
  53.           || die "$0: error: decompression failed!\n";
  54.     }
  55.     elsif ($input !~ $ext || (defined($output) && $output =~ $ext)) {
  56.         $output //= basename($input) . '.' . FORMAT;
  57.         compress($input, $output)
  58.           || die "$0: error: compression failed!\n";
  59.     }
  60.     else {
  61.         warn "$0: don't know what to do...\n";
  62.         usage(1);
  63.     }
  64. }
  65.  
  66. sub walk {
  67.     my ($n, $s, $h) = @_;
  68.     if (exists($n->{a})) {
  69.         $h->{$n->{a}} = $s;
  70.         return 1;
  71.     }
  72.     walk($n->{'0'}, $s . '0', $h);
  73.     walk($n->{'1'}, $s . '1', $h);
  74. }
  75.  
  76. sub mktree {
  77.     my ($text) = @_;
  78.  
  79.     my %letters;
  80.     ++$letters{$_} for (split(//, $text));
  81.  
  82.     my @nodes;
  83.     if ((@nodes = map { {a => $_, freq => $letters{$_}} } keys %letters) == 1) {
  84.         return {$nodes[0]{a} => '0'};
  85.     }
  86.  
  87.     my %n;
  88.     while ((@nodes = sort { $a->{freq} <=> $b->{freq} } @nodes) > 1) {
  89.         %n = ('0' => {%{shift(@nodes)}}, '1' => {%{shift(@nodes)}});
  90.         $n{freq} = $n{'0'}{freq} + $n{'1'}{freq};
  91.         push @nodes, {%n};
  92.  
  93.     }
  94.  
  95.     walk(\%n, '', $n{tree} = {});
  96.     return $n{tree};
  97. }
  98.  
  99. sub huffman_encode {
  100.     my ($str, $dict) = @_;
  101.     join('', map { $dict->{$_} // die("bad char $_") } split(//, $str));
  102. }
  103.  
  104. sub huffman_decode {
  105.     my ($hash, $bytes) = @_;
  106.     local $" = '|';
  107.     unpack('B*', $bytes) =~ s/(@{[sort {length($a) <=> length($b)} keys %{$hash}]})/$hash->{$1}/gr;
  108. }
  109.  
  110. sub valid_archive {
  111.     my ($fh) = @_;
  112.  
  113.     if (read($fh, (my $sig), length(SIGNATURE), 0) == length(SIGNATURE)) {
  114.         $sig eq SIGNATURE || return;
  115.     }
  116.  
  117.     return 1;
  118. }
  119.  
  120. sub compress {
  121.     my ($input, $output) = @_;
  122.  
  123.     # Open the input file
  124.     open my $fh, '<:raw', $input;
  125.  
  126.     # Open the output file and write the archive signature
  127.     open my $out_fh, '>:raw', $output;
  128.     print {$out_fh} SIGNATURE;
  129.  
  130.     while ((my $len = read($fh, (my $block), BUFFER)) > 0) {
  131.  
  132.         my %dict;
  133.         my $max = int($len / 2);
  134.  
  135.         foreach my $i (reverse(MIN .. $max)) {
  136.             foreach my $j (0 .. $len - $i * 2) {
  137.                 if ((my $pos = index($block, substr($block, $j, $i), $j + $i)) != -1) {
  138.                     if (not exists $dict{$pos} or $i > $dict{$pos}[1]) {
  139.                         $dict{$pos} = [$j, $i];
  140.                     }
  141.                 }
  142.             }
  143.         }
  144.  
  145.         my @pairs;
  146.         my $uncompressed = '';
  147.         for (my $i = 0 ; $i < $len ; $i++) {
  148.             if (exists $dict{$i}) {
  149.                 my ($key, $vlen) = @{$dict{$i}};
  150.                 push @pairs, [$i, $key, $vlen];
  151.                 $i += $vlen - 1;
  152.             }
  153.             else {
  154.                 $uncompressed .= substr($block, $i, 1);
  155.             }
  156.         }
  157.  
  158.         my $huffman_hash = mktree($uncompressed);
  159.         my $huffman_enc  = huffman_encode($uncompressed, $huffman_hash);
  160.  
  161.         my %huffman_dict;
  162.         foreach my $k (keys %{$huffman_hash}) {
  163.             push @{$huffman_dict{length($huffman_hash->{$k})}}, [$k, $huffman_hash->{$k}];
  164.         }
  165.  
  166.         {
  167.             use bytes;
  168.  
  169.             my $binary_enc   = pack('B*', $huffman_enc);
  170.             my $encoding_len = length($binary_enc);
  171.  
  172.             printf("%3d -> %3d (%.2f%%)\n", $len, $encoding_len, ($len - $encoding_len) / $len * 100);
  173.             print {$out_fh}
  174.  
  175.               # Length of the uncompressed text
  176.               chr(length($uncompressed) - 1),
  177.  
  178.               # LZT pairs num
  179.               chr($#pairs + 1),
  180.  
  181.               # LZT pairs encoded into bytes
  182.               (
  183.                 map {
  184.                     map { chr }
  185.                       @{$_}
  186.                   } @pairs
  187.               ),
  188.  
  189.               # Huffman dictionary size
  190.               chr(scalar(keys(%huffman_dict)) > 0 ? scalar(keys(%huffman_dict)) - 1 : 0),
  191.  
  192.               # Huffman dictionary into bytes
  193.               (
  194.                 join(
  195.                     '',
  196.                     map {
  197.                             chr($_)
  198.                           . chr($#{$huffman_dict{$_}} + 1)
  199.                           . join('', map { $_->[0] } @{$huffman_dict{$_}})
  200.                           . pack('B*', join('', map { $_->[1] } @{$huffman_dict{$_}}))
  201.                       } sort { $a <=> $b } keys %huffman_dict
  202.                     )
  203.               ),
  204.  
  205.               # Huffman encoded bytes length
  206.               chr($encoding_len - 1),
  207.  
  208.               # Huffman encoded bytes
  209.               $binary_enc
  210.         }
  211.  
  212.         #   exit;
  213.     }
  214.  
  215.     close $fh;
  216.     close $out_fh;
  217. }
  218.  
  219. sub decompress {
  220.     my ($input, $output) = @_;
  221.  
  222.     # Open and validate the input file
  223.     open my $fh, '<:raw', $input;
  224.     valid_archive($fh) || die "$0: file `$input' is not a \U${\FORMAT}\E archive!\n";
  225.  
  226.     # Open the output file
  227.     open my $out_fh, '>:raw', $output;
  228.  
  229.     while (read($fh, (my $len_byte), 1) > 0) {
  230.         read($fh, (my $lzt_pairs), 1);
  231.  
  232.         # Create the LZT dictionary
  233.         my %dict;
  234.         for my $i (1 .. ord($lzt_pairs)) {
  235.             read($fh, (my $at_byte),   1);
  236.             read($fh, (my $from_byte), 1);
  237.             read($fh, (my $size_byte), 1);
  238.             $dict{ord($at_byte)} = [ord($from_byte), ord($size_byte)];
  239.         }
  240.  
  241.         read($fh, (my $huffman_pairs), 1);
  242.  
  243.         # Create the Huffman dictionary
  244.         my %huffman_dict;
  245.         for my $i (1 .. ord($huffman_pairs) + 1) {
  246.             read($fh, (my $pattern_len), 1);
  247.             read($fh, (my $pattern_num), 1);
  248.  
  249.             my $bits_num = ord($pattern_len) * ord($pattern_num);
  250.  
  251.             if ($bits_num % 8 != 0) {
  252.                 $bits_num += 8 - ($bits_num % 8);
  253.             }
  254.  
  255.             read($fh, (my $chars),    ord($pattern_num));
  256.             read($fh, (my $patterns), $bits_num / 8);
  257.  
  258.             my $bits = unpack('B*', $patterns);
  259.             foreach my $char (split(//, $chars)) {
  260.                 $huffman_dict{substr($bits, 0, ord($pattern_len), '')} = $char;
  261.             }
  262.         }
  263.  
  264.         read($fh, (my $bytes_len), 1);
  265.         read($fh, (my $bytes),     ord($bytes_len) + 1);
  266.  
  267.         # Huffman decoding
  268.         my $len   = ord($len_byte) + 1;
  269.         my $block = substr(huffman_decode(\%huffman_dict, $bytes), 0, $len);
  270.  
  271.         my $acc          = 0;
  272.         my $decompressed = '';
  273.  
  274.         # LZT decoding
  275.         for (my $i = 0 ; $i <= $len ; $i++) {
  276.             if (exists($dict{$i + $acc})) {
  277.                 my $pos = $dict{$i + $acc};
  278.                 $decompressed .= substr($decompressed, $pos->[0], $pos->[1]);
  279.                 $acc += $pos->[1];
  280.                 $i--;
  281.             }
  282.             else {
  283.                 $decompressed .= substr($block, $i, 1);
  284.             }
  285.         }
  286.  
  287.         print {$out_fh} $decompressed;
  288.     }
  289.  
  290.     close $fh;
  291.     close $out_fh;
  292. }
  293.  
  294. main();
  295. exit(0);
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement