diff --git a/ps2mc.py b/ps2mc.py index 9d146cf..cc7f668 100755 --- a/ps2mc.py +++ b/ps2mc.py @@ -7,13 +7,21 @@ """Manipulate PS2 memory card images.""" -_SCCS_ID = "@(#) mymc ps2mc.py 1.12 23/07/06 19:48:03\n" - import sys import array import struct -from errno import EACCES, ENOENT, EEXIST, ENOTDIR, EISDIR, EROFS, ENOTEMPTY,\ - ENOSPC, EIO, EBUSY, EINVAL +from errno import ( + EACCES, + ENOENT, + EEXIST, + ENOTDIR, + EISDIR, + ENOTEMPTY, + ENOSPC, + EIO, + EBUSY, + EINVAL, +) import fnmatch import traceback @@ -22,6 +30,8 @@ from ps2mc_dir import * import ps2save +_SCCS_ID = "@(#) mymc ps2mc.py 1.12 23/07/06 19:48:03\n" + PS2MC_MAGIC = "Sony PS2 Memory Card Format " PS2MC_FAT_ALLOCATED_BIT = 0x80000000 PS2MC_FAT_CHAIN_END = 0xFFFFFFFF diff --git a/ps2save.py b/ps2save.py index f36ba13..87ad316 100755 --- a/ps2save.py +++ b/ps2save.py @@ -3,22 +3,34 @@ # # By Ross Ridge # Public Domain -# +# # A simple interface for working with various PS2 save file formats. # -_SCCS_ID = "@(#) mymc ps2save.py 1.9 23/07/06 19:46:30\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 round import round_up +from ps2mc_dir import ( + DF_0400, + DF_DIR, + DF_EXISTS, + DF_FILE, + DF_RWX, + PS2MC_DIRENT_LENGTH, + mode_is_file, + mode_is_dir, + pack_dirent, + tod_to_time, + tod_now, + unpack_tod, + unpack_dirent, + zero_terminate, +) from sjistab import shift_jis_normalize_table try: @@ -26,6 +38,8 @@ except ImportError: lzari = None +_SCCS_ID = "@(#) mymc ps2save.py 1.9 23/07/06 19:46:30\n" + PS2SAVE_MAX_MAGIC = b"Ps2PowerSave" PS2SAVE_SPS_MAGIC = b"\x0d\0\0\0SharkPortSave" PS2SAVE_CBS_MAGIC = b"CFU\0" @@ -33,63 +47,70 @@ # 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] +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): + def __init__(self, msg, f=None): fn = None - if f != None: + if f is not 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): + def __init__(self, f=None): corrupt.__init__(self, "Unexpected EOF", f) + class subdir(corrupt): - def __init__(self, f = None): + 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. @@ -166,7 +187,8 @@ def __init__(self, f = None): u'\u30fc': u'-', } -def shift_jis_conv(src, encoding = None): + +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 @@ -174,8 +196,8 @@ def shift_jis_conv(src, encoding = None): graphically similar characters are used to replace characters not exactly representable in the desired encoding. """ - - if encoding == None: + + if encoding is None: encoding = sys.getdefaultencoding() if encoding == "shift_jis": return src @@ -190,14 +212,15 @@ def shift_jis_conv(src, encoding = None): 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 @@ -217,14 +240,17 @@ def rc4_crypt(s, t): # 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 = 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]) @@ -237,9 +263,10 @@ def unpack_icon_sys(s): a[17] = zero_terminate(a[17]) return a -def icon_sys_title(icon_sys, encoding = None): + +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] encoding = sys.getdefaultencoding() if not encoding else encoding @@ -247,30 +274,33 @@ def icon_sys_title(icon_sys, encoding = None): title1 = shift_jis_conv(title[:offset], encoding).decode(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(" 0 and title[0][-1] != ' ': iconsysname = title[0] + " " + title[1].strip() @@ -430,16 +472,22 @@ def save_max_drive(self, f): s += data s += b"\0" * (round_up(len(s) + 8, 16) - 8 - len(s)) length = len(s) - progress = "compressing " + dirent[8] + ": " + progress = "compressing " + dirent[8] + ": " compressed = lzari.encode(s, progress) - hdr = struct.pack("<12sL32s32sLLL", PS2SAVE_MAX_MAGIC, - 0, dirent[8], iconsysname, - len(compressed) + 4, dirent[2], length) + hdr = struct.pack( + "<12sL32s32sLLL", + PS2SAVE_MAX_MAGIC, + 0, dirent[8], iconsysname, + len(compressed) + 4, dirent[2], length + ) crc = binascii.crc32(hdr) crc = binascii.crc32(compressed, crc) - f.write(struct.pack("<12sL32s32sLLL", PS2SAVE_MAX_MAGIC, - crc & 0xFFFFFFFF, dirent[8], iconsysname, - len(compressed) + 4, dirent[2], length)) + f.write(struct.pack( + "<12sL32s32sLLL", + PS2SAVE_MAX_MAGIC, + crc & 0xFFFFFFFF, dirent[8], iconsysname, + len(compressed) + 4, dirent[2], length + )) f.write(compressed) f.flush() @@ -451,9 +499,10 @@ def load_codebreaker(self, f): if hlen < 92 + 32: raise corrupt("Header lengh too short.", f) (dlen, flen, dirname, created, modified, d44, d48, dirmode, - d50, d54, d58, title) \ - = struct.unpack("= 2 - and dotent[8] == "." and dotdotent[8] == ".."): + 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_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_repl += "()_'_____" _bad_filename_chars2 = _bad_filename_chars + " " - _bad_filename_repl2 = _bad_filename_repl + "_" + _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 + "______(())___" + _bad_filename_repl2 = _bad_filename_repl + "______(())___" + +_filename_trans = str.maketrans(_bad_filename_chars, _bad_filename_repl) +_filename_trans2 = str.maketrans(_bad_filename_chars2, _bad_filename_repl2) -_filename_trans = str.maketrans(_bad_filename_chars, _bad_filename_repl); -_filename_trans2 = str.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: + if icon_sys is not None: title = icon_sys_title(icon_sys, "ascii") title = title[0] + " " + title[1] title = " ".join(title.split()) @@ -625,9 +688,7 @@ def make_longname(dirname, sf): if dirname[2:6] == "DATA": title = "" else: - #dirname = dirname[2:6] + dirname[7:12] + # dirname = dirname[2:6] + dirname[7:12] dirname = dirname[2:12] - return fix_filename("%s %s (%08X)" - % (dirname, title, crc & 0xFFFFFFFF)) - + return fix_filename("%s %s (%08X)" % (dirname, title, crc & 0xFFFFFFFF)) diff --git a/round.py b/round.py index 6c7a5c6..fe7d469 100755 --- a/round.py +++ b/round.py @@ -9,12 +9,15 @@ _SCCS_ID = "@(#) mymc round.py 1.4 23/07/06 02:44:14\n" + def div_round_up(a, b): return int((a + b - 1) / b) + def round_up(a, b): return int((a + b - 1) / b * b) + def round_down(a, b): return int(a / b * b)