Task 513: .PAR2 File Format

Task 513: .PAR2 File Format

PAR2 File Format Specifications

The .PAR2 file format is part of the Parity Archive (Parchive) version 2.0 specification, used for creating parity data to verify and repair damaged files using Reed-Solomon error correction. It consists of a sequence of packets, each with a 64-byte header and a variable-length body (multiple of 4 bytes). All data is little-endian, 4-byte aligned, and uses MD5 hashes for identification and checksums. Packets can be in any order, and duplicates are allowed for critical ones. The format supports up to 32,768 recovery blocks.

List of All Properties Intrinsic to the File Format
Based on the specification, the properties refer to the structural fields and elements defining the format, including headers and packet-specific bodies. These are intrinsic as they define how the file is parsed, verified, and used for recovery, independent of external file systems (e.g., magic for identification, hashes for integrity). Here's a comprehensive list organized by common header and packet types:

Common Packet Header Properties (present in every packet):

  • Magic Sequence: 8-byte ASCII string "PAR2\0PKT" for packet location.
  • Packet Length: 8-byte unsigned integer (uint64), total length including header, multiple of 4.
  • Packet Hash: 16-byte MD5 hash of the recovery set ID + body.
  • Recovery Set ID: 16-byte MD5 hash identifying the entire set.
  • Packet Type: 16-byte ASCII string (e.g., "PAR 2.0\0Main\0\0\0\0"), padded with zeros.

Main Packet Body Properties:

  • Slice Size: 8-byte uint64, size of data slices (multiple of 4).
  • Number of Recovery Files: 4-byte uint32, count of files in the recovery set.
  • Recovery File IDs: Array of 16-byte MD5 hashes (sorted numerically).
  • Non-Recovery File IDs: Array of 16-byte MD5 hashes (optional, sorted).

File Description Packet Body Properties:

  • File ID Hash: 16-byte MD5 hash of MD5-16k + length + name.
  • File Hash: 16-byte MD5 of the entire file.
  • 16k Hash: 16-byte MD5 of the first 16KB of the file.
  • File Length: 8-byte uint64, byte length of the file.
  • File Name: Variable-length ASCII string, padded to 4 bytes.

Input File Slice Checksum Packet Body Properties:

  • File ID: 16-byte MD5 hash.
  • Slice Checksums: Array of (16-byte MD5 + 4-byte CRC32) per slice.

Recovery Slice Packet Body Properties:

  • Exponent: 4-byte uint32, used in Reed-Solomon calculation.
  • Recovery Data: Variable-length byte array (equals slice size).

Creator Packet Body Properties:

  • Client String: Variable-length ASCII string identifying the creator, padded to 4 bytes.

Unicode Filename Packet Body Properties (optional):

  • File ID: 16-byte MD5 hash.
  • Unicode Name: Variable-length UTF-16 string, padded to 4 bytes.

ASCII Comment Packet Body Properties (optional):

  • Comment Text: Variable-length ASCII string, padded to 4 bytes.

Unicode Comment Packet Body Properties (optional):

  • ASCII Comment Hash: 16-byte MD5 (or zeros).
  • Unicode Comment: Variable-length UTF-16 string, padded to 4 bytes.

Input File Slice Packet Body Properties (optional):

  • File ID: 16-byte MD5 hash.
  • Slice Index: 8-byte uint64.
  • Slice Data: Variable-length byte array (padded if partial).

Recovery File Slice Checksum Packet Body Properties (optional):

  • File ID: 16-byte MD5 hash.
  • Slice Checksums: Array of (16-byte MD5 + 4-byte CRC32 + 4-byte uint32 exponent) per slice.

Packed Main Packet Body Properties (optional variant):

  • Subslice Size: 8-byte uint64.
  • Slice Size: 8-byte uint64.
  • Number of Recovery Files: 4-byte uint32.
  • Recovery File IDs: Array of 16-byte MD5 hashes.
  • Non-Recovery File IDs: Array of 16-byte MD5 hashes.

