#!/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 [-o ]\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); }