Task 584: .PUP File Format

Task 584: .PUP File Format

  1. The .PUP file format is the PlayStation Update Package used for system software updates on PlayStation consoles like the PS3. Based on the specifications, here is a list of all the intrinsic properties (fields and structures) that can be extracted from the file format (for the PS3 variant, which uses big-endian encoding and format version 1):
  • Magic number: 7-byte string (typically "SCEUF\0\0")
  • Format flag: 1-byte unsigned integer
  • Package version: 8-byte unsigned integer (uint64_t, big-endian)
  • Image version: 8-byte unsigned integer (uint64_t, big-endian)
  • Segment count: 8-byte unsigned integer (uint64_t, big-endian, number of segments/files in the package)
  • Header length: 8-byte unsigned integer (uint64_t, big-endian, offset to the start of data segments)
  • Data size: 8-byte unsigned integer (uint64_t, big-endian, total size of all data segments)
  • Segments: Array of segment entries (one per segment count), each containing:
  • Segment ID: 8-byte unsigned integer (uint64_t, big-endian, identifies the segment type, e.g., 0x100 for version.txt)
  • Offset: 8-byte unsigned integer (uint64_t, big-endian, file offset of the segment data)
  • Size: 8-byte unsigned integer (uint64_t, big-endian, size of the segment data)
  • Signature algorithm: 4-byte unsigned integer (uint32_t, big-endian, e.g., 0 for HMAC-SHA1)
  • Padding: 4-byte (typically zeros)
  • Digests: Array of digest entries (one per segment count), each containing:
  • Segment index: 8-byte unsigned integer (uint64_t, big-endian)
  • Digest: 20-byte array (HMAC-SHA1 digest for the segment)
  • Padding: 4-byte unsigned integer (uint32_t, big-endian, typically zeros)
  • Header digest: 20-byte array (HMAC-SHA1 for the header)
  • Padding: 12-byte array (typically zeros)

These properties define the structure, metadata, and integrity checks of the file. The data segments themselves follow the header and contain the actual update files (e.g., TAR archives, SELF files), but the properties listed are the format's intrinsic metadata.

  1. Here are two direct download links for .PUP files (official PS3 system update files from archives):
  1. Below is embeddable HTML/JavaScript code for a Ghost blog post. It creates a drop zone where a user can drag and drop a .PUP file, parses it, and dumps all the properties to the screen (in a display area below the drop zone).
Drag and drop a .PUP file here
  1. Below is a Python class that can open, decode, read, write, and print the properties of a .PUP file to the console.
import struct
import os

