The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl

use strict;
use warnings;
use Getopt::Long;
use File::Basename;
use File::Spec;

BEGIN {
    if ($^O eq 'VMS') {
        require VMS::Filespec;
        import VMS::Filespec;
    }
}

Getopt::Long::Configure('no_ignore_case');

our $LastUpdate = -M $0;

sub handle_file {
    my $opts    = shift;
    my $file    = shift or die "Need file\n". usage();
    my $outfile = shift || '';
    $file = vms_check_name($file) if $^O eq 'VMS';
    my $mode    = (stat($file))[2] & 07777;

    open my $fh, "<", $file
        or do { warn "Could not open input file $file: $!"; exit 0 };
    my $str = do { local $/; <$fh> };

    ### unpack?
    my $outstr;
    if( $opts->{u} ) {
        if( !$outfile ) {
            $outfile = $file;
            $outfile =~ s/\.packed\z//;
        }
        my ($head, $body) = split /__UU__\n/, $str;
        die "Can't unpack malformed data in '$file'\n"
            if !$head;
        $outstr = unpack 'u', $body;

    } else {
        $outfile ||= $file . '.packed';

        my $me = basename($0);

        $outstr = <<"EOFBLURB" . pack 'u', $str;
#########################################################################
This is a binary file that was packed with the 'uupacktool.pl' which
is included in the Perl distribution.

To unpack this file use the following command:

     $me -u $outfile $file

To recreate it use the following command:

     $me -p $file $outfile

Created at @{[scalar localtime]}
#########################################################################
__UU__
EOFBLURB
    }

    ### output the file
    if( $opts->{'s'} ) {
        print STDOUT $outstr;
    } else {
        $outfile = VMS::Filespec::vmsify($outfile) if $^O eq 'VMS';
        print "Writing $file into $outfile\n" if $opts->{'v'};
        open my $outfh, ">", $outfile
            or do { warn "Could not open $outfile for writing: $!"; exit 0 };
        binmode $outfh;
        ### $outstr might be empty, if the file was empty
        print $outfh $outstr if $outstr;
        close $outfh;

        chmod $mode, $outfile;
    }

    ### delete source file?
    if( $opts->{'D'} and $file ne $outfile ) {
        1 while unlink $file;
    }
}

sub bulk_process {
    my $opts = shift;
    my $Manifest = $opts->{'m'};

    open my $fh, "<", $Manifest or die "Could not open '$Manifest':$!";

    print "Reading $Manifest\n"
            if $opts->{'v'};

    my $count = 0;
    my $lines = 0;
    while( my $line = <$fh> ) {
        chomp $line;
        my ($file) = split /\s+/, $line;

        $lines++;

        next unless $file =~ /\.packed/;

        $count++;

        my $out = $file;
        $out =~ s/\.packed\z//;
        $out = vms_check_name($out) if $^O eq 'VMS';

        ### unpack
        if( !$opts->{'c'} ) {
            ( $out, $file ) = ( $file, $out ) if $opts->{'p'};
            if (-e $out) {
                my $changed = -M _;
                if ($changed < $LastUpdate and $changed < -M $file) {
                    print "Skipping '$file' as '$out' is up-to-date.\n"
                        if $opts->{'v'};
                    next;
                }
            }
            handle_file($opts, $file, $out);
            print "Converted '$file' to '$out'\n"
                if $opts->{'v'};

        ### clean up
        } else {

            ### file exists?
            unless( -e $out ) {
                print "File '$file' was not unpacked into '$out'. Can not remove.\n";

            ### remove it
            } else {
                print "Removing '$out'\n";
                1 while unlink $out;
            }
        }
    }
    print "Found $count files to process out of $lines in '$Manifest'\n"
            if $opts->{'v'};
}

sub usage {
    return qq[
Usage: $^X $0 [-d dir] [-v] [-c] [-D] -p|-u [orig [packed|-s] | -m [manifest]]

    Handle binary files in source tree. Can be used to pack or
    unpack files individiually or as specified by a manifest file.

Options:
    -u  Unpack files (defaults to -u unless -p is specified)
    -p  Pack files
    -c  Clean up all unpacked files. Implies -m

    -D  Delete source file after encoding/decoding

    -s  Output to STDOUT rather than OUTPUT_FILE
    -m  Use manifest file, if none is explicitly provided defaults to 'MANIFEST'

    -d  Change directory to dir before processing

    -v  Run verbosely
    -h  Display this help message
];
}

sub vms_check_name {

# Packed files tend to have multiple dots, which the CRTL may or may not handle
# properly, so convert to native format.  And depending on how the archive was
# unpacked, foo.bar.baz may be foo_bar.baz or foo.bar_baz.  N.B. This checks for
# existence, so is not suitable as-is to generate ODS-2-safe names in preparation
# for file creation.

    my $file = shift;

    $file = VMS::Filespec::vmsify($file);
    return $file if -e $file;

    my ($vol,$dirs,$base) = File::Spec->splitpath($file);
    my $tmp = $base;
    1 while $tmp =~ s/([^\.]+)\.(.+\..+)/$1_$2/;
    my $try = File::Spec->catpath($vol, $dirs, $tmp);
    return $try if -e $try;

    $tmp = $base;
    1 while $tmp =~ s/(.+\..+)\.([^\.]+)/$1_$2/;
    $try = File::Spec->catpath($vol, $dirs, $tmp);
    return $try if -e $try;

    return $file;
}

my $opts = {};
GetOptions($opts,'u','p','c', 'D', 'm:s','s','d=s','v','h');

die "Can't pack and unpack at the same time!\n", usage()
    if $opts->{'u'} && $opts->{'p'};
die usage() if $opts->{'h'};

if ( $opts->{'d'} ) {
    chdir $opts->{'d'}
        or die "Failed to chdir to '$opts->{'d'}':$!";
}
$opts->{'u'} = 1 if !$opts->{'p'};
binmode STDOUT if $opts->{'s'};
if ( exists $opts->{'m'} or exists $opts->{'c'} ) {
    $opts->{'m'} ||= "MANIFEST";
    bulk_process($opts);
    exit(0);
} else {
    if (@ARGV) {
        handle_file($opts, @ARGV);
    } else {
        die "No file to process specified!\n", usage();
    }
    exit(0);
}


die usage();