#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