class PupFile:
    def __init__(self, filename=None):
        self.magic = b''
        self.format_flag = 0
        self.package_version = 0
        self.image_version = 0
        self.segment_count = 0
        self.header_length = 0
        self.data_size = 0
        self.segments = []
        self.digests = []
        self.header_hmac = b''
        self.padding = b''
        self.segment_data = []
        if filename:
            self.load(filename)

    def load(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        self.magic = data[0:7]
        self.format_flag = data[7]
        self.package_version = struct.unpack('>Q', data[8:16])[0]
        self.image_version = struct.unpack('>Q', data[16:24])[0]
        self.segment_count = struct.unpack('>Q', data[24:32])[0]
        self.header_length = struct.unpack('>Q', data[32:40])[0]
        self.data_size = struct.unpack('>Q', data[40:48])[0]

        offset = 48
        self.segments = []
        for _ in range(self.segment_count):
            id_ = struct.unpack('>Q', data[offset:offset+8])[0]
            off = struct.unpack('>Q', data[offset+8:offset+16])[0]
            size = struct.unpack('>Q', data[offset+16:offset+24])[0]
            hashed = struct.unpack('>I', data[offset+24:offset+28])[0]
            pad = struct.unpack('>I', data[offset+28:offset+32])[0]
            self.segments.append({'id': id_, 'offset': off, 'size': size, 'hashed': hashed, 'pad': pad})
            offset += 32

        self.digests = []
        for _ in range(self.segment_count):
            index = struct.unpack('>Q', data[offset:offset+8])[0]
            digest = data[offset+8:offset+28]
            pad = struct.unpack('>I', data[offset+28:offset+32])[0]
            self.digests.append({'index': index, 'digest': digest, 'pad': pad})
            offset += 32

        self.header_hmac = data[offset:offset+20]
        offset += 20
        self.padding = data[offset:offset+12]

        # Extract segment data
        self.segment_data = []
        for seg in self.segments:
            self.segment_data.append(data[seg['offset']:seg['offset'] + seg['size']])

    def print_properties(self):
        print(f"Magic: {self.magic}")
        print(f"Format flag: {self.format_flag}")
        print(f"Package version: {self.package_version}")
        print(f"Image version: {self.image_version}")
        print(f"Segment count: {self.segment_count}")
        print(f"Header length: {self.header_length}")
        print(f"Data size: {self.data_size}")
        for i, seg in enumerate(self.segments):
            print(f"Segment {i}: ID={seg['id']}, Offset={seg['offset']}, Size={seg['size']}, Hashed={seg['hashed']}, Pad={seg['pad']}")
        for i, dig in enumerate(self.digests):
            print(f"Digest {i}: Index={dig['index']}, Digest={dig['digest'].hex()}, Pad={dig['pad']}")
        print(f"Header HMAC: {self.header_hmac.hex()}")
        print(f"Padding: {self.padding.hex()}")

    def save(self, filename):
        # Build header with placeholders
        header = self.magic + struct.pack('>B', self.format_flag) + struct.pack('>Q', self.package_version) + struct.pack('>Q', self.image_version) + struct.pack('>Q', len(self.segments)) + struct.pack('>Q', 0) + struct.pack('>Q', 0)

        for seg in self.segments:
            header += struct.pack('>Q', seg['id']) + struct.pack('>Q', seg['offset']) + struct.pack('>Q', seg['size']) + struct.pack('>I', seg['hashed']) + struct.pack('>I', seg['pad'])

        for dig in self.digests:
            header += struct.pack('>Q', dig['index']) + dig['digest'] + struct.pack('>I', dig['pad'])

        header += self.header_hmac + self.padding

        header_length = len(header)
        data_size = sum(len(d) for d in self.segment_data)

        # Rebuild with correct header_length and data_size
        header = self.magic + struct.pack('>B', self.format_flag) + struct.pack('>Q', self.package_version) + struct.pack('>Q', self.image_version) + struct.pack('>Q', len(self.segments)) + struct.pack('>Q', header_length) + struct.pack('>Q', data_size)

        current_offset = header_length
        for i, seg in enumerate(self.segments):
            seg['offset'] = current_offset
            header += struct.pack('>Q', seg['id']) + struct.pack('>Q', seg['offset']) + struct.pack('>Q', seg['size']) + struct.pack('>I', seg['hashed']) + struct.pack('>I', seg['pad'])
            current_offset += seg['size']

        for dig in self.digests:
            header += struct.pack('>Q', dig['index']) + dig['digest'] + struct.pack('>I', dig['pad'])

        header += self.header_hmac + self.padding

        # Full file
        full_data = header
        for d in self.segment_data:
            full_data += d

        with open(filename, 'wb') as f:
            f.write(full_data)
  1. Below is a Java class that can open, decode, read, write, and print the properties of a .PUP file to the console.
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.*;

public class PupFile {
    private byte[] magic = new byte[7];
    private byte formatFlag;
    private long packageVersion;
    private long imageVersion;
    private long segmentCount;
    private long headerLength;
    private long dataSize;
    private List<Map<String, Object>> segments = new ArrayList<>();
    private List<Map<String, Object>> digests = new ArrayList<>();
    private byte[] headerHmac = new byte[20];
    private byte[] padding = new byte[12];
    private List<byte[]> segmentData = new ArrayList<>();

    public PupFile(String filename) throws IOException {
        if (filename != null) {
            load(filename);
        }
    }

    public void load(String filename) throws IOException {
        File file = new File(filename);
        ByteBuffer buffer = ByteBuffer.allocate((int) file.length()).order(ByteOrder.BIG_ENDIAN);
        try (FileChannel channel = new FileInputStream(file).getChannel()) {
            channel.read(buffer);
        }
        buffer.rewind();

        buffer.get(magic);
        formatFlag = buffer.get();
        packageVersion = buffer.getLong();
        imageVersion = buffer.getLong();
        segmentCount = buffer.getLong();
        headerLength = buffer.getLong();
        dataSize = buffer.getLong();

        segments.clear();
        for (long i = 0; i < segmentCount; i++) {
            Map<String, Object> seg = new HashMap<>();
            seg.put("id", buffer.getLong());
            seg.put("offset", buffer.getLong());
            seg.put("size", buffer.getLong());
            seg.put("hashed", buffer.getInt() & 0xFFFFFFFFL);
            seg.put("pad", buffer.getInt() & 0xFFFFFFFFL);
            segments.add(seg);
        }

        digests.clear();
        for (long i = 0; i < segmentCount; i++) {
            Map<String, Object> dig = new HashMap<>();
            dig.put("index", buffer.getLong());
            byte[] digest = new byte[20];
            buffer.get(digest);
            dig.put("digest", digest);
            dig.put("pad", buffer.getInt() & 0xFFFFFFFFL);
            digests.add(dig);
        }

        buffer.get(headerHmac);
        buffer.get(padding);

        // Extract segment data
        segmentData.clear();
        for (Map<String, Object> seg : segments) {
            long off = (long) seg.get("offset");
            long size = (long) seg.get("size");
            byte[] data = new byte[(int) size];
            buffer.position((int) off);
            buffer.get(data);
            segmentData.add(data);
        }
    }

    public void printProperties() {
        System.out.println("Magic: " + new String(magic));
        System.out.println("Format flag: " + formatFlag);
        System.out.println("Package version: " + packageVersion);
        System.out.println("Image version: " + imageVersion);
        System.out.println("Segment count: " + segmentCount);
        System.out.println("Header length: " + headerLength);
        System.out.println("Data size: " + dataSize);
        for (int i = 0; i < segments.size(); i++) {
            Map<String, Object> seg = segments.get(i);
            System.out.println("Segment " + i + ": ID=" + seg.get("id") + ", Offset=" + seg.get("offset") + ", Size=" + seg.get("size") + ", Hashed=" + seg.get("hashed") + ", Pad=" + seg.get("pad"));
        }
        for (int i = 0; i < digests.size(); i++) {
            Map<String, Object> dig = digests.get(i);
            byte[] digestBytes = (byte[]) dig.get("digest");
            String digestHex = bytesToHex(digestBytes);
            System.out.println("Digest " + i + ": Index=" + dig.get("index") + ", Digest=" + digestHex + ", Pad=" + dig.get("pad"));
        }
        System.out.println("Header HMAC: " + bytesToHex(headerHmac));
        System.out.println("Padding: " + bytesToHex(padding));
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public void save(String filename) throws IOException {
        ByteBuffer headerBuffer = ByteBuffer.allocate(0x10000).order(ByteOrder.BIG_ENDIAN); // Oversize for safety

        headerBuffer.put(magic);
        headerBuffer.put(formatFlag);
        headerBuffer.putLong(packageVersion);
        headerBuffer.putLong(imageVersion);
        headerBuffer.putLong(segments.size());
        headerBuffer.putLong(0); // Placeholder for headerLength
        headerBuffer.putLong(0); // Placeholder for dataSize

        for (Map<String, Object> seg : segments) {
            headerBuffer.putLong((long) seg.get("id"));
            headerBuffer.putLong((long) seg.get("offset"));
            headerBuffer.putLong((long) seg.get("size"));
            headerBuffer.putInt(((Long) seg.get("hashed")).intValue());
            headerBuffer.putInt(((Long) seg.get("pad")).intValue());
        }

        for (Map<String, Object> dig : digests) {
            headerBuffer.putLong((long) dig.get("index"));
            headerBuffer.put((byte[]) dig.get("digest"));
            headerBuffer.putInt(((Long) dig.get("pad")).intValue());
        }

        headerBuffer.put(headerHmac);
        headerBuffer.put(padding);

        int tempHeaderPos = headerBuffer.position();
        long headerLengthVal = tempHeaderPos;
        long dataSizeVal = 0;
        for (byte[] d : segmentData) {
            dataSizeVal += d.length;
        }

        // Update placeholders
        headerBuffer.position(32);
        headerBuffer.putLong(headerLengthVal);
        headerBuffer.putLong(dataSizeVal);

        // Update segment offsets
        long currentOffset = headerLengthVal;
        int segTableStart = 48;
        headerBuffer.position(segTableStart);
        for (int i = 0; i < segments.size(); i++) {
            Map<String, Object> seg = segments.get(i);
            seg.put("offset", currentOffset);
            headerBuffer.putLong((long) seg.get("id"));
            headerBuffer.putLong(currentOffset);
            headerBuffer.putLong((long) seg.get("size"));
            headerBuffer.putInt(((Long) seg.get("hashed")).intValue());
            headerBuffer.putInt(((Long) seg.get("pad")).intValue());
            currentOffset += (long) seg.get("size");
        }

        // Skip to end of segments table and add digests, hmac, padding (already in temp)
        headerBuffer.position((int) headerLengthVal);

        try (FileOutputStream fos = new FileOutputStream(filename)) {
            fos.getChannel().write(headerBuffer);

            for (byte[] d : segmentData) {
                fos.write(d);
            }
        }
    }
}
  1. Below is a JavaScript class that can open, decode, read, write, and print the properties of a .PUP file to the console (using Node.js for file I/O).
const fs = require('fs');

class PupFile {
  constructor(filename) {
    this.magic = '';
    this.formatFlag = 0;
    this.packageVersion = 0n;
    this.imageVersion = 0n;
    this.segmentCount = 0;
    this.headerLength = 0;
    this.dataSize = 0;
    this.segments = [];
    this.digests = [];
    this.headerHmac = '';
    this.padding = '';
    this.segmentData = [];
    if (filename) {
      this.load(filename);
    }
  }

  load(filename) {
    const data = fs.readFileSync(filename);
    const view = new DataView(data.buffer);

    this.magic = new TextDecoder().decode(data.slice(0, 7));
    this.formatFlag = data[7];
    this.packageVersion = view.getBigUint64(8);
    this.imageVersion = view.getBigUint64(16);
    this.segmentCount = Number(view.getBigUint64(24));
    this.headerLength = Number(view.getBigUint64(32));
    this.dataSize = Number(view.getBigUint64(40));

    let offset = 48;
    this.segments = [];
    for (let i = 0; i < this.segmentCount; i++) {
      this.segments.push({
        id: view.getBigUint64(offset),
        offset: view.getBigUint64(offset + 8),
        size: view.getBigUint64(offset + 16),
        hashed: view.getUint32(offset + 24),
        pad: view.getUint32(offset + 28)
      });
      offset += 32;
    }

    this.digests = [];
    for (let i = 0; i < this.segmentCount; i++) {
      const digestBytes = data.slice(offset + 8, offset + 28);
      this.digests.push({
        index: view.getBigUint64(offset),
        digest: [...digestBytes].map(b => b.toString(16).padStart(2, '0')).join(''),
        pad: view.getUint32(offset + 28)
      });
      offset += 32;
    }

    const headerHmacBytes = data.slice(offset, offset + 20);
    this.headerHmac = [...headerHmacBytes].map(b => b.toString(16).padStart(2, '0')).join('');
    offset += 20;

    const paddingBytes = data.slice(offset, offset + 12);
    this.padding = [...paddingBytes].map(b => b.toString(16).padStart(2, '0')).join('');

    this.segmentData = [];
    for (const seg of this.segments) {
      this.segmentData.push(data.slice(Number(seg.offset), Number(seg.offset) + Number(seg.size)));
    }
  }

  printProperties() {
    console.log(`Magic: ${this.magic}`);
    console.log(`Format flag: ${this.formatFlag}`);
    console.log(`Package version: ${this.packageVersion}`);
    console.log(`Image version: ${this.imageVersion}`);
    console.log(`Segment count: ${this.segmentCount}`);
    console.log(`Header length: ${this.headerLength}`);
    console.log(`Data size: ${this.dataSize}`);
    this.segments.forEach((seg, i) => {
      console.log(`Segment ${i}: ID=${seg.id}, Offset=${seg.offset}, Size=${seg.size}, Hashed=${seg.hashed}, Pad=${seg.pad}`);
    });
    this.digests.forEach((dig, i) => {
      console.log(`Digest ${i}: Index=${dig.index}, Digest=${dig.digest}, Pad=${dig.pad}`);
    });
    console.log(`Header HMAC: ${this.headerHmac}`);
    console.log(`Padding: ${this.padding}`);
  }

  save(filename) {
    let header = new Uint8Array(0x10000); // Oversize
    let view = new DataView(header.buffer);

    header.set(new TextEncoder().encode(this.magic), 0);
    header[7] = this.formatFlag;
    view.setBigUint64(8, this.packageVersion);
    view.setBigUint64(16, this.imageVersion);
    view.setBigUint64(24, BigInt(this.segments.length));
    view.setBigUint64(32, 0n); // Placeholder
    view.setBigUint64(40, 0n); // Placeholder

    let offset = 48;
    for (const seg of this.segments) {
      view.setBigUint64(offset, seg.id);
      view.setBigUint64(offset + 8, seg.offset);
      view.setBigUint64(offset + 16, seg.size);
      view.setUint32(offset + 24, seg.hashed);
      view.setUint32(offset + 28, seg.pad);
      offset += 32;
    }

    for (const dig of this.digests) {
      view.setBigUint64(offset, dig.index);
      const digestBytes = dig.digest.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
      header.set(digestBytes, offset + 8);
      view.setUint32(offset + 28, dig.pad);
      offset += 32;
    }

    const hmacBytes = this.headerHmac.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
    header.set(hmacBytes, offset);
    offset += 20;

    const paddingBytes = this.padding.match(/.{1,2}/g).map(byte => parseInt(byte, 16));
    header.set(paddingBytes, offset);
    offset += 12;

    const headerLengthVal = offset;
    let dataSizeVal = 0n;
    for (const d of this.segmentData) {
      dataSizeVal += BigInt(d.length);
    }

    // Update placeholders
    view.setBigUint64(32, BigInt(headerLengthVal));
    view.setBigUint64(40, dataSizeVal);

    // Update offsets
    let currentOffset = BigInt(headerLengthVal);
    offset = 48;
    for (let i = 0; i < this.segments.length; i++) {
      const seg = this.segments[i];
      seg.offset = currentOffset;
      view.setBigUint64(offset, seg.id);
      view.setBigUint64(offset + 8, currentOffset);
      view.setBigUint64(offset + 16, seg.size);
      view.setUint32(offset + 24, seg.hashed);
      view.setUint32(offset + 28, seg.pad);
      currentOffset += seg.size;
      offset += 32;
    }

    // Full data
    const fullBuffer = new Uint8Array(headerLengthVal + Number(dataSizeVal));
    fullBuffer.set(header.slice(0, headerLengthVal));
    let dataOffset = headerLengthVal;
    for (const d of this.segmentData) {
      fullBuffer.set(d, dataOffset);
      dataOffset += d.length;
    }

    fs.writeFileSync(filename, fullBuffer);
  }
}
  1. Below is a C++ class that can open, decode, read, write, and print the properties of a .PUP file to the console.
#include <iostream>
#include <fstream>
#include <vector>
#include <iomanip>
#include <cstdint>
#include <cstring>

struct Segment {
    uint64_t id;
    uint64_t offset;
    uint64_t size;
    uint32_t hashed;
    uint32_t pad;
};

struct Digest {
    uint64_t index;
    uint8_t digest[20];
    uint32_t pad;
};

class PupFile {
private:
    uint8_t magic[7];
    uint8_t format_flag;
    uint64_t package_version;
    uint64_t image_version;
    uint64_t segment_count;
    uint64_t header_length;
    uint64_t data_size;
    std::vector<Segment> segments;
    std::vector<Digest> digests;
    uint8_t header_hmac[20];
    uint8_t padding[12];
    std::vector<std::vector<uint8_t>> segment_data;

    uint64_t be_to_uint64(const uint8_t* buf) {
        uint64_t val = 0;
        for (int i = 0; i < 8; ++i) {
            val = (val << 8) | buf[i];
        }
        return val;
    }

    uint32_t be_to_uint32(const uint8_t* buf) {
        uint32_t val = 0;
        for (int i = 0; i < 4; ++i) {
            val = (val << 8) | buf[i];
        }
        return val;
    }

    void uint64_to_be(uint64_t val, uint8_t* buf) {
        for (int i = 7; i >= 0; --i) {
            buf[i] = val & 0xFF;
            val >>= 8;
        }
    }

    void uint32_to_be(uint32_t val, uint8_t* buf) {
        for (int i = 3; i >= 0; --i) {
            buf[i] = val & 0xFF;
            val >>= 8;
        }
    }

public:
    PupFile(const std::string& filename = "") {
        memset(magic, 0, 7);
        format_flag = 0;
        package_version = 0;
        image_version = 0;
        segment_count = 0;
        header_length = 0;
        data_size = 0;
        memset(header_hmac, 0, 20);
        memset(padding, 0, 12);
        if (!filename.empty()) {
            load(filename);
        }
    }

    void load(const std::string& filename) {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        if (!file) {
            std::cerr << "Failed to open file" << std::endl;
            return;
        }
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<uint8_t> data(size);
        file.read(reinterpret_cast<char*>(data.data()), size);

        memcpy(magic, data.data(), 7);
        format_flag = data[7];
        package_version = be_to_uint64(&data[8]);
        image_version = be_to_uint64(&data[16]);
        segment_count = be_to_uint64(&data[24]);
        header_length = be_to_uint64(&data[32]);
        data_size = be_to_uint64(&data[40]);

        size_t offset = 48;
        segments.clear();
        for (uint64_t i = 0; i < segment_count; ++i) {
            Segment seg;
            seg.id = be_to_uint64(&data[offset]);
            seg.offset = be_to_uint64(&data[offset + 8]);
            seg.size = be_to_uint64(&data[offset + 16]);
            seg.hashed = be_to_uint32(&data[offset + 24]);
            seg.pad = be_to_uint32(&data[offset + 28]);
            segments.push_back(seg);
            offset += 32;
        }

        digests.clear();
        for (uint64_t i = 0; i < segment_count; ++i) {
            Digest dig;
            dig.index = be_to_uint64(&data[offset]);
            memcpy(dig.digest, &data[offset + 8], 20);
            dig.pad = be_to_uint32(&data[offset + 28]);
            digests.push_back(dig);
            offset += 32;
        }

        memcpy(header_hmac, &data[offset], 20);
        offset += 20;
        memcpy(padding, &data[offset], 12);

        segment_data.clear();
        for (const auto& seg : segments) {
            std::vector<uint8_t> sdata(seg.size);
            memcpy(sdata.data(), &data[seg.offset], seg.size);
            segment_data.push_back(sdata);
        }
    }

    void print_properties() {
        std::cout << "Magic: ";
        for (int i = 0; i < 7; ++i) std::cout << magic[i];
        std::cout << std::endl;
        std::cout << "Format flag: " << (int)format_flag << std::endl;
        std::cout << "Package version: " << package_version << std::endl;
        std::cout << "Image version: " << image_version << std::endl;
        std::cout << "Segment count: " << segment_count << std::endl;
        std::cout << "Header length: " << header_length << std::endl;
        std::cout << "Data size: " << data_size << std::endl;
        for (size_t i = 0; i < segments.size(); ++i) {
            const auto& seg = segments[i];
            std::cout << "Segment " << i << ": ID=" << seg.id << ", Offset=" << seg.offset << ", Size=" << seg.size << ", Hashed=" << seg.hashed << ", Pad=" << seg.pad << std::endl;
        }
        for (size_t i = 0; i < digests.size(); ++i) {
            const auto& dig = digests[i];
            std::cout << "Digest " << i << ": Index=" << dig.index << ", Digest=";
            for (int j = 0; j < 20; ++j) std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)dig.digest[j];
            std::cout << ", Pad=" << dig.pad << std::dec << std::endl;
        }
        std::cout << "Header HMAC: ";
        for (int i = 0; i < 20; ++i) std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)header_hmac[i];
        std::cout << std::dec << std::endl;
        std::cout << "Padding: ";
        for (int i = 0; i < 12; ++i) std::cout << std::hex << std::setw(2) << std::setfill('0') << (int)padding[i];
        std::cout << std::dec << std::endl;
    }

    void save(const std::string& filename) {
        std::vector<uint8_t> header(0x10000); // Oversize

        memcpy(&header[0], magic, 7);
        header[7] = format_flag;
        uint64_to_be(package_version, &header[8]);
        uint64_to_be(image_version, &header[16]);
        uint64_to_be(segments.size(), &header[24]);
        uint64_to_be(0, &header[32]); // Placeholder
        uint64_to_be(0, &header[40]); // Placeholder

        size_t offset = 48;
        for (const auto& seg : segments) {
            uint64_to_be(seg.id, &header[offset]);
            uint64_to_be(seg.offset, &header[offset + 8]);
            uint64_to_be(seg.size, &header[offset + 16]);
            uint32_to_be(seg.hashed, &header[offset + 24]);
            uint32_to_be(seg.pad, &header[offset + 28]);
            offset += 32;
        }

        for (const auto& dig : digests) {
            uint64_to_be(dig.index, &header[offset]);
            memcpy(&header[offset + 8], dig.digest, 20);
            uint32_to_be(dig.pad, &header[offset + 28]);
            offset += 32;
        }

        memcpy(&header[offset], header_hmac, 20);
        offset += 20;
        memcpy(&header[offset], padding, 12);
        offset += 12;

        uint64_t header_length_val = offset;
        uint64_t data_size_val = 0;
        for (const auto& d : segment_data) {
            data_size_val += d.size();
        }

        // Update placeholders
        uint64_to_be(header_length_val, &header[32]);
        uint64_to_be(data_size_val, &header[40]);

        // Update segment offsets
        uint64_t current_offset = header_length_val;
        offset = 48;
        for (auto& seg : segments) {
            seg.offset = current_offset;
            uint64_to_be(seg.id, &header[offset]);
            uint64_to_be(seg.offset, &header[offset + 8]);
            uint64_to_be(seg.size, &header[offset + 16]);
            uint32_to_be(seg.hashed, &header[offset + 24]);
            uint32_to_be(seg.pad, &header[offset + 28]);
            current_offset += seg.size;
            offset += 32;
        }

        std::ofstream out(filename, std::ios::binary);
        out.write(reinterpret_cast<const char*>(header.data()), header_length_val);
        for (const auto& d : segment_data) {
            out.write(reinterpret_cast<const char*>(d.data()), d.size());
        }
    }
};