#!/usr/bin/perl
# -*- mode:perl -*-
use strict;
use warnings;

sub BEGIN {
    # Fri Oct  8 12:21:59 2021 Give up on la vide tainted due to appimage-builder
    # Seal us up a bit for living la vida tainted
    # $ENV{'PATH'} = "/bin:/usr/bin";
    $ENV{'LC_ALL'} = "C";
    delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
}

##
# Begin: common.pl.inc

use Carp;
use File::Path qw(make_path);
use File::Spec;
use File::Temp qw(tempdir);
use IO::Uncompress::Unzip qw($UnzipError);
use Digest::SHA qw(sha512_hex sha512);

sub VERSION { "1.0" }
sub MFZRUN_HEADER { "MFZ(".VERSION.")\n" }
sub MFZ_PUBKEY_NAME { "MFZPUBKEY.DAT" }
sub MFZ_FILE_NAME { "MFZNAME.DAT" }
sub MFZ_SIG_NAME { "MFZSIG.DAT" }
sub MFZ_ZIP_NAME { "MFZ.ZIP" }

use constant CDM_FORMAT_MAGIC => "CDM";
use constant CDM_FORMAT_VERSION_MAJOR => "1";
use constant CDM_FORMAT_VERSION_MINOR => "0";

use constant CDM10_PACK_SIGNED_DATA_FORMAT =>
    ""            #   0
    ."a6"         #   0 +   6 =    6 magic + maj + min + \n
    ."C1"         #   6 +   1 =    7 bits in block size
    ."C1"         #   7 +   1 =    8 regnum
    ."N"          #   8 +   4 =   12 slotstamp
    ."N"          #  12 +   4 =   16 mapped file length
    ."a16"        #  16 +  16 =   32 label
    ."a64"        #  32 +  64 =   96 sha512 mapped file checksum
    ."(a8)100"    #  96 + 800 =  896 100*8 byte xsum map
    #  896 total length
    ;

use constant CDM10_PACK_FULL_FILE_FORMAT =>
    ""            #   0
    ."a896"       #   0 + 896 =  896 as from CDM10_PACK_SIGNED_DATA_FORMAT
    ."a128"       # 896 + 128 = 1024 RSA sig by regnum of bytes 0..895
    # 1024 total length
    ;

my $programName = $0;  # default
sub SetProgramName {
    $programName = shift;
}

sub IDie {
    my $msg = shift;
    print STDERR "\nInternal Error: $msg\n";
    confess "I suck";
}

my $UDIE_MSG;
sub SetUDieMsg {
    $UDIE_MSG = shift;
}

sub UDie {
    my $msg = shift;
    IDie("Unset UDie message") unless defined $UDIE_MSG;
    print STDERR "\nError: $msg\n";
    print STDERR $UDIE_MSG;
    exit(1);
}

sub NoVerb {
    UDie("Missing command");
}

my $KeyDir;

sub InitMFMSubDir {
    my $sub = shift;
    IDie "No sub?" unless defined $sub;
    IDie "No KD?" unless defined $KeyDir;

    my $dir = "$KeyDir/$sub"; # $KeyDir should be clean and $sub should be internal

    if (!-d $dir) {
        make_path($dir)       # So we shouldn't need untainting to do this
            or die "Couldn't mkdir $dir: $!";
    }
    return $dir;
}

sub GetPublicKeyDir {
    return InitMFMSubDir("public_keys");
}

sub GetPublicKeyFile {
    my $handle = shift;
    my $ehandle = EscapeHandle($handle);
    my $dir = GetPublicKeyDir();
    my $pub = "$dir/$ehandle.pub";

    return $pub;
}

sub JoinHandleToKey {
    my ($handle, $keydata) = @_;
    my $data ="[MFM-Handle:$handle]\n$keydata";
    return $data;
}

sub SplitHandleFromKey {
    my $data = shift;
    $data =~ s/^\[MFM-Handle:(:?[a-zA-Z][-., a-zA-Z0-9]{0,62})\]\r?\n//
        or return undef;
    my $handle = $1;
    return ($handle,$data);
}

