OverviewFilesCommitsIssuesDocsReleasesPeers

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

203 lines · c ref:
#ifndef HYPEROVERLAY_CENC_H
#define HYPEROVERLAY_CENC_H

// compact-encoding primitives — wire-compatible with holepunchto/compact-encoding.
// Header-only, little-endian, no dependencies beyond the C++ standard library.
//
// SELF-CONTAINED ON PURPOSE. hyperswarm-peer ships its own compact_encoding.h and
// this does NOT include it. The reason is extractability: liboverlay is meant to
// be liftable into a standalone library by moving the directory, and reaching
// into a sibling project's src/ would make that a refactor instead of a move. It
// also pulls <sodium.h> in transitively, which this layer does not need.
//
// Two implementations of the same wire spec is not duplication debt here — it is
// two independent things held to one reference. Both are checked against fixtures
// generated from the JS (hyper-overlay/test/conformance), so a divergence in
// either shows up as a failing vector rather than as a silent interop bug. That
// said, hyperswarm-peer's copy has no `string` codec, so sharing it would have
// meant extending it anyway.
//
// Varint widths (compact-encoding's rule, mirrored exactly):
//   n <= 0xfc         1 byte,  the value itself
//   n <= 0xffff       0xfd + uint16 LE
//   n <= 0xffffffff   0xfe + uint32 LE
//   otherwise         0xff + uint64 LE
//
// The DECODERS ARE BOUNDS-CHECKED AND NON-THROWING. A remote peer controls these
// bytes, so a malformed frame must not read past the buffer; on underrun the
// cursor is parked at `end` and a zero/empty value is returned. Callers detect
// truncation by comparing `state.start` against `state.end`, which is also
// exactly how the protocol's optional trailing fields work.

#include <cstdint>
#include <cstring>
#include <string>
#include <vector>

