#
# Perl module to expand Microsoft-compressed files
#
# Based on algorithm provided in the GPL'ed mscompress package by:
#   Martin Hinner <[email protected]>
#   M. Winterhoff <[email protected]>
#
# The mscompress package is at:
#   ftp://ftp.penguin.cz/pub/users/mhi/mscompress/

package MSExpand;

use strict;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);

use Carp;
use IO::File;

require Exporter;

$VERSION = "0.01";
@ISA = qw(Exporter);
@EXPORT = qw(ms_expand);
@EXPORT_OK = qw(ms_expand);

sub get_uint8
{
   croak("get_uint8(FILEHANDLE)") if @_ != 1;
   my($fh) = @_;
   my($buf, $count);

   $count = sysread($fh, $buf, 1);
   if (defined($count) and $count == 1) {
       my $value = unpack("C", $buf);
       return $value;
   } else {
       return undef;
   }
}

sub put_uint8
{
   croak("put_uint8(FILEHANDLE, VALUE)") if @_ != 2;
   my($fh, $value) = @_;

   return syswrite($fh, pack("C", $value & 0xFF));
}

sub get_uint16_le
{
   croak("get_uint16_le(FILEHANDLE)") if @_ != 1;
   my($fh) = @_;
   my($buf, $count);

   $count = sysread($fh, $buf, 2);
   if (defined($count) and $count == 2) {
       my $value = unpack("v", $buf);
       return $value;
   } else {
       return undef;
   }
}

sub get_uint32_le
{
   croak("get_uint32_le(FILEHANDLE)") if @_ != 1;
   my($fh) = @_;
   my($buf, $count);

   $count = sysread($fh, $buf, 4);
   if (defined($count) and $count == 4) {
       my $value = unpack("V", $buf);
       return $value;
   } else {
       return undef;
   }
}

sub ms_expand
{
   croak("ms_expand(SRC_FILENAME, DST_FILENAME)") if @_ != 2;
   my($src_fname, $dst_fname) = @_;

   # Manifest constants for the algorithm.
   my $N = 4096;
   my $F = 16;

   # Open the source file.
   my $src = new IO::File "<$src_fname";
   return 0 if not defined $src;

   # Open the output file.
   my $dst = new IO::File ">$dst_fname";
   return 0 if not defined $dst;

   # Read and check the magic values from the file.
   my($magic1, $magic2, $magic3, $reserved, $filesize);
   $magic1 = get_uint32_le($src);
   if ($magic1 == 0x44445A53) {
       $magic2 = get_uint32_le($src);
       $reserved = get_uint16_le($src);
       $filesize = get_uint32_le($src);
       if ($magic2 != 0x3327F088) {
           print STDERR "$src_fname: not a MS-compressed file\n";
           return 0;
       }
   } elsif ($magic1 == 0x4A41574B) {
       $magic2 = get_uint32_le($src);
       $magic3 = get_uint32_le($src);
       $reserved = get_uint16_le($src);
       if ($magic2 != 0xD127F088 || $magic3 != 0x00120003) {
           print STDERR "$src_fname: not a MS-compressed file\n";
           return 0;
       }
   } else {
       print STDERR "$src_fname: not a MS-compressed file\n";
       return 0;
   }

   my($i, $j, $mask, @buffer);

   # Allocate a look-back buffer for the decompression algorithm.
   for ($i = 0; $i < $N; $i++) {
       push(@buffer, ord(' '));
   }

   $i = $N - $F;
   while (1) {
       my $bits = get_uint8($src);
       last if not defined $bits;

       for ($mask = 0x01; $mask & 0xFF; $mask <<= 1) {
           if (($bits & $mask) == 0) {
               $j = get_uint8($src);
               last if not defined $j;

               my $len = get_uint8($src);
               $j += ($len & 0xF0) << 4;
               $len = ($len & 15) + 3;
               while ($len--) {
                   $buffer[$i] = $buffer[$j];
                   if (put_uint8($dst, $buffer[$i]) != 1) {
                       print STDERR "output error: $!\n";
                       return 0;
                   }
                   $j++;
                   $j %= $N;
                   $i++;
                   $i %= $N;
               }
           } else {
               my $ch = get_uint8($src);
               last if not defined $ch;

               $buffer[$i] = $ch;
               if (put_uint8($dst, $buffer[$i]) != 1) {
                   print STDERR "output error: $!\n";
                   return 0;
               }
               $i++;
               $i %= $N;
           }
       }
   }

   return 1;
}

1;