// Runs the C++ message codecs against the fixtures generated from the JS
// reference (hyper-overlay/test/conformance/vectors.json).
//
// Build:
// g++ -O2 -std=c++17 -I liboverlay/include -o /tmp/ho_msg \
// liboverlay/test/test_messages.cpp
// Run:
// /tmp/ho_msg <path-to-vectors.json>
//
// WHAT IS CHECKED, AND WHY THIS SHAPE
//
// ROUND-TRIP decode(fixture bytes) -> encode -> must reproduce those bytes
// EXACTLY. One assertion covering both directions, and byte-exact
// against the JS, without writing field-by-field JSON glue for all
// 15 messages.
//
// PREFIX for the pre-field cases (a frame from a peer that predates a
// trailing field), re-encoding legitimately produces MORE bytes —
// the modern encoder appends the trailer. So the assertion is that
// the fixture is a strict PREFIX of the re-encode. That is exactly
// the back-compat property, stated as a test.
//
// FIELDS round-trip alone cannot catch a decoder that is wrong
// SYMMETRICALLY — swap two same-typed fields in both directions and
// the bytes still match. So a handful of cases additionally assert
// concrete decoded values.
//
// The JSON reader below is deliberately minimal and lives in the TEST, not the
// library: liboverlay stays dependency-free so it can be lifted out as-is.
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#include "overlay/messages.h"
#include "json_mini.h"
using namespace hyperoverlay;
// ---------------------------------------------------------------------------
static std::vector<uint8_t> from_hex(const std::string &h) {
std::vector<uint8_t> out;
for (size_t i = 0; i + 1 < h.size(); i += 2)
out.push_back((uint8_t)strtoul(h.substr(i, 2).c_str(), nullptr, 16));
return out;
}
static std::string to_hex(const std::vector<uint8_t> &b) {
static const char *d = "0123456789abcdef";
std::string s;
for (uint8_t c : b) { s += d[c >> 4]; s += d[c & 0xf]; }
return s;
}
static int passed = 0, failed = 0;
static void check(bool cond, const std::string &what) {
if (cond) passed++;
else { failed++; printf(" FAIL %s\n", what.c_str()); }
}
// decode + re-encode for one message name; returns false if the name is unknown.
static bool roundtrip(const std::string &name, const std::vector<uint8_t> &in,
std::vector<uint8_t> &out) {
cenc::State s;
s.buffer = const_cast<uint8_t *>(in.data());
s.start = 0;
s.end = in.size();
if (name == "handshake") { out = msg::encode(msg::decode_handshake(s)); return true; }
if (name == "syncState") { out = msg::encode(msg::decode_sync_state(s)); return true; }
if (name == "fileOffer") { out = msg::encode(msg::decode_file_offer(s)); return true; }
if (name == "fileRequest") { out = msg::encode(msg::decode_file_request(s)); return true; }
if (name == "chunkHashes") { out = msg::encode(msg::decode_chunk_hashes(s)); return true; }
if (name == "chunkNeed") { out = msg::encode(msg::decode_chunk_need(s)); return true; }
if (name == "chunkData") { out = msg::encode(msg::decode_chunk_data(s)); return true; }
if (name == "chunkCancel") { out = msg::encode(msg::decode_chunk_cancel(s)); return true; }
if (name == "transferComplete") { out = msg::encode(msg::decode_transfer_complete(s)); return true; }
if (name == "conflict") { out = msg::encode(msg::decode_conflict(s)); return true; }
if (name == "treeRequest") { out = msg::encode(msg::decode_tree_request(s)); return true; }
if (name == "treeResponse") { out = msg::encode(msg::decode_tree_response(s)); return true; }
if (name == "contentRequest") { out = msg::encode(msg::decode_content_request(s)); return true; }
if (name == "transferControl") { out = msg::encode(msg::decode_transfer_control(s)); return true; }
if (name == "transferProgress") { out = msg::encode(msg::decode_transfer_progress(s)); return true; }
return false;
}
int main(int argc, char **argv) {
const char *path = argc > 1 ? argv[1]
: "../../hypercore-kb/hyper-fleeky/hyper-overlay/test/conformance/vectors.json";
std::ifstream f(path);
if (!f) { printf("cannot open %s\n", path); return 2; }
std::stringstream ss; ss << f.rdbuf();
const std::string text = ss.str();
JsonParser jp{ text.c_str() };
const Json root = jp.parse();
const Json *cases = root.find("cases");
const Json *ver = root.find("overlayVersion");
if (!cases || cases->type != Json::ARR) { printf("no cases[] in %s\n", path); return 2; }
printf("hyper-overlay C++ message conformance (fixtures @ %s)\n",
ver ? ver->str.c_str() : "?");
std::map<std::string, int> seen;
for (const Json &c : cases->arr) {
const std::string name = c.find("message")->str;
const std::string label = c.find("label")->str;
const std::string hex = c.find("bytes")->str;
const Json *tr = c.find("truncated");
const bool truncated = tr && tr->type == Json::BOOL && tr->b;
seen[name]++;
const std::vector<uint8_t> in = from_hex(hex);
std::vector<uint8_t> out;
if (!roundtrip(name, in, out)) {
check(false, name + " / " + label + ": no C++ codec for this message");
continue;
}
const std::string where = name + " / " + label;
if (truncated) {
// The modern encoder re-adds the trailer the old peer omitted, so the
// fixture must be a strict PREFIX of what we produce.
const bool prefix = out.size() > in.size() &&
std::memcmp(out.data(), in.data(), in.size()) == 0;
check(prefix, where + ": pre-field frame must be a prefix of the re-encode\n"
" fixture " + hex + "\n got " + to_hex(out));
} else {
check(to_hex(out) == hex, where + ": round-trip mismatch\n"
" want " + hex + "\n got " + to_hex(out));
}
}
// ---- field-level spot checks (round-trip cannot catch symmetric errors) --
{
// chunkHashes carries BOTH array conventions and a raw fixed-32 hash.
msg::ChunkHashes m;
m.path = "big.pak";
m.tier = 2;
msg::ChunkInfo a; a.hash.fill(0xaa); a.length = 65536;
msg::ChunkInfo b; b.hash.fill(0xbb); b.length = 12;
m.chunks = { a, b };
m.more = 0;
const std::vector<uint8_t> enc = msg::encode(m);
cenc::State s; s.buffer = const_cast<uint8_t *>(enc.data()); s.start = 0; s.end = enc.size();
const msg::ChunkHashes d = msg::decode_chunk_hashes(s);
check(d.path == "big.pak", "spot: chunkHashes.path survives");
check(d.tier == 2, "spot: chunkHashes.tier survives");
check(d.chunks.size() == 2, "spot: chunkHashes chunk count");
check(d.chunks.size() == 2 && d.chunks[0].length == 65536 && d.chunks[1].length == 12,
"spot: chunk lengths are not transposed");
check(d.chunks.size() == 2 && d.chunks[0].hash[0] == 0xaa && d.chunks[1].hash[0] == 0xbb,
"spot: chunk hashes are not transposed");
}
{
// UTF-8 path: the length prefix counts BYTES, not characters.
msg::FileOffer m;
m.path = "assets/textures/café/naïve—ü.png";
m.contentHash.fill(0x02);
const std::vector<uint8_t> enc = msg::encode(m);
cenc::State s; s.buffer = const_cast<uint8_t *>(enc.data()); s.start = 0; s.end = enc.size();
const msg::FileOffer d = msg::decode_file_offer(s);
check(d.path == m.path, "spot: UTF-8 path round-trips byte-exact");
}
{
// chunkCancel: absent indices must stay absent, not become an empty array,
// or the frame stops being byte-identical to the pre-0.3.0 shape.
msg::ChunkCancel m; m.path = "f";
const std::vector<uint8_t> enc = msg::encode(m);
cenc::State s; s.buffer = const_cast<uint8_t *>(enc.data()); s.start = 0; s.end = enc.size();
const msg::ChunkCancel d = msg::decode_chunk_cancel(s);
check(d.indices.empty(), "spot: chunkCancel without indices encodes nothing extra");
}
printf(" %d assertions pass, %d fail\n", passed, failed);
printf(" %zu message types exercised\n", seen.size());
printf("%s\n", failed == 0 ? "PASS" : "FAIL");
return failed == 0 ? 0 : 1;
}