#!/usr/bin/perl -w
# gc-shrink.pl by ackmed@gotwalls.com
# re-writes a gc iso to save space.
use strict;
use Getopt::Std;
use vars qw($opt_i $opt_o $opt_h);

use constant	VERSION		=> "0.1";
use constant 	AUTHOR		=> "ackmed\@gotwalls.com";

use constant	TRUE		=> 1;
use constant	FALSE		=> 0;
use constant	U32		=> 4;

use constant	FST_POINTER	=> 0x424;
use constant	FST_ISDIR	=> 0x1000000;


main();

sub main {
	
	getopts("hi:o:");
	print "gc-shrink " . VERSION . " by " . AUTHOR . ".\n";
	print "-------------------------------------\n\n";
	
	if(defined($opt_h) || !defined($opt_i)) {
		print "Usage: gc-shrink.pl -i <gc.iso> [-o <out.iso>]\n";
		exit;
	}

	my $fst = GetFST($opt_i);
	
	# real error will be printed in GetFST();
	if(!defined($fst)) {
		exit;
	}

	OptimizeFST($fst);

	my $cursize = (stat($opt_i))[7];
	my $lentry = ${$fst->{OPT_ROOT}}[$fst->{ENTRIES} - 1];
	my $newsize = ru32($lentry->{DISK_ADDRESS} + $lentry->{SIZE});

	printf("Input: $opt_i\n");
	printf("Current Size: %.1f Megs.\n", $cursize / (1024*1024));
	printf("Estimated Shrunk Size: %.1f Megs.\n", $newsize / (1024*1024));

	if(defined($opt_o)) {
	
		if($opt_i eq $opt_o) {
			print "Error: infile and outfile cannot be the same.\n";
			exit;
		}

		printf("\nOutput: $opt_o\n");
		WriteOptimizedISO($opt_i, $opt_o, $fst);
	}

	print "\n";
}

sub WriteOptimizedISO {
	my ($infile, $outfile, $fst) = @_;

	unless(open(INFILE, $infile)) {
		print "WriteOptimizedISO(): unable to open infile '$infile'.\n";
		return FALSE;
	}

	unless(open(OUTFILE, ">$outfile")) {
		print "WriteOptimizedISO(): unable to open outfile '$outfile'.\n";
		return FALSE;
	}

	print "Copying: ISO Header, Apploader, DOL, and old FST header\n";
	## copy over everything upto and including the old FST header.
	CopyBytes(\*INFILE, \*OUTFILE, $fst->{HEADER_OFFSET} + $fst->{HEADER_SIZE});

	## rewind fd to beginning of FST header + 4 bytes
	seek(OUTFILE, $fst->{HEADER_OFFSET} + U32, 0);
	
	print "Rewriting: Optimized FST File locations.\n";
	# update the FST with out opt one
	for my $entry (@{$fst->{OPT_ROOT}}) {
		# dir, dont need to change anything, skip ahead 12 bytes
		if($entry->{ISDIR}) {
			seek(OUTFILE, 3 * U32, 1);
		} else {
			my $diskaddr = pack("N", $entry->{DISK_ADDRESS});
			print OUTFILE $diskaddr;
			seek(OUTFILE, 2 * U32, 1);
		}

	}

	# now start copying files
	my $count = 0;
	while($count < $fst->{ENTRIES}) {

		# only care about files
		if(!(${$fst->{ROOT}}[$count]->{ISDIR})) {
			
			# seek to the proper location in each image
			seek(INFILE, ${$fst->{ROOT}}[$count]->{DISK_ADDRESS}, 0);
			seek(OUTFILE, ${$fst->{OPT_ROOT}}[$count]->{DISK_ADDRESS}, 0);
			
			print "Copying: ${$fst->{ROOT}}[$count]->{FILENAME}\n";
			CopyBytes(\*INFILE, \*OUTFILE, ${$fst->{ROOT}}[$count]->{SIZE});
		}
		$count++;

	}

	print "Done.\n";
	close(INFILE);
	close(OUTFILE);
}

# copy $size bytes from $src to $dst fd's
sub CopyBytes {
	my ($src, $dst, $size) = @_;

	my $bytes_left = $size;
	while($bytes_left > 0) {
		my $buf;
		my $bufsize = 2048;

		if($bytes_left < $bufsize) {
			$bufsize = $bytes_left;
		}

		read($src, $buf, $bufsize);
		print $dst $buf;

		$bytes_left -= $bufsize;
	}
}

# make an optimize FST header and store in OPT_ROOT
sub OptimizeFST {
	my ($fst) = @_;

	delete($fst->{OPT_ROOT});

	my $offset = ru32($fst->{HEADER_OFFSET} + $fst->{HEADER_SIZE} + 1);

	my $count = 0;

	for my $entry (@{$fst->{ROOT}}) {
	
		my $opt_entry;

		$opt_entry->{ISDIR} = $entry->{ISDIR};
		$opt_entry->{NAME_OFFSET} = $entry->{NAME_OFFSET};
		$opt_entry->{ENTRY_NUMBER} = $entry->{ENTRY_NUMBER};
		$opt_entry->{FILENAME} = $entry->{FILENAME};

		if(!($entry->{ISDIR})) {

			$opt_entry->{SIZE} = $entry->{SIZE};

			$opt_entry->{DISK_ADDRESS} = $offset;
			$offset = ru32($offset + $entry->{SIZE} + 1);

		} else {
			$opt_entry->{END_ENTRY} = $entry->{END_ENTRY};
		}

		push @{$fst->{OPT_ROOT}}, $opt_entry;

	}

	return $fst;
}

