3 from urllib2
import Request, urlopen, URLError
17 """Program to extract typical XBox 360 files. 18 It can handle LIVE/PIRS, CON (partially), FMIM, and XUIZ files. 20 What about CRA (aka .arc) files? (Dead Rising demo) 22 Copyright (c) 2007, 2008, Rene Ladan <r.c.ladan@gmail.com>, 2-claused BSD 23 license. Portions from various contributors as mentioned in-source. 25 Note that it dumps UTF-16 characters in text strings as-is. 31 """Ensure that the filesize is at least minsize bytes. 33 @param fsize the filesize 34 @param minsize the minimal file size 35 @return fsize >= minsize 39 print "Input file too small: %i instead of at least %i bytes." % \
41 return fsize >= minsize
46 """Checks if the output file with the given name already exists, 47 and if so, asks for overwrite permission. 49 @param filename name of the output file to open 50 @return overwrite permission 53 if os.path.isfile(filename):
54 print filename,
"already exists, overwrite? (y/n)",
55 answer = raw_input(
"")
56 return len(answer) > 0
and answer[0]
in [
"Y",
"y"]
63 """Checks if the output directory with the given name already exists, 64 and if so, asks for overwrite permission. This means that any file 65 in that directory might be overwritten. 67 @param dirname name of the output directory to open 68 @return overwrite permission 71 if os.path.isdir(dirname):
72 print dirname,
"already exists, ok to overwrite files in it? (y/n)",
73 answer = raw_input(
"")
74 return len(answer) > 0
and answer[0]
in [
"Y",
"y"]
81 """Version of os.mkdir() which does not throw an exception if the directory 84 @param dirname name of the directory to create 89 except OSError, (errno):
96 """Strip the leading and trailing blanks from the input string. 97 Blanks are: 0x00 (only trailing) space \t \n \r \v \f 0xFF 99 @param instring the input string 100 @return stripped version of instring 103 rstr = instring.rstrip(
"\0 \t\n\r\v\f\377")
104 return rstr.lstrip(
" \t\n\r\v\f\377")
109 """Open the informational text file. 110 The name is based on that of the input file. 112 @param infile pointer to the input file 113 @return pointer to the informational text file or None if there was no 117 txtname = os.path.basename(infile.name) +
".txt" 119 print "Writing information file", txtname
120 txtfile = open(txtname,
"w")
128 """Dump the embedded PNG file from the archive file to an output file. 130 @param infile pointer to the archive file 131 @param pnglen size of the PNG file in bytes 132 @param maxlen maximum size of the PNG file in bytes 133 @param pngid indicates if this is the first or second PNG file. 138 outname = os.path.basename(infile.name) +
"_" + pngid +
".png" 140 buf = infile.read(pnglen)
141 print "Writing PNG file", outname
142 outfile = open(outname,
"wb")
143 print >> outfile, buf,
146 print "PNG image %s too large (%i instead of maximal %i bytes), " \
147 "file not written." % (pngid, pnglen, maxlen)
152 """Dumps the 9 information strings from the input file. 154 @param infile pointer to the input file 155 @param txtfile pointer to the resulting text file 156 @param what indicates if the information consists of titles or 160 print >> txtfile,
"\n", what,
":" 164 print >> txtfile, lang[i],
":", info
169 """Convert the time given in Microsoft format to a normal time tuple. 171 @param intime the time in Microsoft format 172 @return the time tuple 175 num_d = (intime & 0xFFFF0000L) >> 16
176 num_t = intime & 0x0000FFFFL
179 return ((num_d >> 9) + 1980, (num_d >> 5) & 0x0F, num_d & 0x1F,
180 (num_t & 0xFFFF) >> 11, (num_t >> 5) & 0x3F, (num_t & 0x1F) * 2,
186 """Set the access and update date/time of the target. 187 Taken from tarfile.py (builtin lib) 189 @param targetname name of the target 190 @param atime the desired access date/time 191 @param mtime the desired update date/time 194 if not hasattr(os,
"utime"):
196 if not (sys.platform ==
"win32" and os.path.isdir(targetname)):
200 (time.mktime(
mstime(atime)), time.mktime(
mstime(mtime))))
205 """Check the SHA1 value of the specified range of the input file. 207 @param sha1 the reported SHA1 value 208 @param entry the id of the hash 209 @param infile the input file to check 210 @param start the start position 211 @param end the end position 212 @return string reporting if the hash is correct 216 found_sha1 = hashlib.sha1(infile.read(end - start))
217 found_digest = found_sha1.digest()
219 ret =
"SHA1 " + hex(entry) +
" " 220 if found_digest == sha1:
221 return ret +
"ok (" + found_sha1.hexdigest() +
")" 229 val += hex(ord(i))[2:]
231 return ret +
"wrong (should be " + hexdig +
" actual " + \
232 found_sha1.hexdigest() +
")" 237 """get the real starting cluster""" 240 while startclust >= 170:
242 rst += (startclust + 1) * offset
248 def fill_directory(infile, txtfile, contents, firstclust, makedir, start,
250 """Fill the directory structure with the files contained in the archive. 252 @param infile pointer to the archive 253 @param txtfile pointer to the resulting information text file 254 @param contents contains the directory information 255 @param firstclust address of the starting cluster of the first file in 256 infile (in 4kB blocks, minus start bytes) 257 @param makedir flag if directory should be filled, useful if only return 259 @param start start of directory data 260 @param offset increment for calculating real starting cluster 269 for i
in xrange(0x1000 * firstclust // 64):
270 cur = contents[i * 64:(i + 1) * 64]
271 if ord(cur[40]) == 0:
274 (outname, namelen, clustsize1, val1, clustsize2, val2, startclust,
275 val3) = struct.unpack(
"<40sBHBHBHB", cur[0:50])
277 clustsize1 += val1 << 16
278 clustsize2 += val2 << 16
279 startclust += val3 << 16
280 (pathind, filelen, dati1, dati2) = struct.unpack(
">HLLL", cur[50:64])
285 nlen = namelen & ~0xC0
286 if nlen < 1
or nlen > 40:
287 print "Filename length (%i) out of range, skipping file." % nlen
289 outname = outname[0:nlen]
292 if namelen & 0x80 == 0x80:
293 print >> txtfile,
"Directory",
295 print >> txtfile,
"File",
296 print >> txtfile,
"name:", outname
297 if namelen & 0x40 == 0x40:
298 print >> txtfile,
"Bit 6 of namelen is set." 300 if clustsize1 != clustsize2:
301 print "Cluster sizes don't match (%i != %i), skipping file." % \
302 (clustsize1, clustsize2)
304 if startclust < 1
and namelen & 0x80 == 0:
305 print "Starting cluster must be 1 or greater, skipping file." 307 if filelen > 0x1000 * clustsize1:
308 print "File length (%i) is greater than the size in clusters " \
309 "(%i), skipping file." % (filelen, clustsize1)
312 if pathind != oldpathind:
314 for _
in xrange(paths[oldpathind].count(
"/")):
316 os.chdir(paths[pathind])
318 if namelen & 0x80 == 0x80:
320 paths[i] = paths[pathind] + outname +
"/" 325 adstart = startclust * 0x1000 + start
327 print >> txtfile,
"Starting: advertized", hex(adstart)
332 realstart = adstart +
get_cluster(startclust, offset)
333 infile.seek(realstart)
334 buf += infile.read(min(0x1000, filelen))
338 outfile = open(outname,
"wb")
339 print >> outfile, buf,
345 for _
in xrange(paths[oldpathind].count(
"/")):
351 """Writes out the common part of PIRS/LIVE and CON files. 353 @param infile pointer to the PIRS/LIVE or CON file 354 @param txtfile pointer to the resulting text file 355 @param png2stop location where the second PNG image stops 356 (PIRS/LIVE : 0xB000, CON : 0xA000) 357 @param start start of directory data, from wxPirs 361 mhash = infile.read(20)
363 (mentry_id, content_type) = struct.unpack(
">LL", infile.read(8))
366 print >> txtfile,
"\nMaster SHA1 hash :", \
367 check_sha1(mhash, mentry_id, infile, 0x0344, png2stop)
368 print >> txtfile,
"\nContent type", hex(content_type),
":",
372 if content_type == 0:
373 print >> txtfile,
"(no type)" 374 elif content_type & 0x00000001:
375 print >> txtfile,
"Game save" 376 elif content_type & 0x00000002:
377 print >> txtfile,
"Game add-on" 378 elif content_type & 0x00030000:
379 print >> txtfile,
"Theme" 380 elif content_type & 0x00090000:
381 print >> txtfile,
"Video clip" 382 elif content_type & 0x000C0000:
383 print >> txtfile,
"Game trailer" 384 elif content_type & 0x000D0000:
385 print >> txtfile,
"XBox Live Arcade" 386 elif content_type & 0x00010000:
387 print >> txtfile,
"Gamer profile" 388 elif content_type & 0x00020000:
389 print >> txtfile,
"Gamer picture" 390 elif content_type & 0x00040000:
391 print >> txtfile,
"System update" 392 elif content_type & 0x00080000:
393 print >> txtfile,
"Full game demo" 395 print >> txtfile,
"(unknown)" 397 print >> txtfile,
"\nDirectory data at (hex)", hex(start)
400 dump_info(infile, txtfile,
"Descriptions")
401 print >> txtfile,
"\nPublisher:",
strip_blanks(infile.read(0x80)),
"\n" 402 print >> txtfile,
"\nFilename:",
strip_blanks(infile.read(0x80)),
"\n" 404 (val1, png1len, png2len) = struct.unpack(
">HLL", infile.read(10))
406 print >> txtfile,
"Value:", val1
409 dump_png(infile, png1len, 0x571A - 0x171A,
"1")
413 dump_png(infile, png2len, png2stop - 0x571A,
"2")
417 infile.seek(start + 0x2F)
418 (firstclust, ) = struct.unpack(
"<H", infile.read(2))
421 buf = infile.read(0x1000 * firstclust)
423 outname = os.path.basename(infile.name) +
".dir" 426 print "Creating and filling content directory", outname
429 if png2stop == 0xB000
and start == 0xC000:
433 fill_directory(infile, txtfile, buf, firstclust, makedir, start, offset)
438 infile.seek(png2stop)
439 buf = infile.read(start - png2stop)
441 for i
in xrange(len(buf) // 24):
442 entry = buf[i * 24: i * 24 + 24]
443 if entry.count(
"\0") < 24:
445 print >> txtfile,
"\nEmpty entries:", numempty
447 print >> txtfile,
"Hash (hex):",
449 print >> txtfile, hex(ord(entry[j])),
450 (j, ) = struct.unpack(
">L", entry[20:24])
451 print >> txtfile,
"\nEntry id:", hex(j)
455 print >> txtfile,
"\nTrailing data (hex):",
456 for i
in buf[len(buf) - (len(buf) % 24):]:
457 print >> txtfile, hex(ord(i)),
465 """LIVE and PIRS files are archive files. 466 They contain a certificate, payload, SHA1 checksums, 467 2 icons, textual information, and the files themselves. 469 @param infile pointer to the archive file 470 @param fsize size of infile 473 print "Handling LIVE/PIRS file." 480 print >> txtfile,
"Certificate (hex):",
481 cert = infile.read(0x100)
483 print >> txtfile, hex(ord(i)),
485 print >> txtfile,
"\n\nData (hex):",
486 data = infile.read(0x228)
488 print >> txtfile, hex(ord(i)),
493 (pathind, ) = struct.unpack(
">H", infile.read(2))
494 if pathind == 0xFFFF:
510 print "Found", filename,
"cached on disk, using local copy" 515 print "Downloading", filename,
"from", url
518 response = urlopen(req)
520 if hasattr(e,
'reason'):
521 print "Failed to reach download server. Reason:", e.reason
522 elif hasattr(e,
'code'):
523 print "The server couldn't fulfill the request. Error code:", e.code
524 print "Reading response..." 525 retval = response.read()
527 f = open(filename,
"wb")
530 print "done, saved to", filename
534 print "Extracting $systemupdate/FFFE07DF00000001 from system update file..." 535 updatefile = StringIO.StringIO(systemupdate)
536 z = zipfile.ZipFile(updatefile)
538 pirs = z.open(
"$systemupdate/FFFE07DF00000001").read()
542 if __name__ ==
"__main__":
543 target =
"audios.bin" 544 if len(sys.argv) == 2:
546 if not os.path.isfile(target):
547 fw =
getFileOrURL(
"SystemUpdate.zip",
"http://www.xbox.com/system-update-usb")
550 lang = [
"English",
"Japanese",
"German",
"French",
"Spanish",
"Italian",
551 "Korean",
"Chinese",
"Portuguese"]
552 sio = StringIO.StringIO(pirs)
553 basename =
"FFFE07DF00000001" 559 print "Moving audios.bin to current folder" 560 os.rename(os.path.join(basename +
".dir",
"audios.bin"), target)
563 os.unlink(basename +
".txt")
564 for root, dirs, files
in os.walk(basename +
".dir"):
566 os.remove(os.path.join(root, name))
568 os.rmdir(os.path.join(root, name))
570 os.unlink(
"SystemUpdate.zip")
573 print "Already have audios.bin" def handle_live_pirs(infile, fsize)
def check_sha1(sha1, entry, infile, start, end)
def open_info_file(infile)
def nice_open_dir(dirname)
def write_common_part(infile, txtfile, png2stop, start)
def fill_directory(infile, txtfile, contents, firstclust, makedir, start, offset)
def dump_png(infile, pnglen, maxlen, pngid)
def strip_blanks(instring)
def nice_open_file(filename)
def get_cluster(startclust, offset)
def getFileOrURL(filename, url)
def dump_info(infile, txtfile, what)
def extractPirsFromZip(systemupdate)
def do_utime(targetname, atime, mtime)
def check_size(fsize, minsize)