Q-W1 pt1: extract core/json (json::Reader/Writer), collapse wire Cursor family into core/wire, shared readFileBytes — five JSON decoders and three cursor copies deleted, byte-identical formats, 59/59 green

This commit is contained in:
2026-07-28 19:59:02 -04:00
parent 88e7765ee5
commit 67a41728f3
25 changed files with 1695 additions and 1696 deletions
+319
View File
@@ -0,0 +1,319 @@
// core/json implementation — see json.h. The bodies are the (previously
// quintuplicated) bank_model / view_mode_model lexical layer, verbatim; any
// behavioral change here changes five persisted-blob parsers at once.
#include "core/json/json.h"
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
namespace reasampler::json {
// ---------------------------------------------------------------------------
// emit helpers
// ---------------------------------------------------------------------------
void writeEscaped(std::string& out, const std::string& s) {
out += '"';
for (char c : s) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (static_cast<unsigned char>(c) < 0x20) {
char buf[8];
std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast<unsigned char>(c));
out += buf;
} else {
out += c;
}
}
}
out += '"';
}
std::string numToStr(double v) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.17g", v);
return buf;
}
std::string numToStr(std::int64_t v) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%lld", static_cast<long long>(v));
return buf;
}
std::string numToStr(int v) {
char buf[16];
std::snprintf(buf, sizeof(buf), "%d", v);
return buf;
}
void writeStringArray(std::string& out, const std::vector<std::string>& v) {
out += '[';
for (std::size_t i = 0; i < v.size(); ++i) {
if (i) out += ',';
writeEscaped(out, v[i]);
}
out += ']';
}
void writeIntArray(std::string& out, const std::vector<int>& v) {
out += '[';
for (std::size_t i = 0; i < v.size(); ++i) {
if (i) out += ',';
out += numToStr(v[i]);
}
out += ']';
}
// ---------------------------------------------------------------------------
// Reader
// ---------------------------------------------------------------------------
void Reader::skipWs() {
while (!eof()) {
char c = s_[pos_];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
else break;
}
}
bool Reader::consume(char c) {
skipWs();
if (eof() || s_[pos_] != c) return false;
++pos_;
return true;
}
// Parses a JSON string literal (with the escapes our writers emit, plus \uXXXX
// for control chars). Positioned before the opening quote (skips leading ws).
bool Reader::parseString(std::string& out) {
skipWs();
if (eof() || s_[pos_] != '"') return false;
++pos_;
out.clear();
while (!eof()) {
char c = s_[pos_++];
if (c == '"') return true;
if (c == '\\') {
if (eof()) return false;
char e = s_[pos_++];
switch (e) {
case '"': out += '"'; break;
case '\\': out += '\\'; break;
case '/': out += '/'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'n': out += '\n'; break;
case 'r': out += '\r'; break;
case 't': out += '\t'; break;
case 'u': {
// Decode a \uXXXX escape to its code point.
auto readHex4 = [&](unsigned int& cp) -> bool {
if (pos_ + 4 > s_.size()) return false;
cp = 0;
for (int i = 0; i < 4; ++i) {
char h = s_[pos_++];
cp <<= 4;
if (h >= '0' && h <= '9') cp |= static_cast<unsigned>(h - '0');
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(h - 'A' + 10);
else return false;
}
return true;
};
unsigned int hi = 0;
if (!readHex4(hi)) return false;
unsigned int codePoint = hi;
if (hi >= 0xD800 && hi <= 0xDBFF) {
// High surrogate — must be followed by \uDC00\uDFFF.
if (pos_ + 6 > s_.size()) return false;
if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false;
pos_ += 2;
unsigned int lo = 0;
if (!readHex4(lo)) return false;
if (lo < 0xDC00 || lo > 0xDFFF) return false; // unpaired high surrogate
codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
} else if (hi >= 0xDC00 && hi <= 0xDFFF) {
return false; // unpaired low surrogate — malformed
}
// Encode codePoint as UTF-8.
if (codePoint <= 0x7F) {
out += static_cast<char>(codePoint);
} else if (codePoint <= 0x7FF) {
out += static_cast<char>(0xC0 | (codePoint >> 6));
out += static_cast<char>(0x80 | (codePoint & 0x3F));
} else if (codePoint <= 0xFFFF) {
out += static_cast<char>(0xE0 | (codePoint >> 12));
out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
out += static_cast<char>(0x80 | (codePoint & 0x3F));
} else {
out += static_cast<char>(0xF0 | (codePoint >> 18));
out += static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F));
out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
out += static_cast<char>(0x80 | (codePoint & 0x3F));
}
break;
}
default: return false;
}
} else {
out += c;
}
}
return false; // unterminated string
}
bool Reader::parseRawScalar(std::string& out) {
skipWs();
std::size_t start = pos_;
while (!eof()) {
char c = s_[pos_];
if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' ||
c == '\n' || c == '\r')
break;
++pos_;
}
if (pos_ == start) return false;
out.assign(s_, start, pos_ - start);
return true;
}
bool Reader::parseDouble(double& out) {
std::string tok;
if (!parseRawScalar(tok)) return false;
const char* b = tok.c_str();
char* end = nullptr;
errno = 0;
double v = std::strtod(b, &end);
if (end != b + tok.size()) return false;
if (errno == ERANGE) return false; // overflow / underflow -> malformed
out = v;
return true;
}
bool Reader::parseInt64(std::int64_t& out) {
std::string tok;
if (!parseRawScalar(tok)) return false;
const char* b = tok.c_str();
char* end = nullptr;
errno = 0;
long long v = std::strtoll(b, &end, 10);
if (end != b + tok.size()) return false;
if (errno == ERANGE) return false; // overflow -> malformed
out = static_cast<std::int64_t>(v);
return true;
}
bool Reader::parseInt(int& out) {
std::int64_t v = 0;
if (!parseInt64(v)) return false;
if (v < INT_MIN || v > INT_MAX) return false;
out = static_cast<int>(v);
return true;
}
bool Reader::parseBool(bool& out) {
std::string tok;
if (!parseRawScalar(tok)) return false;
if (tok == "true") { out = true; return true; }
if (tok == "false") { out = false; return true; }
return false;
}
bool Reader::expectNullOr(bool& wasNull) {
skipWs();
if (eof()) return false;
if (s_.compare(pos_, 4, "null") == 0) {
pos_ += 4;
wasNull = true;
} else {
wasNull = false;
}
return true;
}
bool Reader::parseKey(std::string& key) {
if (!parseString(key)) return false;
return consume(':');
}
bool Reader::parseStringArray(std::vector<std::string>& out) {
if (!consume('[')) return false;
skipWs();
if (consume(']')) return true; // empty array
do {
std::string s;
if (!parseString(s)) return false;
out.push_back(std::move(s));
} while (consume(','));
return consume(']');
}
bool Reader::parseIntArray(std::vector<int>& out) {
if (!consume('[')) return false;
skipWs();
if (consume(']')) return true;
do {
int v = 0;
if (!parseInt(v)) return false;
out.push_back(v);
} while (consume(','));
return consume(']');
}
bool Reader::skipValue() {
std::string raw;
return captureValue(raw);
}
// Records the raw source span of one JSON value starting at the current position
// (after whitespace). Handles nested objects/arrays with string-aware brace
// matching (braces inside strings ignored).
bool Reader::captureValue(std::string& raw) {
skipWs();
if (eof()) return false;
std::size_t start = pos_;
char c = s_[pos_];
if (c == '"') {
std::string tmp;
if (!parseString(tmp)) return false;
raw.assign(s_, start, pos_ - start);
return true;
}
if (c == '{' || c == '[') {
char open = c, close = (c == '{') ? '}' : ']';
++pos_;
int depth = 1;
while (!eof() && depth > 0) {
char d = s_[pos_];
if (d == '"') {
std::string tmp;
if (!parseString(tmp)) return false; // advances past the string
continue;
}
if (d == open) ++depth;
else if (d == close) --depth;
++pos_;
}
if (depth != 0) return false;
raw.assign(s_, start, pos_ - start);
return true;
}
// bare scalar (number / true / false / null)
return parseRawScalar(raw);
}
} // namespace reasampler::json
+149
View File
@@ -0,0 +1,149 @@
// core/json — the ONE hand-rolled JSON lexical layer (Q-W1; audit T2-02 / §2
// "Parser ×4"). Pure: standard library only — NO REAPER, NO SWELL, NO VST3.
//
// This module owns the lexical half of the house JSON dialect: the escape-aware
// string literal (incl. \uXXXX + surrogate pairs re-encoded as UTF-8), the bare
// scalar tokens, the number parses (strtod/strtoll with full-token + ERANGE
// rejection), key+':' consumption, unknown-value skipping, and the emit side
// (escaping, %.17g / %d / %lld number rendering, the scoped object writer).
// The DOMAIN grammars — which keys exist, what shape each value takes, what is
// rejected at the model boundary — stay in the consumers (bank_model, bank_book,
// view_mode_model, owned_manifest, tail_control). One lexical definition means
// the five decoders can no longer drift on tolerance or escaping.
//
// Byte-compatibility contract (load-bearing): the emit helpers reproduce the
// prior per-module writers EXACTLY — writeEscaped's escape set, %.17g for
// doubles (shortest form that round-trips every IEEE-754 double bit-for-bit),
// plain decimal for ints — so a re-serialized blob is byte-identical to what
// the pre-extraction writers produced. This was a structural dedupe, not a
// format change; persisted .rpp ext-state must not shift by a byte.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace reasampler::json {
// ---------------------------------------------------------------------------
// emit helpers (writer side)
// ---------------------------------------------------------------------------
// Appends `s` as a quoted JSON string literal: the seven short escapes, \uXXXX
// for remaining control chars, everything else verbatim (UTF-8 passes through).
void writeEscaped(std::string& out, const std::string& s);
// Number rendering. %.17g is the shortest form that round-trips every IEEE-754
// double exactly, so deserialize(serialize(x)) == x holds bit-for-bit.
std::string numToStr(double v);
std::string numToStr(std::int64_t v);
std::string numToStr(int v);
// Flat homogeneous arrays: ["a","b"] / [1,2]. Empty vector -> "[]".
void writeStringArray(std::string& out, const std::vector<std::string>& v);
void writeIntArray(std::string& out, const std::vector<int>& v);
// Scoped object writer: appends '{' on construction and '}' on destruction, with
// comma separation handled internally. Nested values are written by keyBegin()
// followed by the caller emitting the value (e.g. a nested Writer scope or an
// array). NOTE the destructor-close means an enclosing scope must END (brace
// block) before the built string is returned — see the NRVO note in the
// consumers' serialize() implementations.
class Writer {
public:
explicit Writer(std::string& out) : out_(out) { out_ += '{'; }
~Writer() { out_ += '}'; }
Writer(const Writer&) = delete;
Writer& operator=(const Writer&) = delete;
// "key":<rawValue> — rawValue appended verbatim (numbers, bools, null,
// pre-serialized nested blobs).
void keyRaw(const char* key, const std::string& rawValue) {
sep();
writeEscaped(out_, key);
out_ += ':';
out_ += rawValue;
}
// "key":"value" — value escaped.
void keyStr(const char* key, const std::string& value) {
sep();
writeEscaped(out_, key);
out_ += ':';
writeEscaped(out_, value);
}
// "key": — caller writes the value immediately after.
void keyBegin(const char* key) {
sep();
writeEscaped(out_, key);
out_ += ':';
}
private:
void sep() {
if (first_) first_ = false; else out_ += ',';
}
std::string& out_;
bool first_ = true;
};
// ---------------------------------------------------------------------------
// Reader — the lexical cursor (parser side)
// ---------------------------------------------------------------------------
//
// Every method returns false on malformed input and never reads out of bounds.
// Only the subset the house writers emit is supported. The reader borrows the
// input string — it must outlive the Reader.
class Reader {
public:
explicit Reader(const std::string& s) : s_(s) {}
bool eof() const { return pos_ >= s_.size(); }
void skipWs();
// Consumes `c` (after whitespace). False without advancing past `c` if the
// next non-ws char differs.
bool consume(char c);
// JSON string literal (escapes + \uXXXX incl. surrogate pairs -> UTF-8).
bool parseString(std::string& out);
// Bare token (number / true / false / null) up to the next structural char.
bool parseRawScalar(std::string& out);
// Numbers: full-token parse; trailing bytes or ERANGE reject. parseInt
// additionally rejects values outside [INT_MIN, INT_MAX].
bool parseDouble(double& out);
bool parseInt64(std::int64_t& out);
bool parseInt(int& out);
bool parseBool(bool& out);
// Peeks for the `null` token; consumes it if present (wasNull=true),
// otherwise leaves the position untouched (wasNull=false). Returns false
// only on eof.
bool expectNullOr(bool& wasNull);
// An object member key + ':'.
bool parseKey(std::string& key);
// Homogeneous arrays. Appends to `out`; empty array is valid.
bool parseStringArray(std::vector<std::string>& out);
bool parseIntArray(std::vector<int>& out);
// Skips one value of any shape (string / object / array / bare scalar) —
// forward-compat for unknown keys.
bool skipValue();
// Captures the raw source text of one value verbatim (string-aware brace
// matching), so a nested blob can be handed to its own parser — the
// bank_book -> BankIndex::deserialize seam.
bool captureValue(std::string& raw);
private:
const std::string& s_;
std::size_t pos_ = 0;
};
} // namespace reasampler::json
+21
View File
@@ -0,0 +1,21 @@
// core/util/file_bytes implementation — see file_bytes.h.
#include "core/util/file_bytes.h"
#include <fstream>
namespace reasampler {
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f) return {};
const std::streamoff size = f.tellg();
if (size <= 0) return {};
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(size));
f.seekg(0);
f.read(reinterpret_cast<char*>(bytes.data()), size);
if (!f) return {};
return bytes;
}
} // namespace reasampler
+19
View File
@@ -0,0 +1,19 @@
// core/util/file_bytes — the ONE whole-file byte loader (Q-W1; audit T2-03).
// Pure standard library — NO REAPER, NO SWELL, NO VST3 — but it does blocking
// file I/O: NEVER call it on the audio thread (off-thread only, the same rule
// every prior hand-rolled copy carried). Linked by both artifacts.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
namespace reasampler {
// Reads the whole file at `path` into a byte buffer. Empty on ANY failure —
// unopenable, empty file, or short read — so the caller has exactly one
// "nothing to work with" branch.
std::vector<std::uint8_t> readFileBytes(const std::string& path);
} // namespace reasampler
+134
View File
@@ -0,0 +1,134 @@
// core/wire implementation — see wire.h. The bodies are the hardened
// assignment_request / sample_usage / provenance (post Q-W0 T2-01a backport)
// cursor, unified; any behavioral change here changes every ext-state wire
// seam at once.
#include "core/wire/wire.h"
#include <cstdlib>
#include <limits>
namespace reasampler::wire {
void putField(std::string& out, const std::string& field) {
out += std::to_string(field.size());
out += ':';
out += field;
}
bool parseUnsignedDecimal(const std::string& s, std::int64_t& out) {
if (s.empty()) return false;
std::int64_t value = 0;
constexpr std::int64_t kMax = std::numeric_limits<std::int64_t>::max();
for (const char c : s) {
if (c < '0' || c > '9') return false; // any non-digit -> reject whole
const int digit = c - '0';
// Guard value*10 + digit against overflow before performing it.
if (value > (kMax - digit) / 10) return false;
value = value * 10 + digit;
}
out = value;
return true;
}
bool Cursor::literal(const char* lit) {
if (!ok_) return false;
std::size_t i = 0;
for (; lit[i] != '\0'; ++i) {
if (pos_ + i >= s_.size() || s_[pos_ + i] != lit[i]) return fail();
}
pos_ += i;
return true;
}
bool Cursor::field(std::string& out) {
if (!ok_) return false;
const std::size_t colon = s_.find(':', pos_);
if (colon == std::string::npos) return fail();
if (colon == pos_) return fail(); // empty length token
// Cap: SIZE_MAX fits in at most 20 decimal digits; a longer run is bogus.
if (colon - pos_ > 20u) return fail();
std::size_t len = 0;
for (std::size_t i = pos_; i < colon; ++i) {
const char c = s_[i];
if (c < '0' || c > '9') return fail();
const std::size_t digit = static_cast<std::size_t>(c - '0');
// Overflow guard: if len would exceed SIZE_MAX after multiply+add, fail.
if (len > (std::numeric_limits<std::size_t>::max() - digit) / 10u)
return fail();
len = len * 10u + digit;
}
const std::size_t start = colon + 1;
// Subtraction-first form: start + len cannot wrap on a huge len.
if (start > s_.size() || len > s_.size() - start) return fail();
out.assign(s_, start, len);
pos_ = start + len;
return true;
}
bool Cursor::fieldInt64(std::int64_t& out) {
std::string f;
if (!field(f)) return false;
if (f.empty()) return fail();
std::size_t i = 0;
bool neg = false;
if (f[0] == '-') {
neg = true;
i = 1;
if (f.size() == 1) return fail(); // bare "-"
}
// Cap at 19 digits (INT64_MAX = 9223372036854775807 — 19 digits). A 20-digit
// positive value would overflow INT64_MAX; a 20-digit negative might be valid
// (INT64_MIN) but is conservatively rejected too — see header.
if (f.size() - i > 19u) return fail();
std::int64_t v = 0;
for (; i < f.size(); ++i) {
const char c = f[i];
if (c < '0' || c > '9') return fail();
const std::int64_t digit = static_cast<std::int64_t>(c - '0');
// Overflow guard: v * 10 + digit must not exceed INT64_MAX.
if (v > (std::numeric_limits<std::int64_t>::max() - digit) / 10)
return fail();
v = v * 10 + digit;
}
out = neg ? -v : v;
return true;
}
bool Cursor::fieldInt(int& out) {
std::int64_t v = 0;
if (!fieldInt64(v)) return false;
if (v < std::numeric_limits<int>::min() || v > std::numeric_limits<int>::max())
return fail();
out = static_cast<int>(v);
return true;
}
bool Cursor::fieldSizeT(std::size_t& out) {
std::string f;
if (!field(f)) return false;
if (f.empty() || f.size() > 20u) return fail();
std::size_t v = 0;
for (const char c : f) {
if (c < '0' || c > '9') return fail();
const std::size_t digit = static_cast<std::size_t>(c - '0');
if (v > (std::numeric_limits<std::size_t>::max() - digit) / 10u)
return fail();
v = v * 10u + digit;
}
out = v;
return true;
}
bool Cursor::fieldDouble(double& out) {
std::string f;
if (!field(f)) return false;
const char* b = f.c_str();
char* end = nullptr;
double v = std::strtod(b, &end);
if (end != b + f.size()) return fail();
out = v;
return true;
}
} // namespace reasampler::wire
+88
View File
@@ -0,0 +1,88 @@
// core/wire — the ONE length-prefixed ext-state wire codec (Q-W1; audit
// T2-01(b)). Pure: standard library only — NO REAPER, NO SWELL, NO VST3.
//
// The `<decimal-len>':'<bytes>` field grammar ("one grammar across every
// ext-state seam") was previously implemented as three near-identical
// putField + Cursor copies (provenance / assignment_request / sample_usage)
// plus a fourth guarded decimal accumulate (bank_sync::parseBankGeneration) —
// and the copies drifted on the hardening. This is the single survivor,
// carrying the FULL hardening everywhere:
// - length digit-run capped at 20 (SIZE_MAX's decimal width) so a crafted
// digit run cannot accumulate past SIZE_MAX via repeated multiply;
// - overflow guard on every accumulate (multiply+add checked BEFORE applied);
// - subtraction-first bounds check so a huge len cannot wrap `start + len`;
// - fieldInt/fieldInt64 parse sign+digits manually with an INT64 overflow
// guard and an int range check — an out-of-range field FAILS the parse
// (closing the strtol errno/range gap the provenance copy carried).
//
// Wire formats on disk / ext-state are FROZEN: encode is byte-identical to the
// pre-collapse writers (std::to_string length + ':' + bytes), decode is
// tolerant-identical for every value a house writer can emit. "Never UB, never
// a partial value" is the parse-integrity promise.
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
namespace reasampler::wire {
// Append one length-prefixed field: <decimal-len> ':' <bytes>
void putField(std::string& out, const std::string& field);
// Whole-string, non-negative decimal parse WITHOUT exceptions or locale
// surprises (the bank_sync generation-stamp core). False on empty, any
// non-digit (incl. a leading '+'/'-'), or overflow past INT64_MAX; the
// accumulate is overflow-guarded so a pathologically long digit run can never
// wrap into a bogus small value.
bool parseUnsignedDecimal(const std::string& s, std::int64_t& out);
// Bounds-checked cursor over an encoded string. All reads are bounds-checked;
// any short read fails the whole parse (ok_ latches false — every subsequent
// read also fails, so a caller may check ok() once at the end).
class Cursor {
public:
explicit Cursor(const std::string& s) : s_(s) {}
bool ok() const { return ok_; }
bool atEnd() const { return pos_ >= s_.size(); }
// Consumes an exact literal at the cursor (the magic tag). Fails if absent.
bool literal(const char* lit);
// Reads one length-prefixed field into `out`. Fails on a missing ':', an
// empty or non-numeric length, a length that would overflow SIZE_MAX, or a
// length that runs past the end.
bool field(std::string& out);
// Length-prefixed signed 64-bit decimal (optional leading '-'). Digit run
// capped at 19 (INT64_MAX's decimal width); overflow fails the parse. A
// 20-digit negative (only INT64_MIN itself) is conservatively rejected —
// house writers emit generation timestamps and small enums, never that.
bool fieldInt64(std::int64_t& out);
// fieldInt64 narrowed to int; a value outside [INT_MIN, INT_MAX] FAILS the
// parse (the fixed form of the provenance copy's silent strtol narrowing).
bool fieldInt(int& out);
// Length-prefixed unsigned decimal (element counts). Digit run capped at
// 20; overflow-guarded accumulate. Callers still apply their own
// count-vs-wire-size sanity bound BEFORE any reserve() on the result.
bool fieldSizeT(std::size_t& out);
// Length-prefixed %.17g double. Full-token strtod; trailing bytes fail.
// Deliberately NO errno/ERANGE rejection: the writers emit %.17g of live
// doubles (incl. "inf"), and those must decode back — same accept set as
// every prior copy.
bool fieldDouble(double& out);
private:
bool fail() { ok_ = false; return false; }
const std::string& s_;
std::size_t pos_ = 0;
bool ok_ = true;
};
} // namespace reasampler::wire