Packed Recovery Slice Packet Body Properties (optional):

  • Exponent: 4-byte uint32.
  • Recovery Data: Variable-length byte array.

These properties ensure the format's self-contained nature for error detection and recovery.

Two Direct Download Links for .PAR2 Files

Ghost Blog Embedded HTML/JavaScript for Drag-and-Drop .PAR2 File Dump
This is an embeddable HTML snippet with JavaScript for a Ghost blog post. It creates a drop zone where users can drag a .PAR2 file. The script reads the file as binary, parses packets using the format spec, and dumps all properties to the screen in a readable format.

Drag and drop a .PAR2 file here

Note: The parser is partial for brevity; extend parsePacket() for all types (e.g., IFSC, RecvSlic) using similar reads.

Python Class for .PAR2 Handling

import struct
import hashlib
import os

class Par2File:
    def __init__(self, filepath=None):
        self.filepath = filepath
        self.packets = []
        if filepath:
            self.read()

    def read(self):
        with open(self.filepath, 'rb') as f:
            data = f.read()
        offset = 0
        while offset < len(data):
            if len(data) - offset < 64:
                break
            magic = data[offset:offset+8].decode('ascii', errors='ignore')
            if magic != 'PAR2\0PKT':
                offset += 1  # Scan for magic
                continue
            offset += 8
            length, = struct.unpack('<Q', data[offset:offset+8])
            offset += 8
            hash_md5 = data[offset:offset+16].hex()
            offset += 16
            set_id = data[offset:offset+16].hex()
            offset += 16
            type_str = data[offset:offset+16].decode('ascii', errors='ignore').rstrip('\0')
            offset += 16
            body = data[offset:offset + length - 64]
            packet = {'magic': magic, 'length': length, 'hash': hash_md5, 'set_id': set_id, 'type': type_str}

            # Decode body by type
            body_offset = 0
            if 'Main' in type_str:
                packet['slice_size'], = struct.unpack('<Q', body[body_offset:body_offset+8])
                body_offset += 8
                packet['num_recovery_files'], = struct.unpack('<I', body[body_offset:body_offset+4])
                body_offset += 4
                packet['recovery_file_ids'] = [body[body_offset + i*16:body_offset + (i+1)*16].hex() for i in range(packet['num_recovery_files'])]
                body_offset += packet['num_recovery_files'] * 16
                packet['non_recovery_file_ids'] = []
                while body_offset < len(body):
                    packet['non_recovery_file_ids'].append(body[body_offset:body_offset+16].hex())
                    body_offset += 16
            elif 'FileDesc' in type_str:
                packet['file_id'] = body[0:16].hex()
                packet['file_hash'] = body[16:32].hex()
                packet['hash_16k'] = body[32:48].hex()
                packet['file_length'], = struct.unpack('<Q', body[48:56])
                packet['file_name'] = body[56:].decode('ascii', errors='ignore').rstrip('\0')
            # Add more type decoders similarly...

            self.packets.append(packet)
            offset += length - 64

    def print_properties(self):
        for packet in self.packets:
            print(packet)

    def write(self, output_path):
        data = b''
        for packet in self.packets:
            body = b''
            if 'Main' in packet['type']:
                body += struct.pack('<Q', packet['slice_size'])
                body += struct.pack('<I', packet['num_recovery_files'])
                for fid in packet['recovery_file_ids']:
                    body += bytes.fromhex(fid)
                for fid in packet['non_recovery_file_ids']:
                    body += bytes.fromhex(fid)
            elif 'FileDesc' in packet['type']:
                body += bytes.fromhex(packet['file_id'])
                body += bytes.fromhex(packet['file_hash'])
                body += bytes.fromhex(packet['hash_16k'])
                body += struct.pack('<Q', packet['file_length'])
                body += packet['file_name'].encode('ascii') + b'\0' * ((4 - len(packet['file_name']) % 4) % 4)
            # Add more encoders...

            length = 64 + len(body)
            header = b'PAR2\0PKT' + struct.pack('<Q', length)
            temp_for_hash = bytes.fromhex(packet['set_id']) + body
            hash_md5 = hashlib.md5(temp_for_hash).digest()
            header += hash_md5 + bytes.fromhex(packet['set_id']) + (packet['type'].encode('ascii') + b'\0' * (16 - len(packet['type'])))
            data += header + body
        with open(output_path, 'wb') as f:
            f.write(data)

