Task 710: .SXG File Format

Task 710: .SXG File Format

1. List of all the properties of the .SXG file format intrinsic to its file system

The .SXG file format (Signed HTTP Exchange) is a binary format defined in the IETF draft for Signed HTTP Exchanges. The intrinsic properties (fields) of the file structure are as follows, in order:

  • File Signature: 8 bytes fixed sequence ("sxg1" ASCII followed by 4 null bytes 0x00). Serves as a magic number to identify the file type.
  • Fallback URL Length: 2 bytes big-endian unsigned integer. Indicates the length of the following Fallback URL field.
  • Fallback URL: Variable length (as specified by the previous field) byte sequence. Must UTF-8 decode to an absolute HTTPS URL, used as the effective request URI.
  • Signature Length: 3 bytes big-endian unsigned integer (max 16384). Indicates the length of the following Signature Header field.
  • Header Length: 3 bytes big-endian unsigned integer (max 524288). Indicates the length of the following Signed Headers field.
  • Signature Header Field Value: Variable length (as specified by Signature Length) byte sequence. Contains the value of the Signature header, a structured header with one or more signatures.
  • Signed Headers: Variable length (as specified by Header Length) byte sequence. Canonical CBOR serialization of a map containing the response status (key ':status' as byte string mapped to 3-digit status code as byte string) and response headers (lowercase name byte strings mapped to value byte strings).
  • Payload Body: Variable length byte stream (remainder of the file). The HTTP response body with transfer encodings removed.

3. Ghost blog embedded HTML JavaScript for drag and drop .SXG file dump

SXG File Dumper
Drag and drop .SXG file here

4. Python class for .SXG file handling

import struct
import json

class SXGFile:
    def __init__(self, filepath=None):
        self.signature = None
        self.fallback_url_length = None
        self.fallback_url = None
        self.sig_length = None
        self.header_length = None
        self.signature_header = None
        self.signed_headers = None  # dict from CBOR
        self.payload = None
        if filepath:
            self.read(filepath)

    def read(self, filepath):
        with open(filepath, 'rb') as f:
            data = f.read()
        offset = 0

        # File Signature
        self.signature = data[offset:offset+8]
        offset += 8

        # Fallback URL Length
        self.fallback_url_length = struct.unpack('>H', data[offset:offset+2])[0]
        offset += 2

        # Fallback URL
        self.fallback_url = data[offset:offset+self.fallback_url_length].decode('utf-8')
        offset += self.fallback_url_length

        # Signature Length
        self.sig_length = struct.unpack('>I', b'\x00' + data[offset:offset+3])[0]
        offset += 3

        # Header Length
        self.header_length = struct.unpack('>I', b'\x00' + data[offset:offset+3])[0]
        offset += 3

        # Signature Header
        self.signature_header = data[offset:offset+self.sig_length].decode('utf-8', errors='replace')
        offset += self.sig_length

        # Signed Headers (simple CBOR parser for map of bytes to bytes)
        cbor_data = data[offset:offset+self.header_length]
        self.signed_headers = self._parse_cbor_map(cbor_data)
        offset += self.header_length

        # Payload
        self.payload = data[offset:]

    def _parse_cbor_map(self, cbor_bytes):
        # Simple parser assuming definite length map, byte string keys/values
        offset = 0
        head = cbor_bytes[offset]
        offset += 1
        major = head >> 5
        info = head & 0x1f
        if major != 5:
            raise ValueError('Not a CBOR map')
        if info < 24:
            num_pairs = info
        elif info == 24:
            num_pairs = cbor_bytes[offset]
            offset += 1
        elif info == 25:
            num_pairs = struct.unpack('>H', cbor_bytes[offset:offset+2])[0]
            offset += 2
        else:
            raise ValueError('Unsupported CBOR length')
        map_dict = {}
        for _ in range(num_pairs):
            # Key
            k_head = cbor_bytes[offset]
            offset += 1
            if k_head >> 5 != 2:
                raise ValueError('Key not byte string')
            k_len = k_head & 0x1f
            if k_len < 24:
                pass
            elif k_len == 24:
                k_len = cbor_bytes[offset]
                offset += 1
            elif k_len == 25:
                k_len = struct.unpack('>H', cbor_bytes[offset:offset+2])[0]
                offset += 2
            key = cbor_bytes[offset:offset+k_len].decode('utf-8')
            offset += k_len
            # Value
            v_head = cbor_bytes[offset]
            offset += 1
            if v_head >> 5 != 2:
                raise ValueError('Value not byte string')
            v_len = v_head & 0x1f
            if v_len < 24:
                pass
            elif v_len == 24:
                v_len = cbor_bytes[offset]
                offset += 1
            elif v_len == 25:
                v_len = struct.unpack('>H', cbor_bytes[offset:offset+2])[0]
                offset += 2
            value = cbor_bytes[offset:offset+v_len].decode('utf-8')
            offset += v_len
            map_dict[key] = value
        return map_dict

    def print_properties(self):
        print(f"File Signature: {self.signature.decode('ascii', errors='replace')} (hex: {' '.join(f'{b:02x}' for b in self.signature)})")
        print(f"Fallback URL Length: {self.fallback_url_length}")
        print(f"Fallback URL: {self.fallback_url}")
        print(f"Signature Length: {self.sig_length}")
        print(f"Header Length: {self.header_length}")
        print(f"Signature Header: {self.signature_header}")
        print(f"Signed Headers: {json.dumps(self.signed_headers, indent=2)}")
        print(f"Payload Length: {len(self.payload)}")
        # Print payload if small
        if len(self.payload) < 100:
            print(f"Payload: {self.payload.decode('utf-8', errors='replace')}")
        else:
            print(f"Payload (first 100 bytes): {self.payload[:100].decode('utf-8', errors='replace')}...")

    def write(self, filepath):
        with open(filepath, 'wb') as f:
            f.write(self.signature)
            f.write(struct.pack('>H', self.fallback_url_length))
            f.write(self.fallback_url.encode('utf-8'))
            f.write(struct.pack('>I', self.sig_length)[1:])  # 3 bytes
            f.write(struct.pack('>I', self.header_length)[1:])  # 3 bytes
            f.write(self.signature_header.encode('utf-8'))
            # For write, assume signed_headers is dict, need to encode to CBOR
            f.write(self._encode_cbor_map(self.signed_headers))
            f.write(self.payload)

    def _encode_cbor_map(self, map_dict):
        # Simple encoder for map of str to str as byte strings
        num_pairs = len(map_dict)
        cbor = bytearray()
        if num_pairs < 24:
            cbor.append(0xa0 + num_pairs)  # major 5, info = num
        elif num_pairs < 256:
            cbor.append(0xb8)  # 24
            cbor.append(num_pairs)
        else:
            cbor.append(0xb9)  # 25
            cbor.extend(struct.pack('>H', num_pairs))
        for key, value in map_dict.items():
            k_bytes = key.encode('utf-8')
            k_len = len(k_bytes)
            if k_len < 24:
                cbor.append(0x40 + k_len)
            elif k_len < 256:
                cbor.append(0x58)
                cbor.append(k_len)
            else:
                cbor.append(0x59)
                cbor.extend(struct.pack('>H', k_len))
            cbor.extend(k_bytes)
            v_bytes = value.encode('utf-8')
            v_len = len(v_bytes)
            if v_len < 24:
                cbor.append(0x40 + v_len)
            elif v_len < 256:
                cbor.append(0x58)
                cbor.append(v_len)
            else:
                cbor.append(0x59)
                cbor.extend(struct.pack('>H', v_len))
            cbor.extend(v_bytes)
        return bytes(cbor)

# Example usage:
# sxg = SXGFile('example.sxg')
# sxg.print_properties()
# sxg.write('output.sxg')

5. Java class for .SXG file handling

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class SXGFile {
    private byte[] signature;
    private int fallbackUrlLength;
    private String fallbackUrl;
    private int sigLength;
    private int headerLength;
    private String signatureHeader;
    private Map<String, String> signedHeaders; // from CBOR
    private byte[] payload;

    public SXGFile(String filepath) throws IOException {
        read(filepath);
    }

    public void read(String filepath) throws IOException {
        byte[] data;
        try (FileInputStream fis = new FileInputStream(filepath)) {
            data = fis.readAllBytes();
        }
        ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        int offset = 0;

        // File Signature
        signature = new byte[8];
        buffer.position(offset);
        buffer.get(signature);
        offset += 8;

        // Fallback URL Length
        fallbackUrlLength = buffer.getShort(offset) & 0xFFFF;
        offset += 2;

        // Fallback URL
        byte[] fallbackBytes = new byte[fallbackUrlLength];
        buffer.position(offset);
        buffer.get(fallbackBytes);
        fallbackUrl = new String(fallbackBytes, StandardCharsets.UTF_8);
        offset += fallbackUrlLength;

        // Signature Length
        sigLength = ((data[offset] & 0xFF) << 16) | ((data[offset+1] & 0xFF) << 8) | (data[offset+2] & 0xFF);
        offset += 3;

        // Header Length
        headerLength = ((data[offset] & 0xFF) << 16) | ((data[offset+1] & 0xFF) << 8) | (data[offset+2] & 0xFF);
        offset += 3;

        // Signature Header
        byte[] sigBytes = new byte[sigLength];
        buffer.position(offset);
        buffer.get(sigBytes);
        signatureHeader = new String(sigBytes, StandardCharsets.UTF_8);
        offset += sigLength;

        // Signed Headers CBOR
        byte[] cborBytes = new byte[headerLength];
        buffer.position(offset);
        buffer.get(cborBytes);
        signedHeaders = parseCBORMap(cborBytes);
        offset += headerLength;

        // Payload
        payload = new byte[data.length - offset];
        buffer.position(offset);
        buffer.get(payload);
    }

    private Map<String, String> parseCBORMap(byte[] cborBytes) throws IOException {
        ByteBuffer buf = ByteBuffer.wrap(cborBytes);
        int head = buf.get() & 0xFF;
        int major = head >> 5;
        int info = head & 0x1f;
        if (major != 5) throw new IOException("Not a CBOR map");
        int numPairs;
        if (info < 24) numPairs = info;
        else if (info == 24) numPairs = buf.get() & 0xFF;
        else if (info == 25) numPairs = buf.getShort() & 0xFFFF;
        else throw new IOException("Unsupported CBOR length");
        Map<String, String> map = new HashMap<>();
        for (int i = 0; i < numPairs; i++) {
            // Key
            int kHead = buf.get() & 0xFF;
            if ((kHead >> 5) != 2) throw new IOException("Key not byte string");
            int kLen = kHead & 0x1f;
            if (kLen == 24) kLen = buf.get() & 0xFF;
            else if (kLen == 25) kLen = buf.getShort() & 0xFFFF;
            byte[] kBytes = new byte[kLen];
            buf.get(kBytes);
            String key = new String(kBytes, StandardCharsets.UTF_8);
            // Value
            int vHead = buf.get() & 0xFF;
            if ((vHead >> 5) != 2) throw new IOException("Value not byte string");
            int vLen = vHead & 0x1f;
            if (vLen == 24) vLen = buf.get() & 0xFF;
            else if (vLen == 25) vLen = buf.getShort() & 0xFFFF;
            byte[] vBytes = new byte[vLen];
            buf.get(vBytes);
            String value = new String(vBytes, StandardCharsets.UTF_8);
            map.put(key, value);
        }
        return map;
    }

    public void printProperties() {
        System.out.printf("File Signature: %s (hex: %s)%n", new String(signature, StandardCharsets.US_ASCII), bytesToHex(signature));
        System.out.printf("Fallback URL Length: %d%n", fallbackUrlLength);
        System.out.printf("Fallback URL: %s%n", fallbackUrl);
        System.out.printf("Signature Length: %d%n", sigLength);
        System.out.printf("Header Length: %d%n", headerLength);
        System.out.printf("Signature Header: %s%n", signatureHeader);
        System.out.printf("Signed Headers: %s%n", signedHeaders);
        System.out.printf("Payload Length: %d%n", payload.length);
        if (payload.length < 100) {
            System.out.printf("Payload: %s%n", new String(payload, StandardCharsets.UTF_8));
        } else {
            System.out.printf("Payload (first 100 bytes): %s...%n", new String(payload, 0, 100, StandardCharsets.UTF_8));
        }
    }

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

    public void write(String filepath) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filepath)) {
            fos.write(signature);
            ByteBuffer buf = ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort((short) fallbackUrlLength);
            fos.write(buf.array());
            fos.write(fallbackUrl.getBytes(StandardCharsets.UTF_8));
            byte[] sigLenBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(sigLength).array();
            fos.write(sigLenBytes, 1, 3);
            byte[] headLenBytes = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(headerLength).array();
            fos.write(headLenBytes, 1, 3);
            fos.write(signatureHeader.getBytes(StandardCharsets.UTF_8));
            fos.write(encodeCBORMap(signedHeaders));
            fos.write(payload);
        }
    }

    private byte[] encodeCBORMap(Map<String, String> map) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int num = map.size();
        if (num < 24) {
            baos.write(0xa0 + num);
        } else if (num < 256) {
            baos.write(0xb8);
            baos.write(num);
        } else {
            baos.write(0xb9);
            try {
                baos.write(ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort((short) num).array());
            } catch (IOException e) {
                // Won't happen
            }
        }
        for (Map.Entry<String, String> entry : map.entrySet()) {
            byte[] k = entry.getKey().getBytes(StandardCharsets.UTF_8);
            int kl = k.length;
            if (kl < 24) {
                baos.write(0x40 + kl);
            } else if (kl < 256) {
                baos.write(0x58);
                baos.write(kl);
            } else {
                baos.write(0x59);
                try {
                    baos.write(ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort((short) kl).array());
                } catch (IOException e) {
                    // Won't happen
                }
            }
            try {
                baos.write(k);
            } catch (IOException e) {
                // Won't happen
            }
            byte[] v = entry.getValue().getBytes(StandardCharsets.UTF_8);
            int vl = v.length;
            if (vl < 24) {
                baos.write(0x40 + vl);
            } else if (vl < 256) {
                baos.write(0x58);
                baos.write(vl);
            } else {
                baos.write(0x59);
                try {
                    baos.write(ByteBuffer.allocate(2).order(ByteOrder.BIG_ENDIAN).putShort((short) vl).array());
                } catch (IOException e) {
                    // Won't happen
                }
            }
            try {
                baos.write(v);
            } catch (IOException e) {
                // Won't happen
            }
        }
        return baos.toByteArray();
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     SXGFile sxg = new SXGFile("example.sxg");
    //     sxg.printProperties();
    //     sxg.write("output.sxg");
    // }
}

6. JavaScript class for .SXG file handling

class SXGFile {
    constructor(arrayBuffer = null) {
        this.signature = null;
        this.fallbackUrlLength = null;
        this.fallbackUrl = null;
        this.sigLength = null;
        this.headerLength = null;
        this.signatureHeader = null;
        this.signedHeaders = null; // object from CBOR
        this.payload = null;
        if (arrayBuffer) {
            this.read(arrayBuffer);
        }
    }

    read(arrayBuffer) {
        const dataView = new DataView(arrayBuffer);
        let offset = 0;

        // File Signature
        this.signature = new Uint8Array(arrayBuffer, offset, 8);
        offset += 8;

        // Fallback URL Length
        this.fallbackUrlLength = dataView.getUint16(offset, false);
        offset += 2;

        // Fallback URL
        const fallbackBytes = new Uint8Array(arrayBuffer, offset, this.fallbackUrlLength);
        this.fallbackUrl = new TextDecoder('utf-8').decode(fallbackBytes);
        offset += this.fallbackUrlLength;

        // Signature Length
        this.sigLength = (dataView.getUint8(offset) << 16) | (dataView.getUint8(offset + 1) << 8) | dataView.getUint8(offset + 2);
        offset += 3;

        // Header Length
        this.headerLength = (dataView.getUint8(offset) << 16) | (dataView.getUint8(offset + 1) << 8) | dataView.getUint8(offset + 2);
        offset += 3;

        // Signature Header
        const sigBytes = new Uint8Array(arrayBuffer, offset, this.sigLength);
        this.signatureHeader = new TextDecoder('utf-8').decode(sigBytes);
        offset += this.sigLength;

        // Signed Headers CBOR
        const cborBytes = new Uint8Array(arrayBuffer, offset, this.headerLength);
        this.signedHeaders = this.parseCBORMap(cborBytes);
        offset += this.headerLength;

        // Payload
        this.payload = new Uint8Array(arrayBuffer, offset);
    }

    parseCBORMap(bytes) {
        const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
        let off = 0;
        const head = view.getUint8(off++);
        const major = head >> 5;
        let info = head & 0x1f;
        if (major !== 5) throw new Error('Not a CBOR map');
        let numPairs;
        if (info < 24) numPairs = info;
        else if (info === 24) numPairs = view.getUint8(off++);
        else if (info === 25) numPairs = view.getUint16(off, false), off += 2;
        else throw new Error('Unsupported CBOR length');
        const map = {};
        for (let i = 0; i < numPairs; i++) {
            const kHead = view.getUint8(off++);
            if ((kHead >> 5) !== 2) throw new Error('Key not byte string');
            let kLen = kHead & 0x1f;
            if (kLen === 24) kLen = view.getUint8(off++);
            else if (kLen === 25) kLen = view.getUint16(off, false), off += 2;
            const key = new TextDecoder('utf-8').decode(new Uint8Array(bytes.buffer, bytes.byteOffset + off, kLen));
            off += kLen;
            const vHead = view.getUint8(off++);
            if ((vHead >> 5) !== 2) throw new Error('Value not byte string');
            let vLen = vHead & 0x1f;
            if (vLen === 24) vLen = view.getUint8(off++);
            else if (vLen === 25) vLen = view.getUint16(off, false), off += 2;
            const value = new TextDecoder('utf-8').decode(new Uint8Array(bytes.buffer, bytes.byteOffset + off, vLen));
            off += vLen;
            map[key] = value;
        }
        return map;
    }

    printProperties() {
        console.log(`File Signature: ${new TextDecoder('ascii').decode(this.signature)} (hex: ${Array.from(this.signature).map(b => b.toString(16).padStart(2, '0')).join(' ')})`);
        console.log(`Fallback URL Length: ${this.fallbackUrlLength}`);
        console.log(`Fallback URL: ${this.fallbackUrl}`);
        console.log(`Signature Length: ${this.sigLength}`);
        console.log(`Header Length: ${this.headerLength}`);
        console.log(`Signature Header: ${this.signatureHeader}`);
        console.log(`Signed Headers:`, this.signedHeaders);
        console.log(`Payload Length: ${this.payload.length}`);
        if (this.payload.length < 100) {
            console.log(`Payload: ${new TextDecoder('utf-8').decode(this.payload)}`);
        } else {
            console.log(`Payload (first 100 bytes): ${new TextDecoder('utf-8').decode(this.payload.slice(0, 100))}...`);
        }
    }

    write() {
        // Returns an ArrayBuffer for the file
        let totalLength = 8 + 2 + this.fallbackUrlLength + 3 + 3 + this.sigLength + this.headerLength + this.payload.length;
        const buffer = new ArrayBuffer(totalLength);
        const view = new DataView(buffer);
        let offset = 0;

        // Signature
        new Uint8Array(buffer, offset, 8).set(this.signature);
        offset += 8;

        // Fallback URL Length
        view.setUint16(offset, this.fallbackUrlLength, false);
        offset += 2;

        // Fallback URL
        new Uint8Array(buffer, offset, this.fallbackUrlLength).set(new TextEncoder().encode(this.fallbackUrl));
        offset += this.fallbackUrlLength;

        // Signature Length
        view.setUint8(offset, (this.sigLength >> 16) & 0xFF);
        view.setUint8(offset + 1, (this.sigLength >> 8) & 0xFF);
        view.setUint8(offset + 2, this.sigLength & 0xFF);
        offset += 3;

        // Header Length
        view.setUint8(offset, (this.headerLength >> 16) & 0xFF);
        view.setUint8(offset + 1, (this.headerLength >> 8) & 0xFF);
        view.setUint8(offset + 2, this.headerLength & 0xFF);
        offset += 3;

        // Signature Header
        new Uint8Array(buffer, offset, this.sigLength).set(new TextEncoder().encode(this.signatureHeader));
        offset += this.sigLength;

        // Signed Headers
        const cborBytes = this.encodeCBORMap(this.signedHeaders);
        new Uint8Array(buffer, offset, this.headerLength).set(cborBytes);
        offset += this.headerLength;

        // Payload
        new Uint8Array(buffer, offset).set(this.payload);

        return buffer;
    }

    encodeCBORMap(map) {
        const entries = Object.entries(map);
        const num = entries.length;
        let cbor = [];
        if (num < 24) {
            cbor.push(0xa0 + num);
        } else if (num < 256) {
            cbor.push(0xb8);
            cbor.push(num);
        } else {
            cbor.push(0xb9);
            cbor.push((num >> 8) & 0xFF);
            cbor.push(num & 0xFF);
        }
        for (const [key, value] of entries) {
            const kBytes = new TextEncoder().encode(key);
            const kl = kBytes.length;
            if (kl < 24) {
                cbor.push(0x40 + kl);
            } else if (kl < 256) {
                cbor.push(0x58);
                cbor.push(kl);
            } else {
                cbor.push(0x59);
                cbor.push((kl >> 8) & 0xFF);
                cbor.push(kl & 0xFF);
            }
            cbor.push(...kBytes);
            const vBytes = new TextEncoder().encode(value);
            const vl = vBytes.length;
            if (vl < 24) {
                cbor.push(0x40 + vl);
            } else if (vl < 256) {
                cbor.push(0x58);
                cbor.push(vl);
            } else {
                cbor.push(0x59);
                cbor.push((vl >> 8) & 0xFF);
                cbor.push(vl & 0xFF);
            }
            cbor.push(...vBytes);
        }
        return new Uint8Array(cbor);
    }
}

// Example usage:
// const reader = new FileReader();
// reader.onload = () => {
//     const sxg = new SXGFile(reader.result);
//     sxg.printProperties();
//     const outputBuffer = sxg.write();
//     // Use outputBuffer, e.g., save as file
// };
// reader.readAsArrayBuffer(file);

7. C++ class for .SXG file handling

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <iomanip>
#include <cstdint>
#include <endian.h> // For big endian conversions, assume available or use alternatives

class SXGFile {
private:
    std::vector<uint8_t> signature;
    uint16_t fallback_url_length;
    std::string fallback_url;
    uint32_t sig_length;
    uint32_t header_length;
    std::string signature_header;
    std::map<std::string, std::string> signed_headers;
    std::vector<uint8_t> payload;

public:
    SXGFile(const std::string& filepath) {
        read(filepath);
    }

    void read(const std::string& filepath) {
        std::ifstream file(filepath, std::ios::binary | std::ios::ate);
        if (!file) {
            throw std::runtime_error("Failed to open file");
        }
        std::streamsize size = file.tellg();
        file.seekg(0, std::ios::beg);
        std::vector<uint8_t> data(size);
        if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
            throw std::runtime_error("Failed to read file");
        }
        size_t offset = 0;

        // File Signature
        signature.assign(data.begin() + offset, data.begin() + offset + 8);
        offset += 8;

        // Fallback URL Length
        fallback_url_length = be16toh(*reinterpret_cast<uint16_t*>(&data[offset]));
        offset += 2;

        // Fallback URL
        fallback_url.assign(reinterpret_cast<char*>(&data[offset]), fallback_url_length);
        offset += fallback_url_length;

        // Signature Length
        sig_length = (data[offset] << 16) | (data[offset + 1] << 8) | data[offset + 2];
        offset += 3;

        // Header Length
        header_length = (data[offset] << 16) | (data[offset + 1] << 8) | data[offset + 2];
        offset += 3;

        // Signature Header
        signature_header.assign(reinterpret_cast<char*>(&data[offset]), sig_length);
        offset += sig_length;

        // Signed Headers CBOR
        std::vector<uint8_t> cbor_data(data.begin() + offset, data.begin() + offset + header_length);
        signed_headers = parse_cbor_map(cbor_data);
        offset += header_length;

        // Payload
        payload.assign(data.begin() + offset, data.end());
    }

    std::map<std::string, std::string> parse_cbor_map(const std::vector<uint8_t>& cbor_bytes) {
        size_t off = 0;
        uint8_t head = cbor_bytes[off++];
        uint8_t major = head >> 5;
        uint8_t info = head & 0x1f;
        if (major != 5) throw std::runtime_error("Not a CBOR map");
        uint16_t num_pairs;
        if (info < 24) num_pairs = info;
        else if (info == 24) num_pairs = cbor_bytes[off++];
        else if (info == 25) {
            num_pairs = (cbor_bytes[off] << 8) | cbor_bytes[off + 1];
            off += 2;
        } else throw std::runtime_error("Unsupported CBOR length");
        std::map<std::string, std::string> map;
        for (uint16_t i = 0; i < num_pairs; ++i) {
            uint8_t k_head = cbor_bytes[off++];
            if ((k_head >> 5) != 2) throw std::runtime_error("Key not byte string");
            uint16_t k_len = k_head & 0x1f;
            if (k_len == 24) k_len = cbor_bytes[off++];
            else if (k_len == 25) {
                k_len = (cbor_bytes[off] << 8) | cbor_bytes[off + 1];
                off += 2;
            }
            std::string key(reinterpret_cast<const char*>(&cbor_bytes[off]), k_len);
            off += k_len;
            uint8_t v_head = cbor_bytes[off++];
            if ((v_head >> 5) != 2) throw std::runtime_error("Value not byte string");
            uint16_t v_len = v_head & 0x1f;
            if (v_len == 24) v_len = cbor_bytes[off++];
            else if (v_len == 25) {
                v_len = (cbor_bytes[off] << 8) | cbor_bytes[off + 1];
                off += 2;
            }
            std::string value(reinterpret_cast<const char*>(&cbor_bytes[off]), v_len);
            off += v_len;
            map[key] = value;
        }
        return map;
    }

    void print_properties() {
        std::cout << "File Signature: ";
        for (char c : signature) std::cout << (c >= 32 && c <= 126 ? c : '.');
        std::cout << " (hex: ";
        for (uint8_t b : signature) std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b) << " ";
        std::cout << ")\n";
        std::cout << "Fallback URL Length: " << fallback_url_length << "\n";
        std::cout << "Fallback URL: " << fallback_url << "\n";
        std::cout << "Signature Length: " << sig_length << "\n";
        std::cout << "Header Length: " << header_length << "\n";
        std::cout << "Signature Header: " << signature_header << "\n";
        std::cout << "Signed Headers:\n";
        for (const auto& [key, value] : signed_headers) {
            std::cout << "  " << key << ": " << value << "\n";
        }
        std::cout << "Payload Length: " << payload.size() << "\n";
        if (payload.size() < 100) {
            std::cout << "Payload: " << std::string(reinterpret_cast<char*>(payload.data()), payload.size()) << "\n";
        } else {
            std::cout << "Payload (first 100 bytes): " << std::string(reinterpret_cast<char*>(payload.data()), 100) << "...\n";
        }
    }

    void write(const std::string& filepath) {
        std::ofstream file(filepath, std::ios::binary);
        if (!file) {
            throw std::runtime_error("Failed to open file for writing");
        }
        file.write(reinterpret_cast<const char*>(signature.data()), 8);
        uint16_t be_ful = htobe16(fallback_url_length);
        file.write(reinterpret_cast<const char*>(&be_ful), 2);
        file.write(fallback_url.c_str(), fallback_url_length);
        uint8_t sig_len_bytes[3] = {static_cast<uint8_t>(sig_length >> 16), static_cast<uint8_t>(sig_length >> 8), static_cast<uint8_t>(sig_length)};
        file.write(reinterpret_cast<const char*>(sig_len_bytes), 3);
        uint8_t head_len_bytes[3] = {static_cast<uint8_t>(header_length >> 16), static_cast<uint8_t>(header_length >> 8), static_cast<uint8_t>(header_length)};
        file.write(reinterpret_cast<const char*>(head_len_bytes), 3);
        file.write(signature_header.c_str(), sig_length);
        auto cbor = encode_cbor_map(signed_headers);
        file.write(reinterpret_cast<const char*>(cbor.data()), cbor.size());
        file.write(reinterpret_cast<const char*>(payload.data()), payload.size());
    }

    std::vector<uint8_t> encode_cbor_map(const std::map<std::string, std::string>& map) {
        std::vector<uint8_t> cbor;
        uint16_t num = map.size();
        if (num < 24) {
            cbor.push_back(0xa0 + num);
        } else if (num < 256) {
            cbor.push_back(0xb8);
            cbor.push_back(num);
        } else {
            cbor.push_back(0xb9);
            uint16_t be_num = htobe16(num);
            cbor.insert(cbor.end(), reinterpret_cast<uint8_t*>(&be_num), reinterpret_cast<uint8_t*>(&be_num) + 2);
        }
        for (const auto& [key, value] : map) {
            auto k_bytes = reinterpret_cast<const uint8_t*>(key.c_str());
            uint16_t kl = key.length();
            if (kl < 24) {
                cbor.push_back(0x40 + kl);
            } else if (kl < 256) {
                cbor.push_back(0x58);
                cbor.push_back(kl);
            } else {
                cbor.push_back(0x59);
                uint16_t be_kl = htobe16(kl);
                cbor.insert(cbor.end(), reinterpret_cast<uint8_t*>(&be_kl), reinterpret_cast<uint8_t*>(&be_kl) + 2);
            }
            cbor.insert(cbor.end(), k_bytes, k_bytes + kl);
            auto v_bytes = reinterpret_cast<const uint8_t*>(value.c_str());
            uint16_t vl = value.length();
            if (vl < 24) {
                cbor.push_back(0x40 + vl);
            } else if (vl < 256) {
                cbor.push_back(0x58);
                cbor.push_back(vl);
            } else {
                cbor.push_back(0x59);
                uint16_t be_vl = htobe16(vl);
                cbor.insert(cbor.end(), reinterpret_cast<uint8_t*>(&be_vl), reinterpret_cast<uint8_t*>(&be_vl) + 2);
            }
            cbor.insert(cbor.end(), v_bytes, v_bytes + vl);
        }
        return cbor;
    }
};

// Example usage:
// int main() {
//     try {
//         SXGFile sxg("example.sxg");
//         sxg.print_properties();
//         sxg.write("output.sxg");
//     } catch (const std::exception& e) {
//         std::cerr << e.what() << std::endl;
//     }
//     return 0;
// }