sub ComputeFingerprintFromFullPublicKey {
    my $fullpubkey = shift;
    my $fingerprint = lc(sha512_hex($fullpubkey));
    $fingerprint =~ s/^(....)(...)(....).+$/$1-$2-$3/
        or IDie("you're a cow");
    return $fingerprint;
}

sub ComputeChecksumOfString {
    my $string = shift;
    my $fingerprint = lc(sha512_hex($string));
    $fingerprint =~ s/^(......)(..).+(..)(......)$/$1-$2$3-$4/
        or IDie("give me some milk or else go home");
    return $fingerprint;
}

sub ComputeChecksumPrefixOfString {
    my ($string,$len) = @_;
    IDie("something is happening here but you don't know what it is")
        unless defined $len && $len >= 0 && $len <= 64;
    my $checksum = sha512($string);
    return substr($checksum,0,$len);
}

sub ReadPublicKeyFile {
    my $handle = shift;
    my $file = GetPublicKeyFile($handle);
    my $data = ReadWholeFile($file);
    my ($pubhandle, $key) = SplitHandleFromKey($data);
    UDie("Bad format in public key file '$file' for '$handle'")
        unless defined $pubhandle;
    UDie("Non-matching handle in public key file '$file' ('$handle' vs '$pubhandle')")
        unless $pubhandle eq $handle;
    return ($key, ComputeFingerprintFromFullPublicKey($data));
}

sub CheckForPubKey {
    my $handle = shift;
    my $path = GetPublicKeyFile($handle);
    if (-r $path) {
        return ($path, ReadPublicKeyFile($handle));
    }
    return ($path);
}

sub GetConfigDir {
    my $cfgdir = InitMFMSubDir("config");
    chmod 0700, $cfgdir;
    return $cfgdir;
}

sub GetLegalHandle {
    my $handle = shift;
    if ($handle eq "-") {
        my $defaulthandle = GetDefaultHandle();
        if (!defined $defaulthandle) {
            print STDERR "ERROR: No default handle, so cannot use '-' as handle (try 'mfzmake help'?)\n";
            exit 1;
        }
        return $defaulthandle;
    }

    UntaintHandleIfLegal(\$handle)
        or UDie("Bad handle '$handle'");
    return $handle;
}

sub GetLegalRegnum {
    my $regnum = shift;
    UDie("Not a number '$regnum'")
        unless $regnum =~ /^(\d+)$/;
    my $num = $1;
    UDie("Illegal regnum $num")
        unless $regnum >= 0 && $regnum < (1<<16);
    my @regnumHandles = (
        "t2-keymaster-release-10"
        );
    my $handle = $regnumHandles[$num];
    UDie("Invalid regnum $num")
        unless defined $handle;
    return ($num, $handle);
}

sub GetDefaultHandleFile {
    my $dir = GetConfigDir();
    my $def = "$dir/defaultHandle";
    return $def;
}

sub GetDefaultHandle {
    my $file = GetDefaultHandleFile();
    if (-r $file) {
        my $handle = ReadWholeFile($file);
        return $handle if UntaintHandleIfLegal(\$handle);
    }
    return undef;
}

sub GetPrivateKeyDir {
    my $privdir = InitMFMSubDir("private_keys");
    chmod 0700, $privdir;
    return $privdir;
}

sub GetPrivateKeyFile {
    my $handle = shift;
    my $ehandle = EscapeHandle($handle);
    my $dir = GetPrivateKeyDir();
    my $pub = "$dir/$ehandle.priv";
    return $pub;
}

sub ReadPrivateKeyFile {
    my $handle = shift;
    my $file = GetPrivateKeyFile($handle);
    my $data = ReadWholeFile($file);
    my ($privhandle, $key) = SplitHandleFromKey($data);
    UDie("Bad format in private key file for '$handle'")
        unless defined $privhandle;
    UDie("Non-matching handle in private key file ('$handle' vs '$privhandle')")
        unless $privhandle eq $handle;
    return $key;
}

sub VersionExit {
    my $pname = shift;
    $pname = "" unless defined $pname;
    print "$pname-".VERSION."\n";
    exit(0);
}

sub GetKeyDir {
    IDie("No key dir?") unless defined $KeyDir;
    return $KeyDir;
}

