OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / src / compact_encoding.h

362 lines · c ref:
#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