mymc/mymc.py
2016-10-01 16:25:49 -05:00

813 lines
20 KiB
Python
Executable File

#
# mymc.py
#
# By Ross Ridge
# Public Domain
#
"""A utility for manipulating PS2 memory card images."""
_SCCS_ID = "@(#) mysc mymc.py 1.12 12/10/04 19:09:16\n"[:-1]
import sys
import os
import time
import optparse
import textwrap
import binascii
import string
from errno import EEXIST, EIO
import ps2mc
import ps2save
from ps2mc_dir import *
from round import *
import verbuild
class subopt_error(Exception):
pass
io_error = ps2mc.io_error
if os.name == "nt":
import codecs
class file_wrap(object):
""" wrap a file-like object with a new encoding attribute. """
def __init__(self, f, encoding):
object.__setattr__(self, "_f", f)
object.__setattr__(self, "encoding", encoding)
def __getattribute__(self, name):
if name == "encoding":
return object.__getattribute__(self, name)
return getattr(object.__getattribute__(self, "_f"),
name)
def __setattr__(self, name, value):
if name == "encoding":
raise TypeError, "readonly attribute"
return setattr(object.__getattribute__(self, "_f"),
name, value)
for name in ["stdin", "stdout", "stderr"]:
f = getattr(sys, name)
cur = getattr(f, "encoding", None)
if cur == "ascii" or cur == None:
f = file_wrap(f, "mbcs")
else:
try:
codecs.lookup(cur)
except LookupError:
f = file_wrap(f, "mbcs")
setattr(sys, name, f)
if os.name in ["nt", "os2", "ce"]:
from glob import glob
else:
# assume globing is done by the shell
glob = lambda pattern: [pattern]
def glob_args(args, globfn):
ret = []
for arg in args:
match = globfn(arg)
if len(match) == 0:
ret.append(arg)
else:
ret += match
return ret
def _copy(fout, fin):
"""copy the contents of one file to another"""
while True:
s = fin.read(1024)
if s == "":
break
fout.write(s)
def do_ls(cmd, mc, opts, args, opterr):
mode_bits = "rwxpfdD81C+KPH4"
if len(args) == 0:
args = ["/"]
out = sys.stdout
args = glob_args(args, mc.glob)
for dirname in args:
dir = mc.dir_open(dirname)
try:
if len(args) > 1:
sys.stdout.write("\n" + dirname + ":\n")
for ent in dir:
mode = ent[0]
if (mode & DF_EXISTS) == 0:
continue
for bit in range(0, 15):
if mode & (1 << bit):
out.write(mode_bits[bit])
else:
out.write("-")
if opts.creation_time:
tod = ent[3]
else:
tod = ent[6]
tm = time.localtime(tod_to_time(tod))
out.write(" %7d %04d-%02d-%02d"
" %02d:%02d:%02d %s\n"
% (ent[2],
tm.tm_year, tm.tm_mon, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
ent[8]))
finally:
dir.close()
def do_add(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
if opts.directory != None:
mc.chdir(opts.directory)
for src in glob_args(args, glob):
f = open(src, "rb")
dest = os.path.basename(src)
out = mc.open(dest, "wb")
_copy(out, f)
out.close()
f.close()
def do_extract(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
if opts.directory != None:
mc.chdir(opts.directory)
close_out = False
out = None
if opts.output != None:
if opts.use_stdout:
opterr("The -o and -p options are mutually exclusive.")
dont_close_out = True
out = file(opts.output, "wb")
elif opts.use_stdout:
out = sys.stdout
try:
for filename in glob_args(args, mc.glob):
f = mc.open(filename, "rb")
try:
if out != None:
_copy(out, f)
continue
a = filename.split("/")
o = file(a[-1], "wb")
try:
_copy(o, f)
finally:
o.close()
finally:
f.close()
finally:
if close_out:
out.close()
def do_mkdir(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Directory required.")
for filename in args:
mc.mkdir(filename)
def do_remove(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
for filename in args:
mc.remove(filename)
def do_import(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
args = glob_args(args, glob)
if opts.directory != None and len(args) > 1:
opterr("The -d option can only be used with a"
"single savefile.")
for filename in args:
sf = ps2save.ps2_save_file()
f = file(filename, "rb")
try:
ftype = ps2save.detect_file_type(f)
f.seek(0)
if ftype == "max":
sf.load_max_drive(f)
elif ftype == "psu":
sf.load_ems(f)
elif ftype == "cbs":
sf.load_codebreaker(f)
elif ftype == "sps":
sf.load_sharkport(f)
elif ftype == "npo":
raise io_error, (EIO, "nPort saves"
" are not supported.",
filename)
else:
raise io_error, (EIO, "Save file format not"
" recognized", filename)
finally:
f.close()
dirname = opts.directory
if dirname == None:
dirname = sf.get_directory()[8]
print "Importing", filename, "to", dirname
if not mc.import_save_file(sf, opts.ignore_existing,
opts.directory):
print (filename + ": already in memory card image,"
" ignored.")
#re_num = re.compile("[0-9]+")
def do_export(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Directory name required")
if opts.overwrite_existing and opts.ignore_existing:
opterr("The -i and -f options are mutually exclusive.")
args = glob_args(args, mc.glob)
if opts.output_file != None:
if len(args) > 1:
opterr("Only one directory can be exported"
" when the -o option is used.")
if opts.longnames:
opterr("The -o and -l options are mutually exclusive.")
if opts.directory != None:
os.chdir(opts.directory)
for dirname in args:
sf = mc.export_save_file(dirname)
filename = opts.output_file
if opts.longnames:
filename = (ps2save.make_longname(dirname, sf)
+ "." + opts.type)
if filename == None:
filename = dirname + "." + opts.type
if not opts.overwrite_existing:
exists = True
try:
open(filename, "rb").close()
except EnvironmentError:
exists = False
if exists:
if opts.ignore_existing:
continue
raise io_error(EEXIST, "File exists", filename)
f = file(filename, "wb")
try:
print "Exporing", dirname, "to", filename
if opts.type == "max":
sf.save_max_drive(f)
else:
sf.save_ems(f)
finally:
f.close()
def do_delete(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Directory required.")
for dirname in args:
mc.rmdir(dirname)
def do_setmode(cmd, mc, opts, args, opterr):
set_mask = 0
clear_mask = ~0
for (opt, bit) in [(opts.read, DF_READ),
(opts.write, DF_WRITE),
(opts.execute, DF_EXECUTE),
(opts.protected, DF_PROTECTED),
(opts.psx, DF_PSX),
(opts.pocketstation, DF_POCKETSTN),
(opts.hidden, DF_HIDDEN)]:
if opt != None:
if opt:
set_mask |= bit
else:
clear_mask ^= bit
value = opts.hex_value
if set_mask == 0 and clear_mask == ~0:
if value == None:
opterr("At least one option must be given.")
if value.startswith("0x") or value.startswith("0X"):
value = int(value[2:], 16)
else:
value = int(value, 16)
else:
if value != None:
opterr("The -X option can't be combined with"
" other options.")
for arg in glob_args(args, mc.glob):
ent = mc.get_dirent(arg)
if value == None:
ent[0] = (ent[0] & clear_mask) | set_mask
# print "new %04x" % ent[0]
else:
ent[0] = value
mc.set_dirent(arg, ent)
def _get_ps2_title(mc, enc):
s = mc.get_icon_sys(".");
if s == None:
return None
a = ps2save.unpack_icon_sys(s)
return ps2save.icon_sys_title(a, enc)
def _get_psx_title(mc, savename, enc):
mode = mc.get_mode(savename)
if mode == None or not mode_is_file(mode):
return None
f = mc.open(savename)
s = f.read(128)
if len(s) != 128:
return None
(magic, icon, blocks, title) = struct.unpack("<2sBB64s28x32x", s)
if magic != "SC":
return None
return [ps2save.shift_jis_conv(zero_terminate(title), enc), ""]
def do_dir(cmd, mc, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
f = None
dir = mc.dir_open("/")
try:
for ent in list(dir)[2:]:
dirmode = ent[0]
if not mode_is_dir(dirmode):
continue
dirname = "/" + ent[8]
mc.chdir(dirname)
length = mc.dir_size(".");
enc = getattr(sys.stdout, "encoding", None)
if dirmode & DF_PSX:
title = _get_psx_title(mc, ent[8], enc)
else:
title = _get_ps2_title(mc, enc)
if title == None:
title = ["Corrupt", ""]
protection = dirmode & (DF_PROTECTED | DF_WRITE)
if protection == 0:
protection = "Delete Protected"
elif protection == DF_WRITE:
protection = "Not Protected"
elif protection == DF_PROTECTED:
protection = "Copy & Delete Protected"
else:
protection = "Copy Protected"
type = None
if dirmode & DF_PSX:
type = "PlayStation"
if dirmode & DF_POCKETSTN:
type = "PocketStation"
if type != None:
protection = type
print "%-32s %s" % (ent[8], title[0])
print ("%4dKB %-25s %s"
% (length / 1024, protection, title[1]))
print
finally:
if f != None:
f.close()
dir.close()
free = mc.get_free_space() / 1024
if free > 999999:
free = "%d,%03d,%03d" % (free / 1000000, free / 1000 % 1000,
free % 1000)
elif free > 999:
free = "%d,%03d" % (free / 1000, free % 1000)
else:
free = "%d" % free
print free + " KB Free"
def do_df(cmd, mc, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
print mc.f.name + ":", mc.get_free_space(), "bytes free."
def do_check(cmd, mc, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
if mc.check():
print "No errors found."
return 0
return 1
def do_format(cmd, mcname, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
pages_per_card = ps2mc.PS2MC_STANDARD_PAGES_PER_CARD
if opts.clusters != None:
pages_per_cluster = (ps2mc.PS2MC_CLUSTER_SIZE
/ ps2mc.PS2MC_STANDARD_PAGE_SIZE)
pages_per_card = opts.clusters * pages_per_cluster
params = (not opts.no_ecc,
ps2mc.PS2MC_STANDARD_PAGE_SIZE,
ps2mc.PS2MC_STANDARD_PAGES_PER_ERASE_BLOCK,
pages_per_card)
if not opts.overwrite_existing:
exists = True
try:
file(mcname, "rb").close()
except EnvironmentError:
exists = False
if exists:
raise io_error, (EEXIST, "file exists", mcname)
f = file(mcname, "w+b")
try:
ps2mc.ps2mc(f, True, params).close()
finally:
f.close()
def do_gui(cmd, mcname, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
try:
import gui
except ImportError:
write_error(None, "GUI not available")
return 1
gui.run(mcname)
return 0
def do_create_pad(cmd, mc, opts, args, opterr):
length = mc.clusters_per_card
if len(args) > 1:
length = int(args[1])
pad = "\0" * mc.cluster_size
f = mc.open(args[0], "wb")
try:
for i in xrange(length):
f.write(pad)
finally:
f.close()
def do_frob(cmd, mc, opts, args, opterr):
mc.write_superblock()
_trans = string.maketrans("".join(map(chr, range(32))), " " * 32)
def _print_bin(base, s):
for off in range(0, len(s), 16):
print "%04X" % (base + off),
a = s[off : off + 16]
for b in a:
print "%02X" % ord(b),
print "", a.translate(_trans)
def _print_erase_block(mc, n):
ppb = mc.pages_per_erase_block
base = n * ppb
for i in range(ppb):
s = mc.read_page(base + i)
_print_bin(i * mc.page_size, s)
print
def do_print_good_blocks(cmd, mc, opts, args, opterr):
print "good_block2:"
_print_erase_block(mc, mc.good_block2)
print "good_block1:"
_print_erase_block(mc, mc.good_block1)
def do_ecc_check(cmd, mc, opts, args, opterr):
for i in range(mc.clusters_per_card * mc.pages_per_cluster):
try:
mc.read_page(i)
except ps2mc.ecc_error:
print "bad: %05x" % i
opt = optparse.make_option
#
# Each value in the dictionary is a tuple consisting of:
# - function implementing the command
# - mode to use to open the ps2 save file
# - help description of the command
# - list of options supported by the command
#
cmd_table = {
"ls": (do_ls, "rb",
"[directory ...]",
"List the contents of a directory.",
[opt("-c", "--creation-time", action="store_true",
help = "Display creation times.")]),
"extract": (do_extract, "rb",
"filename ...",
"Extract files from the memory card.",
[opt("-o", "--output", metavar = "FILE",
help = 'Extract file to "FILE".'),
opt("-d", "--directory",
help = 'Extract files from "DIRECTORY".'),
opt("-p", "--use-stdout", action="store_true",
help = "Extract files to standard output.")]),
"add": (do_add, "r+b",
"filename ...",
"Add files to the memory card.",
[opt("-d", "--directory",
help = 'Add files to "directory".')]),
"mkdir": (do_mkdir, "r+b",
"directory ...",
"Make directories.",
[]),
"remove": (do_remove, "r+b",
"filename ...",
"Remove files and directories.",
[]),
"import": (do_import, "r+b",
"savefile ...",
"Import save files into the memory card.",
[opt("-i", "--ignore-existing", action="store_true",
help = ("Ignore files that already exist"
"on the image.")),
opt("-d", "--directory", metavar="DEST",
help = 'Import to "DEST".')]),
"export": (do_export, "rb",
"directory ...",
"Export save files from the memory card.",
[opt("-f", "--overwrite-existing", action = "store_true",
help = "Overwrite any save files already exported."),
opt("-i", "--ignore-existing", action = "store_true",
help = "Ingore any save files already exported."),
opt("-o", "--output-file", metavar = "filename",
help = 'Use "filename" as the name of the save file.'),
opt("-d", "--directory", metavar = "directory",
help = 'Export save files to "directory".'),
opt("-l", "--longnames", action = "store_true",
help = ("Generate longer, more descriptive,"
" filenames.")),
opt("-p", "--ems", action = "store_const",
dest = "type", const = "psu", default = "psu",
help = "Use the EMS .psu save file format. [default]"),
opt("-m", "--max-drive", action = "store_const",
dest = "type", const = "max",
help = "Use the MAX Drive save file format.")]),
"delete": (do_delete, "r+b",
"dirname ...",
"Recursively delete a directory (save file).",
[]),
"set": (do_setmode, "r+b",
"filename ...",
"Set mode flags on files and directories",
[opt("-p", "--protected", action="store_true",
help = "Set copy protected flag"),
opt("-P", "--psx", action="store_true",
help = "Set PSX flag"),
opt("-K", "--pocketstation", action="store_true",
help = "Set PocketStation flag"),
opt("-H", "--hidden", action="store_true",
help = "Set hidden flag"),
opt("-r", "--read", action="store_true",
help = "Set read allowed flag"),
opt("-w", "--write", action="store_true",
help = "Set write allowed flag"),
opt("-x", "--execute", action="store_true",
help = "Set executable flag"),
opt("-X", "--hex-value", metavar="mode",
help = 'Set mode to "mode".')]),
"clear": (do_setmode, "r+b",
"filename ...",
"Clear mode flags on files and directories",
[opt("-p", "--protected", action="store_false",
help = "Clear copy protected flag"),
opt("-P", "--psx", action="store_false",
help = "Clear PSX flag"),
opt("-K", "--pocketstation", action="store_false",
help = "Clear PocketStation flag"),
opt("-H", "--hidden", action="store_false",
help = "Clear hidden flag"),
opt("-r", "--read", action="store_false",
help = "Clear read allowed flag"),
opt("-w", "--write", action="store_false",
help = "Clear write allowed flag"),
opt("-x", "--execute", action="store_false",
help = "Clear executable flag"),
opt("-X", dest="hex_value", default=None,
help = optparse.SUPPRESS_HELP)]),
"dir": (do_dir, "rb",
None,
"Display save file information.",
[]),
"df": (do_df, "rb",
None,
"Display the amount free space.",
[]),
"check": (do_check, "rb",
"",
"Check for file system errors.",
[]),
"format": (do_format, None,
"",
"Creates a new memory card image.",
[opt("-c", "--clusters", type="int",
help = "Size in clusters of the memory card."),
opt("-f", "--overwrite-existing", action="store_true",
help = "Overwrite any existing file"),
opt("-e", "--no-ecc", action="store_true",
help = "Create an image without ECC")]),
"gui": (do_gui, None,
"",
"Starts the graphical user interface.",
[]),
}
#
# secret commands for debugging purposes.
#
debug_cmd_table = {
"frob": (do_frob, "r+b",
"",
None,
[]),
"print_good_blocks": (do_print_good_blocks, "rb",
"",
None,
[]),
"ecc_check": (do_ecc_check, "rb",
"",
None,
[]),
"create_pad": (do_create_pad, "r+b",
"",
None,
[])
}
del opt # clean up name space
def write_error(filename, msg):
if filename == None:
sys.stderr.write(msg + "\n")
else:
sys.stderr.write(filename + ": " + msg + "\n")
class suboption_parser(optparse.OptionParser):
def exit(self, status = 0, msg = None):
if msg:
sys.stderr.write(msg)
raise subopt_error, status
class my_help_formatter(optparse.IndentedHelpFormatter):
"""A better formatter for optparser's help message"""
def format_description(self, description):
if not description:
return ""
desc_width = self.width - self.current_indent
indent = " " * self.current_indent
lines = []
for line in description.split('\n'):
ii = indent
si = indent
if line.startswith("\t"):
line = line[1:]
ii = indent + " " * 4
si = ii + " " * line.find(":") + 2
line = textwrap.fill(line, desc_width,
initial_indent = ii,
subsequent_indent = si)
lines.append(line)
return "\n".join(lines) + "\n"
def main():
prog = sys.argv[0].decode(sys.getdefaultencoding(), "replace")
usage = "usage: %prog [-ih] memcard.ps2 command [...]"
description = ("Manipulate PS2 memory card images.\n\n"
"Supported commands: ")
for cmd in sorted(cmd_table.keys()):
description += "\n " + cmd + ": " + cmd_table[cmd][3]
version = ("mymc "
+ verbuild.MYMC_VERSION_MAJOR
+ "." + verbuild.MYMC_VERSION_BUILD
+ " (" + _SCCS_ID + ")")
optparser = optparse.OptionParser(prog = prog, usage = usage,
description = description,
version = version,
formatter = my_help_formatter())
optparser.add_option("-D", dest = "debug", action = "store_true",
default = False, help = optparse.SUPPRESS_HELP)
optparser.add_option("-i", "--ignore-ecc", action = "store_true",
help = "Ignore ECC errors while reading.")
optparser.disable_interspersed_args()
(opts, args) = optparser.parse_args()
if len(args) == 0:
try:
import gui
except ImportError:
gui = None
if gui != None:
gui.run()
sys.exit(0)
if len(args) < 2:
optparser.error("Incorrect number of arguments.")
if opts.debug:
cmd_table.update(debug_cmd_table)
cmd = args[1]
if cmd not in cmd_table:
optparser.error('Command "%s" not recognized.' % cmd)
(fn, mode, usage_args, description, optlist) = cmd_table[cmd]
usage = "%prog"
if len(optlist) > 0:
usage += " [options]"
if usage_args != None:
usage += " " + usage_args
subprog = prog + " memcard.ps2 " + cmd
subopt_parser = suboption_parser(prog = subprog, usage = usage,
description = description,
option_list = optlist)
subopt_parser.disable_interspersed_args()
f = None
mc = None
ret = 0
mcname = args[0]
try:
(subopts, subargs) = subopt_parser.parse_args(args[2:])
try:
if mode == None:
ret = fn(cmd, mcname, subopts, subargs,
subopt_parser.error)
else:
f = file(mcname, mode)
mc = ps2mc.ps2mc(f, opts.ignore_ecc)
ret = fn(cmd, mc, subopts, subargs,
subopt_parser.error)
finally:
if mc != None:
mc.close()
if f != None:
# print "f.close()"
f.close()
except EnvironmentError, value:
if getattr(value, "filename", None) != None:
write_error(value.filename, value.strerror)
ret = 1
elif getattr(value, "strerror", None) != None:
write_error(mcname, value.strerror)
ret = 1
else:
# something weird
raise
if opts.debug:
raise
except subopt_error, (ret,):
pass
except (ps2mc.error, ps2save.error), value:
fn = getattr(value, "filename", None)
if fn == None:
fn = mcname
write_error(fn, str(value))
if opts.debug:
raise
ret = 1
if ret == None:
ret = 0
return ret
sys.exit(main())