#ifndef HYPERSWARM_COMPACT_ENCODING_H
#define HYPERSWARM_COMPACT_ENCODING_H
// Compact encoding codec — wire-compatible with holepunchto/compact-encoding.
// All multi-byte integers are little-endian.
//
// ⛔ THE BYTES ARE NOT WRITTEN HERE ANY MORE. Every primitive below forwards to
// libcompact, which is holepunch's own C port of compact-encoding and is what
// swarmd's generated wire already links against. Two codecs for one format in
// one repo is one codec too many: the unannounce signable in
// docs/PARITY-hyperswarm.md §7 was written twice and diverged, and that is the
// same failure at a level up.
//
// What remains here is what libcompact does NOT have and hyperswarm DOES: the
// ipv4Address pair from compact-encoding-net, the BLAKE2b node id from
// dht-rpc/lib/peer.js, and the Kademlia XOR helpers.
//
// The two contracts this wrapper keeps, and libcompact does not state:
//
// * decoders return a VALUE, not an error code, because every call site here
// is written that way
// * a truncated decode yields 0 / null AND leaves start == end, so a loop
// over a malformed frame terminates. libcompact returns -1 and leaves
// `start` wherever it stopped; a caller looping `while (start < end)` on
// that would spin. test/test_compact_encoding.cpp pins all of it.
//
// Reference: holepunchto/compact-encoding/index.js
// holepunchto/compact-encoding-net/index.js
// holepunchto/dht-rpc/lib/peer.js
#include <cstdint>
#include <cstring>
#include <vector>
#include <sodium.h>
extern "C" {
#include <compact.h>
}
// ---- Data structures ----
// Layout-identical to what it replaced: { start, end, buffer }. Aggregate
// initialisation at the call sites is unchanged.
typedef compact_state_t EncodeState;
struct IPv4Address {
uint8_t ip[4]; // Network byte order (big-endian octets).
uint16_t port; // Host byte order.
};
struct NodeId {
uint8_t bytes[32];
};
struct BufView {
const uint8_t *data;
size_t len;
};
// ---- Compact encoding namespace ----
namespace cenc {
// ---- Variable-width unsigned integer (varint) ----
// A decode that ran off the end. libcompact tells us with -1; our callers read
// a value, so give them the harmless one and make sure the frame is finished.
inline void _truncated(EncodeState &s) { s.start = s.end; }
inline void uint_preencode(EncodeState &s, uint64_t n) { compact_preencode_uint(&s, n); }
inline void uint_encode(EncodeState &s, uint64_t n) { compact_encode_uint(&s, n); }
inline uint64_t uint_decode(EncodeState &s) {
uint64_t v = 0;
if (compact_decode_uint(&s, &v) < 0) { _truncated(s); return 0; }
return v;
}
// ---- Fixed-width little-endian integers ----
//
// The preencode helpers take no value because these widths do not depend on
// one; libcompact's take one and ignore it for exactly the same reason.
inline void uint8_encode(EncodeState &s, uint8_t n) { compact_encode_uint8(&s, n); }
inline uint8_t uint8_decode(EncodeState &s) {
uint8_t v = 0;
if (compact_decode_uint8(&s, &v) < 0) { _truncated(s); return 0; }
return v;
}
inline void uint16_preencode(EncodeState &s) { compact_preencode_uint16(&s, 0); }
inline void uint16_encode(EncodeState &s, uint16_t n) { compact_encode_uint16(&s, n); }
inline uint16_t uint16_decode(EncodeState &s) {
uint16_t v = 0;
if (compact_decode_uint16(&s, &v) < 0) { _truncated(s); return 0; }
return v;
}
inline void uint32_preencode(EncodeState &s) { compact_preencode_uint32(&s, 0); }
inline void uint32_encode(EncodeState &s, uint32_t n) { compact_encode_uint32(&s, n); }
inline uint32_t uint32_decode(EncodeState &s) {
uint32_t v = 0;
if (compact_decode_uint32(&s, &v) < 0) { _truncated(s); return 0; }
return v;
}
inline void uint64_preencode(EncodeState &s) { compact_preencode_uint64(&s, 0); }
inline void uint64_encode(EncodeState &s, uint64_t n) { compact_encode_uint64(&s, n); }
// ⛔ ONE BYTE-LEVEL BEHAVIOUR CHANGED HERE, deliberately. This used to be two
// uint32 decodes, so four remaining bytes yielded the LOW WORD of a number that
// was never on the wire. It is one decode now and a short read yields 0, which
// is the contract the other decoders already kept.
inline uint64_t uint64_decode(EncodeState &s) {
uint64_t v = 0;
if (compact_decode_uint64(&s, &v) < 0) { _truncated(s); return 0; }
return v;
}
// ---- Fixed-size raw buffers (no length prefix) ----
//
// These decode ZERO-COPY — the pointer is into the caller's frame, which is
// what every call site expects. libcompact's fixed32/64 decoders copy into a
// destination, so the pointer is taken here and libcompact is asked to do the
// bounds check and the advance with a null destination.
inline void fixed32_preencode(EncodeState &s) { compact_preencode_fixed32(&s, nullptr); }
inline void fixed32_encode(EncodeState &s, const uint8_t *data) {
compact_encode_fixed32(&s, data);
}
inline const uint8_t *fixed32_decode(EncodeState &s) {
const uint8_t *ptr = s.buffer + s.start;
if (compact_decode_fixed32(&s, nullptr) < 0) { _truncated(s); return nullptr; }
return ptr;
}
inline void fixed64_preencode(EncodeState &s) { compact_preencode_fixed64(&s, nullptr); }
inline void fixed64_encode(EncodeState &s, const uint8_t *data) {
compact_encode_fixed64(&s, data);
}
inline const uint8_t *fixed64_decode(EncodeState &s) {
const uint8_t *ptr = s.buffer + s.start;
if (compact_decode_fixed64(&s, nullptr) < 0) { _truncated(s); return nullptr; }
return ptr;
}
// ---- Length-prefixed buffer ----
//
// compact-encoding's `buffer` is uint8array with one extra rule this codebase
// relies on: a null buffer is a single 0x00, and it decodes back to null rather
// than to a zero-length view. libcompact has no null, so that rule stays here.
inline void buffer_preencode(EncodeState &s, const uint8_t *data, size_t len) {
if (data == nullptr) {
s.end += 1;
} else {
compact_preencode_uint8array(&s, data, len);
}
}
inline void buffer_encode(EncodeState &s, const uint8_t *data, size_t len) {
if (data == nullptr) {
compact_encode_uint8(&s, 0);
} else {
compact_encode_uint8array(&s, data, len);
}
}
inline BufView buffer_decode(EncodeState &s) {
uint8_t *ptr = nullptr;
size_t len = 0;
if (compact_decode_uint8array(&s, &ptr, &len) < 0) { _truncated(s); return { nullptr, 0 }; }
if (len == 0) return { nullptr, 0 };
return { ptr, len };
}
// ---- IPv4 Address (6 bytes: 4 IP octets + 2 port LE) ----
// Wire format matches compact-encoding-net ipv4Address:
// ipv4.encode(host) + c.uint16.encode(port)
inline void ipv4_address_preencode(EncodeState &s) {
s.end += 6;
}
inline void ipv4_address_encode(EncodeState &s, const IPv4Address &addr) {
// IP: 4 octets in network order (as stored).
s.buffer[s.start++] = addr.ip[0];
s.buffer[s.start++] = addr.ip[1];
s.buffer[s.start++] = addr.ip[2];
s.buffer[s.start++] = addr.ip[3];
// Port: uint16 LE (compact-encoding uses c.uint16 which is LE).
uint16_encode(s, addr.port);
}
inline IPv4Address ipv4_address_decode(EncodeState &s) {
IPv4Address addr;
memset(&addr, 0, sizeof(addr));
if (s.start + 6 > s.end) { s.start = s.end; return addr; }
addr.ip[0] = s.buffer[s.start++];
addr.ip[1] = s.buffer[s.start++];
addr.ip[2] = s.buffer[s.start++];
addr.ip[3] = s.buffer[s.start++];
addr.port = uint16_decode(s);
return addr;
}
// ---- Array of IPv4 Addresses ----
inline void ipv4_array_preencode(EncodeState &s, const IPv4Address *addrs, int count) {
uint_preencode(s, static_cast<uint64_t>(count));
s.end += static_cast<size_t>(count) * 6;
}
inline void ipv4_array_encode(EncodeState &s, const IPv4Address *addrs, int count) {
uint_encode(s, static_cast<uint64_t>(count));
for (int i = 0; i < count; i++) {
ipv4_address_encode(s, addrs[i]);
}
}
inline int ipv4_array_decode(EncodeState &s, IPv4Address *out, int max_count) {
uint64_t count = uint_decode(s);
int n = (count > static_cast<uint64_t>(max_count)) ? max_count : static_cast<int>(count);
for (int i = 0; i < n; i++) {
if (s.start >= s.end) return i;
out[i] = ipv4_address_decode(s);
}
// Skip any excess entries.
for (uint64_t i = n; i < count; i++) {
if (s.start + 6 > s.end) { s.start = s.end; break; }
s.start += 6;
}
return n;
}
// ---- Node ID: BLAKE2b hash of 6-byte IPv4Address ----
// Matches dht-rpc/lib/peer.js: id(host, port)
inline NodeId node_id_from_address(const IPv4Address &addr) {
NodeId nid;
uint8_t buf[6];
buf[0] = addr.ip[0];
buf[1] = addr.ip[1];
buf[2] = addr.ip[2];
buf[3] = addr.ip[3];
buf[4] = static_cast<uint8_t>(addr.port & 0xff);
buf[5] = static_cast<uint8_t>((addr.port >> 8) & 0xff);
// crypto_generichash defaults to 32-byte output (BLAKE2b-256).
crypto_generichash(nid.bytes, 32, buf, 6, nullptr, 0);
return nid;
}
// ---- XOR distance comparison for Kademlia ----
// Returns <0 if a is closer to target than b, >0 if b is closer, 0 if equal.
inline int xor_compare(const NodeId &target, const NodeId &a, const NodeId &b) {
for (int i = 0; i < 32; i++) {
uint8_t da = target.bytes[i] ^ a.bytes[i];
uint8_t db = target.bytes[i] ^ b.bytes[i];
if (da < db) return -1;
if (da > db) return 1;
}
return 0;
}
// Returns the XOR distance bit position (0-255) between two node IDs.
// 0 = most significant bit differs. Returns 256 if equal.
inline int xor_distance_bit(const NodeId &a, const NodeId &b) {
for (int i = 0; i < 32; i++) {
uint8_t diff = a.bytes[i] ^ b.bytes[i];
if (diff == 0) continue;
// Count leading zeros in the differing byte.
int clz = 0;
if (diff < 0x10) { clz += 4; diff <<= 4; }
if (diff < 0x40) { clz += 2; diff <<= 2; }
if (diff < 0x80) { clz += 1; }
return i * 8 + clz;
}
return 256; // Equal.
}
// ---- IPv4Address string parsing utilities ----
// Parse "A.B.C.D" into ip[4]. Returns true on success.
inline bool ipv4_parse(const char *str, uint8_t *ip) {
int octet = 0;
int idx = 0;
for (const char *p = str; ; p++) {
if (*p >= '0' && *p <= '9') {
octet = octet * 10 + (*p - '0');
} else if (*p == '.' || *p == '\0') {
if (idx >= 4 || octet > 255) return false;
ip[idx++] = static_cast<uint8_t>(octet);
octet = 0;
if (*p == '\0') break;
} else {
return false;
}
}
return idx == 4;
}
// Parse "A.B.C.D:PORT" into IPv4Address. Returns true on success.
inline bool ipv4_address_parse(const char *str, IPv4Address &addr) {
// Find the colon separator.
const char *colon = nullptr;
for (const char *p = str; *p; p++) {
if (*p == ':') { colon = p; break; }
}
if (colon == nullptr) return false;
// Parse IP part.
char ip_buf[16];
size_t ip_len = static_cast<size_t>(colon - str);
if (ip_len >= sizeof(ip_buf)) return false;
memcpy(ip_buf, str, ip_len);
ip_buf[ip_len] = '\0';
if (!ipv4_parse(ip_buf, addr.ip)) return false;
// Parse port.
int port = 0;
for (const char *p = colon + 1; *p; p++) {
if (*p < '0' || *p > '9') return false;
port = port * 10 + (*p - '0');
if (port > 65535) return false;
}
if (port < 0) return false;
addr.port = static_cast<uint16_t>(port);
return true;
}
// Format IPv4Address to "A.B.C.D:PORT" string. Returns bytes written (excluding null).
inline int ipv4_address_format(const IPv4Address &addr, char *buf, size_t buf_size) {
return snprintf(buf, buf_size, "%d.%d.%d.%d:%d",
addr.ip[0], addr.ip[1], addr.ip[2], addr.ip[3], addr.port);
}
// ---- Allocate buffer after preencode pass ----
inline std::vector<uint8_t> state_alloc(EncodeState &s) {
std::vector<uint8_t> buf(s.end);
s.buffer = buf.data();
s.start = 0;
return buf;
}
} // namespace cenc
#endif // HYPERSWARM_COMPACT_ENCODING_H