#!/usr/bin/perl -w

# decompile the resource fork of a CodeWarrior .rsrc file
# Waider 1999/2000

$| = 1;

# Pilot resources -------------------------------------------------------------
package Pilot::Resource;
# In theory this is a private function
# Also it's incomplete.
sub unmacify {
  my $string = shift;
  $string =~ s|\xda|/|g;
  $string =~ s|\r|\\n|g;

  $string;
}

sub new {
  my $obj = shift;

  my $res = bless {}, $obj;

  return $res;
}

sub parent {
  my $obj = shift;
  my $newval = shift;
  $newval ? $obj->{'parent'} = $newval : $obj->{'parent'};
}

sub id {
  my $obj = shift;
  my $id = main::findname( $obj->{'type'}, $obj->{'id'}, $obj->{'parent'});
  $id ||= $obj->{'id'};
}

sub type {
  my $obj = shift;
  my $newval = shift;
  $newval ? $obj->{'type'} = $newval : $obj->{'type'};
}

# dump a resource based on the data it holds.
# should do this via subclassing. FIXME.
sub dump {
  my $obj = shift;
  my $indent = shift;

  $indent ||= "";

  # We need to know the object's type.
  my $type = $obj->{'type'};
  return undef unless defined( $type );

  my $id = $obj->id;
  return undef unless defined( $id );

  my $data = $obj->{'data'};
  return undef unless defined( $data );

  # Application Icon Name. Complete.
  if ( $type eq "tAIN" ) {
	print "applicationiconname id ", $obj->id;
	# Data is just a null-terminated string
	#chop( $data ); # lose the null
	$data =~ s/^(.*?)\x00.*$/$1/;  # hrm.
	$data = unmacify( $data );
	print " \"$data\"\n";
	$data = "";

	# Version tag. Complete.
  } elsif ( $type eq "tver" ) {
	print "version id ", $obj->id;
	chop( $data );
	print " \"$data\"\n";
	$data = "";

	# Alert. Mostly complete.
  } elsif ( $type eq "Talt" ) {
	print "alert id ", $obj->id, "\n";
	my ( $icon, $helpid, $defbut, $what ) =
	  unpack( "nnnn", substr( $data, 0, 8 ));
	printf "/* ?? %04x ?? */\n", $what; # I have no idea.
	my ( $title, $message, @buttons ) = split( "\0", substr( $data, 8 ));
	$title = unmacify( $title );
	$message = unmacify( $message );
	print "helpid $helpid\n" if $helpid;
	print "defaultbutton $defbut\n" if $defbut && @buttons && $#buttons > 0;
	print "information\n" if $icon == 0;
	print "confirmation\n" if $icon == 1;
	print "warning\n" if $icon == 2;
	print "error\n" if $icon == 3;
	print "begin\n    title \"$title\"\n    message \"$message\"\n    ";
	printf "buttons \"%s\"\n", join( '" "', @buttons );
	print "end\n";
	$data = "";

	# Form. Incomplete.
  } elsif ( $type eq "tFRM" ) {
	print 'form ';
	print 'id ', $obj->id, ' ';
	printf "at ( %d %d %d %d )\n", unpack( "nnnn", substr( $data, 0, 8 ));
	$data = substr( $data, 8 );

	# Don't know yet what the first 10 bytes are.
    main::hexdump( substr( $data, 0, 10 ), $indent );

	my ( $formid, $helpid, $menuid, $btnid ) =
	  unpack( "nnnn", substr( $data, 10, 8 ));

	# formid is already printed
	# FIXME may need parents for these
	printf "helpid %s\n", $helpid if $helpid;
	printf "menuid %s\n", main::findname( "MBAR", $menuid ) if $menuid;
	printf "defaultbtnid %s\n", main::findname( "tBTN", $btnid ) if $btnid;

	# unknown
	printf "/* ?? %04x */\n", unpack( "n", substr( $data, 18, 2 ));

	print "begin\n";
	$indent .= "  ";
	for my $parts ( 1 .. unpack( "N", substr( $data, 20, 4 ))) {
	  my ( $id, $type ) = 
		unpack( "nA4", substr( $data, 18 + ( 6 * $parts ), 6 ));
	  $main::resources{ "$id-$type" }->parent( $obj );
	  $main::resources{ "$id-$type" }->dump( $indent );
	}
	$indent =~ s/..$//;
	print "end\n";
	$data = ""; # See above for unknowns.

  } elsif ( $type eq "tSTR" ) {
	print "string id ", $obj->id;
	chop( $data );
	print " \"$data\"\n";
	$data = "";

  } elsif ( $type eq "tBTN" ) {
	print $indent, 'button "';
	# Label is at the end of the table
	print unmacify( substr( $data, 19, -1 ));
	$data = substr( $data, 0, 19 );
	print '" id ', $obj->id, " ";
	printf "at ( %d %d %d %d ) ", unpack( "nnnn", substr( $data, 2, 8 ));
	print "nonusable " unless unpack( "c", substr( $data, 10, 1 ));
	$data = substr( $data, 11 );
	my $f = substr( $data, -1 );
	printf "font %d", ord( $f ) if ord( $f );
	$data = substr( $data, 0, 6 );
	print "\n";
	# 6 bytes we don't know what to do with.

  } elsif ( $type eq "tCBX" ) {
	print $indent,'checkbox ', $obj->id, "\n";
    print "Data: ", length( $data ), " bytes\n";

	# Form Title. Complete.
  } elsif ( $type eq "tTTL" ) {
	print 'title ';
	chop( $data );
	print "\"$data\"\n";
	$data = "";

  } elsif ( $type eq "tPUT" ) {
	print $indent, 'popuptrigger "';

	# We've got four bytes at the end that I don't know what to do
	# with for now, so:
	chop( $data ); # lose trailing slash
	if ( substr( $data, 15, 1 ) eq "\0" ) {
	  print "";
	} else {
	  print substr( $data, 15, 1 );
	}

	print '" id ', $obj->id, ' ';
	printf "at ( %d %d %d %d )", unpack( "nnnn", substr( $data, 2, 8 ));
	print " ";
	$data = substr( $data, 10 );
	printf "%s", unpack( "c", substr( $data, 0, 1 ))?"":"nonusable ";
	printf "%s", unpack( "c", substr( $data, 1, 1 ))?"disabled ":"";
	printf "%s",
	  unpack( "c", substr( $data, 2, 1 ))?"leftanchor ":"rightanchor ";
	printf "font %d\n", unpack( "n", substr( $data, 3, 2 ));

  } elsif ( $type eq "tLST" ) {
	my $out = sprintf "list \"%s\" id %d ", join( '" "', split( "\0", substr( $data, 16 ))) ,$obj->id;
	print $indent, $out;
	$data = substr( $data, 2 );
	printf "at ( %d %d %d %d ) ", unpack( "nnnn", substr( $data, 0, 8 ));
	$data = substr( $data, 8 );
	printf "%s", unpack( "c", substr( $data, 0, 1 ))?"":"nonusable ";
	printf "%s", unpack( "c", substr( $data, 1, 1 ))?"":"disabled ";
	printf "font %d\n", unpack( "n", substr( $data, 2, 2 ));

	# Label. Complete (modulo font unpacking)
  } elsif ( $type eq "tLBL" ) {
	print $indent;
	printf "label \"%s\" id %s ",
	  unmacify( substr( $data, 9, -1 )), $obj->id, ' ';
	$data = substr( $data, 0, 9 ); # skip name
	$data = substr( $data, 2 ); # skip ID
	printf "at ( %d %d ) ", unpack( "nn", substr( $data, 0, 4 ));
	$data = substr( $data, 4 );
	printf "%s", unpack( "c", substr( $data, 0, 1 ))?"":"nonusable ";
	$data = substr( $data, 1 );
	my $f = substr( $data, -1 );
	printf "font %d", ord( $f ) if ord( $f );
	$data = ""; # except for that 0xff, which I'm ignoring
	print "\n";

  } elsif ( $type eq "tPUL" ) {
	print $indent,"popuplist id ";
	printf "%d %d\n", unpack( "nn", $data );

	# Graffiti State Indicator. Complete.
  } elsif ( $type eq "tGSI" ) {
	print $indent, "graffitistateindicator at ( ";
	printf "%d, %d", unpack( "nn", $data );
	print " )\n";
	$data = "";

	# Field. Incomplete.
  } elsif ( $type eq "tFLD" ) {
	print $indent, "field id ", $obj->id, " ";
	$data = substr( $data, 2 );
	printf "at ( %d %d %d %d )\n", unpack( "nnnn", substr( $data, 0, 8 ));
	$data = substr( $data, 8 );

	printf "$ {indent}  maxchars %d", unpack( "n", substr( $data, 14, 2 ));
	printf "\n";

	# Scrollbar. Incomplete.
  } elsif ( $type eq "tSCL" ) {
	print $indent, "scrollbar id ", $obj->id, " ";
	$data = substr( $data, 2 );
	printf "at ( %d %d %d %d )\n", unpack( "nnnn", substr( $data, 0, 8 ));
	$data = substr( $data, 8 );

	# Gadget. Incomplete.
  } elsif ( $type eq "tGDT" ) {
	print $indent, "gadget id ", $obj->id, " ";
	$data = substr( $data, 2 );
	printf "at ( %d %d %d %d )", unpack( "nnnn", substr( $data, 0, 8 ));
	$data = substr( $data, 8 );
	print " nonusable" unless substr( $data, 0, 1 ) eq "\1";
	print "\n"; $data = ""; # again with the 0xff?

	# Menu Bar. Complete.
  } elsif ( $type eq "MBAR" ) {
	print $indent, "menu id ", $obj->id, "\n";
	print "begin\n";
	$indent .= "  ";
	for my $i ( 1 .. unpack( "n", substr( $data, 0, 2 ))) {
	  my $id =  unpack( "n", substr( $data, $i * 2, 2 ));
	  $main::resources{ "$id-MENU" }->parent( $obj );
	  $main::resources{ "$id-MENU" }->dump( $indent );
	}
	$indent =~ s/..$//;
	print "end\n";
	$data = "";

	# Menu. Incomplete.
  } elsif ( $type eq "MENU" ) {
	main::hexdump( $data, $indent );
	$data = substr( $data, 2 ); # discard ID
	$data = substr( $data, 12 ); # discard things we know not of
	print "$ {indent}pulldown ";
	my $title = substr( $data, 1, unpack( "C", substr( $data, 0, 1 )));
	$data = substr( $data, 1 + unpack( "C", substr( $data, 0, 1 )));
	print '"', unmacify( $title ), '"', "\n$ {indent}begin\n";
	$indent .= "  ";
	while ( $data !~ /^\x00/ ) {
	  my $entry = 
		unmacify( substr( $data, 1, unpack( "C", substr( $data, 0, 1 ))));
	  $data = substr( $data, 1 + unpack( "C", substr( $data, 0, 1 )));
	  my $shortcut =
		unmacify( substr( $data, 1, 1 ));
	  $data = substr( $data, 2 );
	  $data = substr( $data, 2 ); # unknown
	  print "$ {indent}menuitem \"$ {entry}\" id ??";
	  if ( $shortcut ne "\0" ) {
		print " \"$shortcut\"";
	  }
	  print "\n";
	}
	$indent =~ s/^..//;
	print "$ {indent}end\n";
	$data = "";

  }	else {
	print $indent, "/* ==================================> $type */\n";
    print $indent, "/* Data: ", length( $data ), " bytes */\n";
  }

  if ( $data ) {
	print $indent, "/* Undefined data: */\n";
	main::hexdump( $data, $indent );
  }
}

