ines-replace.py with syntax coloring

This HTML file was generated with Kalle's syntaxcolor.py


   1"""
   2Replaces PRG-ROM or CHR-ROM in an iNES ROM file (.nes).
   3By Kalle (http://qalle.net)
   4Source of format: http://wiki.nesdev.com/w/index.php/INES
   5"""
   6
   7import sys
   8import os.path
   9
  10HELP_TEXT = """\
  11Replaces PRG-ROM or CHR-ROM in an iNES ROM file (.nes).
  12
  13Arguments: SOURCE MODE NEW_DATA TARGET
  14    SOURCE:   name of iNES ROM file to be read
  15    MODE:     "p" or "P" = replace PRG-ROM, "c" or "C" = replace CHR-ROM
  16    NEW_DATA: name of PRG-ROM or CHR-ROM file to be read (must be same size
  17              as original PRG-ROM/CHR-ROM)
  18    TARGET:   name of iNES ROM file to be written\
  19"""
  20
  21INES_HEADER_SIZE = 0x10
  22INES_IDENTIFIER = b"NES\x1a"
  23
  24# file buffer size in bytes (1 or greater; larger values are faster but take up
  25# more RAM)
  26BUFFER_SIZE = 2 ** 20
  27
  28def INES_get_info(handle):
  29    """Read an iNES file, validate it and return essential info."""
  30
  31    fileSize = handle.seek(0, 2)
  32    if fileSize < INES_HEADER_SIZE:
  33        exit("Invalid iNES file (smaller than correct header size).")
  34
  35    handle.seek(0)
  36    header = handle.read(INES_HEADER_SIZE)
  37
  38    if header[:4] != INES_IDENTIFIER:
  39        exit("Invalid iNES file (invalid identifier).")
  40
  41    prgSize = header[4] * 16 * 1024
  42    chrSize = header[5] * 8 * 1024
  43    trainerSize = (header[6] >> 2 & 0x1) * 512
  44
  45    if fileSize < INES_HEADER_SIZE + trainerSize + prgSize + chrSize:
  46        exit("Invalid iNES file (too small).")
  47
  48    return {
  49        "prgAddr": INES_HEADER_SIZE + trainerSize,
  50        "prgSize": prgSize,
  51        "chrAddr": INES_HEADER_SIZE + trainerSize + prgSize,
  52        "chrSize": chrSize,
  53    }
  54
  55def chunk_copy(sourceHnd, bytesLeft, targetHnd):
  56    """Copies part of source file to target file in BUFFER_SIZE chunks.
  57    Returns number of bytes copied.
  58    """
  59
  60    while bytesLeft > 0:
  61        chunkSize = min(bytesLeft, BUFFER_SIZE)
  62        targetHnd.write(sourceHnd.read(chunkSize))
  63        bytesLeft -= chunkSize
  64
  65    return targetHnd.tell()
  66
  67def INES_replace(sourceHnd, mode, newDataHnd, targetHnd):
  68    """Copy sourceHnd to targetHnd, with PRG-ROM (if mode = "P") or CHR-ROM
  69    (if mode = "C") replaced with contents of newDataHnd."""
  70
  71    INESInfo = INES_get_info(sourceHnd)
  72
  73    # check size of replacement
  74    replacementSize = INESInfo["prgSize" if mode == "P" else "chrSize"]
  75    if newDataHnd.seek(0, 2) != replacementSize:
  76        exit("New PRG-ROM/CHR-ROM data must be the same size as the old one.")
  77
  78    # copy bytes before the replacement
  79    sourceHnd.seek(0)
  80    targetHnd.seek(0)
  81    size = INESInfo["prgAddr" if mode == "P" else "chrAddr"]
  82    chunk_copy(sourceHnd, size, targetHnd)
  83
  84    # copy the replacement
  85    newDataHnd.seek(0)
  86    chunk_copy(newDataHnd, replacementSize, targetHnd)
  87
  88    # copy bytes after the replacement, if necessary
  89    if mode == "P":
  90        sourceHnd.seek(INESInfo["chrAddr"])
  91        chunk_copy(sourceHnd, INESInfo["chrSize"], targetHnd)
  92
  93def main():
  94    if len(sys.argv) != 5:
  95        exit(HELP_TEXT)
  96
  97    (source, mode, newData, target) = sys.argv[1:]
  98    mode = mode.upper()
  99
 100    if mode not in ("P", "C"):
 101        exit("Argument error. Run without arguments to see help.")
 102
 103    # all input files must be different
 104
 105    try:
 106        if os.path.samefile(source, newData):
 107            exit("All input files must be different.")
 108    except OSError:
 109        pass
 110
 111    try:
 112        if os.path.samefile(source, target):
 113            exit("All input files must be different.")
 114    except OSError:
 115        pass
 116
 117    try:
 118        if os.path.samefile(newData, target):
 119            exit("All input files must be different.")
 120    except OSError:
 121        pass
 122
 123    # copy and replace
 124    try:
 125        with \
 126        open(source, "rb") as sourceHnd, \
 127        open(newData, "rb") as newDataHnd, \
 128        open(target, "wb") as targetHnd:
 129            INES_replace(sourceHnd, mode, newDataHnd, targetHnd)
 130    except FileNotFoundError:
 131        exit("File or path not found.")
 132    except PermissionError:
 133        exit("Permission denied.")
 134    except OSError:
 135        exit("File read/write error.")
 136
 137if __name__ == "__main__":
 138    main()