#!/usr/bin/perl -w
# (C) 2003-2007 Willem Jan Hengeveld <itsme@xs4all.nl>
# Web: http://www.xs4all.nl/~itsme/
#      http://wiki.xda-developers.com/
#
# $Id$
#
use strict;
use IO::File;
use Getopt::Long qw(:config no_ignore_case);

my $filebaseofs= 0;
my $g_test= 0;
my @patches;

my %datasize= (
    V=>4,
    N=>4,
    v=>2,
    n=>2,
    C=>1,
);
sub usage {
    return <<__EOF__
Usage: hexedit [patches]  file
   -pd  ofs:dword,dword
   -pN  ofs:netdword,netdword
   -pw  ofs:word,word
   -pn  ofs:netword,netword
   -pb  ofs:byte,byte  OR ofs:bytebytebytebyte
   -ps  ofs:string
   -pu  ofs:string   stored as unicode
   -pf  ofs-ofs:byte   fill inclusive range.
   -pl  ofs:file:seek:len
   -t   test only

   all numbers hexdigits
__EOF__
}
GetOptions(
    "b=s"=> sub { $filebaseofs= eval($_[1]); },
    "pd=s"=> sub { push @patches, ParseData($_[1], "V"); },
    "pN=s"=> sub { push @patches, ParseData($_[1], "N"); },
    "pw=s"=> sub { push @patches, ParseData($_[1], "v"); },
    "pn=s"=> sub { push @patches, ParseData($_[1], "n"); },
    "pb=s"=> sub { push @patches, ParseData($_[1], "C"); },
    "ps=s"=> sub { push @patches, ParseStringData($_[1]); },
    "pu=s"=> sub { push @patches, ParseWStringData($_[1]); },
    "pf=s"=> sub { push @patches, ParseFill($_[1]); },
    "pl=s"=> sub { push @patches, ParseFileData($_[1]); },
    "t"  => \$g_test,
) or die usage();

sub ParseData {
    my ($ext, $datatype)= @_;
    
    if ($ext =~ /^(\w+):(.*)/) {
        my $ofs= hex($1);
        my $ascdata= $2;
        if ($datatype eq "C" && $ascdata =~ /^\w+$/ && length($ascdata)>2) {
            # right align odd length data
            $ascdata="0$ascdata" if length($ascdata)&1;
            return {
                offset=>$ofs,
                data=>pack("H*", $ascdata)
            };
        }
        my @data= map { hex($_) } split /,/, $ascdata;

        return {
            offset=>$ofs,
            data=>pack($datatype."*", @data),
        };
    }
    die "Invalid data edit format $ext\n";
}
sub ParseFill {
    my ($ext)= @_;
    if ($ext =~ /^(\w+)-(\w+):(\w+)/) {
        my ($sofs, $eofs, $val)= (hex($1), hex($2), hex($3));

        return {
            offset=>$sofs,
            data => chr($val) x ($eofs-$sofs+1),
        };
    }
    die "invalid data fill spec: $ext\n";
}
sub ParseStringData {
    my ($ext)= @_;
    
    if ($ext =~ /^(\w+):(.*)/) {
        my $ofs= hex($1);
        my $data= eval("\"$2\"");

        return {
            offset=>$ofs,
            data=>$data,
        };
    }
    die "Invalid data edit format $ext\n";
}
# todo: this does not convert utf8 from the commandline properly
sub ParseWStringData {
    my ($ext)= @_;
    
    if ($ext =~ /^(\w+):(.*)/) {
        my $ofs= hex($1);
        my $data= pack("v*", unpack("C*", eval("\"$2\"")));

        return {
            offset=>$ofs,
            data=>$data,
        };
    }
    die "Invalid data edit format $ext\n";
}
sub ParseFileData {
    my ($ext)= @_;
    
    if ($ext =~ /^(\w+):(.+?)(?::(\w+)(?::(\w+))?)?$/) {
        my $romofs= hex($1);
        my $filename= $2;
        my $fileofs= defined $3 ? hex($3) : 0;
        my $filelen= defined $4 ? hex($4) : (-s $filename)-$fileofs;

        my $fh=IO::File->new($filename, "r") or die "$filename: $!\n";
        binmode $fh;

        $fh->seek($fileofs, SEEK_SET);

        my $data;
        $fh->read($data, $filelen);
        $fh->close();
        if (length($data) != $filelen) {
            die "error reading $filelen bytes from $filename\n";
        }

        return {
            offset=>$romofs,
            data=>$data,
        };
    }
    die "Invalid data edit format $ext\n";
}

my $filename = shift || die usage();

my $fh=IO::File->new($filename, "r+") or die "$filename: $!\n";
binmode $fh;

for (sort { $a->{offset} <=> $b->{offset} } @patches) {
    $fh->seek($_->{offset}-$filebaseofs, SEEK_SET) or warn "$!";

    if ($g_test) {
        my $data;
        $fh->read($data, length($_->{data})) or warn "$!";
        print unpack("H*", $data), "\n";
    }
    else {
        $fh->write($_->{data}) or warn "$!";
    }
}
$fh->seek(0, SEEK_END) or warn "$!";
$fh->close();

