3 MPAK package handling utility
4 Version 1.4 (Python-implementation)
5 Copyright (c) 2008, Mika Halttunen. <http://www.mhgames.co.nr>
7 This command line tool allows creation and extraction of MPAK (.mpk) packages used
8 in several of my games. MPAK is a simple WAD-like file format of mine, that allows storing
9 the game data in one single .mpk file. I originally had a very crude command line program
10 bit like this one (written in C++), and decided to write this Python-implementation as
11 an Python-programming excercise. So, it's my first Python program. :)
14 v1.4: The first Python version
15 v1.0 -- v1.31: The original C++ implementation
24 from ctypes
import c_uint32
28 Prints the program usage.
30 print "MPAK package handling utility"
31 print "Version 1.4 (Python-implementation)"
32 print "Copyright (c) 2008, Mika Halttunen."
34 print "Usage:", sys
.argv
[0],"[switch]","-f pakfile.mpk","[file1]","[file2]", "[...]", "[fileN]"
35 print "where [switch] is one of the following:"
36 print " -f, --file=FILE Use package FILE"
37 print " -c, --create Create a new package with files 'file1' to 'fileN'"
38 print " -l, --list List the files from given package"
39 print " -e, --extract Extract all files (by default) from given package. If you"
40 print " want to only extract some specific files, you can name"
41 print " them individually, and/or use wildcards (i.e. *.png)."
42 print " You can supply path where to extract with -p."
43 print " -p, --path=PATH Extract to PATH (created if doesn't exist)"
44 print " -h, --help Print this usage text"
50 Prints an error message and exits.
53 pos
= traceback
.extract_stack(limit
=2)
55 print "ERROR: In %s:%s, line %d:" % (pos
[0][0], pos
[0][2], pos
[0][1])
67 Prints the separator line.
72 def computeCRC(file, offset
):
74 Computes the CRC32 for the file, starting at given offset.
80 # Compute a running CRC32 for the file in 16kb chunks
82 buffer = f
.read(16384)
83 if buffer == "": break # End of file
85 crc
= binascii
.crc32(buffer, crc
)
91 def createPackage(pakFile
, files
):
93 Creates a new MPAK package.
95 This copies the given files into the new package file, writes the file table
96 and closes the package. MPAK doesn't support adding new files to an existing
99 print "Creating '%s'.." % pakFile
100 if len(files
) < 1: errorMsg("No input files specified!")
103 # Open the package file for writing
104 out
= open(pakFile
, "wb")
106 # Write the header and reserve 4+4 bytes for CRC32 and file table offset
111 package
= { "fileNames": [], "fileOffsets": [] }
115 filename
= os
.path
.basename(file)
116 print " <",filename
,"...",
117 package
["fileNames"].append(filename
)
119 # Get the file size in bytes
120 stats
= os
.stat(file)
122 # Store the current offset
123 package
["fileOffsets"].append(out
.tell())
125 # Open the file and copy its contents
127 shutil
.copyfileobj(f
, out
, 16384)
130 print "OK. (%.1f KB)" % (stats
.st_size
/ 1024.0)
135 # Grab the file table offset and write the table
136 ftOffset
= out
.tell()
138 # Write the number of files
139 out
.write(struct
.pack("<L", count
))
141 # Write the file information
142 for i
in range(count
):
144 length
= len(package
["fileNames"][i
]) + 1
145 out
.write(struct
.pack("B", length
))
146 # File name, plus one zero for the C++ implementation
147 out
.write(package
["fileNames"][i
])
148 out
.write(struct
.pack("B", 0))
150 out
.write(struct
.pack("<L", package
["fileOffsets"][i
]))
152 # Update the header to have the correct file table offset
154 out
.write(struct
.pack("<L", ftOffset
))
156 # Compute the CRC32 and write it to the header
159 crc32
.value
= computeCRC(pakFile
, 8)
161 out
.write(struct
.pack("<L", crc32
.value
))
163 print "Added %d files to %s" % (count
, pakFile
)
164 print "Package '%s' created successfully. CRC32 checksum is %s." % (pakFile
, hex(crc32
.value
))
168 def readPackage(pakFile
):
170 Opens the given MPAK package, reads its information and stores it to a
171 package dictionary. Returns the dictionary.
173 packageInfo
= { "filename": pakFile
}
175 f
= open(pakFile
, 'rb')
176 if f
.read(4) != "MPK1": errorMsg("Unsupported file format!")
178 # Read the CRC32 checksum and the file table header offset
180 crc32
, headerOffset
= struct
.unpack("<LL", buffer)
182 packageInfo
["crc"] = crc32
184 # Check that the CRC32 matches
185 checksum
= c_uint32(0)
186 checksum
.value
= computeCRC(pakFile
, 8)
187 if checksum
.value
!= crc32
:
189 errorMsg("Checksum doesn't match; perhaps a corrupted package?")
191 # Seek to the file table, and read the number of files
193 numFiles
= struct
.unpack("<L", f
.read(4))[0]
194 packageInfo
["numFiles"] = numFiles
196 # Read the file information
199 for i
in range(numFiles
):
200 namelen
= struct
.unpack("B", f
.read(1))[0]
201 file = f
.read(namelen
)
202 offset
= struct
.unpack("<L", f
.read(4))[0]
203 fileNames
.append(file[:-1]) # Remove the trailing null character
204 fileOffsets
.append(offset
)
206 # Compute the file sizes from the offsets
208 for i
in range(numFiles
-1):
209 fileSizes
.append(fileOffsets
[i
+1] - fileOffsets
[i
])
210 fileSizes
.append(headerOffset
- fileOffsets
[numFiles
-1])
212 # Store the information
213 packageInfo
["fileNames"] = fileNames
214 packageInfo
["fileOffsets"] = fileOffsets
215 packageInfo
["fileSizes"] = fileSizes
220 def listPackage(pakFile
):
222 Lists the contents of a MPAK package.
224 print "Listing '%s'.." % pakFile
225 package
= readPackage(pakFile
)
228 numFiles
= package
["numFiles"]
229 print "'%s' (CRC32: %s) contains %d files:" % (pakFile
, hex(package
["crc"]), numFiles
)
231 print " NUM : FILE : SIZE(KB) : OFFSET"
233 for i
in range(numFiles
):
234 print " %3d : %30s : %-10.1f : (at %s)" % (i
+1, package
["fileNames"][i
], package
["fileSizes"][i
] / 1024.0, hex(package
["fileOffsets"][i
]))
237 print " NUM : FILE : SIZE(KB) : OFFSET"
240 def extractPackage(pakFile
, path
, filters
):
242 Extracts files from a package to given path.
244 By default extracts all the files. Can be given list of wildcards (i.e. *.png) to
245 extract only the files that match given wildcards. Wildcards can also be file names
248 The given path is created if it doesn't exist.
249 If the path is just a single directory name, it's assumed to exist in the current
252 print "Extracting files from '%s' to %s.." % (pakFile
, path
)
253 package
= readPackage(pakFile
)
255 # Try to create the path if it doesn't exist
256 path
= os
.path
.abspath(path
)
257 if not os
.path
.exists(path
):
258 print "Path",path
,"doesn't exist, creating it.."
262 errorMsg("Unable to create directory " + path
+ "!");
266 # Open the file, and extract all the individual files from it
268 f
= open(pakFile
, "rb")
269 for i
in range(package
["numFiles"]):
270 # Test if the file name matches the given wildcard
272 for filter in filters
:
273 if fnmatch
.fnmatch(package
["fileNames"][i
], filter):
277 print " >", package
["fileNames"][i
],"...",
278 # Seek to the correct offset
279 f
.seek(package
["fileOffsets"][i
])
281 # Open a new file for writing, and write the file out in 16kb chunks
282 out
= open(os
.path
.join(path
, package
["fileNames"][i
]), "wb")
284 bytesTotal
= package
["fileSizes"][i
];
286 # We have to watch not to write too much
287 bytesLeft
= bytesTotal
- bytesWritten
288 if bytesLeft
> 16384: bytesLeft
= 16384
290 buffer = f
.read(bytesLeft
)
292 bytesWritten
= bytesWritten
+ bytesLeft
294 if bytesWritten
== bytesTotal
:
303 print "%d (of %d) files extracted to %s." % (count
, package
["numFiles"], path
)
312 opts
, args
= getopt
.getopt(sys
.argv
[1:], "f:clep:h", ["file=", "create", "list", "extract", "path=", "help"])
313 except getopt
.GetoptError
, err
:
314 # Print the program usage and exit
315 print "ERROR:", str(err
)
319 extractPath
= os
.getcwd()
325 if o
in ("-f", "--file"):
326 pakFile
= a
# Grab the pakfile
327 elif o
in ("-c", "--create"):
329 elif o
in ("-l", "--list"):
331 elif o
in ("-e", "--extract"):
333 elif o
in ("-p", "--path"):
334 extractPath
= a
# Grab the path
335 elif o
in ("-h", "--help"):
339 assert False, "Unhandled option"
341 # Check that we got a pakfile
346 if action
== "create": createPackage(pakFile
, args
)
347 elif action
== "list": listPackage(pakFile
)
348 elif action
== "extract": extractPackage(pakFile
, extractPath
, args
)
352 if __name__
== "__main__":