Task 519: .PBO File Format

Task 519: .PBO File Format

File Format Specifications for .PBO

The .PBO file format is a packed container used by Bohemia Interactive games (such as Operation Flashpoint, Arma series, and DayZ) to bundle files and folders into a single archive, similar to ZIP or RAR. It supports optional compression for individual files using a run-length encoding variant. The format includes headers for properties and file entries, followed by data blocks, and optional checksums/signatures in later versions (e.g., Arma). The structure is as follows (based on detailed documentation from reliable sources):

Overall Structure:

  • Header section with one or more entries (each  variable length due to zero-terminated strings, but fields are fixed-size ulongs).
  • Optional header extension (product entry with key-value strings).
  • File entries.
  • Terminating null entry.
  • Contiguous data block for file contents.
  • Optional trailing checksum (5 bytes in OFP Elite, 21 bytes in Arma, starting with a null byte).

Entry Structure (for both header and file entries):

  • Filename: ASCIIZ (zero-terminated string, variable length).
  • PackingMethod: unsigned long (4 bytes) – 0x00000000 (uncompressed), 0x43707273 (compressed), 0x56657273 (product/version entry for headers).
  • OriginalSize: unsigned long (4 bytes) – Uncompressed size of the file (0 for headers or uncompressed files where it matches DataSize).
  • Reserved: unsigned long (4 bytes) – Usually 0, reserved for future use.
  • TimeStamp: unsigned long (4 bytes) – Unix timestamp (seconds since Jan 1, 1970), often 0 if not set.
  • DataSize: unsigned long (4 bytes) – Stored size in the data block (matches file size if uncompressed, smaller if compressed).

Header Extension (optional, first entry if present):

  • Starts with a product entry (filename = \0, PackingMethod = 0x56657273, other fields 0).
  • Followed by zero-terminated key\0value\0 pairs (e.g., "prefix\0some_value\0"), ending with \0.

File Entries: One per file, with filename including path (e.g., "folder\file.txt"), followed by the fields above.

Terminating Entry: Filename = \0, all fields 0, marks the end of headers and start of data block.

Data Block: Contiguous raw or compressed data for each file in entry order.

Compression (if PackingMethod = 0x43707273): Run-length encoding with packets, each starting with a format byte, followed by direct bytes or pointers to repeat data. Ends with a 4-byte checksum (additive sum of decompressed data).

Notes: No support for empty folders (folders are implied by filenames). Compression is not used in some versions (e.g., Elite/Arma for streaming). Binarized files inside (e.g., .bin) may require separate decoding.

1. List of All Properties Intrinsic to the File Format's File System

The .PBO acts as a virtual file system, where "properties" refer to the metadata attributes stored for the archive and its contained files/folders. These are extracted from the header entries and extensions. The intrinsic properties are:

Global/Archive Properties (from Header Extension):

  • Key-value pairs (e.g., "prefix" = virtual path prefix, "product" = game version like "OFP: Resistance", "author" = creator name). These are optional and variable; common ones include prefix, version, and custom metadata.

Per-File Properties (from File Entries):

  • Filename: The relative path and name (e.g., "scripts\script.sqf").
  • PackingMethod: Indicates if the file is uncompressed (0x00000000), compressed (0x43707273), or a special header (0x56657273).
  • OriginalSize: The uncompressed size in bytes.
  • Reserved: Reserved field, typically 0.
  • TimeStamp: Unix timestamp of the file.
  • DataSize: The stored (possibly compressed) size in bytes.

Folders are not explicitly stored; they are derived from filenames. The checksum/signature is an archive-level integrity property but not per-file.

Here are two direct download links to ZIP archives containing sample .PBO files from official Bohemia Interactive licensed data packs (these ZIPs include multiple .PBO files from Arma: Cold War Crisis):

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .PBO Dumper

Assuming "ghost blog embedded html javascript" means an HTML page with embedded JavaScript (suitable for embedding in a Ghost CMS blog post), here's a complete self-contained HTML file. It allows drag-and-drop of a .PBO file and dumps the properties (header extensions and per-file properties) to the screen. It uses FileReader for binary parsing in the browser (no server needed). Note: Compression decompression is not implemented here for simplicity, as the task focuses on properties (metadata parsing); data blocks are not read fully.