sub GetFST {
	my ($file) = @_;

	my $fst;

	unless(open(FILE, $file)) {
		print "GetFST(): error opening file '$file'\n";
		return undef;
	}

	# seek to the location that has pointer to fst header offset
	if(!seek(FILE, FST_POINTER, 0)) {
		print "GetFST(): unable to seek() to FST_POINTER\n";
		return undef;
	}
	
	# location of the fst header within the disc image
	if(read(FILE, $fst->{HEADER_OFFSET}, U32) != U32) {
		print "GetFST(): unable to read FST offset\n";
		return undef;
	}
	$fst->{HEADER_OFFSET} = unpack("N", $fst->{HEADER_OFFSET});

	# size of the fst header is bytes
	if(read(FILE, $fst->{HEADER_SIZE}, U32) != U32) {
		print "GetFST(): unable to read FST size\n";
		return undef;
	}
	$fst->{HEADER_SIZE} = unpack("N", $fst->{HEADER_SIZE});
	
	# see the the fst header
	if(!seek(FILE, $fst->{HEADER_OFFSET}, 0)) {
		print "GetFST(): unable to see() to fst header.\n";
		return undef;
	}

	# first entry, the root dir, has the number of total files/dirs
	my ($r_isdir, $r_nameoffset, $r_diskaddr, $r_size) = GetFSTEntry(\*FILE);

	# error within GetFSTEntry(), just return, it would have printed an error.
	if(!defined($r_isdir)) {
		return undef;
	}

	# total entries including the root dir
	$fst->{ENTRIES} = $r_size;

	# if root entry is not a dir then something is wrong.
	if(!($r_isdir)) {
		print "GetFST(): root entry is not a dir.\n";
		return undef;
	}

	# save root entry
	my $r_entry;
	$r_entry->{ISDIR} = $r_isdir;
	$r_entry->{END_ENTRY} = $r_size;
	$r_entry->{ENTRY_NUMBER} = 1;
	push @{$fst->{ROOT}}, $r_entry;

	my $count = 2;
	while($count <= $fst->{ENTRIES}) {
		my ($isdir, $nameoffset, $diskaddr, $size) = GetFSTEntry(\*FILE);

		# error within GetFSTEntry(), just return, it would have printed an error.
		if(!defined($isdir)) {
			return undef;
		}
		
		# create new entry
		my $entry;
		$entry->{ISDIR} = $isdir;
		$entry->{NAME_OFFSET} = $nameoffset;
		$entry->{ENTRY_NUMBER} = $count;

		if($isdir) {
			$entry->{END_ENTRY} = $size;
		} else {
			$entry->{DISK_ADDRESS} = $diskaddr;
			$entry->{SIZE} = $size;
		}

		# save
		push @{$fst->{ROOT}}, $entry;

		$count++;
	}

	# now we should be at the start of the name offsets;
	$fst->{NAME_OFFSET_START} = tell(FILE);
	
	$count = 1;  # skip the root entry
	while($count < $fst->{ENTRIES}) {
		my $filename = GetFSTEntryName(\*FILE, $fst->{NAME_OFFSET_START},${$fst->{ROOT}}[$count]->{NAME_OFFSET});
		${$fst->{ROOT}}[$count]->{FILENAME} = $filename;
		$count++;
	}

	close(FILE);
	return $fst;
}

# gets file name
sub GetFSTEntryName {
	my ($fd, $start, $offset) = @_;

	
	if(!seek($fd, $start + $offset, 0)) {
		return undef;
	}
	
	my ($name, $char);
	while(read($fd, $char, 1) && $char ne "\0") {
		$name .= $char;
	}

	return $name;
}

# gets the next fst entry from the passed fd
# returns ($isdir, $nameoffset, $diskaddr, $size);
sub GetFSTEntry {
	my ($fd) = @_;

	my $isdir = FALSE;
	my ($nameoffset, $diskaddr, $size);
	
	if(read($fd, $nameoffset, U32) != U32) {
		print "GetFSTEntry(): unable to read nameoffset.\n";
		return undef;
	}
	$nameoffset = unpack("N", $nameoffset);

	if($nameoffset & FST_ISDIR) {
		$isdir = TRUE;
		$nameoffset ^= FST_ISDIR;
	}

	if(read($fd, $diskaddr, U32) != U32) {
		print "GetFSTEntry(): unable to read diskaddr.\n";
		return undef;
	}
	$diskaddr = unpack("N", $diskaddr);

	if(read($fd, $size, U32) != U32) {
		print "GetFSTEntry(): unable to read size.\n";
		return undef;
	}
	$size = unpack("N", $size);

	return ($isdir, $nameoffset, $diskaddr, $size);
}

# round up the passed number to the next 32 bytes
sub ru32 {
	return ($_[0] + 32 - 1) & ~(32 - 1);
}
