# # ps2save.py # # By Ross Ridge # Public Domain # # A simple interface for working with various PS2 save file formats. # _SCCS_ID = "@(#) mymc ps2save.py 1.8 22/01/15 01:25:25\n" import sys import os import string import struct import binascii import array import zlib from round import div_round_up, round_up from ps2mc_dir import * from sjistab import shift_jis_normalize_table try: import lzari except ImportError: lzari = None PS2SAVE_MAX_MAGIC = "Ps2PowerSave" PS2SAVE_SPS_MAGIC = "\x0d\0\0\0SharkPortSave" PS2SAVE_CBS_MAGIC = "CFU\0" PS2SAVE_NPO_MAGIC = "nPort" # This is the initial permutation state ("S") for the RC4 stream cipher # algorithm used to encrpyt and decrypt Codebreaker saves. PS2SAVE_CBS_RC4S = [0x5f, 0x1f, 0x85, 0x6f, 0x31, 0xaa, 0x3b, 0x18, 0x21, 0xb9, 0xce, 0x1c, 0x07, 0x4c, 0x9c, 0xb4, 0x81, 0xb8, 0xef, 0x98, 0x59, 0xae, 0xf9, 0x26, 0xe3, 0x80, 0xa3, 0x29, 0x2d, 0x73, 0x51, 0x62, 0x7c, 0x64, 0x46, 0xf4, 0x34, 0x1a, 0xf6, 0xe1, 0xba, 0x3a, 0x0d, 0x82, 0x79, 0x0a, 0x5c, 0x16, 0x71, 0x49, 0x8e, 0xac, 0x8c, 0x9f, 0x35, 0x19, 0x45, 0x94, 0x3f, 0x56, 0x0c, 0x91, 0x00, 0x0b, 0xd7, 0xb0, 0xdd, 0x39, 0x66, 0xa1, 0x76, 0x52, 0x13, 0x57, 0xf3, 0xbb, 0x4e, 0xe5, 0xdc, 0xf0, 0x65, 0x84, 0xb2, 0xd6, 0xdf, 0x15, 0x3c, 0x63, 0x1d, 0x89, 0x14, 0xbd, 0xd2, 0x36, 0xfe, 0xb1, 0xca, 0x8b, 0xa4, 0xc6, 0x9e, 0x67, 0x47, 0x37, 0x42, 0x6d, 0x6a, 0x03, 0x92, 0x70, 0x05, 0x7d, 0x96, 0x2f, 0x40, 0x90, 0xc4, 0xf1, 0x3e, 0x3d, 0x01, 0xf7, 0x68, 0x1e, 0xc3, 0xfc, 0x72, 0xb5, 0x54, 0xcf, 0xe7, 0x41, 0xe4, 0x4d, 0x83, 0x55, 0x12, 0x22, 0x09, 0x78, 0xfa, 0xde, 0xa7, 0x06, 0x08, 0x23, 0xbf, 0x0f, 0xcc, 0xc1, 0x97, 0x61, 0xc5, 0x4a, 0xe6, 0xa0, 0x11, 0xc2, 0xea, 0x74, 0x02, 0x87, 0xd5, 0xd1, 0x9d, 0xb7, 0x7e, 0x38, 0x60, 0x53, 0x95, 0x8d, 0x25, 0x77, 0x10, 0x5e, 0x9b, 0x7f, 0xd8, 0x6e, 0xda, 0xa2, 0x2e, 0x20, 0x4f, 0xcd, 0x8f, 0xcb, 0xbe, 0x5a, 0xe0, 0xed, 0x2c, 0x9a, 0xd4, 0xe2, 0xaf, 0xd0, 0xa9, 0xe8, 0xad, 0x7a, 0xbc, 0xa8, 0xf2, 0xee, 0xeb, 0xf5, 0xa6, 0x99, 0x28, 0x24, 0x6c, 0x2b, 0x75, 0x5d, 0xf8, 0xd3, 0x86, 0x17, 0xfb, 0xc0, 0x7b, 0xb3, 0x58, 0xdb, 0xc7, 0x4b, 0xff, 0x04, 0x50, 0xe9, 0x88, 0x69, 0xc9, 0x2a, 0xab, 0xfd, 0x5b, 0x1b, 0x8a, 0xd9, 0xec, 0x27, 0x44, 0x0e, 0x33, 0xc8, 0x6b, 0x93, 0x32, 0x48, 0xb6, 0x30, 0x43, 0xa5] class error(Exception): """Base for all exceptions specific to this module.""" pass class corrupt(error): """Corrupt save file.""" def __init__(self, msg, f = None): fn = None if f != None: fn = getattr(f, "name", None) self.filename = fn error.__init__(self, "Corrupt save file: " + msg) class eof(corrupt): """Save file is truncated.""" def __init__(self, f = None): corrupt.__init__(self, "Unexpected EOF", f) class subdir(corrupt): def __init__(self, f = None): corrupt.__init__(self, "Non-file in save file.", f) # # Table of graphically similar ASCII characters that can be used # as substitutes for Unicode characters. # char_substs = { u'\u00a2': u"c", u'\u00b4': u"'", u'\u00d7': u"x", u'\u00f7': u"/", u'\u2010': u"-", u'\u2015': u"-", u'\u2018': u"'", u'\u2019': u"'", u'\u201c': u'"', u'\u201d': u'"', u'\u2032': u"'", u'\u2212': u"-", u'\u226a': u"<<", u'\u226b': u">>", u'\u2500': u"-", u'\u2501': u"-", u'\u2502': u"|", u'\u2503': u"|", u'\u250c': u"+", u'\u250f': u"+", u'\u2510': u"+", u'\u2513': u"+", u'\u2514': u"+", u'\u2517': u"+", u'\u2518': u"+", u'\u251b': u"+", u'\u251c': u"+", u'\u251d': u"+", u'\u2520': u"+", u'\u2523': u"+", u'\u2524': u"+", u'\u2525': u"+", u'\u2528': u"+", u'\u252b': u"+", u'\u252c': u"+", u'\u252f': u"+", u'\u2530': u"+", u'\u2533': u"+", u'\u2537': u"+", u'\u2538': u"+", u'\u253b': u"+", u'\u253c': u"+", u'\u253f': u"+", u'\u2542': u"+", u'\u254b': u"+", u'\u25a0': u"#", u'\u25a1': u"#", u'\u2605': u"*", u'\u2606': u"*", u'\u3001': u",", u'\u3002': u".", u'\u3003': u'"', u'\u3007': u'0', u'\u3008': u'<', u'\u3009': u'>', u'\u300a': u'<<', u'\u300b': u'>>', u'\u300a': u'<<', u'\u300b': u'>>', u'\u300c': u'[', u'\u300d': u']', u'\u300e': u'[', u'\u300f': u']', u'\u3010': u'[', u'\u3011': u']', u'\u3014': u'[', u'\u3015': u']', u'\u301c': u'~', u'\u30fc': u'-', } def shift_jis_conv(src, encoding = None): """Convert Shift-JIS strings to a graphically similar representation. If encoding is "unicode" then a Unicode string is returned, otherwise a string in the encoding specified is returned. If necessary, graphically similar characters are used to replace characters not exactly representable in the desired encoding. """ if encoding == None: encoding = sys.getdefaultencoding() if encoding == "shift_jis": return src u = src.decode("shift_jis", "replace") if encoding == "unicode": return u a = [] for uc in u: try: uc.encode(encoding) a.append(uc) except UnicodeError: for uc2 in shift_jis_normalize_table.get(uc, uc): a.append(char_substs.get(uc2, uc2)) return u"".join(a).encode(encoding, "replace") def rc4_crypt(s, t): """RC4 encrypt/decrypt the string t using the permutation s. Returns a byte array.""" s = array.array('B', s) t = array.array('B', t) j = 0 for ii in range(len(t)): i = (ii + 1) % 256 j = (j + s[i]) % 256 (s[i], s[j]) = (s[j], s[i]) t[ii] ^= s[(s[i] + s[j]) % 256] return t # def sps_check(s): # """Calculate the checksum for a SharkPort save.""" # # h = 0 # for c in array.array('B', s): # h += c << (h % 24) # h &= 0xFFFFFFFF # return h def unpack_icon_sys(s): """Unpack an icon.sys file into a tuple.""" # magic, title offset, ... # [14] title, normal icon, copy icon, del icon a = struct.unpack("<4s2xH4x" "L" "16s16s16s16s" "16s16s16s" "16s16s16s" "16s" "68s64s64s64s512x", s) a = list(a) for i in range(3, 7): a[i] = struct.unpack("<4L", a[i]) a[i] = map(hex, a[i]) for i in range(7, 14): a[i] = struct.unpack("<4f", a[i]) a[14] = zero_terminate(a[14]) a[15] = zero_terminate(a[15]) a[16] = zero_terminate(a[16]) a[17] = zero_terminate(a[17]) return a def icon_sys_title(icon_sys, encoding = None): """Extract the two lines of the title stored in an icon.sys tuple.""" offset = icon_sys[1] title = icon_sys[14] title2 = shift_jis_conv(title[offset:], encoding) title1 = shift_jis_conv(title[:offset], encoding) return (title1, title2) def _read_fixed(f, n): """Read a string of a fixed length from a file.""" s = f.read(n) if len(s) != n: raise eof, f return s def _read_long_string(f): """Read a string prefixed with a 32-bit length from a file.""" length = struct.unpack("= 964: return unpack_icon_sys(data[:964]) return None def load_ems(self, f): """Load EMS (.psu) save files.""" cluster_size = 1024 dirent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) dotent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) dotdotent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) if (not mode_is_dir(dirent[0]) or not mode_is_dir(dotent[0]) or not mode_is_dir(dotdotent[0]) or dirent[2] < 2): raise corrupt, ("Not a EMS (.psu) save file.", f) dirent[2] -= 2 self.set_directory(dirent) for i in range(dirent[2]): ent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH)) if not mode_is_file(ent[0]): raise subdir, f flen = ent[2] self.set_file(i, ent, _read_fixed(f, flen)) _read_fixed(f, round_up(flen, cluster_size) - flen) def save_ems(self, f): cluster_size = 1024 dirent = self.dirent[:] dirent[2] += 2 f.write(pack_dirent(dirent)) f.write(pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS, 0, 0, dirent[3], 0, 0, dirent[3], 0, "."))) f.write(pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS, 0, 0, dirent[3], 0, 0, dirent[3], 0, ".."))) for i in range(dirent[2] - 2): (ent, data) = self.get_file(i) f.write(pack_dirent(ent)) if not mode_is_file(ent[0]): # print ent # print hex(ent[0]) raise error, "Directory has a subdirectory." f.write(data) f.write("\0" * (round_up(len(data), cluster_size) - len(data))) f.flush() def _load_max_drive_2(self): (length, s) = self._compressed self._compressed = None if lzari == None: raise error, ("The lzari module is needed to " " decompress MAX Drive saves.") s = lzari.decode(s, length, "decompressing " + self.dirent[8] + ": ") dirlen = self.dirent[2] timestamp = self.dirent[3] off = 0 for i in range(dirlen): if len(s) - off < 36: raise eof, f (l, name) = struct.unpack(" 0 and title[0][-1] != ' ': iconsysname = title[0] + " " + title[1].strip() else: iconsysname = title[0] + title[1].rstrip() s = "" dirent = self.dirent for i in range(dirent[2]): (ent, data) = self.get_file(i) if not mode_is_file(ent[0]): raise error, "Non-file in save file." s += struct.pack("= 2 and dotent[8] == "." and dotdotent[8] == ".."): return "psu" return None # # Set up tables of illegal and problematic characters in file names. # _bad_filename_chars = ("".join(map(chr, range(32))) + "".join(map(chr, range(127, 256)))) _bad_filename_repl = "_" * len(_bad_filename_chars) if os.name in ["nt", "os2", "ce"]: _bad_filename_chars += '<>:"/\\|?*' _bad_filename_repl += "()_'_____" _bad_filename_chars2 = _bad_filename_chars + " " _bad_filename_repl2 = _bad_filename_repl + "_" else: _bad_filename_chars += "/" _bad_filename_repl += "_" _bad_filename_chars2 = _bad_filename_chars + "?*'&|:[<>] \\\"" _bad_filename_repl2 = _bad_filename_repl + "______(())___" _filename_trans = string.maketrans(_bad_filename_chars, _bad_filename_repl); _filename_trans2 = string.maketrans(_bad_filename_chars2, _bad_filename_repl2); def fix_filename(filename): """Replace illegal or problematic characters from a filename.""" return filename.translate(_filename_trans) def make_longname(dirname, sf): """Return a string containing a verbose filename for a save file.""" icon_sys = sf.get_icon_sys() title = "" if icon_sys != None: title = icon_sys_title(icon_sys, "ascii") title = title[0] + " " + title[1] title = " ".join(title.split()) crc = binascii.crc32("") for (ent, data) in sf: crc = binascii.crc32(data, crc) if len(dirname) >= 12 and (dirname[0:2] in ("BA", "BJ", "BE", "BK")): if dirname[2:6] == "DATA": title = "" else: #dirname = dirname[2:6] + dirname[7:12] dirname = dirname[2:12] return fix_filename("%s %s (%08X)" % (dirname, title, crc & 0xFFFFFFFF))