sub KDGetVerb {
    my $mustExist = shift;
    my $verb = NextArg();
    NoVerb() if $mustExist && !defined $verb;
    my $kdir;
    if (defined($verb) && $verb eq "-kd") {
        $kdir = NextArg();
        UDie("Missing argument to '-kd' switch") 
            unless defined $kdir;
        $verb = NextArg();
        NoVerb() if $mustExist && !defined $verb;
    } else {
        $kdir = glob "~/.mfm";
    }

    # Let's avoid accidentally creating keydir 'help' or whatever..
    UDie("-kd argument ('$kdir') must begin with '/', './', or '../'")
        unless $kdir =~ m!^([.]{0,2}/.*)$!;
    $KeyDir = $1;

    if (-e $KeyDir) {
        UDie("'$KeyDir' exists but is not a directory")
            if ! -d $KeyDir;
    }
    return $verb;
}

sub NextArg {
    my $arg = shift @ARGV;
    return $arg;
}

sub LastArg {
    my $arg = NextArg();
    my @more = RestOfArgs();
    UDie("Too many arguments: ".join(" ",@more))
        if scalar(@more);
    return $arg;
}

sub RestOfArgs {
    my @args = @ARGV;
    @ARGV = ();
    return @args;
}

sub ReadableFileOrDie {
    my ($text, $path) = @_;
    UDie "No $text provided" unless defined $path;
    UDie "Non-existent or unreadable $text: '$path'"
        unless -r $path and -f $path;
    $path =~ /^(.+)$/
      or IDie("am i here all alone");
    return $1;
}

sub WritableFileOrDie {
    my ($text, $path) = @_;
    UDie "No $text provided" unless defined $path;
    UDie "Unwritable $text: '$path': $!" unless -w $path or !-e $path;
    $path =~ /^(.+)$/
      or IDie("hands you a bone");
    return $1;
}

sub ReadWholeFile {
    my $file = shift;
    open (my $fh, "<", $file) or IDie("Can't read '$file': $!");
    local $/ = undef;
    my $content = <$fh>;
    close $fh or IDie("Failed closing '$file': $!");
    return $content;
}

sub WriteWholeFile {
    my ($file, $content, $perm) = @_;
    open (my $fh, ">", $file) or UDie("Can't write '$file': $!");
    chmod $perm, $fh
        if defined $perm;
    print $fh $content;
    close $fh or IDie("Failed closing '$file': $!");
}

sub UntaintHandleIfLegal {
    my $ref = shift;
    return 0
        unless $$ref =~ /^\s*(:?[a-zA-Z][-., a-zA-Z0-9]{0,62})\s*$/;
    $$ref = $1;
    return 1;
}

sub EscapeHandle {
    my $handle = shift;
    chomp($handle);
    $handle =~ s/([^a-zA-Z0-9])/sprintf("%%%02x",ord($1))/ge;
    return $handle;
}

sub UnzipStreamToMemory {
    my ($u) = @_;
    my @paths;

    my $status;
    my $count = 0;
    for ($status = 1; $status > 0; $status = $u->nextStream(), ++$count) {
        my $header = $u->getHeaderInfo();
        my $stored_time = $header->{'Time'};
        $stored_time =~ /^(\d+)$/ or die "Bad stored time: '$stored_time'";
        $stored_time = $1;  # Untainted

        my $fullpath = $header->{Name};
        my (undef, $path, $name) = File::Spec->splitpath($fullpath);

        if ($name eq "" or $name =~ m!/$!) {
            last if $status < 0;
        } else {

            my $data = "";
            my $buff;
            while (($status = $u->read($buff)) > 0) {
                $data .= $buff;
            }
            if ($status == 0) {
                push @paths, [$path, $name, $stored_time, $data];
            }
        }
    }

    die "Error in processing: $!\n"
        if $status < 0 ;
    return @paths;
}