# -----------------------------------------------------------------------------
package main;

# file appears to work like so:
# 8 bytes block size, followed by data. lather, rinse, repeat.

$file = shift;
$file or die "No file specified.\nDied";

# Tree:
# Project -> Rsc -> $file.rsrc (empty)
#                -> .AppleDouble -> $file.rsrc (the actual meat)
#         -> Src -> ${file}Rsc.h
# This is a bit broken and needs more work.

$hdr = $ARGV[-1];

if ( $file =~ /\// ) {
  # specified a path, so we'll try the above trick
  ( $path, $file ) = $file =~ m|(.*/)(.*?$)|;
  if ( $path !~ /.AppleDouble\/$/) {
	$path .= ".AppleDouble/";
  }

  $file =~ s/\.rsrc$//;
}

if ( !defined( $hdr )) {
  $hdr = "$ {path}../../Src/$ {file}Rsc.h";

  # Sometimes...
  if ( ! -f $hdr ) {
	$hdr = "$ {path}../$ {file}Rsc.h";
  }
}

$file = "$ {path}$ {file}.rsrc";

print STDERR "File: $file\nHeader: $hdr\n";

# "Parse" the header file. This is gross.
$resourceseen = 0;
open( HDR, "< $hdr" ) or die "$hdr: $!\nDied";
while(<HDR>) {
  next if /^\s*$/;

  if ( /Resource:/i ) {
	( $type, $id ) = m/Resource:\s+(\w+)\s+(\d+)/;
	$currentresource = $type;
	$currentid = $id;
	$resourceseen = 1;
	next;
  }

  if ( /#define\s+(\w+)\s+(\d+)/) {
	( $name, $id ) = ( $1, $2 );
	my %items;
	my %parts;

	# this lot should be a map
	if ( !$resourceseen ) {
	  if ( $name =~ /Form$/ ) {
		$currentresource = "tFRM";
		$currentid = $id;
	  }

	  if ( $name =~ /Alert$/ ) {
		$currentresource = "Talt";
		$currentid = $id;
	  }

	  if ( $name =~ /Str$/ ) {
		$currentresource = "tSTR";
		$currentid = $id;
	  }
	}

	$itemsr = $bits{$currentresource} || \%items; # set one way or another
	%items = %{$itemsr};
	$partsr = $items{ $currentid } || \%parts;
	%parts = %{$partsr};
	if ( $currentid == $id ) {
	  $id = $currentresource;
	}

	$parts{ $id } = $name;
	$items{ $currentid } = \%parts;
	$bits{ $currentresource } = \%items;
  }
}
close( HDR );

