Task 508: .PAK File Format
Task 508: .PAK File Format
1. List of Properties of the .PAK File Format (Quake Engine Variant)
The .PAK file format (as used in the Quake game engine, which is one of the most common and well-documented variants) is a simple uncompressed archive format for storing game resources. It consists of a header followed by file data and a directory of entries. All integers are little-endian. The intrinsic properties (structural fields and derived attributes) are:
- Magic Signature: A 4-byte ASCII string 'PACK' identifying the file type.
- Directory Offset: A 4-byte unsigned integer indicating the byte offset from the start of the file to the directory section.
- Directory Size: A 4-byte unsigned integer indicating the total size in bytes of the directory section.
- Number of Files: Derived as Directory Size / 64 (since each directory entry is fixed at 64 bytes).
- File Entries: An array of directory entries, each containing:
- Filename: A 56-byte null-terminated and null-padded string (UTF-8 encoded, paths use forward slashes, max 55 characters plus null).
- File Offset: A 4-byte unsigned integer indicating the byte offset from the start of the file to the file's data.
- File Size: A 4-byte unsigned integer indicating the size in bytes of the file's data.
File data is stored raw and uncompressed at the specified offsets, with no additional metadata per file beyond the directory entry.
2. Two Direct Download Links for .PAK Files
- https://github.com/rictorres/quake-ktx-server/raw/master/id1/pak0.pak (Quake shareware data archive, ~18 MB)
- https://github.com/rictorres/quake-ktx-server/raw/master/id1/pak1.pak (Quake full game data archive extension, ~34 MB)
3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .PAK File Parsing
Drag and Drop .PAK File
Drop .PAK file here
4. Python Class for .PAK File Handling
import struct
import os
class PakFile:
def __init__(self, filepath=None):
self.magic = None
self.dir_offset = None
self.dir_size = None
self.num_files = None
self.entries = [] # List of dicts: {'name': str, 'offset': int, 'size': int, 'data': bytes (optional)}
if filepath:
self.read(filepath)
def read(self, filepath):
with open(filepath, 'rb') as f:
data = f.read()
self._parse(data)
def _parse(self, data):
self.magic = data[0:4].decode('ascii')
if self.magic != 'PACK':
raise ValueError('Invalid PAK file: Missing "PACK" signature.')
self.dir_offset, self.dir_size = struct.unpack('<II', data[4:12])
self.num_files = self.dir_size // 64
for i in range(self.num_files):
entry_offset = self.dir_offset + i * 64
name = data[entry_offset:entry_offset+56].decode('utf-8').rstrip('\x00')
offset, size = struct.unpack('<II', data[entry_offset+56:entry_offset+64])
self.entries.append({'name': name, 'offset': offset, 'size': size, 'data': data[offset:offset+size]})
def print_properties(self):
print(f"Magic Signature: {self.magic}")
print(f"Directory Offset: {self.dir_offset}")
print(f"Directory Size: {self.dir_size}")
print(f"Number of Files: {self.num_files}")
print("File Entries:")
for entry in self.entries:
print(f"- Filename: {entry['name']}")
print(f" Offset: {entry['offset']}")
print(f" Size: {entry['size']}")
def write(self, filepath):
if not self.entries:
raise ValueError('No entries to write.')
file_data = b''
dir_entries = b''
current_offset = 12 # Header size
for entry in self.entries:
if 'data' not in entry:
raise ValueError(f'Missing data for file: {entry["name"]}')
file_data += entry['data']
name_padded = entry['name'].encode('utf-8').ljust(56, b'\x00')
dir_entries += name_padded + struct.pack('<II', current_offset, entry['size'])
current_offset += entry['size']
dir_offset = 12 + len(file_data)
dir_size = len(dir_entries)
header = b'PACK' + struct.pack('<II', dir_offset, dir_size)
with open(filepath, 'wb') as f:
f.write(header + file_data + dir_entries)
# Example usage:
# pak = PakFile('example.pak')
# pak.print_properties()
# pak.write('new.pak')
5. Java Class for .PAK File Handling
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.util.*;
public class PakFile {
private String magic;
private int dirOffset;
private int dirSize;
private int numFiles;
private List<PakEntry> entries = new ArrayList<>();
public static class PakEntry {
String name;
int offset;
int size;
byte[] data;
PakEntry(String name, int offset, int size, byte[] data) {
this.name = name;
this.offset = offset;
this.size = size;
this.data = data;
}
}
public PakFile(String filepath) throws IOException {
read(filepath);
}
public PakFile() {}
private void read(String filepath) throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filepath));
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
magic = new String(data, 0, 4, "ASCII");
if (!"PACK".equals(magic)) {
throw new IOException("Invalid PAK file: Missing 'PACK' signature.");
}
dirOffset = buffer.getInt(4);
dirSize = buffer.getInt(8);
numFiles = dirSize / 64;
for (int i = 0; i < numFiles; i++) {
int entryOffset = dirOffset + i * 64;
String name = new String(data, entryOffset, 56, "UTF-8").replaceAll("\0.*", "").trim();
int offset = buffer.getInt(entryOffset + 56);
int size = buffer.getInt(entryOffset + 60);
byte[] fileData = Arrays.copyOfRange(data, offset, offset + size);
entries.add(new PakEntry(name, offset, size, fileData));
}
}
public void printProperties() {
System.out.println("Magic Signature: " + magic);
System.out.println("Directory Offset: " + dirOffset);
System.out.println("Directory Size: " + dirSize);
System.out.println("Number of Files: " + numFiles);
System.out.println("File Entries:");
for (PakEntry entry : entries) {
System.out.println("- Filename: " + entry.name);
System.out.println(" Offset: " + entry.offset);
System.out.println(" Size: " + entry.size);
}
}
public void write(String filepath) throws IOException {
if (entries.isEmpty()) {
throw new IOException("No entries to write.");
}
try (RandomAccessFile raf = new RandomAccessFile(filepath, "rw");
FileChannel channel = raf.getChannel()) {
ByteBuffer header = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN);
header.put("PACK".getBytes("ASCII"));
int currentOffset = 12;
ByteBuffer fileDataBuffer = ByteBuffer.allocate(entries.stream().mapToInt(e -> e.size).sum());
ByteBuffer dirBuffer = ByteBuffer.allocate(entries.size() * 64).order(ByteOrder.LITTLE_ENDIAN);
for (PakEntry entry : entries) {
if (entry.data == null) {
throw new IOException("Missing data for file: " + entry.name);
}
fileDataBuffer.put(entry.data);
byte[] namePadded = Arrays.copyOf(entry.name.getBytes("UTF-8"), 56);
dirBuffer.put(namePadded).putInt(currentOffset).putInt(entry.size);
currentOffset += entry.size;
}
int dirOffset = 12 + fileDataBuffer.position();
header.putInt(dirOffset).putInt(dirBuffer.capacity());
header.flip();
fileDataBuffer.flip();
dirBuffer.flip();
channel.write(header);
channel.write(fileDataBuffer);
channel.write(dirBuffer);
}
}
// Example usage:
// public static void main(String[] args) throws IOException {
// PakFile pak = new PakFile("example.pak");
// pak.printProperties();
// pak.write("new.pak");
// }
}
6. JavaScript Class for .PAK File Handling
class PakFile {
constructor(buffer = null) {
this.magic = null;
this.dirOffset = null;
this.dirSize = null;
this.numFiles = null;
this.entries = []; // Array of {name: str, offset: int, size: int, data: Uint8Array}
if (buffer) {
this.read(buffer);
}
}
read(buffer) {
const dataView = new DataView(buffer);
this.magic = String.fromCharCode(...new Uint8Array(buffer, 0, 4));
if (this.magic !== 'PACK') {
throw new Error('Invalid PAK file: Missing "PACK" signature.');
}
this.dirOffset = dataView.getUint32(4, true);
this.dirSize = dataView.getUint32(8, true);
this.numFiles = this.dirSize / 64;
for (let i = 0; i < this.numFiles; i++) {
const entryOffset = this.dirOffset + i * 64;
const nameBytes = new Uint8Array(buffer, entryOffset, 56);
const name = new TextDecoder('utf-8').decode(nameBytes).replace(/\0/g, '').trim();
const offset = dataView.getUint32(entryOffset + 56, true);
const size = dataView.getUint32(entryOffset + 60, true);
const data = new Uint8Array(buffer, offset, size);
this.entries.push({name, offset, size, data});
}
}
printProperties() {
console.log(`Magic Signature: ${this.magic}`);
console.log(`Directory Offset: ${this.dirOffset}`);
console.log(`Directory Size: ${this.dirSize}`);
console.log(`Number of Files: ${this.numFiles}`);
console.log('File Entries:');
this.entries.forEach(entry => {
console.log(`- Filename: ${entry.name}`);
console.log(` Offset: ${entry.offset}`);
console.log(` Size: ${entry.size}`);
});
}
write() {
if (this.entries.length === 0) {
throw new Error('No entries to write.');
}
let fileDataLength = 0;
this.entries.forEach(entry => {
if (!entry.data) {
throw new Error(`Missing data for file: ${entry.name}`);
}
fileDataLength += entry.data.byteLength;
});
const dirSize = this.entries.length * 64;
const dirOffset = 12 + fileDataLength;
const totalSize = 12 + fileDataLength + dirSize;
const buffer = new ArrayBuffer(totalSize);
const dataView = new DataView(buffer);
new TextEncoder().encode('PACK').forEach((byte, i) => dataView.setUint8(i, byte));
dataView.setUint32(4, dirOffset, true);
dataView.setUint32(8, dirSize, true);
let currentOffset = 12;
this.entries.forEach(entry => {
new Uint8Array(buffer, currentOffset, entry.size).set(entry.data);
currentOffset += entry.size;
});
this.entries.forEach((entry, i) => {
const entryOffset = dirOffset + i * 64;
const nameBytes = new TextEncoder().encode(entry.name);
for (let j = 0; j < 56; j++) {
dataView.setUint8(entryOffset + j, j < nameBytes.length ? nameBytes[j] : 0);
}
dataView.setUint32(entryOffset + 56, entry.offset, true);
dataView.setUint32(entryOffset + 60, entry.size, true);
});
return buffer; // Return ArrayBuffer for saving (e.g., via Blob)
}
}
// Example usage:
// const fs = require('fs'); // For Node.js
// const buffer = fs.readFileSync('example.pak').buffer;
// const pak = new PakFile(buffer);
// pak.printProperties();
// const newBuffer = pak.write();
// fs.writeFileSync('new.pak', new Uint8Array(newBuffer));
7. C Code for .PAK File Handling (Using Struct for Class-Like Behavior)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define PAK_MAGIC "PACK"
#define ENTRY_SIZE 64
#define NAME_SIZE 56
typedef struct {
char name[NAME_SIZE];
uint32_t offset;
uint32_t size;
uint8_t *data;
} PakEntry;
typedef struct {
char magic[5];
uint32_t dir_offset;
uint32_t dir_size;
uint32_t num_files;
PakEntry *entries;
} PakFile;
PakFile* pak_create() {
PakFile* pak = (PakFile*)malloc(sizeof(PakFile));
memset(pak, 0, sizeof(PakFile));
return pak;
}
void pak_free(PakFile* pak) {
if (pak->entries) {
for (uint32_t i = 0; i < pak->num_files; i++) {
free(pak->entries[i].data);
}
free(pak->entries);
}
free(pak);
}
int pak_read(PakFile* pak, const char* filepath) {
FILE* f = fopen(filepath, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = (uint8_t*)malloc(file_size);
fread(data, 1, file_size, f);
fclose(f);
memcpy(pak->magic, data, 4);
pak->magic[4] = '\0';
if (strcmp(pak->magic, PAK_MAGIC) != 0) {
free(data);
return -1;
}
memcpy(&pak->dir_offset, data + 4, 4);
memcpy(&pak->dir_size, data + 8, 4);
pak->num_files = pak->dir_size / ENTRY_SIZE;
pak->entries = (PakEntry*)malloc(pak->num_files * sizeof(PakEntry));
for (uint32_t i = 0; i < pak->num_files; i++) {
uint32_t entry_offset = pak->dir_offset + i * ENTRY_SIZE;
strncpy(pak->entries[i].name, (char*)(data + entry_offset), NAME_SIZE);
pak->entries[i].name[NAME_SIZE - 1] = '\0';
for (int j = strlen(pak->entries[i].name) - 1; j >= 0; j--) {
if (pak->entries[i].name[j] == '\0') pak->entries[i].name[j] = '\0';
else break;
}
memcpy(&pak->entries[i].offset, data + entry_offset + NAME_SIZE, 4);
memcpy(&pak->entries[i].size, data + entry_offset + NAME_SIZE + 4, 4);
pak->entries[i].data = (uint8_t*)malloc(pak->entries[i].size);
memcpy(pak->entries[i].data, data + pak->entries[i].offset, pak->entries[i].size);
}
free(data);
return 0;
}
void pak_print_properties(const PakFile* pak) {
printf("Magic Signature: %s\n", pak->magic);
printf("Directory Offset: %u\n", pak->dir_offset);
printf("Directory Size: %u\n", pak->dir_size);
printf("Number of Files: %u\n", pak->num_files);
printf("File Entries:\n");
for (uint32_t i = 0; i < pak->num_files; i++) {
printf("- Filename: %s\n", pak->entries[i].name);
printf(" Offset: %u\n", pak->entries[i].offset);
printf(" Size: %u\n", pak->entries[i].size);
}
}
int pak_write(const PakFile* pak, const char* filepath) {
if (pak->num_files == 0) return -1;
FILE* f = fopen(filepath, "wb");
if (!f) return -1;
uint32_t current_offset = 12;
for (uint32_t i = 0; i < pak->num_files; i++) {
if (!pak->entries[i].data) {
fclose(f);
return -1;
}
current_offset += pak->entries[i].size;
}
uint32_t dir_offset = 12 + (current_offset - 12);
uint32_t dir_size = pak->num_files * ENTRY_SIZE;
fwrite(PAK_MAGIC, 1, 4, f);
fwrite(&dir_offset, 4, 1, f);
fwrite(&dir_size, 4, 1, f);
current_offset = 12;
for (uint32_t i = 0; i < pak->num_files; i++) {
fwrite(pak->entries[i].data, 1, pak->entries[i].size, f);
}
for (uint32_t i = 0; i < pak->num_files; i++) {
char name_padded[NAME_SIZE] = {0};
strncpy(name_padded, pak->entries[i].name, NAME_SIZE - 1);
fwrite(name_padded, 1, NAME_SIZE, f);
fwrite(¤t_offset, 4, 1, f);
fwrite(&pak->entries[i].size, 4, 1, f);
current_offset += pak->entries[i].size;
}
fclose(f);
return 0;
}
// Example usage:
// int main() {
// PakFile* pak = pak_create();
// if (pak_read(pak, "example.pak") == 0) {
// pak_print_properties(pak);
// pak_write(pak, "new.pak");
// }
// pak_free(pak);
// return 0;
// }