sub UnzipStream {
    my ($u, $dest) = @_;
    my @paths;

    $dest = "." unless defined $dest;

    my $status;
    my $count = 0;
    for ($status = 1; $status > 0; $status = $u->nextStream(), ++$count) {
        my $header = $u->getHeaderInfo();
        my $stored_time = $header->{'Time'};
        $stored_time =~ /^(\d+)$/ or die "Bad stored time: '$stored_time'";
        $stored_time = $1;  # Untainted

        my (undef, $path, $name) = File::Spec->splitpath($header->{Name});
        my $destdir = "$dest/$path";

        my $totouch;
        unless (-d $destdir) {
            make_path($destdir)
                or die "Couldn't mkdir $destdir: $!";
            $totouch = $destdir;
        }
        if ($name eq "" or $name =~ m!/$!) {
            last if $status < 0;
        } else {

            my $destfile = "$destdir$name";
            my $buff;
#            print STDERR "Writing $destfile\n";
            my $fh = IO::File->new($destfile, "w")
                or die "Couldn't write to $destfile: $!";
            my $length = 0;
            while (($status = $u->read($buff)) > 0) {
                 $length += length($buff);
#                print STDERR "Read ".length($buff)."\n";
                $fh->write($buff);
            }
            $fh->close();
            $totouch = $destfile;
            push @paths, [$destdir, $name, $stored_time, $length];
        }

        utime ($stored_time, $stored_time, $totouch)
            or die "Couldn't touch $totouch: $!";
    }

    die "Error in processing: $!\n"
        if $status < 0 ;
    return @paths;
}

# Returns:
# undef if $findName (in option $findPath) is not found,
# [$destdir, $name, $stored_time] if $pref is data from UnzipStream
# [$path, $name, $stored_time, $data] if $pref is data from UnzipStreamToMemory

sub FindName {
    my ($pref, $findName, $findPath) = @_;
    my @precs = @{$pref};
    for my $rec (@precs) {
        my @fields = @{$rec};
        my ($path, $name) = @fields;
        if ($name eq $findName) {
            if (!defined($findPath) || $path eq $findPath) {
                return @fields;
            }
        }
    }
    return undef;
}

sub SignStringRaw {
    my ($privkeyfile, $datatosign) = @_;

    my $keystring = ReadWholeFile( $privkeyfile );
    my $privatekey = Crypt::OpenSSL::RSA->new_private_key($keystring);
    $privatekey->use_pkcs1_padding();
    $privatekey->use_sha512_hash();
    my $signature = $privatekey->sign($datatosign);
    return $signature;
}

sub SignString {
    my ($privkeyfile, $datatosign) = @_;
    my $signature = SignStringRaw($privkeyfile, $datatosign);
    return encode_base64($signature, '');
}


# End: common.pl.inc
###
SetUDieMsg("Type '$0' for help\n");

use File::Temp qw(tempdir);
use MIME::Base64 qw(decode_base64);
use Crypt::OpenSSL::RSA;
use Cwd qw(abs_path);

my (undef, $binDir, $scriptName) = File::Spec ->splitpath(abs_path(__FILE__));
$binDir =~ s!/$!!;

my $progname = KDGetVerb(0);
my $optverb = NextArg() || "";
if (!defined($progname)) {
    $progname = "YOURMFZFILE.mfz";
    $optverb = "help";
}
#SetProgramName($progname);

my $verb = lc($optverb);
if ($verb eq "help" || $verb eq "-h") {
    my $v = VERSION;
    print <<EOH;
MFZ format simulation runner version $v

SYNOPSIS: $0 [-kd KEY_DIRECTORY] FILE.mfz [COMMAND]

  KEY_DIRECTORY defaults to ~/.mfm/ if -kd is omitted

$0
$0 $progname help
$0 $progname -h
    Print this help

$0 $progname verify
    Check the file signature but do not run it
    (use VERIFY to report more details)

$0 $progname list
    Check the file signature and list contents but do not run it
    (use LIST to print full paths)

$0 $progname announce
    [OBSOLETE] Print the contents of the ANNOUNCE.PKT metadata file,
    if it exists (use ANNOUNCE to append VERIFY info as well)

$0 $progname unpack [DESTINATION_DIRECTORY]
    Verify and unpack the file but do not run it

$0 $progname
$0 $progname run [ARG ARG..]
    Run the file if it is signed by a recognized handle, possibly
    with additional arguments passed to the simulator.

$0 DEMO demo [ARG ARG..]
    Search in prespecified locations for a built-in demo named DEMO,
    and run it if found, possibly with additional arguments as in run

EOH
    ListDemosDat();
    exit 0;
}

