Skip to content
Switch branches/tags
Go to file
Cannot retrieve contributors at this time
# Copyright (C) 2016 Pietro Albini <>
# Adapted from by LRN on the CivFanatics forum
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <>.
import builtins
import struct
import os
EXPECTED_MAGIC = b"\x06\x00\x00\x00FPK_\x00\x00"
class FpkError(Exception):
class FpkArchive:
"""This class represents a .fpk archive"""
def __init__(self, file):
self.file = file
# Seek to the start of the file
# Load the file header
magic, item_count = struct.unpack("<10s I", + 4))
if magic != EXPECTED_MAGIC:
raise FpkError("Wrong magic string: %s" % magic)
if not item_count:
raise FpkError("Empty archive!")
self.items = {}
first_item_name = None
last_item_name = None
for i in range(item_count):
# Read the length of the name
name_len = struct.unpack("<I",[0]
if not name_len:
raise FpkError("Empty file name (index %s)" % i)
# Read the name
name ="utf-8")
if first_item_name is None:
first_item_name = name
# Check if the first bytes are 0es
extra_bytes = struct.unpack("<I",[0] + 4
if != b"\x00" * extra_bytes:
raise FpkError("File start 0es of item %s are not present" % i)
# Get the file size and offset
file_size, file_offset = struct.unpack("<II", + 4))
if not file_size:
raise FpkError("Empty file size for item %s" % i)
if not file_offset:
raise FpkError("Empty file offset for item %s" % i)
# Sanity check for the offset
if last_item_name is not None:
last_item = self.items[last_item_name]
# Get the hypotetic offset
hypotetic_offset = last_item["offset"] + last_item["size"]
if hypotetic_offset % 4 != 0:
hypotetic_offset += 4 - hypotetic_offset % 4 # Alignment
if hypotetic_offset != file_offset:
raise FpkError(
"Wrong offset for item %s: %s instead of %s" % (
i, file_offset, hypotetic_offset
# Store the item
self.items[name] = {
"name": name,
"size": file_size,
"offset": file_offset,
last_item_name = name
# Get and align the current position
current_pos = file.tell()
if current_pos % 4 != 0:
current_pos += 4 - current_pos % 4
# Check if the current position is right
expected_pos = self.items[first_item_name]["offset"]
if expected_pos != current_pos:
raise FpkError(
"Wrong offset for item 1: %s instead of %s",
current_pos, expected_pos
def files(self):
"""Get a list of all the files"""
return list(self.items.keys())
def extract(self, name, dest, buffer=1024 * 16):
"""Extract a file from the archive"""
if name not in self.items:
raise NameError("File not in the archive: %s" % name)
if not os.path.exists(dest):
item = self.items[name]
with, item["name"]), "wb") as out:
# Go to the right archive spot["offset"])
# Copy the data to the new file
pos = 0
while pos < item["size"]:
data = + buffer, item["size"] - pos))
pos += len(data)
def close(self):
"""Close the file"""
def open(name):
"""Open an existing .fpk archive"""
if not os.path.exists(name):
raise ValueError("File %s not found" % name)
file =, "rb")
return FpkArchive(file)