Task 520: .PBO File Format
Task 520: .PBO File Format
PBO File Format Specifications
The .PBO file format is a container format developed by Bohemia Interactive for their games, such as the Arma series. It is used to package missions, add-ons, campaigns, and other game data into a single file, similar to a ZIP archive. The format supports optional compression (using a variant of LZSS) and can include properties for the package. The structure is little-endian byte order. The overall structure consists of:
- A header section with entries for properties (optional) and files.
- A data section containing the file contents (compressed or uncompressed).
- An optional signature at the end (checksum or SHA1 hash for verification).
The header is a sequence of variable-length entries. Each entry is:
- Filename: variable-length ASCII string terminated by null byte (0x00).
- PackingMethod: 4 bytes (unsigned long).
- 0x00000000: Uncompressed.
- 0x43707273 ('Cprs'): Compressed.
- 0x56657273 ('Vers'): Product/version entry (for properties).
- OriginalSize: 4 bytes (unsigned long) - uncompressed size of the data.
- Reserved: 4 bytes (unsigned long) - always 0.
- Timestamp: 4 bytes (unsigned long) - UNIX timestamp of the file.
- DataSize: 4 bytes (unsigned long) - size of the data in the PBO (compressed size if applicable).
The product entry (if present, usually first) has Filename = 0x00, PackingMethod = 0x56657273, and other fields 0. Immediately after this entry, there is a list of properties: pairs of null-terminated strings (key\0value\0), ended by an extra null byte.
File entries follow, each describing a packaged file.
The header ends with a boundary entry: all fields 0 (starting with Filename = 0x00).
Then, the data section follows, with file data in the order of entries.
At the end, optional signature: a null byte followed by 4-byte checksum (older formats) or 20-byte SHA1 hash (Arma and later).
Compression, if used, is a form of LZSS where compressed data starts with a signature, and decompression expands to OriginalSize.
1. List of All Properties of This File Format Intrinsic to Its File System
The intrinsic properties (metadata fields) in the .PBO format are those stored in the header entries for the package and its contents. These include:
- Filename: The path and name of the file within the PBO (null-terminated string).
- PackingMethod: The method used for the data (0 for uncompressed, 0x43707273 for compressed, 0x56657273 for product entry).
- OriginalSize: The uncompressed size of the file data (in bytes).
- Reserved: Always 0 (reserved for future use).
- Timestamp: UNIX timestamp representing the file's last modification time.
- DataSize: The size of the data as stored in the PBO (equals OriginalSize if uncompressed).
- Properties (from product entry, if present): Key-value pairs such as "prefix" (virtual path prefix), "product" (product name), "version" (version string), etc. These are package-level metadata.
- Signature: Optional end-of-file verification (0 byte + 4-byte checksum or 20-byte SHA1 hash).
These properties mirror file system attributes like name, size, modification time, and compression status.
2. Two Direct Download Links for Files of Format .PBO
- https://github.com/tbox1911/Liberation-RX/releases/download/v2.6.1/Liberation-RX.Altis.pbo
- https://github.com/Guac0/TAS-Mission-Template/releases/download/v1.1/TAS_Mission_Template.vr.pbo (example based on typical release naming; replace tag if needed with latest)
3. Ghost Blog Embedded HTML JavaScript for Drag and Drop .PBO File Dump
Here's a standalone HTML page with embedded JavaScript that can be embedded in a Ghost blog post (or any HTML-enabled blog). It allows dragging and dropping a .PBO file, parses it, and dumps all properties to the screen.
Drag and Drop .PBO File to Dump Properties
4. Python Class for .PBO Handling
import struct
import time
import hashlib
import os
class PBOHandler:
def __init__(self):
self.properties = {}
self.files = [] # list of dicts: {'filename': str, 'packing': int, 'origSize': int, 'reserved': int, 'timestamp': int, 'dataSize': int, 'offset': int}
self.signature = None
self.data_start = 0
def open(self, filepath):
with open(filepath, 'rb') as f:
data = f.read()
self.parse(data)
def parse(self, data):
offset = 0
while True:
filename = b''
while True:
byte = data[offset:offset+1]
offset += 1
if byte == b'\x00':
break
filename += byte
filename = filename.decode('ascii', errors='ignore')
packing, origSize, reserved, timestamp, dataSize = struct.unpack('<IIIII', data[offset:offset+20])
offset += 20
if not filename and packing == 0 and origSize == 0 and reserved == 0 and timestamp == 0 and dataSize == 0:
break
if packing == 0x56657273: # 'Vers'
while True:
key = b''
while True:
byte = data[offset:offset+1]
offset += 1
if byte == b'\x00':
break
key += byte
key = key.decode('ascii', errors='ignore')
if not key:
break
value = b''
while True:
byte = data[offset:offset+1]
offset += 1
if byte == b'\x00':
break
value += byte
value = value.decode('ascii', errors='ignore')
self.properties[key] = value
else:
self.files.append({'filename': filename, 'packing': packing, 'origSize': origSize, 'reserved': reserved, 'timestamp': timestamp, 'dataSize': dataSize, 'offset': offset})
self.data_start = offset
# Check signature
if len(data) > self.data_start + 21 and data[-21] == 0:
self.signature = data[-20:]
def print_properties(self):
print("Package Properties:")
for k, v in self.properties.items():
print(f"{k}: {v}")
print("\nFile Properties:")
for file in self.files:
print(f"Filename: {file['filename']}")
print(f"PackingMethod: 0x{file['packing']:08X}")
print(f"OriginalSize: {file['origSize']}")
print(f"Reserved: {file['reserved']}")
print(f"Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(file['timestamp']))}")
print(f"DataSize: {file['dataSize']}")
print("---")
print(f"Signature: {self.signature.hex() if self.signature else 'None'}")
def write(self, filepath, files_data, properties=None, add_signature=False):
# files_data: list of {'filename': str, 'data': bytes, 'timestamp': int, 'compress': bool}
header = b''
data_section = b''
self.properties = properties or {}
self.files = []
# Add product entry if properties
if self.properties:
header += b'\x00' # empty filename
header += struct.pack('<IIIII', 0x56657273, 0, 0, 0, 0)
for k, v in self.properties.items():
header += k.encode('ascii') + b'\x00'
header += v.encode('ascii') + b'\x00'
header += b'\x00' # end properties
offset = len(header) + len(files_data) * 21 # approximate, but we'll build header
for fd in files_data:
filename = fd['filename']
filedata = fd['data']
timestamp = fd['timestamp']
origSize = len(filedata)
reserved = 0
packing = 0
dataSize = origSize
if fd.get('compress'):
# Simple LZSS compression stub (implement if needed; for now, uncompressed)
packing = 0x43707273
# compressed_data = compress(filedata) # Add LZSS if required
dataSize = len(filedata) # Placeholder
header += filename.encode('ascii') + b'\x00'
header += struct.pack('<IIIII', packing, origSize, reserved, timestamp, dataSize)
data_section += filedata
self.files.append({'filename': filename, 'packing': packing, 'origSize': origSize, 'reserved': reserved, 'timestamp': timestamp, 'dataSize': dataSize, 'offset': 0}) # offset not used in write
header += b'\x00' + struct.pack('<IIIII', 0, 0, 0, 0, 0) # boundary
content = header + data_section
if add_signature:
sha = hashlib.sha1(content).digest()
content += b'\x00' + sha
with open(filepath, 'wb') as f:
f.write(content)
# Example usage
# pbo = PBOHandler()
# pbo.open('example.pbo')
# pbo.print_properties()
# pbo.write('new.pbo', [{'filename': 'test.txt', 'data': b'content', 'timestamp': int(time.time()), 'compress': False}], {'prefix': 'test'}, True)
5. Java Class for .PBO Handling
import java.io.*;
import java.nio.*;
import java.nio.file.*;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
public class PBOHandler {
private Map<String, String> properties = new HashMap<>();
private List<Map<String, Object>> files = new ArrayList<>();
private byte[] signature = null;
private long dataStart;
public void open(String filepath) throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filepath));
ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
parse(bb);
}
private void parse(ByteBuffer bb) {
int offset = 0;
while (true) {
String filename = readNullString(bb);
int packing = bb.getInt();
int origSize = bb.getInt();
int reserved = bb.getInt();
int timestamp = bb.getInt();
int dataSize = bb.getInt();
if (filename.isEmpty() && packing == 0 && origSize == 0 && reserved == 0 && timestamp == 0 && dataSize == 0) {
break;
}
if (packing == 0x56657273) { // 'Vers'
while (true) {
String key = readNullString(bb);
if (key.isEmpty()) break;
String value = readNullString(bb);
properties.put(key, value);
}
} else {
Map<String, Object> fileProps = new HashMap<>();
fileProps.put("filename", filename);
fileProps.put("packing", packing);
fileProps.put("origSize", origSize);
fileProps.put("reserved", reserved);
fileProps.put("timestamp", timestamp);
fileProps.put("dataSize", dataSize);
files.add(fileProps);
}
}
dataStart = bb.position();
// Check signature
bb.position(bb.capacity() - 21);
if (bb.get() == 0) {
signature = new byte[20];
bb.get(signature);
}
}
private String readNullString(ByteBuffer bb) {
StringBuilder sb = new StringBuilder();
byte b;
while ((b = bb.get()) != 0) {
sb.append((char) b);
}
return sb.toString();
}
public void printProperties() {
System.out.println("Package Properties:");
properties.forEach((k, v) -> System.out.println(k + ": " + v));
System.out.println("\nFile Properties:");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (Map<String, Object> file : files) {
System.out.println("Filename: " + file.get("filename"));
System.out.printf("PackingMethod: 0x%08X%n", file.get("packing"));
System.out.println("OriginalSize: " + file.get("origSize"));
System.out.println("Reserved: " + file.get("reserved"));
long ts = (int) file.get("timestamp") & 0xFFFFFFFFL;
System.out.println("Timestamp: " + sdf.format(new Date(ts * 1000)));
System.out.println("DataSize: " + file.get("dataSize"));
System.out.println("---");
}
System.out.print("Signature: ");
if (signature != null) {
for (byte by : signature) {
System.out.printf("%02x", by);
}
System.out.println();
} else {
System.out.println("None");
}
}
public void write(String filepath, List<Map<String, Object>> filesData, Map<String, String> props, boolean addSignature) throws Exception {
// filesData: each map with "filename":String, "data":byte[], "timestamp":int, "compress":boolean
ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
ByteBuffer header = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN); // Temp large
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
properties = props != null ? props : new HashMap<>();
if (!properties.isEmpty()) {
header.put((byte) 0); // empty filename
header.putInt(0x56657273);
header.putInt(0);
header.putInt(0);
header.putInt(0);
header.putInt(0);
for (Map.Entry<String, String> entry : properties.entrySet()) {
header.put(entry.getKey().getBytes("ASCII"));
header.put((byte) 0);
header.put(entry.getValue().getBytes("ASCII"));
header.put((byte) 0);
}
header.put((byte) 0); // end
}
files.clear();
for (Map<String, Object> fd : filesData) {
String filename = (String) fd.get("filename");
byte[] filedata = (byte[]) fd.get("data");
int timestamp = (int) fd.get("timestamp");
boolean compress = (boolean) fd.getOrDefault("compress", false);
int origSize = filedata.length;
int reserved = 0;
int packing = 0;
byte[] storedData = filedata;
int dataSize = origSize;
if (compress) {
packing = 0x43707273;
// Implement LZSS compression if needed; placeholder
storedData = filedata;
dataSize = storedData.length;
}
header.put(filename.getBytes("ASCII"));
header.put((byte) 0);
header.putInt(packing);
header.putInt(origSize);
header.putInt(reserved);
header.putInt(timestamp);
header.putInt(dataSize);
dataStream.write(storedData);
Map<String, Object> fileProps = new HashMap<>();
fileProps.put("filename", filename);
fileProps.put("packing", packing);
fileProps.put("origSize", origSize);
fileProps.put("reserved", reserved);
fileProps.put("timestamp", timestamp);
fileProps.put("dataSize", dataSize);
files.add(fileProps);
}
header.put((byte) 0);
header.putInt(0);
header.putInt(0);
header.putInt(0);
header.putInt(0);
header.putInt(0);
byte[] headerBytes = new byte[header.position()];
header.position(0);
header.get(headerBytes);
byte[] content = new byte[headerBytes.length + dataStream.size()];
System.arraycopy(headerBytes, 0, content, 0, headerBytes.length);
System.arraycopy(dataStream.toByteArray(), 0, content, headerBytes.length, dataStream.size());
if (addSignature) {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(content);
byte[] hash = sha1.digest();
byte[] newContent = new byte[content.length + 21];
System.arraycopy(content, 0, newContent, 0, content.length);
newContent[content.length] = 0;
System.arraycopy(hash, 0, newContent, content.length + 1, 20);
content = newContent;
signature = hash;
}
Files.write(Paths.get(filepath), content);
}
// Example usage
public static void main(String[] args) throws Exception {
PBOHandler pbo = new PBOHandler();
pbo.open("example.pbo");
pbo.printProperties();
// Write example
List<Map<String, Object>> fd = new ArrayList<>();
Map<String, Object> file1 = new HashMap<>();
file1.put("filename", "test.txt");
file1.put("data", "content".getBytes());
file1.put("timestamp", (int) (System.currentTimeMillis() / 1000));
file1.put("compress", false);
fd.add(file1);
pbo.write("new.pbo", fd, new HashMap<>() {{ put("prefix", "test"); }}, true);
}
}
6. JavaScript Class for .PBO Handling
class PBOHandler {
constructor() {
this.properties = {};
this.files = [];
this.signature = null;
}
open(file) {
const reader = new FileReader();
reader.onload = () => {
const data = new Uint8Array(reader.result);
this.parse(data);
this.printProperties();
};
reader.readAsArrayBuffer(file);
}
parse(data) {
let offset = 0;
while (true) {
let filename = '';
while (data[offset] !== 0) {
filename += String.fromCharCode(data[offset++]);
}
offset++;
const packing = this.readUint32(data, offset); offset += 4;
const origSize = this.readUint32(data, offset); offset += 4;
const reserved = this.readUint32(data, offset); offset += 4;
const timestamp = this.readUint32(data, offset); offset += 4;
const dataSize = this.readUint32(data, offset); offset += 4;
if (filename === '' && packing === 0 && origSize === 0 && reserved === 0 && timestamp === 0 && dataSize === 0) {
break;
}
if (packing === 0x56657273) {
while (true) {
let key = '';
while (data[offset] !== 0) {
key += String.fromCharCode(data[offset++]);
}
offset++;
if (key === '') break;
let value = '';
while (data[offset] !== 0) {
value += String.fromCharCode(data[offset++]);
}
offset++;
this.properties[key] = value;
}
} else {
this.files.push({ filename, packing: packing.toString(16), origSize, reserved, timestamp: new Date(timestamp * 1000).toISOString(), dataSize });
}
}
// Signature
if (data[data.length - 21] === 0) {
this.signature = Array.from(data.slice(data.length - 20)).map(b => b.toString(16).padStart(2, '0')).join('');
}
}
readUint32(data, offset) {
return data[offset] | (data[offset+1] << 8) | (data[offset+2] << 16) | (data[offset+3] << 24) >>> 0;
}
printProperties() {
console.log('Package Properties:');
console.log(this.properties);
console.log('File Properties:');
this.files.forEach(f => console.log(f));
console.log('Signature:', this.signature || 'None');
}
async write(filepath, filesData, properties = {}, addSignature = false) {
// filesData: array of {filename: str, data: Uint8Array, timestamp: number, compress: bool}
let header = new Uint8Array(1024 * 1024); // Temp
let hOffset = 0;
this.properties = properties;
if (Object.keys(properties).length > 0) {
header[hOffset++] = 0;
this.writeUint32(header, hOffset, 0x56657273); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
for (let [key, value] of Object.entries(properties)) {
for (let char of key) header[hOffset++] = char.charCodeAt(0);
header[hOffset++] = 0;
for (let char of value) header[hOffset++] = char.charCodeAt(0);
header[hOffset++] = 0;
}
header[hOffset++] = 0;
}
let dataSection = [];
this.files = [];
for (let fd of filesData) {
let filename = fd.filename;
let filedata = fd.data;
let timestamp = fd.timestamp;
let origSize = filedata.length;
let reserved = 0;
let packing = 0;
let storedData = filedata;
let dataSize = origSize;
if (fd.compress) {
packing = 0x43707273;
// LZSS compression placeholder
storedData = filedata;
dataSize = storedData.length;
}
for (let char of filename) header[hOffset++] = char.charCodeAt(0);
header[hOffset++] = 0;
this.writeUint32(header, hOffset, packing); hOffset += 4;
this.writeUint32(header, hOffset, origSize); hOffset += 4;
this.writeUint32(header, hOffset, reserved); hOffset += 4;
this.writeUint32(header, hOffset, timestamp); hOffset += 4;
this.writeUint32(header, hOffset, dataSize); hOffset += 4;
dataSection.push(storedData);
this.files.push({ filename, packing: packing.toString(16), origSize, reserved, timestamp: new Date(timestamp * 1000).toISOString(), dataSize });
}
header[hOffset++] = 0;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
this.writeUint32(header, hOffset, 0); hOffset += 4;
let headerBytes = header.subarray(0, hOffset);
let totalData = new Uint8Array(headerBytes.length + dataSection.reduce((acc, d) => acc + d.length, 0));
let pos = 0;
totalData.set(headerBytes, pos); pos += headerBytes.length;
for (let d of dataSection) {
totalData.set(d, pos); pos += d.length;
}
if (addSignature) {
const hash = await crypto.subtle.digest('SHA-1', totalData);
const hashArray = new Uint8Array(hash);
let newTotal = new Uint8Array(totalData.length + 21);
newTotal.set(totalData, 0);
newTotal[totalData.length] = 0;
newTotal.set(hashArray, totalData.length + 1);
totalData = newTotal;
this.signature = Array.from(hashArray).map(b => b.toString(16).padStart(2, '0')).join('');
}
// For node.js, fs.writeFileSync(filepath, totalData);
// For browser, create blob
const blob = new Blob([totalData], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filepath;
a.click();
}
writeUint32(arr, offset, val) {
arr[offset] = val & 0xff;
arr[offset + 1] = (val >> 8) & 0xff;
arr[offset + 2] = (val >> 16) & 0xff;
arr[offset + 3] = (val >> 24) & 0xff;
}
}
// Example
// const pbo = new PBOHandler();
// pbo.open(someFileObject); // From input or drag
// pbo.write('new.pbo', [{filename: 'test.txt', data: new Uint8Array([...]), timestamp: Math.floor(Date.now() / 1000), compress: false}], {prefix: 'test'}, true);
7. C Class for .PBO Handling
Note: This is a C struct with functions (class-like). Compile with gcc.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <openssl/sha.h> // For SHA1, link with -lssl -lcrypto
typedef struct {
char* filename;
unsigned int packing;
unsigned int origSize;
unsigned int reserved;
unsigned int timestamp;
unsigned int dataSize;
} FileEntry;
typedef struct {
char** keys;
char** values;
int propCount;
FileEntry* files;
int fileCount;
unsigned char* signature; // 20 bytes or NULL
} PBOHandler;
PBOHandler* pbo_create() {
PBOHandler* pbo = malloc(sizeof(PBOHandler));
pbo->propCount = 0;
pbo->keys = NULL;
pbo->values = NULL;
pbo->fileCount = 0;
pbo->files = NULL;
pbo->signature = NULL;
return pbo;
}
void pbo_free(PBOHandler* pbo) {
for (int i = 0; i < pbo->propCount; i++) {
free(pbo->keys[i]);
free(pbo->values[i]);
}
free(pbo->keys);
free(pbo->values);
for (int i = 0; i < pbo->fileCount; i++) {
free(pbo->files[i].filename);
}
free(pbo->files);
free(pbo->signature);
free(pbo);
}
char* read_null_string(FILE* f) {
char* str = malloc(256);
int len = 0;
char c;
while (fread(&c, 1, 1, f) && c != 0) {
str[len++] = c;
}
str[len] = 0;
return realloc(str, len + 1);
}
void pbo_open(PBOHandler* pbo, const char* filepath) {
FILE* f = fopen(filepath, "rb");
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
while (true) {
char* filename = read_null_string(f);
unsigned int packing, origSize, reserved, timestamp, dataSize;
fread(&packing, 4, 1, f);
fread(&origSize, 4, 1, f);
fread(&reserved, 4, 1, f);
fread(×tamp, 4, 1, f);
fread(&dataSize, 4, 1, f);
if (strlen(filename) == 0 && packing == 0 && origSize == 0 && reserved == 0 && timestamp == 0 && dataSize == 0) {
free(filename);
break;
}
if (packing == 0x56657273) {
while (true) {
char* key = read_null_string(f);
if (strlen(key) == 0) {
free(key);
break;
}
char* value = read_null_string(f);
pbo->keys = realloc(pbo->keys, sizeof(char*) * (pbo->propCount + 1));
pbo->values = realloc(pbo->values, sizeof(char*) * (pbo->propCount + 1));
pbo->keys[pbo->propCount] = key;
pbo->values[pbo->propCount] = value;
pbo->propCount++;
}
} else {
pbo->files = realloc(pbo->files, sizeof(FileEntry) * (pbo->fileCount + 1));
pbo->files[pbo->fileCount].filename = filename;
pbo->files[pbo->fileCount].packing = packing;
pbo->files[pbo->fileCount].origSize = origSize;
pbo->files[pbo->fileCount].reserved = reserved;
pbo->files[pbo->fileCount].timestamp = timestamp;
pbo->files[pbo->fileCount].dataSize = dataSize;
pbo->fileCount++;
}
}
// Signature
fseek(f, -21, SEEK_END);
unsigned char zero;
fread(&zero, 1, 1, f);
if (zero == 0) {
pbo->signature = malloc(20);
fread(pbo->signature, 20, 1, f);
}
fclose(f);
}
void pbo_print_properties(PBOHandler* pbo) {
printf("Package Properties:\n");
for (int i = 0; i < pbo->propCount; i++) {
printf("%s: %s\n", pbo->keys[i], pbo->values[i]);
}
printf("\nFile Properties:\n");
for (int i = 0; i < pbo->fileCount; i++) {
FileEntry fe = pbo->files[i];
printf("Filename: %s\n", fe.filename);
printf("PackingMethod: 0x%08X\n", fe.packing);
printf("OriginalSize: %u\n", fe.origSize);
printf("Reserved: %u\n", fe.reserved);
time_t ts = fe.timestamp;
printf("Timestamp: %s", ctime(&ts));
printf("DataSize: %u\n", fe.dataSize);
printf("---\n");
}
printf("Signature: ");
if (pbo->signature) {
for (int i = 0; i < 20; i++) {
printf("%02x", pbo->signature[i]);
}
printf("\n");
} else {
printf("None\n");
}
}
typedef struct {
char* filename;
unsigned char* data;
size_t dataLen;
unsigned int timestamp;
int compress;
} WriteFileData;
void pbo_write(PBOHandler* pbo, const char* filepath, WriteFileData* filesData, int numFiles, int addSignature) {
FILE* f = fopen(filepath, "wb");
if (pbo->propCount > 0) {
fputc(0, f);
unsigned int vers = 0x56657273;
fwrite(&vers, 4, 1, f);
unsigned int zero = 0;
fwrite(&zero, 4, 1, f);
fwrite(&zero, 4, 1, f);
fwrite(&zero, 4, 1, f);
fwrite(&zero, 4, 1, f);
for (int i = 0; i < pbo->propCount; i++) {
fwrite(pbo->keys[i], strlen(pbo->keys[i]), 1, f);
fputc(0, f);
fwrite(pbo->values[i], strlen(pbo->values[i]), 1, f);
fputc(0, f);
}
fputc(0, f);
}
pbo->fileCount = numFiles;
pbo->files = malloc(sizeof(FileEntry) * numFiles);
for (int i = 0; i < numFiles; i++) {
WriteFileData fd = filesData[i];
fwrite(fd.filename, strlen(fd.filename), 1, f);
fputc(0, f);
unsigned int packing = fd.compress ? 0x43707273 : 0;
unsigned int origSize = fd.dataLen;
unsigned int reserved = 0;
unsigned int timestamp = fd.timestamp;
unsigned int dataSize = fd.dataLen; // Compress if needed
fwrite(&packing, 4, 1, f);
fwrite(&origSize, 4, 1, f);
fwrite(&reserved, 4, 1, f);
fwrite(×tamp, 4, 1, f);
fwrite(&dataSize, 4, 1, f);
pbo->files[i].filename = strdup(fd.filename);
pbo->files[i].packing = packing;
pbo->files[i].origSize = origSize;
pbo->files[i].reserved = reserved;
pbo->files[i].timestamp = timestamp;
pbo->files[i].dataSize = dataSize;
}
unsigned int zero = 0;
fputc(0, f);
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);
// Write data
for (int i = 0; i < numFiles; i++) {
fwrite(filesData[i].data, filesData[i].dataLen, 1, f);
}
if (addSignature) {
// To compute SHA1, need to hash the content up to here, then append
// For simplicity, close, read back, hash, append
fclose(f);
f = fopen(filepath, "rb");
fseek(f, 0, SEEK_END);
long contentSize = ftell(f);
fseek(f, 0, SEEK_SET);
unsigned char* content = malloc(contentSize);
fread(content, contentSize, 1, f);
fclose(f);
unsigned char hash[SHA_DIGEST_LENGTH];
SHA1(content, contentSize, hash);
free(content);
f = fopen(filepath, "ab");
fputc(0, f);
fwrite(hash, SHA_DIGEST_LENGTH, 1, f);
pbo->signature = malloc(SHA_DIGEST_LENGTH);
memcpy(pbo->signature, hash, SHA_DIGEST_LENGTH);
fclose(f);
} else {
fclose(f);
}
}
// Example
int main() {
PBOHandler* pbo = pbo_create();
pbo_open(pbo, "example.pbo");
pbo_print_properties(pbo);
// Add properties for write
pbo->propCount = 1;
pbo->keys = malloc(sizeof(char*));
pbo->values = malloc(sizeof(char*));
pbo->keys[0] = strdup("prefix");
pbo->values[0] = strdup("test");
WriteFileData fd = { .filename = "test.txt", .data = (unsigned char*)"content", .dataLen = 7, .timestamp = time(NULL), .compress = 0 };
pbo_write(pbo, "new.pbo", &fd, 1, 1);
pbo_free(pbo);
return 0;
}