my %verbs = (
    "" => 1,
    "verify" => 1,
    "run" => 1,
    "demo" => 1,
    "unpack" => 1,
    "list" => 1,
    "announce" => 1,
    "" => 1,
);

UDie("Unknown command '$optverb'")
    unless defined $verbs{$verb};

if ($verb eq "demo") {
    $progname = FindDemoOrDie($progname);
    $verb = "run";
}

if ($verb ne "run" && $verb ne "unpack") {
    my $fx = $ARGV[0];
    UDie("Unrecognized extra arguments (beginning with '$fx')")
        if defined $fx;
}

if (!open MFZ, "<", $progname) {
    warn "Try just '$0' for help\n"
        if $progname =~ /^-?-?h(elp)?$/;
    die "Can't read '$progname' as .mfz file: $!\n";
}

my $firstLine = <MFZ>;

if (defined($firstLine) && $firstLine =~ /^CDM1\d\n$/) { # detect cdmake version 1 header
    my $remainingLen = 1024-6;
    my $data;
    my $read = read MFZ,$data,$remainingLen;
    die "Malformed CDM header or content"
        unless $read == $remainingLen;
    $firstLine = <MFZ>; # The poifect crime
}

die "Bad .mfz header in '$progname'"
    unless defined $firstLine and $firstLine eq MFZRUN_HEADER;

my $keydir = GetPublicKeyDir();

my $u = new IO::Uncompress::Unzip(*MFZ)
    or die "Cannot read $progname: $UnzipError";

my @outerpaths = UnzipStreamToMemory($u);
if (0) {
    for my $op (@outerpaths) {
        print $op->[0]." ".$op->[1]." ".$op->[2]." ".length($op->[3])."\n";
    }
}

my ($zippath,$zipname,undef,$zipdata) = FindName(\@outerpaths,MFZ_ZIP_NAME,undef);
die "Incorrect .mfz packing" unless defined($zipname);

my ($sigpath,$signame,undef,$sigdata) = FindName(\@outerpaths,MFZ_SIG_NAME,undef);
die ".mfz signature not found" unless defined($signame);

my $u2 = new IO::Uncompress::Unzip(\$zipdata)
    or die "Cannot read $zippath/$zipname: $UnzipError";

my $destdir;
if ($verb eq "unpack") {
    my $optdir = NextArg() || "";
    if ($optdir =~ /^(.+)$/) {
        $destdir = $1;  # Gah.
    } else {
        $destdir = $progname;
        UDie("Specify where to unpack '$progname': Non-standard name (no .mfz extension)")
            unless $destdir =~ /^(.+)[.]mfz$/;
        $destdir = $1;
        UDie("Specify where to unpack '$progname': Default location '$destdir' already exists")
            if -e $destdir;
    }
}

if (!defined $destdir) {
    my $template = "mfzXXXXXXXX";
    my $cleanup = 1;

    # Save 'temporary' dir if unpacking..
    $cleanup = 0 
        if $verb eq "unpack";

    $destdir =
        tempdir( $template,
                 TMPDIR => 1,
                 CLEANUP => $cleanup
        );
}

my @innerpaths = UnzipStream($u2, $destdir);
my ($pubkeypath,$pubkeyname,$pubkeytime) = FindName(\@innerpaths,MFZ_PUBKEY_NAME,undef);
die "Incorrect .mfz packing - missing pubkey" unless defined($pubkeyname);
my $fullpubkeypath = "$pubkeypath/$pubkeyname";

my $fullpubstring = ReadWholeFile($fullpubkeypath);
my ($pubhandle, $pubkey) = SplitHandleFromKey($fullpubstring);
UDie("Bad format public key") unless defined $pubhandle;

my $rsapub = Crypt::OpenSSL::RSA->new_public_key($pubkey);
$rsapub->use_pkcs1_padding();
$rsapub->use_sha512_hash();