PBO Properties Dumper

Drag and Drop .PBO File to Dump Properties

Drop .PBO file here

4. Python Class for .PBO Handling

Here's a Python class that can open a .PBO file, parse/decode the properties, print them to console, and write a new .PBO (simple version: creates a new uncompressed PBO from in-memory data; compression not implemented for brevity).

import struct
import os
import time

class PBOHandler:
    def __init__(self):
        self.header_extensions = {}
        self.files = []  # list of dicts: {'filename': str, 'packing_method': int, 'original_size': int, 'reserved': int, 'timestamp': int, 'data_size': int, 'data': bytes}
        self.data_block = b''

    def open(self, filepath):
        with open(filepath, 'rb') as f:
            data = f.read()
        offset = 0
        is_header_extension = False
        while True:
            filename, offset = self._read_asciiz(data, offset)
            packing_method, offset = self._unpack_ulong(data, offset)
            original_size, offset = self._unpack_ulong(data, offset)
            reserved, offset = self._unpack_ulong(data, offset)
            timestamp, offset = self._unpack_ulong(data, offset)
            data_size, offset = self._unpack_ulong(data, offset)

            if filename == b'' and packing_method == 0 and original_size == 0 and reserved == 0 and timestamp == 0 and data_size == 0:
                break

            if packing_method == 0x56657273:
                is_header_extension = True
                continue

            if is_header_extension:
                while True:
                    key, offset = self._read_asciiz(data, offset)
                    if key == b'':
                        break
                    value, offset = self._read_asciiz(data, offset)
                    self.header_extensions[key.decode()] = value.decode()
                is_header_extension = False
                continue

            file_data = data[offset:offset + data_size] if data_size > 0 else b''
            offset += data_size  # Note: For full read, handle compression if packing_method == 0x43707273
            self.files.append({
                'filename': filename.decode(),
                'packing_method': packing_method,
                'original_size': original_size,
                'reserved': reserved,
                'timestamp': timestamp,
                'data_size': data_size,
                'data': file_data
            })

    def print_properties(self):
        print("Header Extensions:")
        for k, v in self.header_extensions.items():
            print(f"{k}: {v}")
        print("\nFiles:")
        for f in self.files:
            print(f"Filename: {f['filename']}")
            print(f"Packing Method: 0x{f['packing_method']:08x}")
            print(f"Original Size: {f['original_size']}")
            print(f"Reserved: {f['reserved']}")
            print(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(f['timestamp']))}")
            print(f"Data Size: {f['data_size']}")
            print("")

    def write(self, filepath):
        with open(filepath, 'wb') as f:
            # Write product entry if extensions present
            if self.header_extensions:
                f.write(b'\x00')  # Empty filename
                f.write(struct.pack('<I', 0x56657273))  # PackingMethod
                f.write(struct.pack('<IIII', 0, 0, 0, 0))
                for k, v in self.header_extensions.items():
                    f.write(k.encode() + b'\x00')
                    f.write(v.encode() + b'\x00')
                f.write(b'\x00')  # End extensions

            # Write file entries
            data_offset = 0
            for file in self.files:
                f.write(file['filename'].encode() + b'\x00')
                f.write(struct.pack('<I', file['packing_method']))
                f.write(struct.pack('<I', file['original_size']))
                f.write(struct.pack('<I', file['reserved']))
                f.write(struct.pack('<I', file['timestamp']))
                f.write(struct.pack('<I', file['data_size']))
                # Data written later

            # Write terminating entry
            f.write(b'\x00')
            f.write(struct.pack('<IIIIII', 0, 0, 0, 0, 0, 0))

            # Write data blocks
            for file in self.files:
                f.write(file['data'])

            # Optional: Add checksum if needed (omitted for simplicity)

    def _read_asciiz(self, data, offset):
        start = offset
        while data[offset] != 0:
            offset += 1
        return data[start:offset], offset + 1

    def _unpack_ulong(self, data, offset):
        return struct.unpack_from('<I', data, offset)[0], offset + 4

