OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / liboverlay / include / overlay / messages.h

303 lines · c ref:
#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