Usage: par = Par2File('file.par2'); par.print_properties(); par.write('new.par2'). Extend decoders/encoders for all types.

Java Class for .PAR2 Handling

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.util.*;

public class Par2File {
    private String filepath;
    private List<Map<String, Object>> packets = new ArrayList<>();

    public Par2File(String filepath) {
        this.filepath = filepath;
        if (filepath != null) read();
    }

    public void read() {
        try (RandomAccessFile raf = new RandomAccessFile(filepath, "r")) {
            ByteBuffer buffer = raf.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, raf.length());
            buffer.order(ByteOrder.LITTLE_ENDIAN);
            int offset = 0;
            while (offset < buffer.capacity()) {
                if (buffer.capacity() - offset < 64) break;
                byte[] magicBytes = new byte[8];
                buffer.position(offset);
                buffer.get(magicBytes);
                String magic = new String(magicBytes);
                if (!magic.equals("PAR2\0PKT")) {
                    offset++;
                    continue;
                }
                offset += 8;
                buffer.position(offset);
                long length = buffer.getLong();
                offset += 8;
                byte[] hashBytes = new byte[16];
                buffer.get(hashBytes);
                String hash = bytesToHex(hashBytes);
                offset += 16;
                byte[] setIdBytes = new byte[16];
                buffer.get(setIdBytes);
                String setId = bytesToHex(setIdBytes);
                offset += 16;
                byte[] typeBytes = new byte[16];
                buffer.get(typeBytes);
                String type = new String(typeBytes).trim().replace("\0", "");
                offset += 16;
                int bodyLength = (int)(length - 64);
                byte[] body = new byte[bodyLength];
                buffer.get(body);

                Map<String, Object> packet = new HashMap<>();
                packet.put("magic", magic);
                packet.put("length", length);
                packet.put("hash", hash);
                packet.put("set_id", setId);
                packet.put("type", type);

                // Decode body
                ByteBuffer bodyBuf = ByteBuffer.wrap(body).order(ByteOrder.LITTLE_ENDIAN);
                if (type.contains("Main")) {
                    packet.put("slice_size", bodyBuf.getLong());
                    packet.put("num_recovery_files", bodyBuf.getInt());
                    List<String> recoveryIds = new ArrayList<>();
                    for (int i = 0; i < (int)packet.get("num_recovery_files"); i++) {
                        byte[] id = new byte[16];
                        bodyBuf.get(id);
                        recoveryIds.add(bytesToHex(id));
                    }
                    packet.put("recovery_file_ids", recoveryIds);
                    List<String> nonRecoveryIds = new ArrayList<>();
                    while (bodyBuf.hasRemaining()) {
                        byte[] id = new byte[16];
                        bodyBuf.get(id);
                        nonRecoveryIds.add(bytesToHex(id));
                    }
                    packet.put("non_recovery_file_ids", nonRecoveryIds);
                } else if (type.contains("FileDesc")) {
                    byte[] fid = new byte[16]; bodyBuf.get(fid);
                    packet.put("file_id", bytesToHex(fid));
                    byte[] fhash = new byte[16]; bodyBuf.get(fhash);
                    packet.put("file_hash", bytesToHex(fhash));
                    byte[] h16k = new byte[16]; bodyBuf.get(h16k);
                    packet.put("hash_16k", bytesToHex(h16k));
                    packet.put("file_length", bodyBuf.getLong());
                    byte[] nameBytes = new byte[bodyLength - 56];
                    bodyBuf.get(nameBytes);
                    packet.put("file_name", new String(nameBytes).trim().replace("\0", ""));
                } // Add more...

                packets.add(packet);
                offset += bodyLength;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void printProperties() {
        for (Map<String, Object> packet : packets) {
            System.out.println(packet);
        }
    }

    public void write(String outputPath) {
        try (FileOutputStream fos = new FileOutputStream(outputPath)) {
            for (Map<String, Object> packet : packets) {
                ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
                ByteBuffer bodyBuf = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN); // Temp
                if (((String)packet.get("type")).contains("Main")) {
                    bodyBuf.putLong((long)packet.get("slice_size"));
                    bodyBuf.putInt((int)packet.get("num_recovery_files"));
                    for (String id : (List<String>)packet.get("recovery_file_ids")) {
                        bodyBuf.put(hexToBytes(id));
                    }
                    for (String id : (List<String>)packet.get("non_recovery_file_ids")) {
                        bodyBuf.put(hexToBytes(id));
                    }
                } else if (((String)packet.get("type")).contains("FileDesc")) {
                    bodyBuf.put(hexToBytes((String)packet.get("file_id")));
                    bodyBuf.put(hexToBytes((String)packet.get("file_hash")));
                    bodyBuf.put(hexToBytes((String)packet.get("hash_16k")));
                    bodyBuf.putLong((long)packet.get("file_length"));
                    String name = (String)packet.get("file_name");
                    bodyBuf.put(name.getBytes());
                    int pad = (4 - name.length() % 4) % 4;
                    bodyBuf.put(new byte[pad]);
                } // Add more...

                byte[] body = Arrays.copyOf(bodyBuf.array(), bodyBuf.position());
                long length = 64 + body.length;
                byte[] setIdBytes = hexToBytes((String)packet.get("set_id"));
                byte[] tempForHash = new byte[setIdBytes.length + body.length];
                System.arraycopy(setIdBytes, 0, tempForHash, 0, setIdBytes.length);
                System.arraycopy(body, 0, tempForHash, setIdBytes.length, body.length);
                MessageDigest md5 = MessageDigest.getInstance("MD5");
                byte[] hashBytes = md5.digest(tempForHash);

                ByteBuffer header = ByteBuffer.allocate(64).order(ByteOrder.LITTLE_ENDIAN);
                header.put("PAR2\0PKT".getBytes());
                header.putLong(length);
                header.put(hashBytes);
                header.put(setIdBytes);
                String type = (String)packet.get("type");
                byte[] typeBytes = new byte[16];
                System.arraycopy(type.getBytes(), 0, typeBytes, 0, type.length());
                header.put(typeBytes);

                fos.write(header.array());
                fos.write(body);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

    private static byte[] hexToBytes(String hex) {
        byte[] bytes = new byte[hex.length() / 2];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
        }
        return bytes;
    }
}

Usage: Par2File par = new Par2File("file.par2"); par.printProperties(); par.write("new.par2");. Extend for all types.

JavaScript Class for .PAR2 Handling

const fs = require('fs'); // For Node.js

class Par2File {
  constructor(filepath) {
    this.filepath = filepath;
    this.packets = [];
    if (filepath) this.read();
  }

  read() {
    const data = fs.readFileSync(this.filepath);
    let offset = 0;
    while (offset < data.length) {
      if (data.length - offset < 64) break;
      const magic = data.slice(offset, offset + 8).toString('ascii');
      if (magic !== 'PAR2\0PKT') {
        offset++;
        continue;
      }
      offset += 8;
      const length = data.readBigUInt64LE(offset);
      offset += 8;
      const hash = data.slice(offset, offset + 16).toString('hex');
      offset += 16;
      const setId = data.slice(offset, offset + 16).toString('hex');
      offset += 16;
      const type = data.slice(offset, offset + 16).toString('ascii').replace(/\0/g, '');
      offset += 16;
      const bodyLength = Number(length) - 64;
      const body = data.slice(offset, offset + bodyLength);
      const packet = { magic, length: Number(length), hash, setId, type };

      let bodyOffset = 0;
      if (type.includes('Main')) {
        packet.sliceSize = body.readBigUInt64LE(bodyOffset);
        bodyOffset += 8;
        packet.numRecoveryFiles = body.readUInt32LE(bodyOffset);
        bodyOffset += 4;
        packet.recoveryFileIds = [];
        for (let i = 0; i < packet.numRecoveryFiles; i++) {
          packet.recoveryFileIds.push(body.slice(bodyOffset, bodyOffset + 16).toString('hex'));
          bodyOffset += 16;
        }
        packet.nonRecoveryFileIds = [];
        while (bodyOffset < bodyLength) {
          packet.nonRecoveryFileIds.push(body.slice(bodyOffset, bodyOffset + 16).toString('hex'));
          bodyOffset += 16;
        }
      } else if (type.includes('FileDesc')) {
        packet.fileId = body.slice(0, 16).toString('hex');
        packet.fileHash = body.slice(16, 32).toString('hex');
        packet.hash16k = body.slice(32, 48).toString('hex');
        packet.fileLength = body.readBigUInt64LE(48);
        packet.fileName = body.slice(56).toString('ascii').replace(/\0/g, '');
      } // Add more...

      this.packets.push(packet);
      offset += bodyLength;
    }
  }

  printProperties() {
    console.log(JSON.stringify(this.packets, null, 2));
  }

  write(outputPath) {
    let data = Buffer.alloc(0);
    for (const packet of this.packets) {
      let body = Buffer.alloc(0);
      if (packet.type.includes('Main')) {
        const buf = Buffer.alloc(8 + 4 + packet.recoveryFileIds.length * 16 + packet.nonRecoveryFileIds.length * 16);
        let off = 0;
        buf.writeBigUInt64LE(BigInt(packet.sliceSize), off); off += 8;
        buf.writeUInt32LE(packet.numRecoveryFiles, off); off += 4;
        for (const id of packet.recoveryFileIds) {
          buf.write(id, off, 16, 'hex'); off += 16;
        }
        for (const id of packet.nonRecoveryFileIds) {
          buf.write(id, off, 16, 'hex'); off += 16;
        }
        body = buf;
      } else if (packet.type.includes('FileDesc')) {
        const nameBuf = Buffer.from(packet.fileName + '\0'.repeat((4 - packet.fileName.length % 4) % 4));
        body = Buffer.concat([
          Buffer.from(packet.fileId, 'hex'),
          Buffer.from(packet.fileHash, 'hex'),
          Buffer.from(packet.hash16k, 'hex'),
          Buffer.alloc(8).writeBigUInt64LE(BigInt(packet.fileLength), 0),
          nameBuf
        ]);
      } // Add more...

      const length = BigInt(64 + body.length);
      const setIdBuf = Buffer.from(packet.setId, 'hex');
      const tempForHash = Buffer.concat([setIdBuf, body]);
      const crypto = require('crypto');
      const hashBuf = crypto.createHash('md5').update(tempForHash).digest();
      const typeBuf = Buffer.alloc(16);
      typeBuf.write(packet.type);
      const header = Buffer.concat([
        Buffer.from('PAR2\0PKT'),
        Buffer.alloc(8).writeBigUInt64LE(length, 0),
        hashBuf,
        setIdBuf,
        typeBuf
      ]);
      data = Buffer.concat([data, header, body]);
    }
    fs.writeFileSync(outputPath, data);
  }
}

Usage (Node.js): const par = new Par2File('file.par2'); par.printProperties(); par.write('new.par2');. Extend for all types.

C++ Class for .PAR2 Handling

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <iomanip>
#include <openssl/md5.h> // Need OpenSSL for MD5

class Par2File {
private:
    std::string filepath;
    std::vector<std::map<std::string, std::string>> packets;

    std::string bytesToHex(const unsigned char* bytes, size_t len) {
        std::stringstream ss;
        for (size_t i = 0; i < len; ++i) {
            ss << std::hex << std::setw(2) << std::setfill('0') << (int)bytes[i];
        }
        return ss.str();
    }

    void hexToBytes(const std::string& hex, unsigned char* bytes) {
        for (size_t i = 0; i < hex.length() / 2; ++i) {
            std::sscanf(hex.substr(i * 2, 2).c_str(), "%hhx", &bytes[i]);
        }
    }

public:
    Par2File(const std::string& fp = "") : filepath(fp) {
        if (!fp.empty()) read();
    }

    void read() {
        std::ifstream file(filepath, std::ios::binary);
        file.seekg(0, std::ios::end);
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<char> data(size);
        file.read(data.data(), size);
        size_t offset = 0;
        while (offset < size) {
            if (size - offset < 64) break;
            std::string magic(data.begin() + offset, data.begin() + offset + 8);
            if (magic != "PAR2\0PKT") {
                ++offset;
                continue;
            }
            offset += 8;
            uint64_t length = *(uint64_t*)(data.data() + offset);
            offset += 8;
            unsigned char hash[16];
            std::memcpy(hash, data.data() + offset, 16);
            std::string hashStr = bytesToHex(hash, 16);
            offset += 16;
            unsigned char setId[16];
            std::memcpy(setId, data.data() + offset, 16);
            std::string setIdStr = bytesToHex(setId, 16);
            offset += 16;
            std::string type(data.begin() + offset, data.begin() + offset + 16);
            type.erase(std::remove(type.begin(), type.end(), '\0'), type.end());
            offset += 16;
            size_t bodyLength = length - 64;
            std::vector<char> body(data.begin() + offset, data.begin() + offset + bodyLength);

            std::map<std::string, std::string> packet;
            packet["magic"] = magic;
            packet["length"] = std::to_string(length);
            packet["hash"] = hashStr;
            packet["set_id"] = setIdStr;
            packet["type"] = type;

            size_t bodyOffset = 0;
            if (type.find("Main") != std::string::npos) {
                uint64_t sliceSize = *(uint64_t*)(body.data() + bodyOffset);
                packet["slice_size"] = std::to_string(sliceSize);
                bodyOffset += 8;
                uint32_t numRecovery = *(uint32_t*)(body.data() + bodyOffset);
                packet["num_recovery_files"] = std::to_string(numRecovery);
                bodyOffset += 4;
                packet["recovery_file_ids"] = "";
                for (uint32_t i = 0; i < numRecovery; ++i) {
                    unsigned char id[16];
                    std::memcpy(id, body.data() + bodyOffset, 16);
                    packet["recovery_file_ids"] += bytesToHex(id, 16) + " ";
                    bodyOffset += 16;
                }
                packet["non_recovery_file_ids"] = "";
                while (bodyOffset < bodyLength) {
                    unsigned char id[16];
                    std::memcpy(id, body.data() + bodyOffset, 16);
                    packet["non_recovery_file_ids"] += bytesToHex(id, 16) + " ";
                    bodyOffset += 16;
                }
            } else if (type.find("FileDesc") != std::string::npos) {
                unsigned char fid[16]; std::memcpy(fid, body.data(), 16);
                packet["file_id"] = bytesToHex(fid, 16);
                unsigned char fhash[16]; std::memcpy(fhash, body.data() + 16, 16);
                packet["file_hash"] = bytesToHex(fhash, 16);
                unsigned char h16k[16]; std::memcpy(h16k, body.data() + 32, 16);
                packet["hash_16k"] = bytesToHex(h16k, 16);
                uint64_t flen = *(uint64_t*)(body.data() + 48);
                packet["file_length"] = std::to_string(flen);
                std::string fname(body.begin() + 56, body.end());
                fname.erase(std::remove(fname.begin(), fname.end(), '\0'), fname.end());
                packet["file_name"] = fname;
            } // Add more...

            packets.push_back(packet);
            offset += bodyLength;
        }
    }

    void printProperties() {
        for (const auto& packet : packets) {
            for (const auto& kv : packet) {
                std::cout << kv.first << ": " << kv.second << std::endl;
            }
            std::cout << "-------------------" << std::endl;
        }
    }

    void write(const std::string& outputPath) {
        std::ofstream out(outputPath, std::ios::binary);
        for (const auto& packet : packets) {
            std::vector<char> body;
            if (packet.at("type").find("Main") != std::string::npos) {
                uint64_t sliceSize = std::stoull(packet.at("slice_size"));
                body.resize(body.size() + 8);
                std::memcpy(body.data() + body.size() - 8, &sliceSize, 8);
                uint32_t numRecovery = std::stoul(packet.at("num_recovery_files"));
                body.resize(body.size() + 4);
                std::memcpy(body.data() + body.size() - 4, &numRecovery, 4);
                std::istringstream iss(packet.at("recovery_file_ids"));
                std::string id;
                while (iss >> id) {
                    unsigned char bytes[16];
                    hexToBytes(id, bytes);
                    body.insert(body.end(), bytes, bytes + 16);
                }
                iss.str(packet.at("non_recovery_file_ids"));
                iss.clear();
                while (iss >> id) {
                    unsigned char bytes[16];
                    hexToBytes(id, bytes);
                    body.insert(body.end(), bytes, bytes + 16);
                }
            } else if (packet.at("type").find("FileDesc") != std::string::npos) {
                unsigned char fid[16]; hexToBytes(packet.at("file_id"), fid);
                body.insert(body.end(), fid, fid + 16);
                unsigned char fhash[16]; hexToBytes(packet.at("file_hash"), fhash);
                body.insert(body.end(), fhash, fhash + 16);
                unsigned char h16k[16]; hexToBytes(packet.at("hash_16k"), h16k);
                body.insert(body.end(), h16k, h16k + 16);
                uint64_t flen = std::stoull(packet.at("file_length"));
                body.resize(body.size() + 8);
                std::memcpy(body.data() + body.size() - 8, &flen, 8);
                std::string name = packet.at("file_name");
                body.insert(body.end(), name.begin(), name.end());
                int pad = (4 - name.length() % 4) % 4;
                body.insert(body.end(), pad, '\0');
            } // Add more...

            uint64_t length = 64 + body.size();
            unsigned char setId[16];
            hexToBytes(packet.at("set_id"), setId);
            std::vector<unsigned char> tempForHash;
            tempForHash.insert(tempForHash.end(), setId, setId + 16);
            tempForHash.insert(tempForHash.end(), body.begin(), body.end());
            unsigned char hash[MD5_DIGEST_LENGTH];
            MD5(tempForHash.data(), tempForHash.size(), hash);

            std::vector<char> header;
            header.insert(header.end(), "PAR2\0PKT", "PAR2\0PKT" + 8);
            header.resize(header.size() + 8);
            std::memcpy(header.data() + header.size() - 8, &length, 8);
            header.insert(header.end(), hash, hash + 16);
            header.insert(header.end(), setId, setId + 16);
            std::string type = packet.at("type");
            header.insert(header.end(), type.begin(), type.end());
            header.insert(header.end(), 16 - type.length(), '\0');

            out.write(header.data(), header.size());
            out.write(body.data(), body.size());
        }
    }
};

Usage: Par2File par("file.par2"); par.printProperties(); par.write("new.par2");. Compile with OpenSSL. Extend for all types.