#!/usr/bin/perl -w
# unpack a .ar file from MM2
#
# File format:
# *** numbers are stored in little-endian format
# offset     size (bytes) function
# 0x00000000 4            Magic: "DAVE"
# 0x00000004 4            number of files stored in this archive
# 0x00000008 4            offset to filenames OR size of file header block
# 0x00000012 4            offset to data OR size of filename block
# * lots of zeros here
# 0x00000800 16           file header. there's [offset 4] of these
# * [offset 8] bytes of data
#            variable     zero-terminated filename
# * [offset 12] bytes of data
#            variable     possibly deflated file
#
# File header:
# 0x00000000 4            offset from start of filename block to filename
# 0x00000004 4            offset from start of file to data block
# 0x00000008 4            uncompressed size of file
# 0x00000012 4            compressed size of file
#
# if compressed size != uncompressed size, the file data is compressed
# using zlib, maximum compression, WindowBits => -MAX_WBITS (disables
# production of header bytes)
#
use Fcntl qw( SEEK_CUR SEEK_SET );
use Getopt::Long;
use File::Path;
use File::Basename;
use Compress::Zlib;
#use strict;

sub systell( * ) {
    sysseek( $_[0], 0, SEEK_CUR );
}

sub usage {
    print STDERR "usage: unar [-v] [-l] arfile[.ar]\n";
    exit( 1 );
}

my $list = 0;
my $verbose = 0;

GetOptions( "l" => \$list,
            "v" => \$verbose );

# this could become more complex, but for now if only -v is specified,
# take it to mean -v -l
if ( $verbose and !$list ) {
    $list = 1;
}

my $file = shift or die "No file specified\n";
my @filters = @ARGV;

if ( !@filters ) {
    push @filters, "*";
}

if ( ! -r $file ) {
    if ( -r $file . ".ar" ) {
        $file .= ".ar";
    } elsif ( -r $file . ".AR" ) {
        $file .= ".AR";
    } else {
        die "unar: cannot find or open $file, $file.ar, or $file.AR.\n";
    }
}

open( FILE, $file ) or die "unar: $! while opening $file";

# File signature: DAVE
my ( $nr, $buf );
my $unzip = 0;
$nr = sysread( FILE, $buf, 4 );
if ( $buf ne "DAVE" ) {
  if ( $buf eq "PK\x3"."\x4" ) {
	print STDERR "This is a PKZIP file...\n";
    $unzip = 1;
  } else {
	print STDERR "AR file signature not found\n";
  }
  exit(1) unless $unzip;
}

if ( $unzip ) {
#         local file header signature     4 bytes  (0x04034b50)
#         version needed to extract       2 bytes
#         general purpose bit flag        2 bytes
#         compression method              2 bytes
#         last mod file time              2 bytes
#         last mod file date              2 bytes
#         crc-32                          4 bytes
#         compressed size                 4 bytes
#         uncompressed size               4 bytes
#         file name length                2 bytes
#         extra field length              2 bytes

#         file name (variable size)
#         extra field (variable size)

# 504b 0304 PK0304
# 1400 need version 2.0 to extract
# 0800 flags 0000 0000 0000 1000 -> crc, size fields are zero, need desc.
# 0800 compr 0000 0000 0000 1000 -> deflated
# 480e mtime
# 9237 mdate
# 0000 0000 crc
# 0000 0000 compressed
# 0000 0000 uncompressed
# 1000 file name length
# 1000 extra field length
#  i A  T K  O S  _ v  1 .  0 i  . i  s o
# 6941 544b 4f53 5f76 312e 3069 2e69 736f
#
# 5558 0c00 0624 6747 370b 6747 0000 f501
    $nr = sysread( FILE, $buf, 26 );
    my ( $ver, $flag, $z, $mtime, $mdate, $crc, $comp, $uncomp, $fn, $ef ) =
      (
       unpack( "v", substr( $buf, 0, 2 )),
       unpack( "v", substr( $buf, 2, 2 )),
       unpack( "v", substr( $buf, 4, 2 )),
       unpack( "v", substr( $buf, 6, 2 )),
       unpack( "v", substr( $buf, 8, 2 )),
       unpack( "V", substr( $buf, 10, 4 )),
       unpack( "V", substr( $buf, 14, 4 )),
       unpack( "V", substr( $buf, 18, 4 )),
       unpack( "v", substr( $buf, 22, 2 )),
       unpack( "v", substr( $buf, 24, 2 )),
      );

    $nr = sysread( FILE, $buf, $fn + $ef );
    my $name = substr( $buf, 0, $fn );
    my $extra = substr( $buf, $fn, $ef );

    print "$name $comp $uncomp\n";

    my ( $zi, $status ) = inflateInit( WindowBits => -MAX_WBITS );
    open( UNZ, ">$name" ) or die "$name: $!";
    while ( my $b = sysread( FILE, $buf, 10240 )) {
         if ( $b <= 0 ) {
             print "error: $!";
             last;
         }
         my ( $out, $status ) = $zi->inflate( \$buf );

         if ( !defined( $status )) {
             print "inflate error\n";
             last;
         }
         if ( $status != Z_OK ) {
             print "inflate error #2\n";
             last;
         }

         print "inflated " . $zi->total_in() . " to " . $zi->total_out() . "\n";

         print UNZ $out;
     }
    close( UNZ );
} else {
    # Rest of header
    $nr = sysread( FILE, $buf, 28 );

    # Extract record count
    my $numrecs = unpack( "V", substr( $buf, 0, 4 ));
    # start of filenames - 0x800
    my $offset = unpack( "V", substr( $buf, 4, 4 ));
    # size of filename block (offset to start of data from start of filenames)
    my $length = unpack( "V", substr( $buf, 8, 4 ));

    $nr = sysseek( FILE, 0x800, 0 ); # Data appears to start here regardless.

    if ( $nr != 0x800 ) {
        die "unar: error $! while seeking\n";
    }

    # Read NUMRECS records (16 bytes per)

    # This is fairly non-optimal, since it seeks back and forth between
    # the record section and the name section. However, it still runs
    # pretty quickly, so I'll leave it as-is for now.
    my @records;
    for my $rec ( 0..$numrecs - 1 ) {
        $nr = sysread( FILE, $buf, 16 );
        my %rec;
        $rec{nameptr} = unpack( "V", substr( $buf, 0, 4 )) + 0x17000;
        $rec{dataptr} = unpack( "V", substr( $buf, 4, 4 ));
        $rec{fulsize} = unpack( "V", substr( $buf, 8, 4 ));
        $rec{zipsize} = unpack( "V", substr( $buf, 12, 4 ));

        my $here = systell( FILE );
        sysseek( FILE, $rec{nameptr}, SEEK_SET );
        $nr = sysread( FILE, $buf, 1024 );
        $rec{namestr} = unpack( "Z*", $buf );
        sysseek( FILE, $here, SEEK_SET );

        push @records, \%rec;
    }
}