# Example usage
if __name__ == '__main__':
    pbo = PBOHandler()
    pbo.open('example.pbo')
    pbo.print_properties()
    pbo.write('new.pbo')

5. Java Class for .PBO Handling

Here's a Java class with similar functionality (uses ByteBuffer for binary parsing).

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.*;

public class PBOHandler {
    private Map<String, String> headerExtensions = new HashMap<>();
    private List<Map<String, Object>> files = new ArrayList<>();

    public void open(String filepath) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filepath));
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        int offset = 0;
        boolean isHeaderExtension = false;

        while (true) {
            String filename = readAsciiz(bb, offset);
            offset += filename.length() + 1;
            int packingMethod = bb.getInt(offset); offset += 4;
            int originalSize = bb.getInt(offset); offset += 4;
            int reserved = bb.getInt(offset); offset += 4;
            int timestamp = bb.getInt(offset); offset += 4;
            int dataSize = bb.getInt(offset); offset += 4;

            if (filename.isEmpty() && packingMethod == 0 && originalSize == 0 && reserved == 0 && timestamp == 0 && dataSize == 0) {
                break;
            }

            if (packingMethod == 0x56657273) {
                isHeaderExtension = true;
                continue;
            }

            if (isHeaderExtension) {
                while (true) {
                    String key = readAsciiz(bb, offset);
                    offset += key.length() + 1;
                    if (key.isEmpty()) break;
                    String value = readAsciiz(bb, offset);
                    offset += value.length() + 1;
                    headerExtensions.put(key, value);
                }
                isHeaderExtension = false;
                continue;
            }

            byte[] fileData = new byte[dataSize];
            bb.position(offset);
            bb.get(fileData);
            offset += dataSize;

            Map<String, Object> fileMap = new HashMap<>();
            fileMap.put("filename", filename);
            fileMap.put("packingMethod", packingMethod);
            fileMap.put("originalSize", originalSize);
            fileMap.put("reserved", reserved);
            fileMap.put("timestamp", timestamp);
            fileMap.put("dataSize", dataSize);
            fileMap.put("data", fileData);
            files.add(fileMap);
        }
    }

    public void printProperties() {
        System.out.println("Header Extensions:");
        headerExtensions.forEach((k, v) -> System.out.println(k + ": " + v));
        System.out.println("\nFiles:");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        for (Map<String, Object> f : files) {
            System.out.println("Filename: " + f.get("filename"));
            System.out.printf("Packing Method: 0x%08X%n", f.get("packingMethod"));
            System.out.println("Original Size: " + f.get("originalSize"));
            System.out.println("Reserved: " + f.get("reserved"));
            System.out.println("Timestamp: " + sdf.format(new Date((int) f.get("timestamp") * 1000L)));
            System.out.println("Data Size: " + f.get("dataSize"));
            System.out.println();
        }
    }

    public void write(String filepath) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filepath)) {
            // Write product entry if extensions
            if (!headerExtensions.isEmpty()) {
                fos.write(0); // Empty filename
                fos.write(intToBytes(0x56657273));
                fos.write(intToBytes(0));
                fos.write(intToBytes(0));
                fos.write(intToBytes(0));
                fos.write(intToBytes(0));
                headerExtensions.forEach((k, v) -> {
                    try {
                        fos.write(k.getBytes());
                        fos.write(0);
                        fos.write(v.getBytes());
                        fos.write(0);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
                fos.write(0);
            }

            // Write file entries
            for (Map<String, Object> file : files) {
                fos.write(((String) file.get("filename")).getBytes());
                fos.write(0);
                fos.write(intToBytes((int) file.get("packingMethod")));
                fos.write(intToBytes((int) file.get("originalSize")));
                fos.write(intToBytes((int) file.get("reserved")));
                fos.write(intToBytes((int) file.get("timestamp")));
                fos.write(intToBytes((int) file.get("dataSize")));
            }

            // Terminating entry
            fos.write(0);
            fos.write(intToBytes(0));
            fos.write(intToBytes(0));
            fos.write(intToBytes(0));
            fos.write(intToBytes(0));
            fos.write(intToBytes(0));

            // Write data
            for (Map<String, Object> file : files) {
                fos.write((byte[]) file.get("data"));
            }
        }
    }

    private String readAsciiz(ByteBuffer bb, int offset) {
        StringBuilder sb = new StringBuilder();
        bb.position(offset);
        byte b;
        while ((b = bb.get()) != 0) {
            sb.append((char) b);
        }
        return sb.toString();
    }

    private byte[] intToBytes(int value) {
        return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(value).array();
    }

    public static void main(String[] args) throws IOException {
        PBOHandler pbo = new PBOHandler();
        pbo.open("example.pbo");
        pbo.printProperties();
        pbo.write("new.pbo");
    }
}

6. JavaScript Class for .PBO Handling

Here's a Node.js JavaScript class (uses fs for file I/O, Buffer for binary).

const fs = require('fs');

class PBOHandler {
    constructor() {
        this.headerExtensions = {};
        this.files = []; // array of objects
    }

    open(filepath) {
        const data = fs.readFileSync(filepath);
        let offset = 0;
        let isHeaderExtension = false;

        while (true) {
            let { str: filename, newOffset } = this._readAsciiz(data, offset);
            offset = newOffset;
            let packingMethod = data.readUInt32LE(offset); offset += 4;
            let originalSize = data.readUInt32LE(offset); offset += 4;
            let reserved = data.readUInt32LE(offset); offset += 4;
            let timestamp = data.readUInt32LE(offset); offset += 4;
            let dataSize = data.readUInt32LE(offset); offset += 4;

            if (filename === '' && packingMethod === 0 && originalSize === 0 && reserved === 0 && timestamp === 0 && dataSize === 0) {
                break;
            }

            if (packingMethod === 0x56657273) {
                isHeaderExtension = true;
                continue;
            }

            if (isHeaderExtension) {
                while (true) {
                    let { str: key, newOffset: no } = this._readAsciiz(data, offset);
                    offset = no;
                    if (key === '') break;
                    let { str: value, newOffset: vo } = this._readAsciiz(data, offset);
                    offset = vo;
                    this.headerExtensions[key] = value;
                }
                isHeaderExtension = false;
                continue;
            }

            let fileData = data.slice(offset, offset + dataSize);
            offset += dataSize;
            this.files.push({
                filename,
                packingMethod,
                originalSize,
                reserved,
                timestamp,
                dataSize,
                data: fileData
            });
        }
    }

    printProperties() {
        console.log('Header Extensions:');
        for (let [k, v] of Object.entries(this.headerExtensions)) {
            console.log(`${k}: ${v}`);
        }
        console.log('\nFiles:');
        this.files.forEach(f => {
            console.log(`Filename: ${f.filename}`);
            console.log(`Packing Method: 0x${f.packingMethod.toString(16).padStart(8, '0')}`);
            console.log(`Original Size: ${f.originalSize}`);
            console.log(`Reserved: ${f.reserved}`);
            console.log(`Timestamp: ${new Date(f.timestamp * 1000).toISOString()}`);
            console.log(`Data Size: ${f.dataSize}`);
            console.log('');
        });
    }

    write(filepath) {
        let buffers = [];

        // Product entry if extensions
        if (Object.keys(this.headerExtensions).length > 0) {
            buffers.push(Buffer.from([0]));
            buffers.push(Buffer.alloc(4).writeUInt32LE(0x56657273, 0));
            buffers.push(Buffer.alloc(16).fill(0)); // 4 ulongs 0
            for (let [k, v] of Object.entries(this.headerExtensions)) {
                buffers.push(Buffer.from(k + '\0'));
                buffers.push(Buffer.from(v + '\0'));
            }
            buffers.push(Buffer.from([0]));
        }

        // File entries
        this.files.forEach(f => {
            buffers.push(Buffer.from(f.filename + '\0'));
            buffers.push(Buffer.alloc(4).writeUInt32LE(f.packingMethod, 0));
            buffers.push(Buffer.alloc(4).writeUInt32LE(f.originalSize, 0));
            buffers.push(Buffer.alloc(4).writeUInt32LE(f.reserved, 0));
            buffers.push(Buffer.alloc(4).writeUInt32LE(f.timestamp, 0));
            buffers.push(Buffer.alloc(4).writeUInt32LE(f.dataSize, 0));
        });

        // Terminating
        buffers.push(Buffer.from([0]));
        buffers.push(Buffer.alloc(20).fill(0)); // 5 ulongs 0

        // Data
        this.files.forEach(f => buffers.push(f.data));

        fs.writeFileSync(filepath, Buffer.concat(buffers));
    }

    _readAsciiz(data, offset) {
        let start = offset;
        while (data[offset] !== 0) offset++;
        return { str: data.slice(start, offset).toString(), newOffset: offset + 1 };
    }
}

// Example
const pbo = new PBOHandler();
pbo.open('example.pbo');
pbo.printProperties();
pbo.write('new.pbo');

7. C "Class" for .PBO Handling

C isn't object-oriented, so here's a struct with functions for open, print, write (using stdio for console, malloc for memory).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdint.h>

typedef struct {
    char* key;
    char* value;
} Extension;

typedef struct {
    char* filename;
    uint32_t packing_method;
    uint32_t original_size;
    uint32_t reserved;
    uint32_t timestamp;
    uint32_t data_size;
    uint8_t* data;
} PBOFile;

typedef struct {
    Extension* extensions;
    size_t ext_count;
    PBOFile* files;
    size_t file_count;
} PBOHandler;

PBOHandler* pbo_create() {
    PBOHandler* pbo = malloc(sizeof(PBOHandler));
    pbo->extensions = NULL;
    pbo->ext_count = 0;
    pbo->files = NULL;
    pbo->file_count = 0;
    return pbo;
}

void pbo_destroy(PBOHandler* pbo) {
    for (size_t i = 0; i < pbo->ext_count; i++) {
        free(pbo->extensions[i].key);
        free(pbo->extensions[i].value);
    }
    free(pbo->extensions);
    for (size_t i = 0; i < pbo->file_count; i++) {
        free(pbo->files[i].filename);
        free(pbo->files[i].data);
    }
    free(pbo->files);
    free(pbo);
}

char* read_asciiz(const uint8_t* data, size_t* offset) {
    size_t start = *offset;
    while (data[*offset] != 0) (*offset)++;
    char* str = strndup((char*)&data[start], *offset - start);
    (*offset)++;
    return str;
}

uint32_t unpack_ulong(const uint8_t* data, size_t* offset) {
    uint32_t val = *(uint32_t*)&data[*offset];
    *offset += 4;
    return val;
}

void pbo_open(PBOHandler* pbo, const char* filepath) {
    FILE* f = fopen(filepath, "rb");
    fseek(f, 0, SEEK_END);
    size_t size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* data = malloc(size);
    fread(data, 1, size, f);
    fclose(f);

    size_t offset = 0;
    int is_header_extension = 0;

    while (1) {
        char* filename = read_asciiz(data, &offset);
        uint32_t packing_method = unpack_ulong(data, &offset);
        uint32_t original_size = unpack_ulong(data, &offset);
        uint32_t reserved = unpack_ulong(data, &offset);
        uint32_t timestamp = unpack_ulong(data, &offset);
        uint32_t data_size = unpack_ulong(data, &offset);

        if (strlen(filename) == 0 && packing_method == 0 && original_size == 0 && reserved == 0 && timestamp == 0 && data_size == 0) {
            free(filename);
            break;
        }

        if (packing_method == 0x56657273) {
            is_header_extension = 1;
            free(filename);
            continue;
        }

        if (is_header_extension) {
            while (1) {
                char* key = read_asciiz(data, &offset);
                if (strlen(key) == 0) {
                    free(key);
                    break;
                }
                char* value = read_asciiz(data, &offset);
                pbo->extensions = realloc(pbo->extensions, sizeof(Extension) * (pbo->ext_count + 1));
                pbo->extensions[pbo->ext_count].key = key;
                pbo->extensions[pbo->ext_count].value = value;
                pbo->ext_count++;
            }
            is_header_extension = 0;
            free(filename);
            continue;
        }

        uint8_t* file_data = malloc(data_size);
        memcpy(file_data, &data[offset], data_size);
        offset += data_size;

        pbo->files = realloc(pbo->files, sizeof(PBOFile) * (pbo->file_count + 1));
        pbo->files[pbo->file_count].filename = filename;
        pbo->files[pbo->file_count].packing_method = packing_method;
        pbo->files[pbo->file_count].original_size = original_size;
        pbo->files[pbo->file_count].reserved = reserved;
        pbo->files[pbo->file_count].timestamp = timestamp;
        pbo->files[pbo->file_count].data_size = data_size;
        pbo->files[pbo->file_count].data = file_data;
        pbo->file_count++;
    }
    free(data);
}

void pbo_print_properties(const PBOHandler* pbo) {
    printf("Header Extensions:\n");
    for (size_t i = 0; i < pbo->ext_count; i++) {
        printf("%s: %s\n", pbo->extensions[i].key, pbo->extensions[i].value);
    }
    printf("\nFiles:\n");
    for (size_t i = 0; i < pbo->file_count; i++) {
        PBOFile f = pbo->files[i];
        time_t ts = f.timestamp;
        char timebuf[80];
        strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", gmtime(&ts));
        printf("Filename: %s\n", f.filename);
        printf("Packing Method: 0x%08x\n", f.packing_method);
        printf("Original Size: %u\n", f.original_size);
        printf("Reserved: %u\n", f.reserved);
        printf("Timestamp: %s\n", timebuf);
        printf("Data Size: %u\n\n", f.data_size);
    }
}

void pbo_write(const PBOHandler* pbo, const char* filepath) {
    FILE* f = fopen(filepath, "wb");

    // Product entry
    if (pbo->ext_count > 0) {
        fputc(0, f);
        fwrite(&(uint32_t){0x56657273}, 4, 1, f);
        uint32_t zero = 0;
        fwrite(&zero, 4, 1, f);
        fwrite(&zero, 4, 1, f);
        fwrite(&zero, 4, 1, f);
        fwrite(&zero, 4, 1, f);
        for (size_t i = 0; i < pbo->ext_count; i++) {
            fwrite(pbo->extensions[i].key, strlen(pbo->extensions[i].key) + 1, 1, f);
            fwrite(pbo->extensions[i].value, strlen(pbo->extensions[i].value) + 1, 1, f);
        }
        fputc(0, f);
    }

    // File entries
    for (size_t i = 0; i < pbo->file_count; i++) {
        PBOFile file = pbo->files[i];
        fwrite(file.filename, strlen(file.filename) + 1, 1, f);
        fwrite(&file.packing_method, 4, 1, f);
        fwrite(&file.original_size, 4, 1, f);
        fwrite(&file.reserved, 4, 1, f);
        fwrite(&file.timestamp, 4, 1, f);
        fwrite(&file.data_size, 4, 1, f);
    }

    // Terminating
    fputc(0, f);
    uint32_t zero = 0;
    fwrite(&zero, 4, 1, f);
    fwrite(&zero, 4, 1, f);
    fwrite(&zero, 4, 1, f);
    fwrite(&zero, 4, 1, f);
    fwrite(&zero, 4, 1, f);

    // Data
    for (size_t i = 0; i < pbo->file_count; i++) {
        fwrite(pbo->files[i].data, pbo->files[i].data_size, 1, f);
    }

    fclose(f);
}

// Example
int main() {
    PBOHandler* pbo = pbo_create();
    pbo_open(pbo, "example.pbo");
    pbo_print_properties(pbo);
    pbo_write(pbo, "new.pbo");
    pbo_destroy(pbo);
    return 0;
}