OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / liboverlay / test / test_messages.cpp

184 lines · cpp ref:
// 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;
}