__END__
print "Archive:  $file\n";
if ( $list ) {
    if ( $verbose ) {
        print "  Length    Size   Ratio Name\n";
        print " -------- -------- ----- ----\n";
    } else {
        print "  Length  Name\n";
        print " -------- ----\n";
    }
}
my ( $totzip, $totful, $found ) = ( 0, 0, 0 );
for my $rec ( 0..$numrecs - 1 ) {
    # check for filters
    my $ok = 0;
    for my $filter ( @filters ) {
        # ugly. there must be an inline glob somewhere I can use.
        my $filterregexp = $filter;
        $filterregexp =~ s/\./\\./g;
        $filterregexp =~ s/\?/./g;
        $filterregexp =~ s/\*/.*/g;
        if ( $records[$rec]->{namestr} =~ /^$filterregexp$/i ) {
            $ok = 1;
            last;
        }
    }
    next unless $ok;
    $found++;

    if ( $list ) {
        if ( $verbose ) {
            printf " %8d %8d %3d%%  %s\n",
              $records[$rec]->{fulsize},
                $records[$rec]->{zipsize},
                  $records[$rec]->{fulsize} ? 
                    100 - ( $records[$rec]->{zipsize}/$records[$rec]->{fulsize} * 100 )
                      : 0,
                        $records[$rec]->{namestr};
        } else {
            printf " %8d %s\n",
              $records[$rec]->{fulsize},
                $records[$rec]->{namestr};
        }
    } else {
        # extracting files
        # directory
        if ( $records[$rec]->{namestr} =~ m@/$@ ) {
            mkpath( $records[$rec]->{namestr}, 0, 0755 );
        } else {
            print "  inflating: " . $records[$rec]->{namestr} . "\n";
            my $path = dirname( $records[$rec]->{namestr} );
            -d $path or mkpath( $path, 0, 0755 );

            sysseek( FILE, $records[$rec]->{dataptr}, SEEK_SET );
            sysread( FILE, $buffer, $records[$rec]->{zipsize});
            open( UNAR, ">" . $records[$rec]->{namestr} ) or die $!;
            binmode( UNAR );
            if ( $records[$rec]->{zipsize} != $records[$rec]->{fulsize}) {
                my $i = inflateInit( WindowBits => -( MAX_WBITS ) );
                my ( $data, $status ) = $i->inflate( $buffer );
                print UNAR $data;
            } else {
                print UNAR $buffer;
            }
            close( UNAR );
        }
    }

    $totzip += $records[$rec]->{zipsize};
    $totful += $records[$rec]->{fulsize};
}

if ( $list ) {
    if ( $verbose ) {
        print " -------- -------- ----- ----\n";
        printf " %8d %8d %3d%%  %d files\n",
          $totful, $totzip, $totful ? 100 - ( $totzip / $totful * 100 ) : 0, $found;
    } else {
        print " -------- ----\n";
        printf " %8d %d files\n", $totful, $found;
    }
}

exit;