open( FILE, "< $file" ) || die "$file: $!\nDied";

# Start of file is start, end, length of datablock1, length of datablock2
sysread( FILE, $buf, 16 );
my ($start, $end, $length, $len2 ) = unpack( "NNNN", $buf );

# Skip to block 1
seek( FILE, $start, 0 );
sysread( FILE, $filebuf, $length );
$fullblock = $filebuf;

while ( length( $filebuf ) ) {
  $b1 = length( $fullblock ) - length( $filebuf );
  $buf = substr( $filebuf, 0, 4 );
  $filebuf = substr( $filebuf, 4 );
  $blen = unpack( "N", $buf );
  $buf = substr( $filebuf, 0, $blen );
  $filebuf = substr( $filebuf, $blen );
  $memory{ $b1 } = $buf;
}

# Decoding block 2
sysread( FILE, $filebuf, $len2 );
$fullblock = $filebuf;

warn "End of file not reached!" if !eof( FILE );

# Identical header, 16 bytes.
$filebuf = substr( $filebuf, 16 );

# 6 unknown bytes
printf STDERR "Unknown data: %04x %04x %04x\n",
  unpack( "nnn", substr( $filebuf, 0, 6 ));
$filebuf = substr( $filebuf, 6 );

# 4 bytes, 0000 001c?
printf STDERR "Should be 0x1c: %04x %04x\n",
  unpack( "nn", substr( $filebuf, 0, 4 ));