namespace hyperoverlay {
namespace cenc {

struct State {
	size_t start = 0;
	size_t end = 0;
	uint8_t *buffer = nullptr;   // null during the preencode pass
};

struct BufView {
	const uint8_t *data = nullptr;
	size_t len = 0;
};

// True while there are bytes left. THE load-bearing predicate for this protocol:
// six messages append a field and read it only when the frame still has bytes,
// so a peer that predates the field decodes a default instead of failing.
inline bool has_more(const State &s) { return s.start < s.end; }

// ---- varint ----------------------------------------------------------------

inline void uint_preencode(State &s, uint64_t n) {
	if (n <= 0xfc) s.end += 1;
	else if (n <= 0xffff) s.end += 3;
	else if (n <= 0xffffffff) s.end += 5;
	else s.end += 9;
}

inline void uint_encode(State &s, uint64_t n) {
	if (n <= 0xfc) {
		s.buffer[s.start++] = (uint8_t)n;
	} else if (n <= 0xffff) {
		s.buffer[s.start++] = 0xfd;
		s.buffer[s.start++] = (uint8_t)(n & 0xff);
		s.buffer[s.start++] = (uint8_t)((n >> 8) & 0xff);
	} else if (n <= 0xffffffff) {
		s.buffer[s.start++] = 0xfe;
		for (int i = 0; i < 4; i++) s.buffer[s.start++] = (uint8_t)((n >> (8 * i)) & 0xff);
	} else {
		s.buffer[s.start++] = 0xff;
		for (int i = 0; i < 8; i++) s.buffer[s.start++] = (uint8_t)((n >> (8 * i)) & 0xff);
	}
}

inline uint64_t uint_decode(State &s) {
	if (s.start >= s.end) return 0;
	const uint8_t a = s.buffer[s.start++];
	if (a <= 0xfc) return a;
	const int width = (a == 0xfd) ? 2 : (a == 0xfe) ? 4 : 8;
	if (s.start + (size_t)width > s.end) { s.start = s.end; return 0; }
	uint64_t v = 0;
	for (int i = 0; i < width; i++) v |= (uint64_t)s.buffer[s.start + i] << (8 * i);
	s.start += (size_t)width;
	return v;
}

// ---- fixed-width ------------------------------------------------------------

inline void uint8_preencode(State &s) { s.end += 1; }
inline void uint8_encode(State &s, uint8_t n) { s.buffer[s.start++] = n; }
inline uint8_t uint8_decode(State &s) {
	if (s.start >= s.end) return 0;
	return s.buffer[s.start++];
}

inline void uint32_preencode(State &s) { s.end += 4; }
inline void uint32_encode(State &s, uint32_t n) {
	for (int i = 0; i < 4; i++) s.buffer[s.start++] = (uint8_t)((n >> (8 * i)) & 0xff);
}
inline uint32_t uint32_decode(State &s) {
	if (s.start + 4 > s.end) { s.start = s.end; return 0; }
	uint32_t v = 0;
	for (int i = 0; i < 4; i++) v |= (uint32_t)s.buffer[s.start + i] << (8 * i);
	s.start += 4;
	return v;
}

inline void uint64_preencode(State &s) { s.end += 8; }
inline void uint64_encode(State &s, uint64_t n) {
	for (int i = 0; i < 8; i++) s.buffer[s.start++] = (uint8_t)((n >> (8 * i)) & 0xff);
}

// ---- raw fixed-32 -----------------------------------------------------------
//
// DISTINCT FROM buffer_*. A 32-byte hash written with these has NO length
// prefix; the same hash written with buffer_* is preceded by a varint 32. The
// overlay uses BOTH — raw inside chunkInfo/treeEntry arrays, length-prefixed at
// message top level — and conflating them round-trips perfectly inside one
// implementation while failing against every other. See the chunkHashes vectors.

inline void fixed32_preencode(State &s) { s.end += 32; }
inline void fixed32_encode(State &s, const uint8_t *data) {
	std::memcpy(s.buffer + s.start, data, 32);
	s.start += 32;
}
inline bool fixed32_decode(State &s, uint8_t *out) {
	if (s.start + 32 > s.end) { s.start = s.end; std::memset(out, 0, 32); return false; }
	std::memcpy(out, s.buffer + s.start, 32);
	s.start += 32;
	return true;
}

// ---- length-prefixed buffer -------------------------------------------------

inline void buffer_preencode(State &s, size_t len) {
	uint_preencode(s, (uint64_t)len);
	s.end += len;
}
inline void buffer_encode(State &s, const uint8_t *data, size_t len) {
	uint_encode(s, (uint64_t)len);
	if (len) std::memcpy(s.buffer + s.start, data, len);
	s.start += len;
}
inline BufView buffer_decode(State &s) {
	const uint64_t len = uint_decode(s);
	if (s.start + (size_t)len > s.end) { s.start = s.end; return BufView{}; }
	BufView v{ s.buffer + s.start, (size_t)len };
	s.start += (size_t)len;
	return v;
}

// ---- string (UTF-8, length-prefixed) ---------------------------------------
//
// The length prefix counts BYTES, not code points. A path like "café" is 5
// bytes and 4 characters; sizing from character count under-allocates and
// truncates the frame. Covered by the utf8 fixture.

inline void string_preencode(State &s, const std::string &v) {
	uint_preencode(s, (uint64_t)v.size());
	s.end += v.size();
}
inline void string_encode(State &s, const std::string &v) {
	uint_encode(s, (uint64_t)v.size());
	if (!v.empty()) std::memcpy(s.buffer + s.start, v.data(), v.size());
	s.start += v.size();
}
inline std::string string_decode(State &s) {
	const uint64_t len = uint_decode(s);
	if (s.start + (size_t)len > s.end) { s.start = s.end; return std::string(); }
	std::string v((const char *)(s.buffer + s.start), (size_t)len);
	s.start += (size_t)len;
	return v;
}

// ---- helper: encode any codec into a fresh vector ---------------------------
//
// Mirrors the JS two-pass shape (preencode to size, allocate, encode) so a
// reader comparing the two files sees the same structure.

template <typename Fn>
inline std::vector<uint8_t> encode_to_vector(Fn &&fn) {
	State pre;
	fn(pre, true);                       // pass 1: size only
	std::vector<uint8_t> out(pre.end);
	State enc;
	enc.buffer = out.empty() ? nullptr : out.data();
	enc.start = 0;
	enc.end = out.size();
	fn(enc, false);                      // pass 2: write
	return out;
}

} // namespace cenc
} // namespace hyperoverlay

#endif // HYPEROVERLAY_CENC_H