#!/usr/bin/perl -w use Fcntl qw(SEEK_SET); use integer; # Extract information from within a TADS GAM file sub ord1($;$) { my $str = shift; my $ofs = shift || 0; return ord(substr($str, $ofs)); } sub read1(*) { my $FH = shift; return ord(getc($FH)); } sub ord2($;$) { my $str = shift; my $ofs = shift || 0; my $c1 = ord1($str, $ofs); my $c2 = ord1($str, $ofs + 1); return ($c2 << 8) | $c1; } sub ord2s($;$) { my $str = shift; my $ofs = shift || 0; my $result = ord2($str, $ofs); $result -= 0x10000 if ($result >= 0x8000); return $result; } sub read2(*) { my $FH = shift; my $str; read($FH, $str, 2); return ord2($str); } sub ord4($;$) { my $str = shift; my $ofs = shift || 0; my $c1 = ord1($str, $ofs); my $c2 = ord1($str, $ofs + 1); my $c3 = ord1($str, $ofs + 2); my $c4 = ord1($str, $ofs + 3); $c4 -= 256 if ($c4 >= 128); # In case of a signed value return ($c4 << 24) | ($c3 << 16) | ($c2 << 8) | $c1; } sub read4(*) { my $FH = shift; my $str; read($FH, $str, 4); return ord4($str); } $xorseed = 0x3f; $xorinc = 0x40; sub decode($) { my $block = shift; return $block unless ($crypt); my $len = length($block); my $mask = $xorseed; my $maskstr; for (my $i = 0; $i < $len; $i++) { $maskstr .= chr($mask); $mask = ($mask + $xorinc) % 256; } return $block ^ $maskstr; } sub hexl($) { my $block = shift; local *HEXL; open(HEXL, "|/usr/lib/xemacs-21.4.8/i386-debian-linux/hexl"); print HEXL $block; close HEXL; } sub sigend($) { print "(END REACHED)"; return shift; } # Datatype numbers sub DAT_NUMBER() { return 1; } sub DAT_OBJECT() { return 2; } sub DAT_SSTRING() { return 3; } sub DAT_BASEPTR() { return 4; } sub DAT_NIL() { return 5; } sub DAT_CODE() { return 6; } sub DAT_LIST() { return 7; } sub DAT_TRUE() { return 8; } sub DAT_DSTRING() { return 9; } sub DAT_FNADDR() { return 10; } sub DAT_TPL() { return 11; } sub DAT_PROPNUM() { return 13; } sub DAT_DEMAND() { return 14; } sub DAT_SYN() { return 15; } sub DAT_REDIR() { return 16; } sub DAT_TPL2() { return 17; } # Print a value, returning the length it occupied sub printValue($$$) { my $block = shift; my $pos = shift; my $type = shift; my $lenleft = length($block) - $pos; if ($type == DAT_NUMBER) { return sigend($lenleft) if ($lenleft < 4); print ord4($block, $pos); return 4; } elsif ($type == DAT_OBJECT || $type == DAT_FNADDR) { print "&" if ($type == DAT_FNADDR); return sigend($lenleft) if ($lenleft < 2); printObj(ord2($block, $pos)); return 2; } elsif ($type == DAT_SSTRING || $type == DAT_DSTRING) { return sigend($lenleft) if ($lenleft < 2); my $strlen = ord2($block, $pos); # Check for valid string length return sigend($lenleft) if ($strlen > $lenleft || $strlen < 2); my $delim = "\""; $delim = "'" if ($type == DAT_SSTRING); print $delim, substr($block, $pos + 2, $strlen - 2), $delim; return $strlen; } elsif ($type == DAT_NIL) { print "nil"; return 0; } elsif ($type == DAT_LIST) { return sigend($lenleft) if ($lenleft < 2); my $listlen = ord2($block, $pos); # Check for valid list length return sigend($lenleft) if ($listlen > $lenleft || $listlen < 2); printList(substr($block, $pos, $listlen)); return $listlen; } elsif ($type == DAT_TRUE) { print "true"; return 0; } elsif ($type == DAT_PROPNUM) { return sigend($lenleft) if ($lenleft < 2); print "&"; printProp(ord2($block, $pos)); return 2; } else { print "Unexpected data type: $type\n"; return $lenleft; } } sub printList($) { my $block = shift; print "["; my $pos = 2; my $len = ord2($block); return sigend(length($block)) if ($len > length($block) || $len < 2); while ($pos < $len) { my $type = ord1($block, $pos); $pos++; $pos += printValue($block, $pos, $type); print " " if ($pos < $len); } print "]"; return $len; } # Define operand type values sub OPBYTE() { return 1; } sub OPWORD() { return 2; } sub OPQUAD() { return 3; } sub OPOBJ() { return 4; } sub OPFUNC() { return 5; } sub OPPROP() { return 6; } sub OPRET() { return 7; } sub OPLABEL() { return 8; } sub OPDSTR() { return 9; } sub OPBIF() { return 10; } sub OPSSTR() { return 11; } sub OPLIST() { return 12; } sub OPSWITCH() { return 13; } # Switch table sub OPLINE() { return 14; } # Debugger line record sub OPFRAME() { return 15; } # Local variable frame record # Print an operand, returning the number of bytes it occupied sub printOperand($$$) { my $block = shift; my $pos = shift; my $type = shift; my $lenleft = length($block) - $pos; if ($type == OPBYTE) { return sigend($lenleft) if ($lenleft < 1); print ord1($block, $pos); return 1; } elsif ($type == OPWORD || $type == OPRET) { return sigend($lenleft) if ($lenleft < 2); print ord2s($block, $pos); return 2; } elsif ($type == OPQUAD) { return printValue($block, $pos, DAT_NUMBER); } elsif ($type == OPOBJ || $type == OPFUNC) { return printValue($block, $pos, DAT_OBJECT); } elsif ($type == OPPROP) { return sigend($lenleft) if ($lenleft < 2); printProp(ord2($block, $pos)); return 2; } elsif ($type == OPLABEL || $type == OPSWITCH) { return sigend($lenleft) if ($lenleft < 2); print $pos + ord2s($block, $pos); return 2; } elsif ($type == OPDSTR) { return printValue($block, $pos, DAT_DSTRING); } elsif ($type == OPSSTR) { return printValue($block, $pos, DAT_SSTRING); } elsif ($type == OPBIF) { return sigend($lenleft) if ($lenleft < 2); printBuiltin(ord2($block, $pos)); return 2; } elsif ($type == OPLIST) { return printValue($block, $pos, DAT_LIST); } elsif ($type == OPLINE) { return sigend($lenleft) if ($lenleft < 1); my $linelen = ord1($block, $pos); return sigend($lenleft) if ($linelen > $lenleft || $linelen < 1); print "line record (", $linelen, " bytes)"; return $linelen; } elsif ($type == OPFRAME) { return sigend($lenleft) if ($lenleft < 2); my $framelen = ord2($block, $pos); return sigend($lenleft) if ($framelen > $lenleft || $framelen < 2); print "frame record (", $framelen, " bytes)"; return $framelen; } else { print "This shouldn't happen: unknown operand type ", $type, "\n"; die; } } @props = (undef, "doAction", "verb", "noun", "adjective", "preposition", "article", "plural", "sdesc", "thedesc", "doDefault", "ioDefault", "ioAction", "location", "value", "roomAction", "actorAction", "contents", "tpl", "prepDefault", "verActor", "validDo", "validIo", "lookAround", "roomCheck", "statusLine", "locationOK", "isVisible", "cantReach", "isHim", "isHer", "action", "validDoList", "validIoList", "iobjGen", "dobjGen", "nilPrep", "rejectMultiDobj", "moveInto", "construct", "destruct", "validActor", "preferredActor", "isEquivalent", "adesc", "multisdesc", "tpl2", "anyvalue", "newNumbered", "unknown", "parseUnknownDobj", "parseUnknownIobj", "dobjCheck", "iobjCheck", "verbAction", "disambigDobj", "disambigIobj", "prefixdesc", "isThem"); sub printProp($) { my $n = shift; if ($n == 0) { print "noprop"; } elsif ($n > 0 && defined $props[$n]) { print $props[$n]; } else { print "prop", $n; } } @objs = (); sub printObj($) { my $n = shift; if ($n == 65535) { print "nullobj"; } elsif (defined $objs[$n]) { print $objs[$n]; } else { print "obj", $n; } } sub printBuiltin($) { my $n = shift; my @builtins = ("say", "car", "cdr", "length", "randomize", "rand", "substr", "cvtstr", "cvtnum", "upper", "lower", "caps", "find", "getarg", "datatype", "setdaemon", "setfuse", "setversion", "notify", "unnotify", "yorn", "remfuse", "remdaemon", "incturn", "quit", "save", "restore", "logging", "input", "setit", "askfile", "setscore", "firstobj", "nextobj", "isclass", "restart", "debugTrace", "undo", "defined", "proptype", "outhide", "runfuses", "rundaemons", "gettime", "getfuse", "intersect", "inputkey", "objwords", "addword", "delword", "getwords", "nocaps", "skipturn", "clearscreen", "firstsc", "verbinfo", "fopen", "fclose", "fwrite", "fread", "fseek", "fseekeof", "ftell", "outcapture", "systemInfo", "morePrompt", "parserSetMe", "parserGetMe", "reSearch", "reGetGroup", "inputevent", "timeDelay", "setOutputFilter", "execCommand", "parserGetObj", "parseNounList", "parserTokenize", "parserGetTokTypes", "parserDictLookup", "parserResolveObjects", "parserReplaceCommand", "exitobj", "inputdialog", "resourceExists"); if ($n < 0 || $n > $#builtins) { print "invalid builtin $n"; } else { print $builtins[$n]; } } @opcodes = ([], ["pushnum", OPQUAD], ["pushobj", OPOBJ], ["neg"], ["not"], ["add"], ["sub"], ["mul"], ["div"], ["and"], ["or"], ["eq"], ["ne"], ["gt"], ["ge"], ["lt"], ["le"], ["call", OPBYTE, OPFUNC], ["getp", OPBYTE, OPPROP], ["getpdata", OPBYTE, OPPROP], ["getlcl", OPWORD], ["ptrgetpdata", OPBYTE], ["return", OPRET], ["retval", OPRET], ["enter", OPWORD], ["discard"], ["jmp", OPLABEL], ["jf", OPLABEL], ["pushself"], ["say", OPDSTR], ["builtin", OPBYTE, OPBIF], ["pushstr", OPSSTR], ["pushlst", OPLIST], ["pushnil"], ["pushtrue"], ["pushfn", OPFUNC], ["getpselfdata", OPBYTE, OPPROP], [], ["ptrcall", OPBYTE], ["ptrinh", OPBYTE], ["ptrgetp", OPBYTE], ["pass", OPPROP], ["exit"], ["abort"], ["askdo"], ["askio", OPOBJ], ["expinh", OPBYTE, OPPROP, OPOBJ], ["expinhptr", OPBYTE, OPOBJ], ["calld", OPBYTE, OPFUNC], ["getpd", OPBYTE, OPPROP], ["builtind", OPBYTE, OPBIF], ["je", OPLABEL], ["jne", OPLABEL], ["jgt", OPLABEL], ["jge", OPLABEL], ["jlt", OPLABEL], ["jle", OPLABEL], ["jnand", OPLABEL], ["jnor", OPLABEL], ["jt", OPLABEL], ["getpself", OPBYTE, OPPROP], ["getpslfd", OPBYTE, OPPROP], ["getpobj", OPBYTE, OPOBJ, OPPROP], ["getpobjd", OPBYTE, OPOBJ, OPPROP], ["index"], [], [], ["pushpn", OPPROP], ["jst", OPLABEL], ["jsf", OPLABEL], ["jmpd", OPLABEL], ["inherit", OPBYTE, OPPROP], ["callext", OPBYTE, OPWORD], ["dbgret"], ["cons", OPBYTE], ["switch", OPSWITCH], ["argc"], ["chkargc", OPBYTE], ["line", OPLINE], ["frame", OPFRAME], ["bp", OPLINE], ["getdblcl", OPWORD, OPWORD, OPWORD], ["getpptrself", OPBYTE], ["mod"], ["band"], ["bor"], ["xor"], ["bnot"], ["shl"], ["shr"], ["new"], ["delete"]); sub disasm($) { my $block = shift; my @switches; my $pos = 0, $len = length($block); my $numswitches = -1; # Number of switch table entries left, # -1 means not in switch table # (It's this way so that switch tables with only a default get # disassembled correctly.) while ($pos < $len) { print $pos, "\t"; # The one case where we're not pointed at an opcode: the end of # a switch table if ($numswitches == 0) { print "default\t-> "; $pos += printOperand($block, $pos, OPLABEL); $numswitches = -1; print "\n", $pos, "\t"; } if ($#switches >= 0 && $pos == $switches[0]) { print "Switch table\n"; $numswitches = ord2($block, $pos); $pos += 2; shift @switches; } else { my $opc = ord1($block, $pos); $pos++; if ($opc >= 1 && $opc <= $#opcodes) { my @opcdata = @{$opcodes[$opc]}; if ($#opcdata < 0) { print "Invalid opcode $opc\n"; last; } print $opcdata[0], "\t"; for (my $i = 1; $i <= $#opcdata; $i++) { # Handle operand to switch if ($opcdata[$i] == OPSWITCH) { @switches = sort(@switches, $pos + ord2s($block, $pos)); } $pos += printOperand($block, $pos, $opcdata[$i]); print ", " if ($i < $#opcdata); } } elsif (($opc & 0xc0) == 0xc0) { # Assignment print "assign\t"; my $ext = 0; if (($opc & 0x1c) == 0x1c) { $ext = ord1($block, $pos); $pos++; } if (($opc & 3) == 0) { print "local ", ord2($block, $pos); $pos += 2; } elsif (($opc & 3) == 1) { print "property "; printProp(ord2($block, $pos)); $pos += 2; } elsif (($opc & 3) == 2) { print "list"; } else { print "property pointer"; } print " :=" if (($opc & 0x1c) == 0x00); print " +=" if (($opc & 0x1c) == 0x04); print " -=" if (($opc & 0x1c) == 0x08); print " *=" if (($opc & 0x1c) == 0x0c); print " /=" if (($opc & 0x1c) == 0x10); print " ++" if (($opc & 0x1c) == 0x14); print " --" if (($opc & 0x1c) == 0x18); print " %=" if ($ext == 1); print " &=" if ($ext == 2); print " |=" if ($ext == 3); print " ^=" if ($ext == 4); print " <<=" if ($ext == 5); print " >>=" if ($ext == 6); if (($opc & 0x1c) == 0x14 || ($opc & 0x1c) == 0x18) { if ($opc & 0x20) { print " pre"; } else { print " post"; } } else { print " and discard" if ($opc & 0x20); } } else { print "Invalid opcode ", $opc, "\n"; last; } if ($numswitches >= 0) { print "\t-> "; $pos += printOperand($block, $pos, OPLABEL); $numswitches--; } print "\n"; } } } sub processXSI(*$) { my $FH = shift; $xorseed = read1($FH); $xorinc = read1($FH); print " xorseed = ", $xorseed, ", xorinc = ", $xorinc, "\n"; } sub processOBJ(*$) { my $FH = shift; my $len = shift; while ($len > 0) { my $type = read1($FH); my $n = read2($FH); my $size = read2($FH); my $use = read2($FH); print " Object "; printObj($n); print ":\n Type $type"; print " (function)" if ($type == 1); print " (object)" if ($type == 2); print " (extern)" if ($type == 10); print "\n Size $size\n"; print " Size used $use\n"; my $block; read($FH, $block, $use); $block = decode($block); if ($type == 1) { disasm($block); } else { print " Workspace ", ord2($block, 0), "\n"; print " Flags ", ord2($block, 2), "\n"; print " Free ", ord2($block, 8), "\n"; print " Reset ", ord2($block, 10), "\n"; print " Static ", ord2($block, 12), "\n"; print " Superclasses:"; my $n = ord2($block, 4); for (my $i = 0; $i < $n; $i++) { print " "; printObj(ord2($block, 14 + 2 * $i)); } print "\n"; my $flags = ord2($block, 2); my $nprop = ord2($block, 6); my $pos = 14 + 2 * $n; if ($flags & 2) { # Skip the index table if present $pos += 2 * $nprop; } for (my $i = 0; $i < $nprop; $i++) { my $num = ord2($block, $pos); my $type = ord1($block, $pos + 2); my $size = ord2($block, $pos + 3); print " Property "; printProp($num); print ":\n"; print " Datatype ", $type, "\n"; print " Size ", $size, "\n"; print " Flags ", ord1($block, $pos + 5), "\n"; if ($type == DAT_CODE) { # code disasm(substr($block, $pos + 6, $size)); } elsif ($type == DAT_DEMAND) { print " implicit contents list\n"; } elsif ($type == DAT_SYN) { # property synonym print " synonym to property "; printProp(ord2($block, $pos + 6)); print "\n"; } elsif ($type == DAT_REDIR) { # redirection to another object print " redirection to object "; printObj(ord2($block, $pos + 6)); print "\n"; } elsif ($type == DAT_TPL2) { # tpl2 my $i = ord1($block, $pos + 6); my $intpos = $pos + 7; for (my $j = 0; $j < $i; $j++) { print " preposition "; printObj(ord2($block, $intpos)); print ":\n verIoVerb "; printProp(ord2($block, $intpos + 2)); print "\n ioVerb "; printProp(ord2($block, $intpos + 4)); print "\n verDoVerb "; printProp(ord2($block, $intpos + 6)); print "\n doVerb "; printProp(ord2($block, $intpos + 8)); print "\n flags ", ord1($block, $intpos + 10), "\n"; $intpos += 16; } } else { print " "; printValue($block, $pos + 6, $type); print "\n"; } $pos += 6 + $size; } } $len -= $use + 7; } } sub processFST(*$) { my $FH = shift; my $len = shift; while ($len > 0) { my $type = read1($FH); my $n = read2($FH); my $size = read2($FH); my $use = read2($FH); my $ofs = read4($FH); print " Object "; printObj($n); print ":\n Type $type"; print " (function)" if ($type == 1); print " (object)" if ($type == 2); print " (extern)" if ($type == 10); print "\n Size $size\n"; print " Size used $use\n"; print " Offset $ofs\n"; $len -= 11; } } sub processINH(*$) { my $FH = shift; my $len = shift; while ($len > 0) { my $flag = read1($FH); my $n = read2($FH); my $loc = read2($FH); my $ilc = read2($FH); my $i = read2($FH); print " Object "; printObj($n); print ":\n Flags $flag\n"; print " Loc "; printObj($loc); print "\n Ilc "; printObj($ilc); print "\n Superclasses:"; for (my $j = 0; $j < $i; $j++) { print " "; printObj(read2($FH)); } print "\n"; $len -= (9 + 2 * $i); } } sub processREQ(*$) { my $FH = shift; my $len = shift; my @reqnames = ("Me", "takeVerb", "strObj", "numObj", "pardon", "againVerb", "init", "preparse", "parseError", "cmdPrompt", "parseDisambig", "parseError2", "parseDefault", "parseAskobj", "preparseCmd", "parseAskobjActor", "parseErrorParam", "commandAfterRead", "initRestore", "parseUnknownVerb", "parseNounPhrase", "postAction", "endCommand", "preCommand", "parseAskobjIndirect"); foreach $name(@reqnames) { return if ($len <= 0); print " $name: "; printObj(read2($FH)); print "\n"; $len -= 2; } } sub processFMTSTR(*$) { my $FH = shift; my $len = shift; my $block; $len = read2($FH); read($FH, $block, $len); $block = decode($block); my $pos = 0; while ($pos < $len) { my $strlen = ord2($block, $pos + 2); print " ", substr($block, $pos + 4, $strlen - 2), " -> "; printProp(ord2($block, $pos)); print "\n"; $pos += $strlen + 2; } } sub processCMPD(*$) { my $FH = shift; my $len = shift; my $block; $len = read2($FH); read($FH, $block, $len); $block = decode($block); my $pos = 0, $i = 0; while ($pos < $len) { my $thislen = ord2($block, $pos); print " " if ($i % 3 == 0); print substr($block, $pos + 2, $thislen - 2); print " " if ($i % 3 == 0); print " => " if ($i % 3 == 1); print "\n" if ($i % 3 == 2); $i++; $pos += $thislen; } print "\n" unless ($i % 3 == 0); } sub processSPECWORD(*$) { my $FH = shift; my $len = shift; my $block; $len = read2($FH); read($FH, $block, $len); $block = decode($block); my $pos = 0; while ($pos < $len) { $flags = ord1($block, $pos); $strlen = ord1($block, $pos + 1); print " ", substr($block, $pos + 2, $strlen), ", flags ", $flags, "\n"; $pos += $strlen + 2; } } sub processVOC(*$) { my $FH = shift; my $len = shift; my $pos = 0; while ($pos < $len) { my $len1 = read2($FH); my $len2 = read2($FH); my $prpnum = read2($FH); my $objnum = read2($FH); my $classflg = read2($FH); my $block; read($FH, $block, $len1 + $len2); $block = decode($block); print " ", substr($block, 0, $len1); print " ", substr($block, $len1, $len2) if ($len2 != 0); print "\n Prpnum: "; printProp($prpnum); print "\n Object: "; printObj($objnum); print "\n Class flags: $classflg\n"; $pos += 10 + $len1 + $len2; } } if ($#ARGV >= 1 && $ARGV[0] eq '-s') { unless ($return = do $ARGV[1]) { warn "couldn't parse file: $@" if $@; warn "couldn't do $ARGV[1]: $!" unless defined ($return); warn "couldn't run $ARGV[1]" unless $return; } splice(@ARGV, 0, 2); } if ($#ARGV != 0) { die "Usage: untads [-s symbols] file.gam\n"; } open(GAM, $ARGV[0]) || die("Couldn't find file $ARGV[0]"); # Assume for now that it is a TADS game; skip to the header flags seek(GAM, 20, SEEK_SET); $flags = read1(GAM); $crypt = $flags & 8; seek(GAM, 48, SEEK_SET); for (;;) { $namelen = read1(GAM); read(GAM, $name, $namelen); $nextofs = read4(GAM); $curofs = tell(GAM); $sectlen = $nextofs - $curofs; if ($name eq '$EOF') { print "\$EOF marker\n"; close GAM; exit(0); } print "Section $name: $sectlen bytes ($curofs to $nextofs)\n"; processXSI(GAM, $sectlen) if ($name eq 'XSI'); processOBJ(GAM, $sectlen) if ($name eq 'OBJ'); processFST(GAM, $sectlen) if ($name eq 'FST'); processINH(GAM, $sectlen) if ($name eq 'INH'); processREQ(GAM, $sectlen) if ($name eq 'REQ'); processFMTSTR(GAM, $sectlen) if ($name eq 'FMTSTR'); processCMPD(GAM, $sectlen) if ($name eq 'CMPD'); processSPECWORD(GAM, $sectlen) if ($name eq 'SPECWORD'); processVOC(GAM, $sectlen) if ($name eq 'VOC'); seek(GAM, $nextofs, SEEK_SET); }