my $sig = decode_base64($sigdata);
die "Invalid signature '$sigdata'/'$sig'" unless $rsapub->verify($zipdata, $sig);

ValidatePubKey($pubhandle,$pubkey);
my $fingerprint = ComputeFingerprintFromFullPublicKey($fullpubstring);

if ($verb eq "announce") {
    UDie("Obsolete function: $verb");
}

if ($optverb eq "VERIFY") {
    my $checksum = ComputeChecksumOfString($zipdata);
    print "SIGNATURE_CHECK [OK]\n";
    print "INNER_CHECKSUM [$checksum]\n";
    print "INNER_TIMESTAMP [$pubkeytime]\n";
    print "SIGNING_HANDLE [$pubhandle]\n";
    print "HANDLE_FINGERPRINT [$fingerprint]\n";
    print "HANDLE_PUBKEY [$pubkey]\n";
} else {
    print "SIGNED BY RECOGNIZED HANDLE: $pubhandle ($fingerprint)\n";
}
if ($verb eq "verify") {
    exit 0;
}
if ($verb eq "list") {
    my $maxpath = 0;
    my $maxname = 0;
    my $maxtime = 0;
    my $maxsize = 0;
    for my $prec (@innerpaths) {
        my ($path, $name, $time, $size) = @{$prec};
        $path =~ s!^$destdir/!!;
        $time = localtime($time);
        $maxpath = length($path) if length($path) > $maxpath;
        $maxname = length($name) if length($name) > $maxname;
        $maxtime = length($time) if length($time) > $maxtime;
        $maxsize = length($size) if length($size) > $maxsize;
    }
    my $maxlen = 40;
    if ($optverb eq "LIST") {
        $maxlen = $maxpath;
    } else { 
        $maxpath = $maxlen if $maxpath > $maxlen;
    }
    printf("FILES:\n%*s  %*s  %*s  %*s\n",
           -$maxpath, ($maxpath > 4)?"Path":"",
           -$maxname, "Name",
           $maxsize, "Size",
           -$maxtime, "Time"
        );
    for my $prec (@innerpaths) {
        my ($path, $name, $time, $size) = @{$prec};
        $path =~ s!^$destdir/!!;
        my $ellipsis = "[...]";
        my $clip = length($path) - ($maxpath - length($ellipsis));
        if ($maxpath > length($ellipsis) && $clip > 0) {
            # /4 since ends of paths seem more informative
            substr($path, ($maxpath-length($ellipsis))/4, $clip) = $ellipsis;
        }
        $time = localtime($time);
        printf("%*s  %*s  %*s  %*s\n",
               -$maxpath, $path,
               -$maxname, $name,
               $maxsize, $size,
               -$maxtime, $time
            );
    }
    exit 0;
}

if ($verb eq "unpack") {
    print "UNPACKED INTO: $destdir\n";
    exit 0;
}


my @mfmargs;

