#
# Perl module to expand Microsoft-compressed files
#
# Based on algorithm provided in the GPL'ed mscompress package by:
#   Martin Hinner <mhi@penguin.cz>
#   M. Winterhoff <100326.2776@compuserve.com>
#
# 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;
