#!/usr/bin/env perl
# $Source: /var/lib/cvs/ghutils/ungob.pl,v $
# $Id: ungob.pl,v 1.2.1.2 2008-09-01 12:05:58 tarragon Exp $

use strict;
use warnings;
use File::DosGlob 'glob';
use Getopt::Std;
use File::Basename;
use File::Spec;
use File::Path qw( mkpath );
use IO::File;
use IO::Uncompress::AnyInflate qw(anyinflate $AnyInflateError) ;
use String::CRC32;

my $MYNAME = basename($0);
my $VERSION = do { my @r=(q$Revision: 1.2.1.2 $=~/\d+/g); sprintf '%d.'.'%d'x$#r,@r };

sub usage {
	print <<EOF;
$MYNAME v$VERSION
Written by tma, 2008.

This script will extract the files from a a gfc/gob file pair.
It was written to extract the files from the Nintendo DS game,
Guitar Hero: On Tour

Usage: $MYNAME -h <gfc file> -b <gob file> [-a arm9 file] [-d <output directory>]

  -h : the header file to be scanned (required)
  -b : the binary data file to be extracted (required)
  -a : the arm9 file to scan for strings (optional)
  -d : directory to extract files to (defaults to current directory)
  -v : verbose output

EOF

exit;
}

our $opt_h = ''; # header file
our $opt_b = ''; # data file
our $opt_a = ''; # arm9 file
our $opt_d = '.'; # output directory
our $opt_v = 0 ; # to verbose, or not to verbose
getopts('h:b:a:d:v');
usage if $opt_h eq '' ;
usage if $opt_b eq '' ;

# check output dir before we go any further
-d $opt_d || die "Output directory does not exist.\n";

my $filenameCRC_href;
if ( defined $opt_a && -f $opt_a ) {
    &scanARM9;
}

# define some prefixes and suffixes to brute force some filenames
my @prefixes = (
    '',
    'tut3',
    'tut4',
    'tut5',
    'tut6',
    'tut7',
    'tut1',
    'tut2a',
    'tut2c',
    'DoWhatYouWant',
    'AllTheSmallThings',
    'Spiderwebs',
    'AreYouGonnaBeMyGirl',
    'WereNotGonnaTakeIt',
    'AllStar',
    'Breed',
    'JessesGirl',
    'HitMeWithYourBestShot',
    'ThisLove',
    'Heaven',
    'Helicopter',
    'ChinaGrove',
    'RockAndRollAllNite',
    'WhatIWant',
    'JetAirliner',
    'BlackMagicWoman',
    'StrayCatStrut',
    'LaGrange',
    'YouthGoneWild',
    'IDontWannaStop',
    'AnnaMolly',
    'KnockMeDown',
    'PrideNJoy',
    'IKnowALittle',
    'IAmNotYourGameboy',
    'TracksUS',
    'Avalancha',
    'CaMeVexe',
    'Monsoon',
    'Monster',
    'RockTheNight',
);

my @roots = (
    '',
    'camera',
    'lightshow',
    'gems_',
    'gems_med',
    'gems_hard',
    'gems_easy',
    'gems_expert',
    'gems_bass_med',
    'gems_bass_easy',
    'gems_bass_hard',
    'gems_bass_expert',
    'frets',
    'timesig',
    'SMK',
    'English',
    'French',
    'Easy_Star',
    'Easy_StarBattleMode',
    'English',
    'Expert_Star',
    'Expert_StarBattleMode',
    'FaceOffP1',
    'FaceOffP2',
    'French',
    'German',
    'Hard_Star',
    'Hard_StarBattleMode',
    'Italian',
    'Medium_Star',
    'Medium_StarBattleMode',
    'Spanish',
    'anims.qb',
    'cameras_notes',
    'fretbars',
    'lightshow_notes',
    'rhythm_Easy_Star',
    'rhythm_Easy_StarBattleMode',
    'rhythm_Expert_Star',
    'rhythm_Expert_StarBattleMode',
    'rhythm_Hard_Star',
    'rhythm_Hard_StarBattleMode',
    'rhythm_Medium_Star',
    'rhythm_Medium_StarBattleMode',
    'song_Easy',
    'song_Expert',
    'song_Hard',
    'song_Medium',
    'song_rhythm_Easy',
    'song_rhythm_Expert',
    'song_rhythm_Hard',
    'song_rhythm_Medium',
    'timesig',
    'bass_easy',
    'bass_expert',
    'bass_hard',
    'bass_med',
    'bat_Finish_select',
    'bat_Guitar_select',
    'bat_char_select',
    'bat_chooseoutfit',
    'bat_difficulty_sel',
    'bat_finish_select',
    'bat_guitar_select',
    'bat_results_menu',
    'bat_retry_continue_menu',
    'bat_song_select',
    'bat_venue_select',
    'car_Finish_select',
    'car_Guitar_select',
    'car_VenueSelect',
    'car_char_select',
    'car_diff_select',
    'car_retry_continue_menu',
    'car_song_select',
    'character_select',
    'completion_menu',
    'difficulty_select',
    'earnings_menu',
    'easy',
    'expert',
    'fail_menu',
    'file',
    'filename',
    'focus',
    'focus_ua',
    'hard',
    'highscore_edit_menu',
    'highscore_view_menu',
    'idle',
    'instrument_menu',
    'intro',
    'intro_logos',
    'item',
    'left',
    'left_ua',
    'letter_cd_menu',
    'letter_manager_menu',
    'letter_venue_menu',
    'licenseScreens',
    'localize',
    'locked',
    'loopend',
    'looping',
    'loopstart',
    'losefocus',
    'losefocus_ua',
    'mData',
    'medium',
    'menu',
    'miss_note',
    'movie_credits_from_endgame',
    'movie_credits_from_options',
    'movie_license',
    'multimode',
    'muxError',
    'mux_char_select',
    'mux_diff_select',
    'mux_finish_select',
    'mux_guitar_select',
    'mux_outfit_select',
    'mux_song_select',
    'mux_venue_select',
    'name',
    'next',
    'none',
    'outro',
    'param0',
    'param1',
    'placeholder',
    'playall',
    'prac_diff_select',
    'prac_song_select',
    'practice',
    'practice_menu',
    'previous',
    'previous career menu',
    'priority',
    'random',
    'restart',
    'resume',
    'retry_continue_menu',
    'right',
    'right_ua',
    'self',
    'small',
    'song_select',
    'sound',
    'specifically',
    'specified',
    'speed',
    'start',
    'start_ua',
    'stereo',
    'touch',
    'touch_ua',
    'touchactive',
    'touchactive_ua',
    'tutorial_menu',
    'tutorial_play_menu',
    'tutorial_play_nb',
    'tutorial_prompt_menu',
    'unlocked_menu',
    'untitled',
    'up_ua',
    'us_ESRBText',
    'us_HeadphonesBottom',
    'us_HelperLogos',
    'us_HelperText',
    'us_LicenseText',
    'value',
    'volume',
    'vorbis',
    'x_ua',
    'y_ua',
);
my @suffixes = (
    '',
    '.q',
    '.qcm',
    '.qls',
    '.qgm',
    '.qft',
    '.qsig',
    '.xml',
    '.layertiles',
    '.gcb',
    '.meta',
    '.mps',
    '.pal',
    '.bin',
    '.prp',
    '.gob',
    '.gfc',
    '.gxt',
    '.SAC',
    '.ogg',
    '.hwas',
    '.lights',
    '.refs',
);
&buildBruteForceNames( \@prefixes, \@roots, \@suffixes );

# process header file
open HEADFILE, $opt_h or die "Could not open $opt_h! $!\n";
binmode HEADFILE;

# sanity check gfc file
my $magicBytes = &getFromHead_u32;
unless ( $magicBytes == 0x00008008 ) {
    die "$opt_h does not appear to be a gfc file.\n";
}

# sanity check gob file
my $binfileLength = &getFromHead_u32;
my @binfileStat = stat $opt_b;
unless ( $binfileLength == $binfileStat[7] ) {
    die "$opt_b file size does not match gfc file.\n";
}

my $chunkCount = &getFromHead_u32;
my $fileCount = &getFromHead_u32;

# read the chunk table
#  4 x u32 per chunk
#  1 = chunk size
#  2 = offset in binary file
#  3 = next chunk in this file, or 0x7fff for last chunk
#  4 = file type or flags
#      first byte is 0x30 (0) for uncompressed
#      first byte is 0x7a (z) for zlib compressed
my @chunks;
for ( my $counter = 0 ; $counter < $chunkCount ; $counter++ ) {
    push @chunks, {
        'size' => &getFromHead_u32,
        'offset' => &getFromHead_u32,
        'next' => &getFromHead_u32,
        'flags' => &getFromHead_u32,
    };
}

# read the <insert table name here> table
#  1 x u32 per chunk
#  1 = unknown
for ( my $counter = 0 ; $counter < $chunkCount ; $counter++ ) {
    $chunks[$counter]{'unknown'} = &getFromHead_u32;
}

# read the filename index table
#  3 x u32 per file
#  1 = crc32 hash of full filename in lower case, with leading .\
#  2 = file size
#  3 = first chunk of file
my @files;
for ( my $counter = 0 ; $counter < $fileCount ; $counter++ ) {
    push @files, {
        'filename_hash' => &getFromHead_u32,
        'size' => &getFromHead_u32,
        'start' => &getFromHead_u32,
    }
}

close HEADFILE;

# extract the files from the binary
my $BINFILE = new IO::File "<$opt_b" or die "Could not open $opt_b! $!\n";
binmode $BINFILE;

my $filecount = 0;
foreach my $href ( @files ) {
    $filecount++;
    my $filename;
    my $crc32 = sprintf '%08x', $href->{'filename_hash'};
    if ( defined $filenameCRC_href->{$crc32} ) {
        # set filename from arm9 here
        $filename = $filenameCRC_href->{$crc32};
    }
    else {
        # set filename based on hash
        $filename = '.\\unknown\\' . $crc32;
    }

    my @filename_parts = split /\\/, $filename;
    my $basename_actual = pop @filename_parts;
    my $dirname_actual = File::Spec->catfile( $opt_d, @filename_parts );
    my $filename_actual = File::Spec->catfile( $dirname_actual, $basename_actual );
    mkpath $dirname_actual;

    my $filesize = $href->{'size'};
    my $filechunk = $href->{'start'};
    my $filetype = '0';

    printf 'extracting file %05d/%05d (0x%s) as %s' . "\n", $filecount, scalar @files, $crc32, $filename_actual;
    open OUTFILE, ">$filename_actual" or die "could not open output file $filename_actual: $!\n";
    binmode OUTFILE;
    while (1) {
        $opt_v and printf ' chunk %08x hash: %08x' . "\n", $filechunk, $chunks[$filechunk]->{'unknown'};
        seek $BINFILE, $chunks[$filechunk]->{'offset'}, 0;

        my $chunktype = $chunks[$filechunk]->{'flags'};
        if ( $chunktype == 0x7a000000 ) {
            # chunk is compressed
            $opt_v and printf " decompressing chunk %08x\n", $filechunk;

            my $PASSTHRU = new IO::Uncompress::AnyInflate $BINFILE,
                Transparent => 1,
                InputLength => $chunks[$filechunk]->{'size'}
            or die "Cannot uncompress chunk: $AnyInflateError\n";

            while (<$PASSTHRU>) {
                print OUTFILE;
            }
        }
        else {
            my $bytes;
            if ( ( read $BINFILE, $bytes, $chunks[$filechunk]->{'size'} ) < $chunks[$filechunk]->{'size'} ) {
                die "unexpected end of file\n";
            }
            print OUTFILE $bytes;
            undef $bytes; # make sure the memory is released
        }
        
        if ( $chunks[$filechunk]->{'next'} == 0x7fff ) {
            last;
        }
        $filechunk = $chunks[$filechunk]->{'next'};
    }
    close OUTFILE;

    my @filenameStat = stat $filename_actual;
    unless ( $filenameStat[7] == $filesize ) {
        print STDERR "$filename size is not correct after extraction: $filenameStat[7] (expected $filesize)\n";
    }
}

$BINFILE->close;
print "Done!\n";
exit;

sub getFromHead_u32 {
    my $bytes;
    if ( ( read HEADFILE, $bytes, 4 ) < 4 ) {
        die "Premature end of header file.\n";
    }
    return unpack "N*", $bytes;
}

sub crc32string {
    my $string = lc shift;
    return sprintf '%08x', crc32 $string;
}

sub scanARM9 {
    my ( $byte );
    open ARM, $opt_a or die "Couldn't open $opt_a: $!\n";
    binmode ARM;
    while ( read ARM, $byte, 1 ) {
        if ( ord $byte == 0x2e ) { # look for '.'
            unless ( read ARM, $byte, 1 ) {
                last;
            }
            if ( ord $byte == 0x5c ) { # look for '\'
                # looks like we may have a filename. read till the next zero byte
                my $filename = '.\\';
                while ( read ARM, $byte, 1 ) {
                    if ( ord $byte == 0x00 ) {
                        $filenameCRC_href->{ crc32string( $filename ) } = $filename;
                        last;
                    }
                    $filename .= $byte;
                }
            }
            else {
                # second byte wasn't a '\' so backtrack
                seek ARM, -1, 1;
            }
        }
    }
}

sub buildBruteForceNames {
    my $prefix_aref = shift;
    my $prefixdupe_aref = $prefix_aref;
    my $root_aref = shift;
    my $suffix_aref = shift;
    #print "Prefixes to process: " . scalar @$prefix_aref . "\n";
    foreach my $prefix ( @$prefix_aref ) {
        #foreach my $prefixdupe ( @$prefixdupe_aref ) {
            my $prefixdupe = '';
            foreach my $root ( @$root_aref ) {
                foreach my $suffix ( @$suffix_aref ) {
                    my $string = $prefix . $prefixdupe . $root . $suffix;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = '.\\' . $string;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = $prefix . '_' . $prefixdupe . $root . $suffix;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = '.\\' . $string;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = $prefix . '.' . $prefixdupe . $root . $suffix;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = '.\\' . $string;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = $prefix . '\\' . $prefixdupe . $root . $suffix;
                    $filenameCRC_href->{ crc32string $string } = $string;
                    $string = '.\\' . $string;
                    $filenameCRC_href->{ crc32string $string } = $string;
                }
            }
        #}
        #print "done: $prefix\n";
    }

    #foreach ( keys %$filenameCRC_href ) {
    #    print $filenameCRC_href->{ $_ } . "\n";
    #}
    #exit;
}

