diff options
author | Nicolas Iooss | 2015-05-24 11:14:24 +0800 |
---|---|---|
committer | Nicolas Iooss | 2015-05-24 11:14:24 +0800 |
commit | 4f1ba6bbd688961f6890d4ad48cf48adafaf3911 (patch) | |
tree | 54156bf2c95df8419ee30fcaed680c76c12da31a /mpak.py | |
download | aur-4f1ba6bbd688961f6890d4ad48cf48adafaf3911.tar.gz |
Migrate funguloids 1.06.4-7 to AUR4
Diffstat (limited to 'mpak.py')
-rwxr-xr-x | mpak.py | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/mpak.py b/mpak.py new file mode 100755 index 000000000000..5445336b3cc2 --- /dev/null +++ b/mpak.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python +""" + MPAK package handling utility + Version 1.4 (Python-implementation) + Copyright (c) 2008, Mika Halttunen. <http://www.mhgames.co.nr> + + This command line tool allows creation and extraction of MPAK (.mpk) packages used + in several of my games. MPAK is a simple WAD-like file format of mine, that allows storing + the game data in one single .mpk file. I originally had a very crude command line program + bit like this one (written in C++), and decided to write this Python-implementation as + an Python-programming excercise. So, it's my first Python program. :) + + Version history: + v1.4: The first Python version + v1.0 -- v1.31: The original C++ implementation +""" +import getopt, sys +import os +import traceback +import struct +import binascii +import fnmatch +import shutil +from ctypes import c_uint32 + +def usage(): + """ + Prints the program usage. + """ + print("MPAK package handling utility") + print("Version 1.4 (Python-implementation)") + print("Copyright (c) 2008, Mika Halttunen.") + print("") + print("Usage:", sys.argv[0],"[switch]","-f pakfile.mpk","[file1]","[file2]", "[...]", "[fileN]") + print("where [switch] is one of the following:") + print(" -f, --file=FILE Use package FILE") + print(" -c, --create Create a new package with files 'file1' to 'fileN'") + print(" -l, --list List the files from given package") + print(" -e, --extract Extract all files (by default) from given package. If you") + print(" want to only extract some specific files, you can name") + print(" them individually, and/or use wildcards (i.e. *.png).") + print(" You can supply path where to extract with -p.") + print(" -p, --path=PATH Extract to PATH (created if doesn't exist)") + print(" -h, --help Print this usage text") + print("") + + +def errorMsg(msg): + """ + Prints an error message and exits. + """ + try: + pos = traceback.extract_stack(limit=2) + if pos: + print("ERROR: In %s:%s, line %d:" % (pos[0][0], pos[0][2], pos[0][1])) + else: + print("ERROR:") + print("\t",msg) + except: + if __debug__: + traceback.print_exc() + pass + sys.exit(2) + + +def separator(): + """ + Prints the separator line. + """ + print("-"*75) + + +def computeCRC(file, offset): + """ + Computes the CRC32 for the file, starting at given offset. + """ + f = open(file, "rb") + f.seek(offset) + crc = 0 + + # Compute a running CRC32 for the file in 16kb chunks + while True: + buffer = f.read(16384) + if not buffer: break # End of file + + crc = binascii.crc32(buffer, crc) + + f.close() + return crc + + +def createPackage(pakFile, files): + """ + Creates a new MPAK package. + + This copies the given files into the new package file, writes the file table + and closes the package. MPAK doesn't support adding new files to an existing + package. + """ + print("Creating '%s'.." % pakFile) + if len(files) < 1: errorMsg("No input files specified!") + separator() + + # Open the package file for writing + out = open(pakFile, "wb") + + # Write the header and reserve 4+4 bytes for CRC32 and file table offset + out.write(b"MPK1") + out.write(b"."*8) + + # Write each file + package = { "fileNames": [], "fileOffsets": [] } + count = 0 + for file in files: + # Get the basename + filename = os.path.basename(file) + print(" <%s..." % filename, end=' ') + package["fileNames"].append(filename) + + # Get the file size in bytes + stats = os.stat(file) + + # Store the current offset + package["fileOffsets"].append(out.tell()) + + # Open the file and copy its contents + f = open(file, "rb") + shutil.copyfileobj(f, out, 16384) + f.close() + + print("OK. (%.1f KB)" % (stats.st_size / 1024.0)) + count = count + 1 + + separator() + + # Grab the file table offset and write the table + ftOffset = out.tell() + + # Write the number of files + out.write(struct.pack("<L", count)) + + # Write the file information + for i in range(count): + # File name length + length = len(package["fileNames"][i]) + 1 + out.write(struct.pack("B", length)) + # File name, plus one zero for the C++ implementation + out.write(package["fileNames"][i].encode('ascii')) + out.write(struct.pack("B", 0)) + # File offset + out.write(struct.pack("<L", package["fileOffsets"][i])) + + # Update the header to have the correct file table offset + out.seek(8) + out.write(struct.pack("<L", ftOffset)) + + # Compute the CRC32 and write it to the header + out.flush() + crc32 = c_uint32(0) + crc32.value = computeCRC(pakFile, 8) + out.seek(4) + out.write(struct.pack("<L", crc32.value)) + + print("Added %d files to %s" % (count, pakFile)) + print("Package '%s' created successfully. CRC32 checksum is %s." % (pakFile, hex(crc32.value))) + out.close() + + +def readPackage(pakFile): + """ + Opens the given MPAK package, reads its information and stores it to a + package dictionary. Returns the dictionary. + """ + packageInfo = { "filename": pakFile } + + f = open(pakFile, 'rb') + if f.read(4) != b"MPK1": errorMsg("Unsupported file format!") + + # Read the CRC32 checksum and the file table header offset + buffer = f.read(8) + crc32, headerOffset = struct.unpack("<LL", buffer) + crc32 = int(crc32) + packageInfo["crc"] = crc32 + + # Check that the CRC32 matches + checksum = c_uint32(0) + checksum.value = computeCRC(pakFile, 8) + if checksum.value != crc32: + f.close() + errorMsg("Checksum doesn't match; perhaps a corrupted package?") + + # Seek to the file table, and read the number of files + f.seek(headerOffset) + numFiles = struct.unpack("<L", f.read(4))[0] + packageInfo["numFiles"] = numFiles + + # Read the file information + fileNames = [] + fileOffsets = [] + for i in range(numFiles): + namelen = struct.unpack("B", f.read(1))[0] + file = f.read(namelen) + offset = struct.unpack("<L", f.read(4))[0] + fileNames.append(file[:-1].decode('ascii')) # Remove the trailing null character + fileOffsets.append(offset) + + # Compute the file sizes from the offsets + fileSizes = [] + for i in range(numFiles-1): + fileSizes.append(fileOffsets[i+1] - fileOffsets[i]) + fileSizes.append(headerOffset - fileOffsets[numFiles-1]) + + # Store the information + packageInfo["fileNames"] = fileNames + packageInfo["fileOffsets"] = fileOffsets + packageInfo["fileSizes"] = fileSizes + f.close() + return packageInfo + + +def listPackage(pakFile): + """ + Lists the contents of a MPAK package. + """ + print("Listing '%s'.." % pakFile) + package = readPackage(pakFile) + + # Print the listing + numFiles = package["numFiles"] + print("'%s' (CRC32: %s) contains %d files:" % (pakFile, hex(package["crc"]), numFiles)) + print("") + print(" NUM : FILE : SIZE(KB) : OFFSET") + separator() + for i in range(numFiles): + print(" %3d : %30s : %-10.1f : (at %s)" % (i+1, package["fileNames"][i], package["fileSizes"][i] / 1024.0, hex(package["fileOffsets"][i]))) + + separator() + print(" NUM : FILE : SIZE(KB) : OFFSET") + + +def extractPackage(pakFile, path, filters): + """ + Extracts files from a package to given path. + + By default extracts all the files. Can be given list of wildcards (i.e. *.png) to + extract only the files that match given wildcards. Wildcards can also be file names + from the package. + + The given path is created if it doesn't exist. + If the path is just a single directory name, it's assumed to exist in the current + working directory. + """ + print("Extracting files from '%s' to %s.." % (pakFile, path)) + package = readPackage(pakFile) + + # Try to create the path if it doesn't exist + path = os.path.abspath(path) + if not os.path.exists(path): + print("Path",path,"doesn't exist, creating it..") + try: + os.makedirs(path) + except: + errorMsg("Unable to create directory " + path + "!"); + + separator() + + # Open the file, and extract all the individual files from it + count = 0 + f = open(pakFile, "rb") + for i in range(package["numFiles"]): + # Test if the file name matches the given wildcard + if len(filters) > 0: + for filter in filters: + if fnmatch.fnmatch(package["fileNames"][i], filter): + break + else: continue + + print(" >%s..." % package["fileNames"][i], end=' ') + # Seek to the correct offset + f.seek(package["fileOffsets"][i]) + + # Open a new file for writing, and write the file out in 16kb chunks + out = open(os.path.join(path, package["fileNames"][i]), "wb") + bytesWritten = 0 + bytesTotal = package["fileSizes"][i]; + while True: + # We have to watch not to write too much + bytesLeft = bytesTotal - bytesWritten + if bytesLeft > 16384: bytesLeft = 16384 + + buffer = f.read(bytesLeft) + out.write(buffer) + bytesWritten = bytesWritten + bytesLeft + + if bytesWritten == bytesTotal: + break + + out.close() + print("OK.") + count = count + 1 + + f.close() + separator() + print("%d (of %d) files extracted to %s." % (count, package["numFiles"], path)) + + +def main(): + """ + Main method. + """ + try: + # Get the optiosn + opts, args = getopt.getopt(sys.argv[1:], "f:clep:h", ["file=", "create", "list", "extract", "path=", "help"]) + except getopt.GetoptError as err: + # Print the program usage and exit + print("ERROR: " + str(err)) + usage() + sys.exit(2) + + extractPath = os.getcwd() + pakFile = None + action = None + + # Handle the options + for o, a in opts: + if o in ("-f", "--file"): + pakFile = a # Grab the pakfile + elif o in ("-c", "--create"): + action = "create" + elif o in ("-l", "--list"): + action = "list" + elif o in ("-e", "--extract"): + action = "extract" + elif o in ("-p", "--path"): + extractPath = a # Grab the path + elif o in ("-h", "--help"): + usage() + sys.exit() + else: + assert False, "Unhandled option" + + # Check that we got a pakfile + if pakFile == None: + usage() + sys.exit(2) + + if action == "create": createPackage(pakFile, args) + elif action == "list": listPackage(pakFile) + elif action == "extract": extractPackage(pakFile, extractPath, args) + else: usage() + sys.exit() + +if __name__ == "__main__": + main() + |