my ($ulams,$splats,$incs,$goodSOs, $badSOs) = (0,0,0,0,0);
my $platform = `/bin/uname -m`;
chomp $platform;
my $platformsize;
if ($platform eq 'x86_64' || $platform =~ /^armv8.*/) {
    $platformsize = 64;
} elsif ($platform eq 'i386' || $platform eq 'i686' || $platform =~ /^armv7.*/) {
    $platformsize = 32;
} else {
    print STDERR "WARNING: Unrecognized hardware platform '$platform'!  Assuming it's a 32 bit architecture\n";
    $platformsize = 32;
}
for my $prec (@innerpaths) {
    my ($path, $name, $time) = @{$prec};
    my $fullpath = "$path$name";
    if ($name eq "libcue.so") {
        my $oinfo;
        {
            local $/ = undef;
            open my $fh, '-|', '/usr/bin/objdump', "-r", $fullpath or IDie("Can't open pipe: $!");
            $oinfo = <$fh>;  # or read in a loop, which is more likely what you want
            close $fh or IDie("Can't close pipe: $!");
        }
        $oinfo =~ /$fullpath:\s+file format\s+([^\n]+)\n/
            or IDie("Can't parse objdump output");
        my $sotype = $1;
        my $sosize;
        if ($sotype eq "elf64-x86-64") {
            $sosize = 64;
        } elsif ($sotype eq "elf32-i386" || $sotype eq "elf32-littlearm") {
            $sosize = 32;
        } else {
            ++$badSOs;
            print("Unrecognized .so file format '$sotype'\n");
        }
        if ($sosize != $platformsize) {
            print("Skipping $sosize bit .so file; we need $platformsize bits\n");
            ++$badSOs;
        } else {
            ++$goodSOs;
            push @mfmargs, "-ep", $fullpath;
        }
    } elsif ($name =~ /[.]mfs$/) {
        push @mfmargs, "-cp", $fullpath;
    } elsif ($name eq "args.txt") {
        my @fileArgs = processArgs($fullpath);
        if (defined $fileArgs[0]) {
            if ($fileArgs[0] =~ /^\{/) {
                my $geom = shift @fileArgs;
                unshift @mfmargs, $geom;
            }
            push @mfmargs, @fileArgs;
        }
    } elsif ($name eq MFZ_PUBKEY_NAME) {
        # Don't need a complaint about this one
    } elsif ($name eq MFZ_FILE_NAME) {
        # Or this one
    } elsif ($name =~ /[.]ulam$/) {
        ++$ulams;
        print STDERR "Skipping ulam source: $name\n";
    } elsif ($name =~ /[.]splat$/) {
        ++$splats;
        print STDERR "Skipping splat source: $name\n";
    } elsif ($name =~ /[.]inc$/) {
        ++$incs;
        print STDERR "Skipping include source: $name\n";
    } else {
        print STDERR "Unrecognized file type, ignored: $name\n";
    }
}

if ($goodSOs == 0 && $verb eq 'run') {
    print STDERR "CANNOT RUN .mfz file: No usable .so files found\n";
    if ($badSOs > 0) {
        print STDERR "  The $badSOs .so file(s) in this .mfz are incompatible with this $platformsize-bit platform\n";
        if ($ulams > 0) {
            print STDERR "  However, $ulams .ulam file(s) are present\n";
            print STDERR "  You may be able to rebuild it for this platform\n\n";
        } # For now we're not mentioning SPLAT files
    }
    exit 4;
}

if ($verb eq "run" && scalar(@ARGV) > 0) { # any remaining args are for mfms
    my @args = purifyArgs(@ARGV);
    my $arg;
    while (defined($arg = shift @args)) {
        if ($arg =~ /^\{/) {  # geometry if present goes at the front

            if (defined $mfmargs[0] && $mfmargs[0] =~ /^\{/) {
                if ($arg ne $mfmargs[0]) {
                    print STDERR "WARNING: Overriding prior geometry '$mfmargs[0]' with '$arg'\n";
                    $mfmargs[0] = $arg;  # replace at front
                }
            } else {
                unshift @mfmargs, $arg;  # insert at front
            }
        } else {   # everything else goes at the end.
            push @mfmargs, $arg;
        }
    }
}

my $cmd = "$binDir/mfms";
unshift @mfmargs, $cmd;

print "@mfmargs\n";

my $result = system $cmd @mfmargs;
sleep 0.25;
exit $result;

sub processArgs {
    my $path = shift;
    open ARGTXT, "<", $path or die "Can't open args: $!";
    my $line = <ARGTXT>;
    close ARGTXT or die "Can't close args: $!";
    chomp($line);
    my @args = split(" ",$line);
    return purifyArgs(@args);
}

sub purifyArgs {
    my @args = @_;
    for (my $i = 0; $i < scalar(@args); ++$i) {
        my $arg = $args[$i];
        if ($i == 0) {
            if ($arg =~ /^(\{+\d+[a-zA-Z]\d+\}+)$/) {
                $args[0] = $1;
                next;
            }
        }
        UDie("Bad content in argument: '$arg'")
            unless $arg =~ m!^\s*(:?[-=a-zA-Z0-9_:./]*)\s*$!;
        $args[$i] = $1;
    }
    return @args;
}

sub TryToPromoteInstalledPubKey {
    my $handle = shift;
    my $ehandle = EscapeHandle($handle);
    my $dir = TryToFindResPath("public_keys");
    return unless defined($dir);

    my $pub = "$dir/$ehandle.pub";
    if (-r $pub) {
        my $localpub = GetPublicKeyFile($handle);
        IDie("Inconsistent") if -r $localpub; # shouldn't have gotten here
        WriteWholeFile($localpub, ReadWholeFile($pub));
        print "Copying public key for '$handle' from $pub\n";
    }
    return CheckForPubKey($handle);
}

sub SavePubKey {
    my ($handle, $pubkey) = @_;
    my $pubkeypath = GetPublicKeyFile($handle);
    my $data = JoinHandleToKey($handle,$pubkey);
    WriteWholeFile($pubkeypath, $data);
    return $pubkeypath;
}

sub ValidatePubKey {
    my ($handle, $pubstring) = @_;
    my ($path, $knownpub) = CheckForPubKey($handle);
    if (!defined($knownpub)) {
        ($path, $knownpub) = TryToPromoteInstalledPubKey($handle);
    }
    if (defined($knownpub)) {
        chomp($knownpub);  # Try to normalize last line
        $knownpub .= "\n"; # ending to what we think we expect
        if ($pubstring ne $knownpub) {
            print "\nERROR: '$handle' is known (found in '$path') but supplied public key doesn't match!($knownpub:$pubstring)\n";
            exit 3;
        }
    } else {
        my $decided = 0;
        while (!$decided) {
            print <<EOM;
WARNING!
WARNING: $progname signer handle '$handle' is not recognized!
WARNING: Running untrusted code is dangerous!
WARNING: You have three choices here:
WARNING: Q - Quit running this program now (default, recommended)
WARNING: R - Run the program anyway (caution!)
WARNING: S - Save this handle as trusted and run the program now (caution!)
WARNING!
EOM
            print "What do you want to do? [Qrs] ";
            my $answer = <>;
            $answer = "" unless defined $answer;  # Don't warn on eof
            chomp($answer);
            $answer = 'q' if ($answer eq "");
            $answer = lc($answer);
            if ($answer eq 'q') {
                print "Quit\n";
                exit 1;
            }
            if ($answer eq 's') {
                my $path = SavePubKey($handle, $pubstring);
                print "Saved handle to $path\n";
            }
            if ($answer eq 's' || $answer eq 'r') {
                $decided = 1;
            } else {
                print "Unrecognized reply; please choose 'q', 'r', or 's'\n";
            }
        }
    }
}

sub TryToFindResPath {
    my $suffix = shift || "";
    my $path;
    return $path if -r ($path = "$binDir/../res/$suffix");
    return $path if -r ($path = "/usr/lib/ulam/MFM/res/$suffix");
    return $path if -r ($path = "./res/$suffix");
    return undef;
}

# Find and parse the demos.dat file
sub ListDemosDat {
    my $path = TryToFindResPath("elements/demos.dat");
    return 0 unless defined $path;
    my $count = 0;
    print "  Built-in demos:\n";
    open(DDAT,"<$path") or die "Can't read '$path': $!";
    {
        LOOP:
        while (1) {
            my ($name, $mfz, $so, $classes, $reserved);
            {
                local $/ = "\0";
                $name = <DDAT>; 
                last LOOP unless defined $name;
                chomp $name; 
                $mfz = <DDAT>; $so = <DDAT>; $classes = <DDAT>; $reserved = <DDAT>;
                chomp $mfz; chomp $so; chomp $classes; chomp $reserved;
            }
            my $unused = <DDAT>; # \n delimited
            print "   $0 $name demo\n";
            ++$count;
        }
    }
    close DDAT or die $!;
    if ($count == 0) {
        print "  --No demos found--\n";
    } else {
    }
    return $count;
}

# Search for demos in a variety of locations
sub FindDemoOrDie {
    my $demoname = shift;
    # be tight about demo names, they turn into file names
    if ($demoname =~ /[^-A-Za-z0-9]/) {
        UDie("Illegal character '$&' in demo name");
    }
    $demoname .= ".mfz";
    my $path;
    return $path if -r ($path = "~/.mfm/demos/$demoname");
    return $path if $path = TryToFindResPath("elements/demos/$demoname");

    UDie("No '$demoname' found");
}
