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:
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user