Files
reasampler/src/core/wire/bytes.h
T

106 lines
4.0 KiB
C++

// core/wire/bytes.h — the ONE little-endian byte codec. Pure, header-only:
// standard library only — no REAPER, no SWELL, no VST3.
//
// component_state_io is the biggest consumer. Wire format is FROZEN: putLE
// emits fixed-width LSB-first bytes exactly as the hand-rolled copies it
// replaced did, and ByteReader preserves the latch-on-truncation contract —
// once a read runs past the end, ok latches false and every subsequent read
// yields zeros/empties, so a truncated blob degrades to a partial parse,
// never an out-of-bounds read.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
#include <type_traits>
#include <vector>
namespace reasampler::wire {
// Append `v` little-endian (LSB first, sizeof(T) bytes). Unsigned integral types
// only — signed values go on the wire as their two's-complement unsigned image
// (cast at the call site, the established idiom: u32 for int, u64 for int64).
template <class T>
inline void putLE(std::vector<std::uint8_t>& out, T v) {
static_assert(std::is_unsigned_v<T> && !std::is_same_v<T, bool>, "putLE takes the unsigned wire image");
for (std::size_t b = 0; b < sizeof(T); ++b) {
out.push_back(static_cast<std::uint8_t>((v >> (b * 8)) & 0xFF));
}
}
// Signed 64-bit values ride the wire as their two's-complement unsigned image (the putLE
// call site's u64 cast). The one home for that cast — component_state_io and params_payload
// both need it and must not carry their own copies.
inline std::uint64_t asU64(std::int64_t v) { return static_cast<std::uint64_t>(v); }
// IEEE-754 double <-> u64 bit-cast for the wire (memcpy is the only defined
// type-pun in C++17). Doubles ride the wire as their u64 bit image via putLE.
inline std::uint64_t doubleToBits(double d) {
std::uint64_t bits;
std::memcpy(&bits, &d, sizeof(bits));
return bits;
}
inline double bitsToDouble(std::uint64_t bits) {
double d;
std::memcpy(&d, &bits, sizeof(d));
return d;
}
// A bounded little-endian reader over a byte blob (see the file header for the
// truncation-latch contract).
struct ByteReader {
const std::vector<std::uint8_t>& bytes;
std::size_t pos = 0;
bool ok = true;
explicit ByteReader(const std::vector<std::uint8_t>& b) : bytes(b) {}
// Read one unsigned integral little-endian (fixed sizeof(T) width).
template <class T>
T readLE() {
static_assert(std::is_unsigned_v<T>, "readLE yields the unsigned wire image");
if (!ok || pos + sizeof(T) > bytes.size()) {
ok = false;
return 0;
}
T v = 0;
for (std::size_t b = 0; b < sizeof(T); ++b) {
v |= static_cast<T>(bytes[pos + b]) << (b * 8);
}
pos += sizeof(T);
return v;
}
std::uint8_t u8() { return readLE<std::uint8_t>(); }
std::uint32_t u32() { return readLE<std::uint32_t>(); }
std::uint64_t u64() { return readLE<std::uint64_t>(); }
// Signed ints ride the wire as fixed-width two's-complement unsigned images.
int i32() { return static_cast<int>(static_cast<std::int32_t>(u32())); }
std::int64_t i64() { return static_cast<std::int64_t>(u64()); }
std::string str(std::uint32_t len) {
if (!ok || pos + len > bytes.size()) {
ok = false;
return {};
}
std::string s(reinterpret_cast<const char*>(bytes.data() + pos), len);
pos += len;
return s;
}
// Non-consuming peek of the next u32 (format-marker probes). Yields 0 and
// latches nothing when fewer than 4 bytes remain — the caller treats a short
// blob as "no marker" and falls through to its (also-guarded) fallback read.
std::uint32_t peekU32() const {
if (!ok || pos + 4 > bytes.size()) return 0;
return static_cast<std::uint32_t>(bytes[pos]) |
(static_cast<std::uint32_t>(bytes[pos + 1]) << 8) |
(static_cast<std::uint32_t>(bytes[pos + 2]) << 16) |
(static_cast<std::uint32_t>(bytes[pos + 3]) << 24);
}
};
} // namespace reasampler::wire