$filebuf = substr( $filebuf, 4 );

# -----------------------------------------------------------------------------
# START address of string table in block [2 bytes]
# This is in fullblock, not an offest from here.
$string_table_base = unpack( "n", substr( $filebuf, 0, 2 ));
$filebuf = substr( $filebuf, 2 );

# STRING table
#  Length (1 byte)
#  Text
$string_table = substr( $fullblock, $string_table_base );

# COMPONENT table
# Number of entries - 1 [2 bytes]
$component_table = $filebuf;
$count = unpack( "n", substr( $filebuf, 0, 2 ));
$off = 2;

# Entries:
#  Type (4 bytes)
#  Count - 1 (2 bytes)
#  Offset to details ( 2 bytes ) [offset from start of component table]
for $c ( 0 .. $count ) {
  $chunk = substr( $component_table, $off + ( $c * 8 ), 8 );
  ( $type, $cnt, $offset ) = ( unpack( "A4nn", $chunk));

# Details
#  ID (2 bytes)
#  Detail (10 bytes) (???)
  for $i ( 0 .. $cnt ) {
	$chunk = substr( $component_table, $offset + ( $i * 12 ), 12 );
	my ( $ID, $stringoff, $dataoff, $detail ) =
	  ( unpack ( "nnNN", $chunk ));

	if ( $stringoff != 0xffff ) {
	  $slen = unpack( "c", substr( $string_table, $stringoff, 1 ));
	  $string = substr( $string_table, $stringoff + 1, $slen );
	}

	# Push it into the resource table
	$key = "$ID-$type";
	if ( !defined( $resources{ $key })) {
	  $resources{ $key } = new Pilot::Resource;
	}

	$resources{ $key }->{'type'} = $type;
	$resources{ $key }->{'id'} = $ID;
	$resources{ $key }->{'description'} = $string;
	$resources{ $key }->{'leftover'} = $detail;
	if ( defined( $memory{ $dataoff })) {
	  $resources{ $key }->{'data'} = $memory{ $dataoff };
	  delete $memory{ $dataoff };
	} else {
	  print STDERR "    Spang! memory at $dataoff empty!\n";
	}
  }
}

