#ifndef HYPEROVERLAY_MESSAGES_H
#define HYPEROVERLAY_MESSAGES_H
// The hyper-overlay/v2 wire messages, mirroring hyper-overlay/lib/messages.js.
//
// Checked against fixtures generated FROM that file
// (hyper-overlay/test/conformance/vectors.json) — the reference is the spec, and
// this header is only correct insofar as it passes them. Do not "improve" an
// encoding here; a change that looks tidier is a change that stops talking to
// pgh-sidecar and pear-git-v3, which speak this protocol in production.
//
// THREE TRAPS THIS FILE EXISTS TO GET RIGHT
//
// 1. OPTIONAL TRAILING FIELDS. chunkHashes.more, chunkCancel.indices,
// treeRequest.nonce, treeResponse.more+nonce and contentRequest.from are
// appended, and the decoder reads them ONLY while bytes remain. A peer that
// predates the field simply omits it and must decode to the default, not
// fail. Unconditional reads break against old peers; never reading breaks
// against new ones.
//
// 2. TWO ARRAY-LENGTH CONVENTIONS, in the same file. chunkInfoArray, uintArray
// and treeEntryArray write their count as a FIXED uint32. chunkCancel's
// indices use compact-encoding's c.array(c.uint), whose count is a VARINT.
// Using one convention throughout passes casual testing and desynchronises
// the stream on the first list longer than 252.
//
// 3. RAW vs LENGTH-PREFIXED HASHES. Inside the arrays a 32-byte hash is written
// raw; at message top level the same hash goes through the length-prefixed
// buffer codec. Both round-trip within one implementation.
//
// Header-only and dependency-free (cenc.h only), so this directory lifts into a
// standalone library by being moved.
#include <cstdint>
#include <array>
#include <string>
#include <vector>
#include "cenc.h"
namespace hyperoverlay {
namespace msg {
using Hash32 = std::array<uint8_t, 32>;
using Bytes = std::vector<uint8_t>;
inline Hash32 zero_hash() { Hash32 h{}; h.fill(0); return h; }
// ---------------------------------------------------------------------------
// Shared array codecs
// ---------------------------------------------------------------------------
struct ChunkInfo {
Hash32 hash{};
uint64_t length = 0;
};
// count: FIXED uint32. hash: RAW 32 bytes, no length prefix.
inline void chunk_info_array_preencode(cenc::State &s, const std::vector<ChunkInfo> &a) {
cenc::uint32_preencode(s);
for (const ChunkInfo &c : a) { s.end += 32; cenc::uint_preencode(s, c.length); }
}
inline void chunk_info_array_encode(cenc::State &s, const std::vector<ChunkInfo> &a) {
cenc::uint32_encode(s, (uint32_t)a.size());
for (const ChunkInfo &c : a) { cenc::fixed32_encode(s, c.hash.data()); cenc::uint_encode(s, c.length); }
}
inline std::vector<ChunkInfo> chunk_info_array_decode(cenc::State &s) {
const uint32_t n = cenc::uint32_decode(s);
std::vector<ChunkInfo> a;
for (uint32_t i = 0; i < n; i++) {
if (!cenc::has_more(s)) break; // truncated frame: stop, do not fabricate
ChunkInfo c;
cenc::fixed32_decode(s, c.hash.data());
c.length = cenc::uint_decode(s);
a.push_back(c);
}
return a;
}
// count: FIXED uint32 (NOT the varint c.array uses — see trap 2).
inline void uint_array_preencode(cenc::State &s, const std::vector<uint64_t> &a) {
cenc::uint32_preencode(s);
for (uint64_t v : a) cenc::uint_preencode(s, v);
}
inline void uint_array_encode(cenc::State &s, const std::vector<uint64_t> &a) {
cenc::uint32_encode(s, (uint32_t)a.size());
for (uint64_t v : a) cenc::uint_encode(s, v);
}
inline std::vector<uint64_t> uint_array_decode(cenc::State &s) {
const uint32_t n = cenc::uint32_decode(s);
std::vector<uint64_t> a;
for (uint32_t i = 0; i < n && cenc::has_more(s); i++) a.push_back(cenc::uint_decode(s));
return a;
}
// compact-encoding's c.array(c.uint): count is a VARINT.
inline void varint_array_preencode(cenc::State &s, const std::vector<uint64_t> &a) {
cenc::uint_preencode(s, (uint64_t)a.size());
for (uint64_t v : a) cenc::uint_preencode(s, v);
}
inline void varint_array_encode(cenc::State &s, const std::vector<uint64_t> &a) {
cenc::uint_encode(s, (uint64_t)a.size());
for (uint64_t v : a) cenc::uint_encode(s, v);
}
inline std::vector<uint64_t> varint_array_decode(cenc::State &s) {
const uint64_t n = cenc::uint_decode(s);
std::vector<uint64_t> a;
for (uint64_t i = 0; i < n && cenc::has_more(s); i++) a.push_back(cenc::uint_decode(s));
return a;
}
struct TreeEntry {
uint8_t kind = 0;
uint8_t exec = 0;
std::string name;
Hash32 childHash{};
uint64_t size = 0;
};
inline void tree_entry_array_preencode(cenc::State &s, const std::vector<TreeEntry> &a) {
cenc::uint32_preencode(s);
for (const TreeEntry &e : a) {
cenc::uint8_preencode(s); cenc::uint8_preencode(s);
cenc::string_preencode(s, e.name);
s.end += 32;
cenc::uint_preencode(s, e.size);
}
}
inline void tree_entry_array_encode(cenc::State &s, const std::vector<TreeEntry> &a) {
cenc::uint32_encode(s, (uint32_t)a.size());
for (const TreeEntry &e : a) {
cenc::uint8_encode(s, e.kind); cenc::uint8_encode(s, e.exec);
cenc::string_encode(s, e.name);
cenc::fixed32_encode(s, e.childHash.data());
cenc::uint_encode(s, e.size);
}
}
inline std::vector<TreeEntry> tree_entry_array_decode(cenc::State &s) {
const uint32_t n = cenc::uint32_decode(s);
std::vector<TreeEntry> a;
for (uint32_t i = 0; i < n && cenc::has_more(s); i++) {
TreeEntry e;
e.kind = cenc::uint8_decode(s);
e.exec = cenc::uint8_decode(s);
e.name = cenc::string_decode(s);
cenc::fixed32_decode(s, e.childHash.data());
e.size = cenc::uint_decode(s);
a.push_back(e);
}
return a;
}
// Top-level hashes are LENGTH-PREFIXED (c.buffer), unlike the raw ones above.
inline void hash_buf_preencode(cenc::State &s) { cenc::buffer_preencode(s, 32); }
inline void hash_buf_encode(cenc::State &s, const Hash32 &h) { cenc::buffer_encode(s, h.data(), 32); }
inline Hash32 hash_buf_decode(cenc::State &s) {
Hash32 h{}; h.fill(0);
const cenc::BufView v = cenc::buffer_decode(s);
if (v.data && v.len == 32) std::memcpy(h.data(), v.data, 32);
return h;
}
// ---------------------------------------------------------------------------
// Messages
// ---------------------------------------------------------------------------
#define HO_CODEC(NAME) \
inline std::vector<uint8_t> encode(const NAME &m) { \
return cenc::encode_to_vector([&](cenc::State &s, bool pre) { \
if (pre) preencode(s, m); else encode_into(s, m); }); }
struct Handshake { uint8_t version = 0, capabilities = 0; };
inline void preencode(cenc::State &s, const Handshake &) { cenc::uint8_preencode(s); cenc::uint8_preencode(s); }
inline void encode_into(cenc::State &s, const Handshake &m) { cenc::uint8_encode(s, m.version); cenc::uint8_encode(s, m.capabilities); }
inline Handshake decode_handshake(cenc::State &s) { Handshake m; m.version = cenc::uint8_decode(s); m.capabilities = cenc::uint8_decode(s); return m; }
HO_CODEC(Handshake)
struct SyncState { Hash32 feedKey{}; uint64_t localSeq = 0, remoteSeq = 0; };
inline void preencode(cenc::State &s, const SyncState &m) { hash_buf_preencode(s); cenc::uint_preencode(s, m.localSeq); cenc::uint_preencode(s, m.remoteSeq); }
inline void encode_into(cenc::State &s, const SyncState &m) { hash_buf_encode(s, m.feedKey); cenc::uint_encode(s, m.localSeq); cenc::uint_encode(s, m.remoteSeq); }
inline SyncState decode_sync_state(cenc::State &s) { SyncState m; m.feedKey = hash_buf_decode(s); m.localSeq = cenc::uint_decode(s); m.remoteSeq = cenc::uint_decode(s); return m; }
HO_CODEC(SyncState)
struct FileOffer { std::string path; Hash32 contentHash{}; uint64_t size = 0, mtime = 0; uint8_t op = 0; };
inline void preencode(cenc::State &s, const FileOffer &m) { cenc::string_preencode(s, m.path); hash_buf_preencode(s); cenc::uint_preencode(s, m.size); cenc::uint_preencode(s, m.mtime); cenc::uint8_preencode(s); }
inline void encode_into(cenc::State &s, const FileOffer &m) { cenc::string_encode(s, m.path); hash_buf_encode(s, m.contentHash); cenc::uint_encode(s, m.size); cenc::uint_encode(s, m.mtime); cenc::uint8_encode(s, m.op); }
inline FileOffer decode_file_offer(cenc::State &s) { FileOffer m; m.path = cenc::string_decode(s); m.contentHash = hash_buf_decode(s); m.size = cenc::uint_decode(s); m.mtime = cenc::uint_decode(s); m.op = cenc::uint8_decode(s); return m; }
HO_CODEC(FileOffer)
struct FileRequest { std::string path; Hash32 contentHash{}; Bytes chunksHave; };
inline void preencode(cenc::State &s, const FileRequest &m) { cenc::string_preencode(s, m.path); hash_buf_preencode(s); cenc::buffer_preencode(s, m.chunksHave.size()); }
inline void encode_into(cenc::State &s, const FileRequest &m) { cenc::string_encode(s, m.path); hash_buf_encode(s, m.contentHash); cenc::buffer_encode(s, m.chunksHave.data(), m.chunksHave.size()); }
inline FileRequest decode_file_request(cenc::State &s) {
FileRequest m; m.path = cenc::string_decode(s); m.contentHash = hash_buf_decode(s);
const cenc::BufView v = cenc::buffer_decode(s);
m.chunksHave.assign(v.data, v.data ? v.data + v.len : nullptr);
return m;
}
HO_CODEC(FileRequest)
struct ChunkHashes { std::string path; uint8_t tier = 0; std::vector<ChunkInfo> chunks; uint8_t more = 0; };
inline void preencode(cenc::State &s, const ChunkHashes &m) { cenc::string_preencode(s, m.path); cenc::uint8_preencode(s); chunk_info_array_preencode(s, m.chunks); cenc::uint8_preencode(s); }
inline void encode_into(cenc::State &s, const ChunkHashes &m) { cenc::string_encode(s, m.path); cenc::uint8_encode(s, m.tier); chunk_info_array_encode(s, m.chunks); cenc::uint8_encode(s, m.more); }
inline ChunkHashes decode_chunk_hashes(cenc::State &s) {
ChunkHashes m; m.path = cenc::string_decode(s); m.tier = cenc::uint8_decode(s); m.chunks = chunk_info_array_decode(s);
m.more = cenc::has_more(s) ? cenc::uint8_decode(s) : 0; // trap 1
return m;
}
HO_CODEC(ChunkHashes)
struct ChunkNeed { std::string path; std::vector<uint64_t> indices; };
inline void preencode(cenc::State &s, const ChunkNeed &m) { cenc::string_preencode(s, m.path); uint_array_preencode(s, m.indices); }
inline void encode_into(cenc::State &s, const ChunkNeed &m) { cenc::string_encode(s, m.path); uint_array_encode(s, m.indices); }
inline ChunkNeed decode_chunk_need(cenc::State &s) { ChunkNeed m; m.path = cenc::string_decode(s); m.indices = uint_array_decode(s); return m; }
HO_CODEC(ChunkNeed)
struct ChunkData { std::string path; uint64_t index = 0; Bytes data; };
inline void preencode(cenc::State &s, const ChunkData &m) { cenc::string_preencode(s, m.path); cenc::uint_preencode(s, m.index); cenc::buffer_preencode(s, m.data.size()); }
inline void encode_into(cenc::State &s, const ChunkData &m) { cenc::string_encode(s, m.path); cenc::uint_encode(s, m.index); cenc::buffer_encode(s, m.data.data(), m.data.size()); }
inline ChunkData decode_chunk_data(cenc::State &s) {
ChunkData m; m.path = cenc::string_decode(s); m.index = cenc::uint_decode(s);
const cenc::BufView v = cenc::buffer_decode(s);
m.data.assign(v.data, v.data ? v.data + v.len : nullptr);
return m;
}
HO_CODEC(ChunkData)
// `indices` is present only when non-empty, and uses the VARINT-count array.
struct ChunkCancel { std::string path; std::vector<uint64_t> indices; };
inline void preencode(cenc::State &s, const ChunkCancel &m) { cenc::string_preencode(s, m.path); if (!m.indices.empty()) varint_array_preencode(s, m.indices); }
inline void encode_into(cenc::State &s, const ChunkCancel &m) { cenc::string_encode(s, m.path); if (!m.indices.empty()) varint_array_encode(s, m.indices); }
inline ChunkCancel decode_chunk_cancel(cenc::State &s) {
ChunkCancel m; m.path = cenc::string_decode(s);
if (cenc::has_more(s)) m.indices = varint_array_decode(s); // trap 1 + trap 2
return m;
}
HO_CODEC(ChunkCancel)
struct TransferComplete { std::string path; Hash32 contentHash{}; };
inline void preencode(cenc::State &s, const TransferComplete &m) { cenc::string_preencode(s, m.path); hash_buf_preencode(s); }
inline void encode_into(cenc::State &s, const TransferComplete &m) { cenc::string_encode(s, m.path); hash_buf_encode(s, m.contentHash); }
inline TransferComplete decode_transfer_complete(cenc::State &s) { TransferComplete m; m.path = cenc::string_decode(s); m.contentHash = hash_buf_decode(s); return m; }
HO_CODEC(TransferComplete)
struct Conflict { std::string path; Hash32 myHash{}, theirHash{}, ancestorHash{}; };
inline void preencode(cenc::State &s, const Conflict &m) { cenc::string_preencode(s, m.path); hash_buf_preencode(s); hash_buf_preencode(s); hash_buf_preencode(s); }
inline void encode_into(cenc::State &s, const Conflict &m) { cenc::string_encode(s, m.path); hash_buf_encode(s, m.myHash); hash_buf_encode(s, m.theirHash); hash_buf_encode(s, m.ancestorHash); }
inline Conflict decode_conflict(cenc::State &s) { Conflict m; m.path = cenc::string_decode(s); m.myHash = hash_buf_decode(s); m.theirHash = hash_buf_decode(s); m.ancestorHash = hash_buf_decode(s); return m; }
HO_CODEC(Conflict)
struct TreeRequest { Hash32 hash{}; uint64_t nonce = 0; };
inline void preencode(cenc::State &s, const TreeRequest &m) { hash_buf_preencode(s); cenc::uint_preencode(s, m.nonce); }
inline void encode_into(cenc::State &s, const TreeRequest &m) { hash_buf_encode(s, m.hash); cenc::uint_encode(s, m.nonce); }
inline TreeRequest decode_tree_request(cenc::State &s) {
TreeRequest m; m.hash = hash_buf_decode(s);
m.nonce = cenc::has_more(s) ? cenc::uint_decode(s) : 0; // trap 1
return m;
}
HO_CODEC(TreeRequest)
struct TreeResponse { Hash32 hash{}; std::vector<TreeEntry> entries; uint8_t more = 0; uint64_t nonce = 0; };
inline void preencode(cenc::State &s, const TreeResponse &m) { hash_buf_preencode(s); tree_entry_array_preencode(s, m.entries); cenc::uint8_preencode(s); cenc::uint_preencode(s, m.nonce); }
inline void encode_into(cenc::State &s, const TreeResponse &m) { hash_buf_encode(s, m.hash); tree_entry_array_encode(s, m.entries); cenc::uint8_encode(s, m.more); cenc::uint_encode(s, m.nonce); }
inline TreeResponse decode_tree_response(cenc::State &s) {
TreeResponse m; m.hash = hash_buf_decode(s); m.entries = tree_entry_array_decode(s);
// Independently optional: a peer may have shipped `more` but not `nonce`.
m.more = cenc::has_more(s) ? cenc::uint8_decode(s) : 0;
m.nonce = cenc::has_more(s) ? cenc::uint_decode(s) : 0;
return m;
}
HO_CODEC(TreeResponse)
struct ContentRequest { Hash32 contentHash{}; Bytes chunksHave; std::string from; };
inline void preencode(cenc::State &s, const ContentRequest &m) { hash_buf_preencode(s); cenc::buffer_preencode(s, m.chunksHave.size()); cenc::string_preencode(s, m.from); }
inline void encode_into(cenc::State &s, const ContentRequest &m) { hash_buf_encode(s, m.contentHash); cenc::buffer_encode(s, m.chunksHave.data(), m.chunksHave.size()); cenc::string_encode(s, m.from); }
inline ContentRequest decode_content_request(cenc::State &s) {
ContentRequest m; m.contentHash = hash_buf_decode(s);
const cenc::BufView v = cenc::buffer_decode(s);
m.chunksHave.assign(v.data, v.data ? v.data + v.len : nullptr);
m.from = cenc::string_decode(s); // c.string handles absence as empty via bounds check
return m;
}
HO_CODEC(ContentRequest)
struct TransferControl { Hash32 contentHash{}; uint8_t state = 0; };
inline void preencode(cenc::State &s, const TransferControl &m) { hash_buf_preencode(s); cenc::uint8_preencode(s); }
inline void encode_into(cenc::State &s, const TransferControl &m) { hash_buf_encode(s, m.contentHash); cenc::uint8_encode(s, m.state); }
inline TransferControl decode_transfer_control(cenc::State &s) { TransferControl m; m.contentHash = hash_buf_decode(s); m.state = cenc::uint8_decode(s); return m; }
HO_CODEC(TransferControl)
struct TransferProgress { Hash32 contentHash{}; uint64_t have = 0; };
inline void preencode(cenc::State &s, const TransferProgress &m) { hash_buf_preencode(s); cenc::uint_preencode(s, m.have); }
inline void encode_into(cenc::State &s, const TransferProgress &m) { hash_buf_encode(s, m.contentHash); cenc::uint_encode(s, m.have); }
inline TransferProgress decode_transfer_progress(cenc::State &s) { TransferProgress m; m.contentHash = hash_buf_decode(s); m.have = cenc::uint_decode(s); return m; }
HO_CODEC(TransferProgress)
#undef HO_CODEC
} // namespace msg
} // namespace hyperoverlay
#endif // HYPEROVERLAY_MESSAGES_H