diff options
| -rw-r--r-- | crodump.py | 205 |
1 files changed, 153 insertions, 52 deletions
| @@ -7,6 +7,7 @@ from hexdump import hexdump, asasc, tohex, unhex, strescape | |||
| 7 | from koddecoder import kodecode | 7 | from koddecoder import kodecode |
| 8 | from readers import ByteReader | 8 | from readers import ByteReader |
| 9 | import zlib | 9 | import zlib |
| 10 | from collections import defaultdict | ||
| 10 | 11 | ||
| 11 | """ | 12 | """ |
| 12 | python3 crodump.py crodump chechnya_proverki_ul_2012 | 13 | python3 crodump.py crodump chechnya_proverki_ul_2012 |
| @@ -32,23 +33,50 @@ def enumunreferenced(ranges, filesize): | |||
| 32 | 33 | ||
| 33 | class Datafile: | 34 | class Datafile: |
| 34 | """ Represent a single .dat with it's .tad index file """ | 35 | """ Represent a single .dat with it's .tad index file """ |
| 35 | def __init__(self, dat, tad, need_decode=False): | 36 | def __init__(self, name, dat, tad): |
| 37 | self.name = name | ||
| 36 | self.dat = dat | 38 | self.dat = dat |
| 37 | self.tad = tad | 39 | self.tad = tad |
| 38 | 40 | ||
| 39 | self.need_decode = need_decode | 41 | self.readdathdr() |
| 40 | |||
| 41 | self.readtad() | 42 | self.readtad() |
| 42 | 43 | ||
| 43 | self.dat.seek(0, io.SEEK_END) | 44 | self.dat.seek(0, io.SEEK_END) |
| 44 | self.datsize = self.dat.tell() | 45 | self.datsize = self.dat.tell() |
| 45 | 46 | ||
| 47 | |||
| 48 | def readdathdr(self): | ||
| 49 | self.dat.seek(0) | ||
| 50 | hdrdata = self.dat.read(19) | ||
| 51 | |||
| 52 | magic, self.hdrunk, self.version, self.encoding, self.blocksize = struct.unpack("<8sH5sHH", hdrdata) | ||
| 53 | if magic != b"CroFile\x00": | ||
| 54 | print("unknown magic: ", magic) | ||
| 55 | raise Exception("not a Crofile") | ||
| 56 | self.use64bit = self.version == b'01.03' | ||
| 57 | |||
| 58 | # blocksize | ||
| 59 | # 0040 -> Bank | ||
| 60 | # 0400 -> Index or Sys | ||
| 61 | # 0200 -> Stru or Sys | ||
| 62 | |||
| 63 | # encoding | ||
| 64 | # 0000 | ||
| 65 | # 0001 --> 'KOD encoded' | ||
| 66 | # 0002 | ||
| 67 | # 0003 --> encrypted | ||
| 68 | |||
| 46 | def readtad(self): | 69 | def readtad(self): |
| 47 | self.tad.seek(0) | 70 | self.tad.seek(0) |
| 48 | hdrdata = self.tad.read(2*4) | 71 | hdrdata = self.tad.read(2*4) |
| 49 | self.tadhdr = struct.unpack("<2L", hdrdata) | 72 | self.nrdeleted, self.firstdeleted = struct.unpack("<2L", hdrdata) |
| 50 | indexdata = self.tad.read() | 73 | indexdata = self.tad.read() |
| 51 | self.tadidx = [ struct.unpack_from("<3L", indexdata, 12*_) for _ in range(len(indexdata)//12) ] | 74 | if self.use64bit: |
| 75 | # 01.03 has 64 bit file offsets | ||
| 76 | self.tadidx = [ struct.unpack_from("<QLL", indexdata, 16*_) for _ in range(len(indexdata)//16) ] | ||
| 77 | else: | ||
| 78 | # 01.02 and 01.04 have 32 bit offsets. | ||
| 79 | self.tadidx = [ struct.unpack_from("<LLL", indexdata, 12*_) for _ in range(len(indexdata)//12) ] | ||
| 52 | 80 | ||
| 53 | def readdata(self, ofs, size): | 81 | def readdata(self, ofs, size): |
| 54 | self.dat.seek(ofs) | 82 | self.dat.seek(ofs) |
| @@ -68,25 +96,27 @@ class Datafile: | |||
| 68 | if not dat: | 96 | if not dat: |
| 69 | # empty record | 97 | # empty record |
| 70 | encdat = dat | 98 | encdat = dat |
| 71 | elif self.need_decode and not flags: | 99 | elif not flags: |
| 72 | extofs, extlen = struct.unpack("<LL", dat[:8]) | 100 | extofs, extlen = struct.unpack("<LL", dat[:8]) |
| 73 | encdat = dat[8:] | 101 | encdat = dat[8:] |
| 74 | while len(encdat)<extlen: | 102 | while len(encdat)<extlen: |
| 75 | dat = self.readdata(extofs, 0x200) | 103 | dat = self.readdata(extofs, self.blocksize) |
| 76 | extofs, = struct.unpack("<L", dat[:4]) | 104 | extofs, = struct.unpack("<L", dat[:4]) |
| 77 | encdat += dat[4:] | 105 | encdat += dat[4:] |
| 78 | encdat = encdat[:extlen] | 106 | encdat = encdat[:extlen] |
| 79 | else: | 107 | else: |
| 80 | encdat = dat | 108 | encdat = dat |
| 81 | 109 | ||
| 82 | if not self.need_decode: | 110 | if self.encoding == 1: |
| 83 | return encdat | 111 | encdat = kodecode(idx, encdat) |
| 84 | else: | 112 | if self.iscompressed(encdat): |
| 85 | return kodecode(idx, encdat) | 113 | encdat = self.decompress(encdat) |
| 114 | |||
| 115 | return encdat | ||
| 86 | 116 | ||
| 87 | 117 | ||
| 88 | def dump(self, args): | 118 | def dump(self, args): |
| 89 | print("tadhdr: %08x %08x" % tuple(self.tadhdr)) | 119 | print("hdr: %-6s dat: %04x %s enc:%04x bs:%04x, tad: %08x %08x" % (self.name, self.hdrunk, self.version, self.encoding, self.blocksize, self.nrdeleted, self.firstdeleted)) |
| 90 | ranges = [] # keep track of used bytes in the .dat file. | 120 | ranges = [] # keep track of used bytes in the .dat file. |
| 91 | for i, (ofs, ln, chk) in enumerate(self.tadidx): | 121 | for i, (ofs, ln, chk) in enumerate(self.tadidx): |
| 92 | if ln==0xFFFFFFFF: | 122 | if ln==0xFFFFFFFF: |
| @@ -97,39 +127,50 @@ class Datafile: | |||
| 97 | ln &= 0xFFFFFFF | 127 | ln &= 0xFFFFFFF |
| 98 | dat = self.readdata(ofs, ln) | 128 | dat = self.readdata(ofs, ln) |
| 99 | ranges.append((ofs, ofs+ln, "item #%d" % i)) | 129 | ranges.append((ofs, ofs+ln, "item #%d" % i)) |
| 100 | decflag = ' ' | 130 | decflags = [' ', ' '] |
| 101 | infostr = "" | 131 | infostr = "" |
| 102 | tail = b'' | 132 | tail = b'' |
| 103 | 133 | ||
| 104 | if not dat: | 134 | if not dat: |
| 105 | # empty record | 135 | # empty record |
| 106 | encdat = dat | 136 | encdat = dat |
| 107 | elif self.need_decode and not flags: | 137 | elif not flags: |
| 108 | extofs, extlen = struct.unpack("<LL", dat[:8]) | 138 | if self.use64bit: |
| 139 | extofs, extlen = struct.unpack("<QL", dat[:12]) | ||
| 140 | o = 12 | ||
| 141 | else: | ||
| 142 | extofs, extlen = struct.unpack("<LL", dat[:8]) | ||
| 143 | o = 8 | ||
| 109 | infostr = "%08x;%08x" % (extofs, extlen) | 144 | infostr = "%08x;%08x" % (extofs, extlen) |
| 110 | encdat = dat[8:] | 145 | encdat = dat[o:] |
| 111 | while len(encdat)<extlen: | 146 | while len(encdat)<extlen: |
| 112 | dat = self.readdata(extofs, 0x200) | 147 | dat = self.readdata(extofs, self.blocksize) |
| 113 | ranges.append((extofs, extofs+0x200, "item #%d ext" % i)) | 148 | ranges.append((extofs, extofs+self.blocksize, "item #%d ext" % i)) |
| 114 | extofs, = struct.unpack("<L", dat[:4]) | 149 | if self.use64bit: |
| 150 | extofs, = struct.unpack("<Q", dat[:8]) | ||
| 151 | o = 8 | ||
| 152 | else: | ||
| 153 | extofs, = struct.unpack("<L", dat[:4]) | ||
| 154 | o = 4 | ||
| 115 | infostr += ";%08x" % (extofs) | 155 | infostr += ";%08x" % (extofs) |
| 116 | encdat += dat[4:] | 156 | encdat += dat[o:] |
| 117 | tail = encdat[extlen:] | 157 | tail = encdat[extlen:] |
| 118 | encdat = encdat[:extlen] | 158 | encdat = encdat[:extlen] |
| 119 | decflag = '+' | 159 | decflags[0] = '+' |
| 120 | else: | 160 | else: |
| 121 | encdat = dat | 161 | encdat = dat |
| 122 | decflag = '*' | 162 | decflags[0] = '*' |
| 123 | 163 | ||
| 124 | if self.need_decode: | 164 | if self.encoding == 1: |
| 125 | decdat = kodecode(i+1, encdat) | 165 | decdat = kodecode(i+1, encdat) |
| 126 | else: | 166 | else: |
| 127 | decdat = encdat | 167 | decdat = encdat |
| 128 | decflag = ' ' | 168 | decflags[0] = ' ' |
| 129 | 169 | ||
| 130 | if args.decompress and self.iscompressed(decdat): | 170 | if args.decompress and self.iscompressed(decdat): |
| 131 | decdat = self.decompress(decdat) | 171 | decdat = self.decompress(decdat) |
| 132 | print("%5d: %08x-%08x: (%02x:%08x) %s %s%s %s" % (i+1, ofs, ofs+ln, flags, chk, infostr, decflag, toout(args, decdat), tohex(tail))) | 172 | decflags[1] = '@' |
| 173 | print("%5d: %08x-%08x: (%02x:%08x) %s %s%s %s" % (i+1, ofs, ofs+ln, flags, chk, infostr, "".join(decflags), toout(args, decdat), tohex(tail))) | ||
| 133 | 174 | ||
| 134 | if args.verbose: | 175 | if args.verbose: |
| 135 | # output parts not referenced in the .tad file. | 176 | # output parts not referenced in the .tad file. |
| @@ -231,19 +272,19 @@ class Database: | |||
| 231 | def __init__(self, dbdir): | 272 | def __init__(self, dbdir): |
| 232 | self.dbdir = dbdir | 273 | self.dbdir = dbdir |
| 233 | 274 | ||
| 234 | self.stru = self.getfile("Stru", need_decode=True) | 275 | self.stru = self.getfile("Stru") |
| 235 | self.index = self.getfile("Index") | 276 | self.index = self.getfile("Index") |
| 236 | self.bank = self.getfile("Bank") | 277 | self.bank = self.getfile("Bank") |
| 237 | self.sys = self.getfile("Sys", need_decode=True) | 278 | self.sys = self.getfile("Sys") |
| 238 | # BankTemp, Int | 279 | # BankTemp, Int |
| 239 | 280 | ||
| 240 | 281 | ||
| 241 | def getfile(self, name, need_decode=False): | 282 | def getfile(self, name): |
| 242 | try: | 283 | try: |
| 243 | datname = self.getname(name, "dat") | 284 | datname = self.getname(name, "dat") |
| 244 | tadname = self.getname(name, "tad") | 285 | tadname = self.getname(name, "tad") |
| 245 | if datname and tadname: | 286 | if datname and tadname: |
| 246 | return Datafile(open(datname, "rb"), open(tadname, "rb"), need_decode) | 287 | return Datafile(name, open(datname, "rb"), open(tadname, "rb")) |
| 247 | except IOError: | 288 | except IOError: |
| 248 | return | 289 | return |
| 249 | 290 | ||
| @@ -259,21 +300,20 @@ class Database: | |||
| 259 | 300 | ||
| 260 | def dump(self, args): | 301 | def dump(self, args): |
| 261 | if self.stru: | 302 | if self.stru: |
| 262 | print("stru") | ||
| 263 | self.stru.dump(args) | 303 | self.stru.dump(args) |
| 264 | if args.struonly: | ||
| 265 | self.dumptabledefs(args) | ||
| 266 | return | ||
| 267 | if self.index: | 304 | if self.index: |
| 268 | print("index") | ||
| 269 | self.index.dump(args) | 305 | self.index.dump(args) |
| 270 | if self.bank: | 306 | if self.bank: |
| 271 | print("bank") | ||
| 272 | self.bank.dump(args) | 307 | self.bank.dump(args) |
| 273 | if self.sys: | 308 | if self.sys: |
| 274 | print("sys") | ||
| 275 | self.sys.dump(args) | 309 | self.sys.dump(args) |
| 276 | 310 | ||
| 311 | def strudump(self, args): | ||
| 312 | if not self.stru: | ||
| 313 | print("missing CroStru file") | ||
| 314 | return | ||
| 315 | self.dumptabledefs(args) | ||
| 316 | |||
| 277 | def dumptabledefs(self, args): | 317 | def dumptabledefs(self, args): |
| 278 | dbinfo = self.stru.readrec(1) | 318 | dbinfo = self.stru.readrec(1) |
| 279 | dbdef = destruct_bank_definition(args, dbinfo) | 319 | dbdef = destruct_bank_definition(args, dbinfo) |
| @@ -289,6 +329,39 @@ class Database: | |||
| 289 | tbinfo = struct.pack("<B", 4) + idx | 329 | tbinfo = struct.pack("<B", 4) + idx |
| 290 | tbdef = destruct_base_definition(args, tbinfo) | 330 | tbdef = destruct_base_definition(args, tbinfo) |
| 291 | 331 | ||
| 332 | def bankdump(self, args): | ||
| 333 | if not self.bank: | ||
| 334 | print("No CroBank.dat found") | ||
| 335 | return | ||
| 336 | nerr = 0 | ||
| 337 | xref = defaultdict(int) | ||
| 338 | for i in range(args.maxrecs): | ||
| 339 | try: | ||
| 340 | data = self.bank.readrec(i) | ||
| 341 | if not args.stats: | ||
| 342 | if data is None: | ||
| 343 | print("%5d: <deleted>" % i) | ||
| 344 | else: | ||
| 345 | print("%5d: %s" % (i, toout(args, data))) | ||
| 346 | else: | ||
| 347 | if data is None: | ||
| 348 | xref["None"] += 1 | ||
| 349 | elif not len(data): | ||
| 350 | xref["Empty"] += 1 | ||
| 351 | else: | ||
| 352 | xref["%02x" % data[0]] += 1 | ||
| 353 | nerr = 0 | ||
| 354 | except IndexError: | ||
| 355 | break | ||
| 356 | except Exception as e: | ||
| 357 | print("%5d: <%s>" % (i, e)) | ||
| 358 | nerr += 1 | ||
| 359 | if nerr > 5: | ||
| 360 | break | ||
| 361 | if args.stats: | ||
| 362 | print("-- stats --") | ||
| 363 | for k, v in xref.items(): | ||
| 364 | print("%5d * %s" % (v, k)) | ||
| 292 | 365 | ||
| 293 | def incdata(data, s): | 366 | def incdata(data, s): |
| 294 | """ | 367 | """ |
| @@ -359,9 +432,23 @@ def kod_hexdump(args): | |||
| 359 | def cro_dump(args): | 432 | def cro_dump(args): |
| 360 | """ handle 'crodump' subcommand """ | 433 | """ handle 'crodump' subcommand """ |
| 361 | db = Database(args.dbdir) | 434 | db = Database(args.dbdir) |
| 362 | |||
| 363 | db.dump(args) | 435 | db.dump(args) |
| 364 | 436 | ||
| 437 | def stru_dump(args): | ||
| 438 | """ handle 'strudump' subcommand """ | ||
| 439 | db = Database(args.dbdir) | ||
| 440 | db.strudump(args) | ||
| 441 | |||
| 442 | def bank_dump(args): | ||
| 443 | """ hexdump all records """ | ||
| 444 | if args.maxrecs: | ||
| 445 | args.maxrecs = int(args.maxrecs, 0) | ||
| 446 | else: | ||
| 447 | # an arbitrarily large number. | ||
| 448 | args.maxrecs = 0xFFFFFFFF | ||
| 449 | |||
| 450 | db = Database(args.dbdir) | ||
| 451 | db.bankdump(args) | ||
| 365 | 452 | ||
| 366 | def destruct(args): | 453 | def destruct(args): |
| 367 | """ | 454 | """ |
| @@ -396,21 +483,35 @@ def main(): | |||
| 396 | ko.add_argument('filename', type=str, nargs='?', help="dump either stdin, or the specified file") | 483 | ko.add_argument('filename', type=str, nargs='?', help="dump either stdin, or the specified file") |
| 397 | ko.set_defaults(handler=kod_hexdump) | 484 | ko.set_defaults(handler=kod_hexdump) |
| 398 | 485 | ||
| 399 | cro = subparsers.add_parser('crodump', help='CROdumper') | 486 | p = subparsers.add_parser('crodump', help='CROdumper') |
| 400 | cro.add_argument('--verbose', '-v', action='store_true') | 487 | p.add_argument('--verbose', '-v', action='store_true') |
| 401 | cro.add_argument('--kodecode', '-k', action='store_true') | 488 | p.add_argument('--kodecode', '-k', action='store_true') |
| 402 | cro.add_argument('--ascdump', '-a', action='store_true') | 489 | p.add_argument('--ascdump', '-a', action='store_true') |
| 403 | cro.add_argument('--nokod', '-n', action='store_true') | 490 | p.add_argument('--nokod', '-n', action='store_true') |
| 404 | cro.add_argument('--struonly', action='store_true') | 491 | p.add_argument('--nodecompress', action='store_false', dest='decompress', default='true') |
| 405 | cro.add_argument('--nodecompress', action='store_false', dest='decompress', default='true') | 492 | p.add_argument('dbdir', type=str) |
| 406 | cro.add_argument('dbdir', type=str) | 493 | p.set_defaults(handler=cro_dump) |
| 407 | cro.set_defaults(handler=cro_dump) | 494 | |
| 408 | 495 | p = subparsers.add_parser('bankdump', help='BANKdumper') | |
| 409 | des = subparsers.add_parser('destruct', help='Stru dumper') | 496 | p.add_argument('--verbose', '-v', action='store_true') |
| 410 | des.add_argument('--verbose', '-v', action='store_true') | 497 | p.add_argument('--ascdump', '-a', action='store_true') |
| 411 | des.add_argument('--ascdump', '-a', action='store_true') | 498 | p.add_argument('--maxrecs', '-n', type=str, help="max nr or recots to output") |
| 412 | des.add_argument('--type', '-t', type=int, help='what type of record to destruct') | 499 | p.add_argument('--stats', action='store_true', help='calc table stats from the first byte of each record') |
| 413 | des.set_defaults(handler=destruct) | 500 | p.add_argument('dbdir', type=str) |
| 501 | p.set_defaults(handler=bank_dump) | ||
| 502 | |||
| 503 | p = subparsers.add_parser('strudump', help='STRUdumper') | ||
| 504 | p.add_argument('--verbose', '-v', action='store_true') | ||
| 505 | p.add_argument('--ascdump', '-a', action='store_true') | ||
| 506 | p.add_argument('dbdir', type=str) | ||
| 507 | p.set_defaults(handler=stru_dump) | ||
| 508 | |||
| 509 | |||
| 510 | p = subparsers.add_parser('destruct', help='Stru dumper') | ||
| 511 | p.add_argument('--verbose', '-v', action='store_true') | ||
| 512 | p.add_argument('--ascdump', '-a', action='store_true') | ||
| 513 | p.add_argument('--type', '-t', type=int, help='what type of record to destruct') | ||
| 514 | p.set_defaults(handler=destruct) | ||
| 414 | 515 | ||
| 415 | args = parser.parse_args() | 516 | args = parser.parse_args() |
| 416 | 517 | ||