close( FILE );

# check for unpointedto memory
for my $k ( keys %memory ) {
  printf STDERR "Unused memory @[0x%x]\n", $k;
#  hexdump( $memory{$k});
}

# Dump object file
print <<"EOH";
/******************************************************************************
 * Generated from $file
 */

EOH

# Header file
if ( -f $hdr ) {
  $hdr =~ s|.*/||;
  print "#include \"$hdr\"\n\n";
}

for my $k ( sort keys %resources ) {
  # Dump toplevel resources only, let ->dump handle the rest.
  if ( $resources{$k}->{'type'} =~
	   /tFRM|MBAR|tALT|tVER|tSTR|tAIS|tAIN|APPL|tAIB|Tbmp|TRAP|FONT/i ) {
	$resources{$k}->dump;
	print "\n";
  } else {
	print STDERR "Skipping ", $resources{$k}->{'type'}, "\n";
  }
}

# Chunk out a hexdump of the supplied data, indented appropriately.
sub hexdump {
  my $data = shift;
  my $indent = shift;
  $indent ||= "";
  my $hex = "$indent/* ";
  my $txt = "";

  while ( length( $data )) {
	my $x = ord( substr( $data, 0, 1 ));
	$data = substr( $data, 1 );
	$hex .= sprintf "%02x ", $x;
	$txt .= sprintf "%0c", (( $x >= 32 ) && ( $x <= 126 ))?$x:ord(".");
	if ( length( $hex ) == 48 ) {
	  print "$hex $txt */\n";
	  $hex = "$indent/* ";
	  $txt = "";
	}
  }

  if ( $hex ) {
	printf "%-48s %s */\n", $hex, $txt;
  }
}

# This attempts to convert a random number into a symbolic name from
# the header file. Sometimes it works, sometimes it doesn't.
# Params: TYPE of resource, ID we're looking for, PARENT er, I forget.
# Returns either the name it found or the original ID.
sub findname {
  my $type = shift;
  my $id = shift;
  my $parent = shift;
  my %items;
  my $retval = $id; # default return value is whatever was passed in.
  my $realtype = $type; # in case I get smart...

  if ( defined( $parent )) {
	$type = $parent->type;
  } else {
	$parent = "";
  }

  if ( $parent ) {
	$type = $parent->type; # XXX
  }

  if ( defined( $bits{ $type })) {
	%items = %{$bits{$type}};
  } else {
	# We don't know where to look. Give up before we've started.
	return $retval;
  }

  # Search for an appropriate name. This can be fooled, but the end
  # result is approximately the same until you start hand-tweaking
  # constants in the ...Rsc.h file.
  for my $x (keys ( %items )) {
	if ( !$parent ) {
	  if ( $x == $id ) {
		$retval = ${$items{$x}}{ $type };
		last;
	  }
	} else {
	  if (( $parent && $x == $parent->{'id'} ) ||
		  ( !$parent && $x == $id )) {
		for my $y ( keys %{$items{$x}}) {
		  if (( $parent && $y =~ /^[0-9]+$/ && $y == $id ) ||
			  ( !$parent && $y eq $type )) {
			$retval = $ {$items{$x}}{$y};
			last;
		  }
		}
	  }
	}
  }

  $retval;
}
