Skip to content

Instantly share code, notes, and snippets.

@a-sakharov
Created August 24, 2024 18:29
Show Gist options
  • Select an option

  • Save a-sakharov/5ccb418f8037855eeeecac6fc0345ed6 to your computer and use it in GitHub Desktop.

Select an option

Save a-sakharov/5ccb418f8037855eeeecac6fc0345ed6 to your computer and use it in GitHub Desktop.
Plants vs Zombies main.pak unpacker
#!/usr/bin/env python3
# Plants vs Zombies main.pak unpacker
import os
from calendar import timegm
from datetime import datetime, timezone
INPUT_FILE="main.pak"
OUTPUT_DIR="main_unpacked"
HEADER_MAGICK=0xBAC04AC0
HEADER_VERSION=0x00000000
EPOCH_AS_FILETIME = 116444736000000000 # January 1, 1970 as filetime
HUNDREDS_OF_NS = 10000000
def windows_filetime_to_datetime(filetime: int) -> datetime:
"""
Converts a Windows filetime number to a Python datetime. The new
datetime object is timezone-naive but is equivalent to tzinfo=utc.
"""
# Get seconds and remainder in terms of Unix epoch
s, ns100 = divmod(filetime - EPOCH_AS_FILETIME, HUNDREDS_OF_NS)
# Convert to datetime object, with remainder as microseconds.
return datetime.fromtimestamp(s, timezone.utc).replace(microsecond=(ns100 // 10))
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
in_data = open(INPUT_FILE, 'rb').read()
in_data = list(map(lambda byte: byte^0xF7, in_data)) # decode xor "cypher"
# open(OUTPUT_DIR + "/main.pak.decoded", 'wb').write(bytes(in_data)) # save decoded file
data_header = bytes(in_data[:8])
in_data = in_data[8:]
data_magick = int.from_bytes(data_header[0:4], byteorder='little')
data_version = int.from_bytes(data_header[4:8], byteorder='little')
if data_magick != HEADER_MAGICK:
print("Invalid header. Magick is {:x}, should be {:x}".format(data_magick, HEADER_MAGICK))
exit(-1)
if data_version != HEADER_VERSION:
print("Invalid header. Version is {:x}, should be {:x}".format(data_version, HEADER_VERSION))
exit(-1)
class DataItemRecord:
name = ""
file_size = 0
date = 0
def __init__(self, name: str, file_size: int, date: datetime):
self.name = name
self.file_size = file_size
self.date = date
item_records = []
catalog_size = 0
while True:
record_type = in_data[catalog_size]
if record_type != 0:
catalog_size += 1
break
name_length = in_data[catalog_size+1]
name = ''.join([chr(x) for x in in_data[catalog_size+2:catalog_size+2+name_length]])
file_size = int.from_bytes(bytes(in_data[catalog_size+2+name_length+0:catalog_size+2+name_length+4]), 'little')
file_date = int.from_bytes(bytes(in_data[catalog_size+2+name_length+4:catalog_size+2+name_length+4+8]), 'little')
item_records.append(DataItemRecord(name, file_size, windows_filetime_to_datetime(file_date)))
catalog_size+=2+name_length+4+8
content_offset = 0
for record in item_records:
print("{:08x} {} {}".format(record.file_size, record.date, record.name))
output_name = OUTPUT_DIR + '/' + record.name.replace('\\', '/')
dirname = os.path.dirname(os.path.abspath(output_name))
if not os.path.exists(dirname):
os.makedirs(dirname)
res_file = open(output_name, 'wb')
res_file.write(bytes(in_data[catalog_size+content_offset:catalog_size+content_offset+record.file_size]))
res_file.close()
os.utime(output_name, (record.date.timestamp(), record.date.timestamp()))
content_offset += record.file_size
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment