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
Signatureheader, 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.
2. Two direct download links for files of format .SXG
- https://labs.jxck.io/webpackaging/signed-http-exchange-b2/mozaic.sxg
- https://signed-exchange-testing.dev/valid.html.sxg
3. Ghost blog embedded HTML JavaScript for drag and drop .SXG file dump
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;
// }