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:
+46
-2
@@ -82,12 +82,34 @@ set(SDK_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/reaper-sdk/sdk)
|
|||||||
set(WDL_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/WDL/WDL)
|
set(WDL_INC ${CMAKE_CURRENT_SOURCE_DIR}/vendor/WDL/WDL)
|
||||||
set(SWELL ${WDL_INC}/swell)
|
set(SWELL ${WDL_INC}/swell)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 0) core/ — the Q-W1 shared pure substrate. NO REAPER, NO SWELL, NO VST3.
|
||||||
|
# json: the ONE JSON lexical layer (reader + writer) behind the five
|
||||||
|
# persisted-blob (de)serializers (bank_model / bank_book /
|
||||||
|
# view_mode_model / owned_manifest / tail_control). T2-02 / §2.
|
||||||
|
# wire: the ONE length-prefixed ext-state wire codec (putField + Cursor +
|
||||||
|
# the guarded decimal accumulate) behind provenance /
|
||||||
|
# assignment_request / sample_usage / bank_sync. T2-01(b).
|
||||||
|
# file_bytes: the ONE whole-file byte loader both artifacts link. T2-03.
|
||||||
|
# Headers are included as "core/json/json.h" etc. (rooted at src/), so the
|
||||||
|
# include paths survive the Q-W1 part-2 directory relocation unchanged.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
add_library(json STATIC src/core/json/json.cpp)
|
||||||
|
target_include_directories(json PUBLIC src)
|
||||||
|
|
||||||
|
add_library(wire STATIC src/core/wire/wire.cpp)
|
||||||
|
target_include_directories(wire PUBLIC src)
|
||||||
|
|
||||||
|
add_library(file_bytes STATIC src/core/util/file_bytes.cpp)
|
||||||
|
target_include_directories(file_bytes PUBLIC src)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 1) Pure model library — NO REAPER, NO SWELL. Builds & tests anywhere.
|
# 1) Pure model library — NO REAPER, NO SWELL. Builds & tests anywhere.
|
||||||
# The sampler's heart: Sample metadata + BankIndex (Milestone 1).
|
# The sampler's heart: Sample metadata + BankIndex (Milestone 1).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(bank_model STATIC src/bank_model.cpp)
|
add_library(bank_model STATIC src/bank_model.cpp)
|
||||||
target_include_directories(bank_model PUBLIC src)
|
target_include_directories(bank_model PUBLIC src)
|
||||||
|
target_link_libraries(bank_model PRIVATE json)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2) Pure peaks library — NO REAPER, NO SWELL. Waveform min/max thumbnails from
|
# 2) Pure peaks library — NO REAPER, NO SWELL. Waveform min/max thumbnails from
|
||||||
@@ -142,6 +164,7 @@ target_include_directories(tab_strip PUBLIC src)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(view_mode_model STATIC src/view_mode_model.cpp)
|
add_library(view_mode_model STATIC src/view_mode_model.cpp)
|
||||||
target_include_directories(view_mode_model PUBLIC src)
|
target_include_directories(view_mode_model PUBLIC src)
|
||||||
|
target_link_libraries(view_mode_model PRIVATE json)
|
||||||
# The pure lane-minting decision (planLaneMinting) names managed lanes via the ONE
|
# The pure lane-minting decision (planLaneMinting) names managed lanes via the ONE
|
||||||
# durable-key convention in lane_keys (laneNameForMode), so the model depends on that
|
# durable-key convention in lane_keys (laneNameForMode), so the model depends on that
|
||||||
# pure sibling. PUBLIC so every consumer (tests + module) resolves the symbol.
|
# pure sibling. PUBLIC so every consumer (tests + module) resolves the symbol.
|
||||||
@@ -224,6 +247,7 @@ target_include_directories(batch_capture PUBLIC src)
|
|||||||
add_library(tail_control STATIC src/tail_control.cpp)
|
add_library(tail_control STATIC src/tail_control.cpp)
|
||||||
target_include_directories(tail_control PUBLIC src)
|
target_include_directories(tail_control PUBLIC src)
|
||||||
target_link_libraries(tail_control PUBLIC render_settings)
|
target_link_libraries(tail_control PUBLIC render_settings)
|
||||||
|
target_link_libraries(tail_control PRIVATE json)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2g') Pure bank_book library — NO REAPER, NO SWELL. The multi-bank phase heart
|
# 2g') Pure bank_book library — NO REAPER, NO SWELL. The multi-bank phase heart
|
||||||
@@ -236,6 +260,7 @@ target_link_libraries(tail_control PUBLIC render_settings)
|
|||||||
add_library(bank_book STATIC src/bank_book.cpp)
|
add_library(bank_book STATIC src/bank_book.cpp)
|
||||||
target_include_directories(bank_book PUBLIC src)
|
target_include_directories(bank_book PUBLIC src)
|
||||||
target_link_libraries(bank_book PUBLIC bank_model)
|
target_link_libraries(bank_book PUBLIC bank_model)
|
||||||
|
target_link_libraries(bank_book PRIVATE json)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2g'') Pure owned_manifest library — NO REAPER, NO SWELL. The owned-file manifest
|
# 2g'') Pure owned_manifest library — NO REAPER, NO SWELL. The owned-file manifest
|
||||||
@@ -248,6 +273,7 @@ target_link_libraries(bank_book PUBLIC bank_model)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(owned_manifest STATIC src/owned_manifest.cpp)
|
add_library(owned_manifest STATIC src/owned_manifest.cpp)
|
||||||
target_include_directories(owned_manifest PUBLIC src)
|
target_include_directories(owned_manifest PUBLIC src)
|
||||||
|
target_link_libraries(owned_manifest PRIVATE json)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2g''') Pure prune_reconcile library — NO REAPER, NO SWELL, NO filesystem. The
|
# 2g''') Pure prune_reconcile library — NO REAPER, NO SWELL, NO filesystem. The
|
||||||
@@ -323,6 +349,7 @@ target_include_directories(app_version PUBLIC src ${CMAKE_CURRENT_BINARY_DIR}/ge
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(provenance STATIC src/provenance.cpp)
|
add_library(provenance STATIC src/provenance.cpp)
|
||||||
target_include_directories(provenance PUBLIC src)
|
target_include_directories(provenance PUBLIC src)
|
||||||
|
target_link_libraries(provenance PRIVATE wire)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2j') Pure assignment_request library — NO REAPER, NO SWELL, NO VST3. The S8 ingest
|
# 2j') Pure assignment_request library — NO REAPER, NO SWELL, NO VST3. The S8 ingest
|
||||||
@@ -336,6 +363,7 @@ target_include_directories(provenance PUBLIC src)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(assignment_request STATIC src/assignment_request.cpp)
|
add_library(assignment_request STATIC src/assignment_request.cpp)
|
||||||
target_include_directories(assignment_request PUBLIC src)
|
target_include_directories(assignment_request PUBLIC src)
|
||||||
|
target_link_libraries(assignment_request PRIVATE wire)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2j'') Pure sample_usage library — NO REAPER, NO SWELL, NO VST3. The pS-usage seam:
|
# 2j'') Pure sample_usage library — NO REAPER, NO SWELL, NO VST3. The pS-usage seam:
|
||||||
@@ -349,6 +377,7 @@ target_include_directories(assignment_request PUBLIC src)
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
add_library(sample_usage STATIC src/sample_usage.cpp)
|
add_library(sample_usage STATIC src/sample_usage.cpp)
|
||||||
target_include_directories(sample_usage PUBLIC src)
|
target_include_directories(sample_usage PUBLIC src)
|
||||||
|
target_link_libraries(sample_usage PRIVATE wire)
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2l) Pure drag_out library — NO REAPER, NO SWELL, NO OS/OLE. The Milestone 11
|
# 2l) Pure drag_out library — NO REAPER, NO SWELL, NO OS/OLE. The Milestone 11
|
||||||
@@ -538,6 +567,20 @@ target_link_libraries(sampler_core PUBLIC peaks pitch_shift velocity_curve)
|
|||||||
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
# 3) Standalone tests for the pure modules (run without launching REAPER).
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
enable_testing()
|
enable_testing()
|
||||||
|
|
||||||
|
# core/ (Q-W1): the shared JSON lexical layer, wire codec, and file loader.
|
||||||
|
add_executable(json_tests tests/test_json.cpp)
|
||||||
|
target_link_libraries(json_tests PRIVATE json)
|
||||||
|
add_test(NAME json_tests COMMAND json_tests)
|
||||||
|
|
||||||
|
add_executable(wire_tests tests/test_wire.cpp)
|
||||||
|
target_link_libraries(wire_tests PRIVATE wire)
|
||||||
|
add_test(NAME wire_tests COMMAND wire_tests)
|
||||||
|
|
||||||
|
add_executable(file_bytes_tests tests/test_file_bytes.cpp)
|
||||||
|
target_link_libraries(file_bytes_tests PRIVATE file_bytes)
|
||||||
|
add_test(NAME file_bytes_tests COMMAND file_bytes_tests)
|
||||||
|
|
||||||
add_executable(bank_model_tests tests/test_bank_model.cpp)
|
add_executable(bank_model_tests tests/test_bank_model.cpp)
|
||||||
target_link_libraries(bank_model_tests PRIVATE bank_model)
|
target_link_libraries(bank_model_tests PRIVATE bank_model)
|
||||||
add_test(NAME bank_model_tests COMMAND bank_model_tests)
|
add_test(NAME bank_model_tests COMMAND bank_model_tests)
|
||||||
@@ -822,6 +865,7 @@ target_link_libraries(waveform_view PUBLIC editor_geometry peaks)
|
|||||||
add_library(bank_sync STATIC src/vst/bank_sync.cpp)
|
add_library(bank_sync STATIC src/vst/bank_sync.cpp)
|
||||||
target_include_directories(bank_sync PUBLIC src/vst src)
|
target_include_directories(bank_sync PUBLIC src/vst src)
|
||||||
target_link_libraries(bank_sync PUBLIC assignment_request)
|
target_link_libraries(bank_sync PUBLIC assignment_request)
|
||||||
|
target_link_libraries(bank_sync PRIVATE wire)
|
||||||
|
|
||||||
# browser_scroll (Phase S12) — PURE scroll-window + scrollbar-thumb + type-to-filter-search
|
# browser_scroll (Phase S12) — PURE scroll-window + scrollbar-thumb + type-to-filter-search
|
||||||
# geometry LAYERED over the S10 capture_browser: the visible-card window, thumb rect +
|
# geometry LAYERED over the S10 capture_browser: the visible-card window, thumb rect +
|
||||||
@@ -1039,7 +1083,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
src/card_drag.cpp
|
src/card_drag.cpp
|
||||||
src/usage_scan.cpp
|
src/usage_scan.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage)
|
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths peaks bank_grid mode_switch tab_strip view_mode_model insert_plan render_settings batch_capture tail_control realtime_record bank_book wav_trim owned_manifest prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage)
|
||||||
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reaper_reasampler PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
# OUTPUT_NAME is channel-derived (Phase V, V4): "reaper_reasampler" (stable, default) or
|
||||||
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
# "reaper_reasampler_beta" (beta). REAPER dlopen's any reaper_* module, so both channels'
|
||||||
@@ -1201,7 +1245,7 @@ if(WIN32 AND EXISTS "${VST3_SDK}/public.sdk/source/main/pluginfactory.cpp")
|
|||||||
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
sample_map capture_paths embed_strip app_version capture_browser keyboard_strip
|
||||||
waveform_view bank_sync browser_scroll note_entry param_slider
|
waveform_view bank_sync browser_scroll note_entry param_slider
|
||||||
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
theme component_geometry bank_grid trigger_seam envelope_overlay envelope_edit
|
||||||
knob_deck curve_popup master_gain sample_usage)
|
knob_deck curve_popup master_gain sample_usage file_bytes)
|
||||||
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
# SDK_INC gives reaper_vst3_interfaces.h + reaper_plugin_functions.h for the bridge;
|
||||||
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
# WDL_INC gives LICE for the editor. The VST3 SDK headers come from vst3_sdk PUBLIC.
|
||||||
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
target_include_directories(reasampler_vst PRIVATE ${SDK_INC} ${WDL_INC})
|
||||||
|
|||||||
+6
-110
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
#include "assignment_request.h"
|
#include "assignment_request.h"
|
||||||
|
|
||||||
#include <cstddef>
|
#include "core/wire/wire.h"
|
||||||
#include <limits>
|
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
@@ -11,114 +10,11 @@ namespace {
|
|||||||
|
|
||||||
constexpr const char* kMagic = "rsassign1";
|
constexpr const char* kMagic = "rsassign1";
|
||||||
|
|
||||||
// Append one length-prefixed field: <decimal-len> ':' <bytes>. Mirror of
|
// The shared core/wire codec (Q-W1, T2-01b) — the same field grammar + hardening
|
||||||
// provenance's putField so the two seams share one wire idiom.
|
// this file previously carried as its own Cursor copy. "never UB, never a
|
||||||
void putField(std::string& out, const std::string& field) {
|
// partial value" is upheld in the codec.
|
||||||
out += std::to_string(field.size());
|
using wire::putField;
|
||||||
out += ':';
|
using Cursor = wire::Cursor;
|
||||||
out += field;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor over the encoded string. All reads are bounds-checked; a short read fails
|
|
||||||
// the whole parse (ok_ latches false). Mirror of provenance's Cursor, trimmed to the
|
|
||||||
// three field kinds this record needs.
|
|
||||||
class Cursor {
|
|
||||||
public:
|
|
||||||
explicit Cursor(const std::string& s) : s_(s) {}
|
|
||||||
|
|
||||||
bool ok() const { return ok_; }
|
|
||||||
bool atEnd() const { return pos_ >= s_.size(); }
|
|
||||||
|
|
||||||
// Reads one length-prefixed field into `out`. Fails on a missing ':', an empty or
|
|
||||||
// non-numeric length, a length that overflows SIZE_MAX, or a length that runs past
|
|
||||||
// the end. The digit count is capped at 20 (the decimal width of SIZE_MAX on a
|
|
||||||
// 64-bit host) so a crafted 200-digit length cannot accumulate past SIZE_MAX via
|
|
||||||
// repeated multiply. "never UB" promise from the header is upheld here.
|
|
||||||
bool 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;
|
|
||||||
// Guard: start may equal s_.size() (empty remainder), in which case only len==0
|
|
||||||
// is valid; start > s_.size() cannot happen (colon < s_.size() by find()).
|
|
||||||
// Use subtraction-first form to avoid start+len wrapping on a huge len.
|
|
||||||
if (start > s_.size() || len > s_.size() - start) return fail();
|
|
||||||
out.assign(s_, start, len);
|
|
||||||
pos_ = start + len;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads a length-prefixed field and parses it as a signed 64-bit decimal (an
|
|
||||||
// optional leading '-'). Fails on empty, non-digit, trailing bytes, or a value
|
|
||||||
// that would overflow INT64_MAX / underflow INT64_MIN. The digit count is capped
|
|
||||||
// at 19 (the decimal width of INT64_MAX, plus 1 for the optional sign = 20
|
|
||||||
// characters maximum) so a crafted 21-digit field cannot accumulate UB. "never UB"
|
|
||||||
// promise from the header is upheld: all arithmetic is done on positive digits
|
|
||||||
// and capped before applying the sign.
|
|
||||||
bool 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 = -9223372036854775808) but we conservatively reject it too: the
|
|
||||||
// generation field is a unix timestamp, never near INT64 limits in practice.
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consumes an exact literal at the cursor (the magic tag). Fails if absent.
|
|
||||||
bool 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool fail() {
|
|
||||||
ok_ = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
bool ok_ = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
+68
-306
@@ -1,14 +1,14 @@
|
|||||||
#include "bank_book.h"
|
#include "bank_book.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include "core/json/json.h"
|
||||||
|
|
||||||
// bank_book implementation.
|
// bank_book implementation.
|
||||||
//
|
//
|
||||||
// JSON is hand-rolled and self-contained, matching the house style of bank_model
|
// JSON rides on the shared core/json lexical layer (Q-W1), matching bank_model
|
||||||
// and view_mode_model (brief: keep the pure core dependency-free — no third-party
|
// and view_mode_model. The book blob nests one bank object per bank, each carrying that
|
||||||
// JSON lib). The book blob nests one bank object per bank, each carrying that
|
|
||||||
// bank's BankIndex serialized by bank_model's OWN writer (BankIndex::serialize),
|
// bank's BankIndex serialized by bank_model's OWN writer (BankIndex::serialize),
|
||||||
// so per-bank sample serialization stays owned by bank_model and is not duplicated
|
// so per-bank sample serialization stays owned by bank_model and is not duplicated
|
||||||
// here. The book writer emits the bank envelope (id / displayName / ordinal) plus a
|
// here. The book writer emits the bank envelope (id / displayName / ordinal) plus a
|
||||||
@@ -138,7 +138,7 @@ SlotMap SlotMap::fromEntries(const std::vector<std::pair<std::string, int>>& pai
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SlotMap::serialize is defined in the JSON writer section below (it reuses the
|
// SlotMap::serialize is defined in the JSON writer section below (it reuses the
|
||||||
// file-local ObjWriter / intToStr helpers).
|
// shared core/json emit helpers).
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// BankBook — construction + bank lookup
|
// BankBook — construction + bank lookup
|
||||||
@@ -550,64 +550,10 @@ std::vector<std::string> BankBook::referencedPaths() const {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
void writeEscaped(std::string& out, const std::string& s) {
|
// Shared core/json emit helpers (Q-W1): the same escape set + %d rendering the
|
||||||
out += '"';
|
// prior file-local writer carried, so the emitted blob is byte-identical.
|
||||||
for (char c : s) {
|
std::string intToStr(int v) { return json::numToStr(v); }
|
||||||
switch (c) {
|
using ObjWriter = json::Writer;
|
||||||
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 intToStr(int v) {
|
|
||||||
char buf[16];
|
|
||||||
std::snprintf(buf, sizeof(buf), "%d", v);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObjWriter {
|
|
||||||
public:
|
|
||||||
explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; }
|
|
||||||
~ObjWriter() { out_ += '}'; }
|
|
||||||
|
|
||||||
void keyRaw(const char* key, const std::string& rawValue) {
|
|
||||||
sep();
|
|
||||||
writeEscaped(out_, key);
|
|
||||||
out_ += ':';
|
|
||||||
out_ += rawValue;
|
|
||||||
}
|
|
||||||
void keyStr(const char* key, const std::string& value) {
|
|
||||||
sep();
|
|
||||||
writeEscaped(out_, key);
|
|
||||||
out_ += ':';
|
|
||||||
writeEscaped(out_, value);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -660,223 +606,38 @@ std::string BankBook::serialize() const {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class Parser {
|
// The book DOMAIN grammar over the shared core/json lexical layer (Q-W1).
|
||||||
public:
|
// parseBank parses one bank object; parseSlots the "slots" array ([{id, slot},
|
||||||
explicit Parser(const std::string& s) : s_(s) {}
|
// ...]) into (id, slot) pairs (empty array valid; the pair-level defensive
|
||||||
|
// repair — dupes/conflicts — lives in SlotMap::fromEntries); parseBook the root
|
||||||
|
// blob, distinguishing the legacy shape (a bare bank_index object: has
|
||||||
|
// "samples", no "banks") from the book shape (has "banks"): a legacy blob
|
||||||
|
// yields a single pool bank carrying the migrated index and an empty active id
|
||||||
|
// (⇒ pool). The member deserialize() adopts the result (ordinal normalize +
|
||||||
|
// active resolve).
|
||||||
|
bool parseSlots(json::Reader& r, std::vector<std::pair<std::string, int>>& out);
|
||||||
|
|
||||||
// Parses a book blob into a bank set + active id. On success fills the out-params
|
bool parseBank(json::Reader& r, Bank& b) {
|
||||||
// and returns true. Distinguishes the legacy shape (a bare bank_index object: has
|
if (!r.consume('{')) return false;
|
||||||
// "samples", no "banks") from the book shape (has "banks"): a legacy blob yields a
|
r.skipWs();
|
||||||
// single pool bank carrying the migrated index and an empty active id (⇒ pool). The
|
if (r.consume('}')) return false; // a bank object must at least carry an id
|
||||||
// member deserialize() adopts the result (ordinal normalize + active resolve).
|
|
||||||
bool parseBook(std::vector<Bank>& banks, std::string& activeBank);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
|
|
||||||
bool eof() const { return pos_ >= s_.size(); }
|
|
||||||
|
|
||||||
void skipWs() {
|
|
||||||
while (!eof()) {
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool consume(char c) {
|
|
||||||
skipWs();
|
|
||||||
if (eof() || s_[pos_] != c) return false;
|
|
||||||
++pos_;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseString(std::string& out);
|
|
||||||
bool parseInt(int& out);
|
|
||||||
bool parseKey(std::string& key);
|
|
||||||
bool skipValue();
|
|
||||||
// Captures the raw source text of one JSON value (object / array / string /
|
|
||||||
// scalar) verbatim, so a nested BankIndex blob can be handed to its own parser.
|
|
||||||
bool captureValue(std::string& raw);
|
|
||||||
|
|
||||||
bool parseBank(Bank& out);
|
|
||||||
// Parses the "slots" array ([{id, slot}, ...]) into (id, slot) pairs. An empty
|
|
||||||
// array is valid (an empty bank). Malformed structure fails the whole parse; the
|
|
||||||
// pair-level defensive repair (dupes/conflicts) lives in SlotMap::fromEntries.
|
|
||||||
bool parseSlots(std::vector<std::pair<std::string, int>>& out);
|
|
||||||
};
|
|
||||||
|
|
||||||
bool Parser::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': {
|
|
||||||
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) {
|
|
||||||
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;
|
|
||||||
codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
|
|
||||||
} else if (hi >= 0xDC00 && hi <= 0xDFFF) {
|
|
||||||
return false; // unpaired low surrogate
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::parseInt(int& out) {
|
|
||||||
skipWs();
|
|
||||||
std::size_t start = pos_;
|
|
||||||
if (!eof() && (s_[pos_] == '-' || s_[pos_] == '+')) ++pos_;
|
|
||||||
std::size_t digitsStart = pos_;
|
|
||||||
while (!eof() && s_[pos_] >= '0' && s_[pos_] <= '9') ++pos_;
|
|
||||||
if (pos_ == digitsStart) return false; // no digits
|
|
||||||
long v = 0;
|
|
||||||
try {
|
|
||||||
v = std::stol(s_.substr(start, pos_ - start));
|
|
||||||
} catch (...) {
|
|
||||||
return false; // out of long range → malformed
|
|
||||||
}
|
|
||||||
if (v < INT_MIN || v > INT_MAX) return false;
|
|
||||||
out = static_cast<int>(v);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::parseKey(std::string& key) {
|
|
||||||
if (!parseString(key)) return false;
|
|
||||||
if (!consume(':')) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::skipValue() {
|
|
||||||
std::string raw;
|
|
||||||
return captureValue(raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Records the raw source span of one JSON value starting at the current position
|
|
||||||
// (after whitespace) so it can be re-parsed by a nested parser. Handles nested
|
|
||||||
// objects/arrays with string-aware brace matching (braces inside strings ignored).
|
|
||||||
bool Parser::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)
|
|
||||||
while (!eof()) {
|
|
||||||
char d = s_[pos_];
|
|
||||||
if (d == ',' || d == '}' || d == ']' || d == ' ' || d == '\t' ||
|
|
||||||
d == '\n' || d == '\r')
|
|
||||||
break;
|
|
||||||
++pos_;
|
|
||||||
}
|
|
||||||
if (pos_ == start) return false;
|
|
||||||
raw.assign(s_, start, pos_ - start);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::parseBank(Bank& b) {
|
|
||||||
if (!consume('{')) return false;
|
|
||||||
skipWs();
|
|
||||||
if (consume('}')) return false; // a bank object must at least carry an id
|
|
||||||
|
|
||||||
bool haveId = false;
|
bool haveId = false;
|
||||||
bool haveIndex = false;
|
bool haveIndex = false;
|
||||||
do {
|
do {
|
||||||
std::string key;
|
std::string key;
|
||||||
if (!parseKey(key)) return false;
|
if (!r.parseKey(key)) return false;
|
||||||
|
|
||||||
if (key == "id") {
|
if (key == "id") {
|
||||||
if (!parseString(b.id)) return false;
|
if (!r.parseString(b.id)) return false;
|
||||||
haveId = true;
|
haveId = true;
|
||||||
} else if (key == "displayName") {
|
} else if (key == "displayName") {
|
||||||
if (!parseString(b.displayName)) return false;
|
if (!r.parseString(b.displayName)) return false;
|
||||||
} else if (key == "ordinal") {
|
} else if (key == "ordinal") {
|
||||||
if (!parseInt(b.ordinal)) return false;
|
if (!r.parseInt(b.ordinal)) return false;
|
||||||
} else if (key == "index") {
|
} else if (key == "index") {
|
||||||
std::string raw;
|
std::string raw;
|
||||||
if (!captureValue(raw)) return false;
|
if (!r.captureValue(raw)) return false;
|
||||||
auto idx = BankIndex::deserialize(raw);
|
auto idx = BankIndex::deserialize(raw);
|
||||||
if (!idx) return false; // a malformed nested index fails the whole parse
|
if (!idx) return false; // a malformed nested index fails the whole parse
|
||||||
b.index = std::move(*idx);
|
b.index = std::move(*idx);
|
||||||
@@ -886,49 +647,50 @@ bool Parser::parseBank(Bank& b) {
|
|||||||
// nothing because the key never appears); when present it drives the
|
// nothing because the key never appears); when present it drives the
|
||||||
// bank's SlotMap. reconcileSlots() (post-adopt) squares it with membership.
|
// bank's SlotMap. reconcileSlots() (post-adopt) squares it with membership.
|
||||||
std::vector<std::pair<std::string, int>> pairs;
|
std::vector<std::pair<std::string, int>> pairs;
|
||||||
if (!parseSlots(pairs)) return false;
|
if (!parseSlots(r, pairs)) return false;
|
||||||
b.slots = SlotMap::fromEntries(pairs);
|
b.slots = SlotMap::fromEntries(pairs);
|
||||||
} else {
|
} else {
|
||||||
if (!skipValue()) return false; // forward-compat unknown keys
|
if (!r.skipValue()) return false; // forward-compat unknown keys
|
||||||
}
|
}
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
|
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
if (!haveId || b.id.empty()) return false; // id keys the registry
|
if (!haveId || b.id.empty()) return false; // id keys the registry
|
||||||
if (!haveIndex) return false; // every bank persists its index
|
if (!haveIndex) return false; // every bank persists its index
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseSlots(std::vector<std::pair<std::string, int>>& out) {
|
bool parseSlots(json::Reader& r, std::vector<std::pair<std::string, int>>& out) {
|
||||||
out.clear();
|
out.clear();
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume(']')) return true; // empty slot array — a bank with no positions yet
|
if (r.consume(']')) return true; // empty slot array — a bank with no positions yet
|
||||||
do {
|
do {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
std::string id;
|
std::string id;
|
||||||
int slot = 0;
|
int slot = 0;
|
||||||
bool haveId = false, haveSlot = false;
|
bool haveId = false, haveSlot = false;
|
||||||
do {
|
do {
|
||||||
std::string k;
|
std::string k;
|
||||||
if (!parseKey(k)) return false;
|
if (!r.parseKey(k)) return false;
|
||||||
if (k == "id") { if (!parseString(id)) return false; haveId = true; }
|
if (k == "id") { if (!r.parseString(id)) return false; haveId = true; }
|
||||||
else if (k == "slot") { if (!parseInt(slot)) return false; haveSlot = true; }
|
else if (k == "slot") { if (!r.parseInt(slot)) return false; haveSlot = true; }
|
||||||
else { if (!skipValue()) return false; } // forward-compat
|
else { if (!r.skipValue()) return false; } // forward-compat
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
if (!haveId || !haveSlot) return false; // a slot entry needs both
|
if (!haveId || !haveSlot) return false; // a slot entry needs both
|
||||||
out.emplace_back(std::move(id), slot);
|
out.emplace_back(std::move(id), slot);
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
return consume(']');
|
return r.consume(']');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseBook(std::vector<Bank>& banks, std::string& activeBank) {
|
bool parseBook(json::Reader& r, const std::string& raw, std::vector<Bank>& banks,
|
||||||
|
std::string& activeBank) {
|
||||||
banks.clear();
|
banks.clear();
|
||||||
activeBank.clear();
|
activeBank.clear();
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume('}')) return false; // an empty object is neither shape → malformed
|
if (r.consume('}')) return false; // an empty object is neither shape → malformed
|
||||||
|
|
||||||
// Decide the shape by which structural key we saw. A "banks" key ⇒ book shape; a
|
// Decide the shape by which structural key we saw. A "banks" key ⇒ book shape; a
|
||||||
// "samples" key with no "banks" ⇒ legacy shape (promote into the pool).
|
// "samples" key with no "banks" ⇒ legacy shape (promote into the pool).
|
||||||
@@ -938,41 +700,41 @@ bool Parser::parseBook(std::vector<Bank>& banks, std::string& activeBank) {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
std::string key;
|
std::string key;
|
||||||
if (!parseKey(key)) return false;
|
if (!r.parseKey(key)) return false;
|
||||||
|
|
||||||
if (key == "banks") {
|
if (key == "banks") {
|
||||||
sawBanks = true;
|
sawBanks = true;
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!consume(']')) {
|
if (!r.consume(']')) {
|
||||||
do {
|
do {
|
||||||
Bank b;
|
Bank b;
|
||||||
if (!parseBank(b)) return false;
|
if (!parseBank(r, b)) return false;
|
||||||
parsedBanks.push_back(std::move(b));
|
parsedBanks.push_back(std::move(b));
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume(']')) return false;
|
if (!r.consume(']')) return false;
|
||||||
}
|
}
|
||||||
} else if (key == "activeBank") {
|
} else if (key == "activeBank") {
|
||||||
if (!parseString(activeBank)) return false;
|
if (!r.parseString(activeBank)) return false;
|
||||||
} else if (key == "samples") {
|
} else if (key == "samples") {
|
||||||
// Legacy marker. The legacy index is re-parsed from the whole input below
|
// Legacy marker. The legacy index is re-parsed from the whole input below
|
||||||
// (BankIndex::deserialize owns that shape); here we only skip the value to
|
// (BankIndex::deserialize owns that shape); here we only skip the value to
|
||||||
// keep the scan well-formed and note that we saw it.
|
// keep the scan well-formed and note that we saw it.
|
||||||
sawSamples = true;
|
sawSamples = true;
|
||||||
if (!skipValue()) return false;
|
if (!r.skipValue()) return false;
|
||||||
} else {
|
} else {
|
||||||
if (!skipValue()) return false; // version, or unknown
|
if (!r.skipValue()) return false; // version, or unknown
|
||||||
}
|
}
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
|
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!eof()) return false; // trailing garbage
|
if (!r.eof()) return false; // trailing garbage
|
||||||
|
|
||||||
// --- Legacy migration: a bare bank_index (samples, no banks) → pool. ---
|
// --- Legacy migration: a bare bank_index (samples, no banks) → pool. ---
|
||||||
if (!sawBanks) {
|
if (!sawBanks) {
|
||||||
if (!sawSamples) return false; // neither shape's marker → malformed
|
if (!sawSamples) return false; // neither shape's marker → malformed
|
||||||
auto legacy = BankIndex::deserialize(s_);
|
auto legacy = BankIndex::deserialize(raw);
|
||||||
if (!legacy) return false;
|
if (!legacy) return false;
|
||||||
Bank pool;
|
Bank pool;
|
||||||
pool.id = kPoolBankId;
|
pool.id = kPoolBankId;
|
||||||
@@ -1059,11 +821,11 @@ void BankBook::adoptBanks(std::vector<Bank>&& banks, const std::string& activeBa
|
|||||||
activeBankId_ = (bank(activeBank) != nullptr) ? activeBank : std::string(kPoolBankId);
|
activeBankId_ = (bank(activeBank) != nullptr) ? activeBank : std::string(kPoolBankId);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BankBook> BankBook::deserialize(const std::string& json) {
|
std::optional<BankBook> BankBook::deserialize(const std::string& blob) {
|
||||||
std::vector<Bank> banks;
|
std::vector<Bank> banks;
|
||||||
std::string activeBank;
|
std::string activeBank;
|
||||||
Parser p(json);
|
json::Reader r(blob);
|
||||||
if (!p.parseBook(banks, activeBank)) return std::nullopt;
|
if (!parseBook(r, blob, banks, activeBank)) return std::nullopt;
|
||||||
|
|
||||||
BankBook book;
|
BankBook book;
|
||||||
book.adoptBanks(std::move(banks), activeBank);
|
book.adoptBanks(std::move(banks), activeBank);
|
||||||
|
|||||||
+89
-395
@@ -1,17 +1,15 @@
|
|||||||
#include "bank_model.h"
|
#include "bank_model.h"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cerrno>
|
|
||||||
#include <cstdio>
|
#include "core/json/json.h"
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
// bank_model implementation.
|
// bank_model implementation.
|
||||||
//
|
//
|
||||||
// JSON is hand-rolled and self-contained (brief: keep the pure core
|
// JSON rides on the shared core/json lexical layer (Q-W1: one reader/writer,
|
||||||
// dependency-free — no third-party JSON lib, no WDL coupling). The field set is
|
// no per-module Parser copy). The field set is a flat struct of primitives,
|
||||||
// a flat struct of primitives, strings, one enum, a small string array, and a
|
// strings, one enum, a small string array, and a few optionals, so a compact
|
||||||
// few optionals, so a compact writer + recursive-descent parser is the simplest
|
// writer + recursive-descent DOMAIN parser over json::Reader is the simplest
|
||||||
// thing that works. Doubles are emitted with 17 significant digits (%.17g), the
|
// thing that works. Doubles are emitted with 17 significant digits (%.17g), the
|
||||||
// shortest form that round-trips every IEEE-754 double exactly, so the
|
// shortest form that round-trips every IEEE-754 double exactly, so the
|
||||||
// deserialize(serialize(x)) == x invariant holds bit-for-bit.
|
// deserialize(serialize(x)) == x invariant holds bit-for-bit.
|
||||||
@@ -147,84 +145,10 @@ std::vector<Sample> BankIndex::byTier(Tier tier) const {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
void writeEscaped(std::string& out, const std::string& s) {
|
using json::numToStr;
|
||||||
out += '"';
|
using json::writeEscaped;
|
||||||
for (char c : s) {
|
using json::writeStringArray;
|
||||||
switch (c) {
|
using ObjWriter = json::Writer;
|
||||||
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) { return numToStr(static_cast<std::int64_t>(v)); }
|
|
||||||
|
|
||||||
class ObjWriter {
|
|
||||||
public:
|
|
||||||
explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; }
|
|
||||||
~ObjWriter() { out_ += '}'; }
|
|
||||||
|
|
||||||
void keyRaw(const char* key, const std::string& rawValue) {
|
|
||||||
sep();
|
|
||||||
writeEscaped(out_, key);
|
|
||||||
out_ += ':';
|
|
||||||
out_ += rawValue;
|
|
||||||
}
|
|
||||||
void keyStr(const char* key, const std::string& value) {
|
|
||||||
sep();
|
|
||||||
writeEscaped(out_, key);
|
|
||||||
out_ += ':';
|
|
||||||
writeEscaped(out_, value);
|
|
||||||
}
|
|
||||||
// Begin a nested value; 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 writeSample(std::string& out, const Sample& s) {
|
void writeSample(std::string& out, const Sample& s) {
|
||||||
ObjWriter w(out);
|
ObjWriter w(out);
|
||||||
@@ -317,346 +241,116 @@ std::string BankIndex::serialize() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JSON parser (recursive descent). Returns false on any malformed input; never
|
// JSON parser (recursive descent over the shared json::Reader). Returns false
|
||||||
// reads out of bounds. Only supports the subset our writer emits.
|
// on any malformed input; never reads out of bounds. Only supports the subset
|
||||||
|
// our writer emits. The lexical layer (strings, numbers, skip) lives in
|
||||||
|
// core/json; only the Sample/index DOMAIN grammar lives here.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class Parser {
|
bool parseSample(json::Reader& r, Sample& s) {
|
||||||
public:
|
if (!r.consume('{')) return false;
|
||||||
explicit Parser(const std::string& s) : s_(s) {}
|
r.skipWs();
|
||||||
|
if (r.consume('}')) return true; // empty object (shouldn't happen, but valid)
|
||||||
bool parseIndex(BankIndex& out);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
|
|
||||||
bool eof() const { return pos_ >= s_.size(); }
|
|
||||||
char peek() const { return s_[pos_]; }
|
|
||||||
|
|
||||||
void skipWs() {
|
|
||||||
while (!eof()) {
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool consume(char c) {
|
|
||||||
skipWs();
|
|
||||||
if (eof() || s_[pos_] != c) return false;
|
|
||||||
++pos_;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseString(std::string& out);
|
|
||||||
bool parseRawScalar(std::string& out); // number / true / false / null token
|
|
||||||
bool parseDouble(double& out);
|
|
||||||
bool parseInt64(std::int64_t& out);
|
|
||||||
bool parseInt(int& out);
|
|
||||||
bool parseBool(bool& out);
|
|
||||||
bool expectNullOr(bool& wasNull); // peeks for `null`; consumes if present
|
|
||||||
|
|
||||||
bool parseSample(Sample& out);
|
|
||||||
bool parseKey(std::string& key); // an object member key + ':'
|
|
||||||
bool skipValue(); // for forward-compat unknown keys
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parses a JSON string literal (with the escapes our writer emits, plus \uXXXX
|
|
||||||
// for control chars). Positioned at the opening quote after whitespace.
|
|
||||||
bool Parser::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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reads a bare token (number, true, false, null) up to the next structural char.
|
|
||||||
bool Parser::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 Parser::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 Parser::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 Parser::parseInt(int& out) {
|
|
||||||
std::int64_t v = 0;
|
|
||||||
if (!parseInt64(v)) return false;
|
|
||||||
out = static_cast<int>(v);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the next value is the `null` token, consumes it and sets wasNull=true.
|
|
||||||
// Otherwise leaves the position untouched and sets wasNull=false. Returns false
|
|
||||||
// only on eof.
|
|
||||||
bool Parser::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 Parser::parseKey(std::string& key) {
|
|
||||||
if (!parseString(key)) return false;
|
|
||||||
if (!consume(':')) return false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skips one JSON value (object / array / string / scalar) for forward-compat
|
|
||||||
// with keys we don't recognize. Assumes position is at the start of the value.
|
|
||||||
bool Parser::skipValue() {
|
|
||||||
skipWs();
|
|
||||||
if (eof()) return false;
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == '"') {
|
|
||||||
std::string tmp;
|
|
||||||
return parseString(tmp);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (d == open) ++depth;
|
|
||||||
else if (d == close) --depth;
|
|
||||||
++pos_;
|
|
||||||
}
|
|
||||||
return depth == 0;
|
|
||||||
}
|
|
||||||
std::string tmp;
|
|
||||||
return parseRawScalar(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::parseSample(Sample& s) {
|
|
||||||
if (!consume('{')) return false;
|
|
||||||
skipWs();
|
|
||||||
if (consume('}')) return true; // empty object (shouldn't happen, but valid)
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
std::string key;
|
std::string key;
|
||||||
if (!parseKey(key)) return false;
|
if (!r.parseKey(key)) return false;
|
||||||
|
|
||||||
if (key == "id") {
|
if (key == "id") {
|
||||||
if (!parseString(s.id)) return false;
|
if (!r.parseString(s.id)) return false;
|
||||||
} else if (key == "displayName") {
|
} else if (key == "displayName") {
|
||||||
if (!parseString(s.displayName)) return false;
|
if (!r.parseString(s.displayName)) return false;
|
||||||
} else if (key == "relativePath") {
|
} else if (key == "relativePath") {
|
||||||
if (!parseString(s.relativePath)) return false;
|
if (!r.parseString(s.relativePath)) return false;
|
||||||
} else if (key == "sourceMode") {
|
} else if (key == "sourceMode") {
|
||||||
int v = 0;
|
int v = 0;
|
||||||
if (!parseInt(v)) return false;
|
if (!r.parseInt(v)) return false;
|
||||||
// Valid range: MasterMix(0) .. Realtime(5).
|
// Valid range: MasterMix(0) .. Realtime(5).
|
||||||
if (v < static_cast<int>(SourceMode::MasterMix) ||
|
if (v < static_cast<int>(SourceMode::MasterMix) ||
|
||||||
v > static_cast<int>(SourceMode::Realtime))
|
v > static_cast<int>(SourceMode::Realtime))
|
||||||
return false;
|
return false;
|
||||||
s.sourceMode = static_cast<SourceMode>(v);
|
s.sourceMode = static_cast<SourceMode>(v);
|
||||||
} else if (key == "sourceRange") {
|
} else if (key == "sourceRange") {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
do {
|
do {
|
||||||
std::string rk;
|
std::string rk;
|
||||||
if (!parseKey(rk)) return false;
|
if (!r.parseKey(rk)) return false;
|
||||||
double dv = 0.0;
|
double dv = 0.0;
|
||||||
if (!parseDouble(dv)) return false;
|
if (!r.parseDouble(dv)) return false;
|
||||||
if (rk == "startSeconds") s.sourceRange.startSeconds = dv;
|
if (rk == "startSeconds") s.sourceRange.startSeconds = dv;
|
||||||
else if (rk == "endSeconds") s.sourceRange.endSeconds = dv;
|
else if (rk == "endSeconds") s.sourceRange.endSeconds = dv;
|
||||||
else if (rk == "startPpq") s.sourceRange.startPpq = dv;
|
else if (rk == "startPpq") s.sourceRange.startPpq = dv;
|
||||||
else if (rk == "endPpq") s.sourceRange.endPpq = dv;
|
else if (rk == "endPpq") s.sourceRange.endPpq = dv;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
} else if (key == "trackGuids") {
|
} else if (key == "trackGuids") {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!consume(']')) {
|
if (!r.consume(']')) {
|
||||||
do {
|
do {
|
||||||
std::string g;
|
std::string g;
|
||||||
if (!parseString(g)) return false;
|
if (!r.parseString(g)) return false;
|
||||||
s.trackGuids.push_back(g);
|
s.trackGuids.push_back(g);
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume(']')) return false;
|
if (!r.consume(']')) return false;
|
||||||
}
|
}
|
||||||
} else if (key == "wetDry") {
|
} else if (key == "wetDry") {
|
||||||
if (!parseDouble(s.wetDry)) return false;
|
if (!r.parseDouble(s.wetDry)) return false;
|
||||||
} else if (key == "channelCount") {
|
} else if (key == "channelCount") {
|
||||||
if (!parseInt(s.channelCount)) return false;
|
if (!r.parseInt(s.channelCount)) return false;
|
||||||
} else if (key == "sampleRate") {
|
} else if (key == "sampleRate") {
|
||||||
if (!parseInt(s.sampleRate)) return false;
|
if (!r.parseInt(s.sampleRate)) return false;
|
||||||
} else if (key == "lengthSeconds") {
|
} else if (key == "lengthSeconds") {
|
||||||
if (!parseDouble(s.lengthSeconds)) return false;
|
if (!r.parseDouble(s.lengthSeconds)) return false;
|
||||||
} else if (key == "lengthBeats") {
|
} else if (key == "lengthBeats") {
|
||||||
if (!parseDouble(s.lengthBeats)) return false;
|
if (!r.parseDouble(s.lengthBeats)) return false;
|
||||||
} else if (key == "captureTempo") {
|
} else if (key == "captureTempo") {
|
||||||
if (!parseDouble(s.captureTempo)) return false;
|
if (!r.parseDouble(s.captureTempo)) return false;
|
||||||
} else if (key == "captureTimeSigNum") {
|
} else if (key == "captureTimeSigNum") {
|
||||||
if (!parseInt(s.captureTimeSigNum)) return false;
|
if (!r.parseInt(s.captureTimeSigNum)) return false;
|
||||||
} else if (key == "captureTimeSigDenom") {
|
} else if (key == "captureTimeSigDenom") {
|
||||||
if (!parseInt(s.captureTimeSigDenom)) return false;
|
if (!r.parseInt(s.captureTimeSigDenom)) return false;
|
||||||
} else if (key == "key") {
|
} else if (key == "key") {
|
||||||
bool wasNull = false;
|
bool wasNull = false;
|
||||||
if (!expectNullOr(wasNull)) return false;
|
if (!r.expectNullOr(wasNull)) return false;
|
||||||
if (wasNull) {
|
if (wasNull) {
|
||||||
s.key.reset();
|
s.key.reset();
|
||||||
} else {
|
} else {
|
||||||
std::string k;
|
std::string k;
|
||||||
if (!parseString(k)) return false;
|
if (!r.parseString(k)) return false;
|
||||||
s.key = k;
|
s.key = k;
|
||||||
}
|
}
|
||||||
} else if (key == "rootNote") {
|
} else if (key == "rootNote") {
|
||||||
bool wasNull = false;
|
bool wasNull = false;
|
||||||
if (!expectNullOr(wasNull)) return false;
|
if (!r.expectNullOr(wasNull)) return false;
|
||||||
if (wasNull) {
|
if (wasNull) {
|
||||||
s.rootNote.reset();
|
s.rootNote.reset();
|
||||||
} else {
|
} else {
|
||||||
int v = 0;
|
int v = 0;
|
||||||
if (!parseInt(v)) return false;
|
if (!r.parseInt(v)) return false;
|
||||||
// Valid MIDI note range: 0..127 inclusive (boundaries valid).
|
// Valid MIDI note range: 0..127 inclusive (boundaries valid).
|
||||||
if (v < 0 || v > 127) return false;
|
if (v < 0 || v > 127) return false;
|
||||||
s.rootNote = v;
|
s.rootNote = v;
|
||||||
}
|
}
|
||||||
} else if (key == "loop") {
|
} else if (key == "loop") {
|
||||||
bool wasNull = false;
|
bool wasNull = false;
|
||||||
if (!expectNullOr(wasNull)) return false;
|
if (!r.expectNullOr(wasNull)) return false;
|
||||||
if (wasNull) {
|
if (wasNull) {
|
||||||
s.loop.reset();
|
s.loop.reset();
|
||||||
} else {
|
} else {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
LoopPoints lp;
|
LoopPoints lp;
|
||||||
do {
|
do {
|
||||||
std::string lk;
|
std::string lk;
|
||||||
if (!parseKey(lk)) return false;
|
if (!r.parseKey(lk)) return false;
|
||||||
std::int64_t lv = 0;
|
std::int64_t lv = 0;
|
||||||
if (!parseInt64(lv)) return false;
|
if (!r.parseInt64(lv)) return false;
|
||||||
if (lk == "start") lp.start = lv;
|
if (lk == "start") lp.start = lv;
|
||||||
else if (lk == "end") lp.end = lv;
|
else if (lk == "end") lp.end = lv;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
// Invariant: 0 <= start <= end. start == end is a valid zero-length
|
// Invariant: 0 <= start <= end. start == end is a valid zero-length
|
||||||
// marker; a negative index or start > end is malformed, not silently
|
// marker; a negative index or start > end is malformed, not silently
|
||||||
// clamped (mirrors the enum-range rejection above).
|
// clamped (mirrors the enum-range rejection above).
|
||||||
@@ -664,89 +358,89 @@ bool Parser::parseSample(Sample& s) {
|
|||||||
s.loop = lp;
|
s.loop = lp;
|
||||||
}
|
}
|
||||||
} else if (key == "levels") {
|
} else if (key == "levels") {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
do {
|
do {
|
||||||
std::string lk;
|
std::string lk;
|
||||||
if (!parseKey(lk)) return false;
|
if (!r.parseKey(lk)) return false;
|
||||||
double dv = 0.0;
|
double dv = 0.0;
|
||||||
if (!parseDouble(dv)) return false;
|
if (!r.parseDouble(dv)) return false;
|
||||||
if (lk == "peakDb") s.levels.peakDb = dv;
|
if (lk == "peakDb") s.levels.peakDb = dv;
|
||||||
else if (lk == "rmsDb") s.levels.rmsDb = dv;
|
else if (lk == "rmsDb") s.levels.rmsDb = dv;
|
||||||
else if (lk == "lufs") s.levels.lufs = dv;
|
else if (lk == "lufs") s.levels.lufs = dv;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
} else if (key == "clipped") {
|
} else if (key == "clipped") {
|
||||||
if (!parseBool(s.clipped)) return false;
|
if (!r.parseBool(s.clipped)) return false;
|
||||||
} else if (key == "tier") {
|
} else if (key == "tier") {
|
||||||
int v = 0;
|
int v = 0;
|
||||||
if (!parseInt(v)) return false;
|
if (!r.parseInt(v)) return false;
|
||||||
// Valid range: Scratch(0) .. Archive(1).
|
// Valid range: Scratch(0) .. Archive(1).
|
||||||
if (v < static_cast<int>(Tier::Scratch) ||
|
if (v < static_cast<int>(Tier::Scratch) ||
|
||||||
v > static_cast<int>(Tier::Archive))
|
v > static_cast<int>(Tier::Archive))
|
||||||
return false;
|
return false;
|
||||||
s.tier = static_cast<Tier>(v);
|
s.tier = static_cast<Tier>(v);
|
||||||
} else if (key == "contentHash") {
|
} else if (key == "contentHash") {
|
||||||
if (!parseString(s.contentHash)) return false;
|
if (!r.parseString(s.contentHash)) return false;
|
||||||
} else if (key == "provenance") {
|
} else if (key == "provenance") {
|
||||||
bool wasNull = false;
|
bool wasNull = false;
|
||||||
if (!expectNullOr(wasNull)) return false;
|
if (!r.expectNullOr(wasNull)) return false;
|
||||||
if (wasNull) {
|
if (wasNull) {
|
||||||
s.provenance.reset();
|
s.provenance.reset();
|
||||||
} else {
|
} else {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
Provenance p;
|
Provenance p;
|
||||||
do {
|
do {
|
||||||
std::string pk;
|
std::string pk;
|
||||||
if (!parseKey(pk)) return false;
|
if (!r.parseKey(pk)) return false;
|
||||||
std::string pv;
|
std::string pv;
|
||||||
if (!parseString(pv)) return false;
|
if (!r.parseString(pv)) return false;
|
||||||
if (pk == "parentSampleId") p.parentSampleId = pv;
|
if (pk == "parentSampleId") p.parentSampleId = pv;
|
||||||
else if (pk == "fxChainSnapshot") p.fxChainSnapshot = pv;
|
else if (pk == "fxChainSnapshot") p.fxChainSnapshot = pv;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
s.provenance = p;
|
s.provenance = p;
|
||||||
}
|
}
|
||||||
} else if (key == "createdTimestamp") {
|
} else if (key == "createdTimestamp") {
|
||||||
if (!parseInt64(s.createdTimestamp)) return false;
|
if (!r.parseInt64(s.createdTimestamp)) return false;
|
||||||
} else {
|
} else {
|
||||||
if (!skipValue()) return false; // forward-compat: ignore unknown
|
if (!r.skipValue()) return false; // forward-compat: ignore unknown
|
||||||
}
|
}
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
|
|
||||||
return consume('}');
|
return r.consume('}');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseIndex(BankIndex& out) {
|
bool parseIndex(json::Reader& r, BankIndex& out) {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume('}')) return true; // empty object — vacuously an empty index
|
if (r.consume('}')) return true; // empty object — vacuously an empty index
|
||||||
|
|
||||||
std::vector<Sample> parsed;
|
std::vector<Sample> parsed;
|
||||||
do {
|
do {
|
||||||
std::string key;
|
std::string key;
|
||||||
if (!parseKey(key)) return false;
|
if (!r.parseKey(key)) return false;
|
||||||
|
|
||||||
if (key == "samples") {
|
if (key == "samples") {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!consume(']')) {
|
if (!r.consume(']')) {
|
||||||
do {
|
do {
|
||||||
Sample s;
|
Sample s;
|
||||||
if (!parseSample(s)) return false;
|
if (!parseSample(r, s)) return false;
|
||||||
parsed.push_back(std::move(s));
|
parsed.push_back(std::move(s));
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume(']')) return false;
|
if (!r.consume(']')) return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!skipValue()) return false; // version, or unknown keys
|
if (!r.skipValue()) return false; // version, or unknown keys
|
||||||
}
|
}
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
|
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
|
|
||||||
// Trailing garbage after the root object is malformed.
|
// Trailing garbage after the root object is malformed.
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!eof()) return false;
|
if (!r.eof()) return false;
|
||||||
|
|
||||||
// Rebuild via add() so the same invariants (relative-path, dedup) that guard
|
// Rebuild via add() so the same invariants (relative-path, dedup) that guard
|
||||||
// live inserts also guard deserialized data. Rejected/collapsed entries are
|
// live inserts also guard deserialized data. Rejected/collapsed entries are
|
||||||
@@ -757,10 +451,10 @@ bool Parser::parseIndex(BankIndex& out) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::optional<BankIndex> BankIndex::deserialize(const std::string& json) {
|
std::optional<BankIndex> BankIndex::deserialize(const std::string& blob) {
|
||||||
BankIndex idx;
|
BankIndex idx;
|
||||||
Parser p(json);
|
json::Reader r(blob);
|
||||||
if (!p.parseIndex(idx)) return std::nullopt;
|
if (!parseIndex(r, idx)) return std::nullopt;
|
||||||
return idx;
|
return idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-14
@@ -43,6 +43,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "capture_paths.h"
|
#include "capture_paths.h"
|
||||||
|
#include "core/util/file_bytes.h"
|
||||||
#include "render_settings.h"
|
#include "render_settings.h"
|
||||||
|
|
||||||
#define REAPERAPI_MINIMAL
|
#define REAPERAPI_MINIMAL
|
||||||
@@ -226,20 +227,9 @@ std::string makeUniqueTag() {
|
|||||||
return std::to_string(static_cast<long long>(now));
|
return std::to_string(static_cast<long long>(now));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the whole file into a byte buffer. Returns an empty vector on any I/O
|
// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03):
|
||||||
// failure (the caller then leaves contentHash empty — the safe, confirm-eliciting
|
// empty on any I/O failure (the caller then leaves contentHash empty — the safe,
|
||||||
// direction for an unreadable file).
|
// confirm-eliciting direction for an unreadable file).
|
||||||
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
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "capture_paths.h" // hashBytes, deriveBankPaths
|
#include "capture_paths.h" // hashBytes, deriveBankPaths
|
||||||
|
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
|
||||||
#include "peaks.h" // lastFrameAboveThreshold, AudioSample
|
#include "peaks.h" // lastFrameAboveThreshold, AudioSample
|
||||||
#include "realtime_record.h"
|
#include "realtime_record.h"
|
||||||
#include "render_settings.h" // autoTrimEndRatio, realtimeRecordWindowEnd
|
#include "render_settings.h" // autoTrimEndRatio, realtimeRecordWindowEnd
|
||||||
@@ -323,20 +324,9 @@ private:
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Reads the whole file into a byte buffer. Empty vector on any I/O failure — the
|
// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03):
|
||||||
// caller treats an unreadable file as "skip the trim" (keep the untrimmed window),
|
// empty on any I/O failure — the caller treats an unreadable file as "skip the
|
||||||
// never as a corruption of the recorded audio.
|
// trim" (keep the untrimmed window), never as a corruption of the recorded audio.
|
||||||
std::vector<std::uint8_t> readAllBytes(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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patches a little-endian uint32 into a byte buffer at `off` (the header size fields).
|
// Patches a little-endian uint32 into a byte buffer at `off` (the header size fields).
|
||||||
void writeU32LE(std::vector<std::uint8_t>& bytes, std::size_t off, std::uint32_t v) {
|
void writeU32LE(std::vector<std::uint8_t>& bytes, std::size_t off, std::uint32_t v) {
|
||||||
@@ -371,7 +361,7 @@ double trimAutoTailInPlace(const std::string& path,
|
|||||||
double rangeEndSeconds) {
|
double rangeEndSeconds) {
|
||||||
constexpr double kNoTrim = -1.0;
|
constexpr double kNoTrim = -1.0;
|
||||||
|
|
||||||
std::vector<std::uint8_t> bytes = readAllBytes(path);
|
std::vector<std::uint8_t> bytes = readFileBytes(path);
|
||||||
if (bytes.empty()) return kNoTrim;
|
if (bytes.empty()) return kNoTrim;
|
||||||
|
|
||||||
const reasampler::WavLayout layout = parseWavLayout(bytes);
|
const reasampler::WavLayout layout = parseWavLayout(bytes);
|
||||||
@@ -533,7 +523,7 @@ CaptureResult finalizeRecording(RealtimeCaptureState& st) {
|
|||||||
// contentHash empty — the safe, confirm-eliciting direction (bank_model treats
|
// contentHash empty — the safe, confirm-eliciting direction (bank_model treats
|
||||||
// "" as non-participating).
|
// "" as non-participating).
|
||||||
{
|
{
|
||||||
const std::vector<std::uint8_t> fileBytes = readAllBytes(destPath);
|
const std::vector<std::uint8_t> fileBytes = readFileBytes(destPath);
|
||||||
if (!fileBytes.empty()) {
|
if (!fileBytes.empty()) {
|
||||||
result.sample.contentHash = hashWavContent(fileBytes);
|
result.sample.contentHash = hashWavContent(fileBytes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+3
-13
@@ -22,6 +22,7 @@
|
|||||||
#include "bank_model.h" // Sample, AddResult, findByHash
|
#include "bank_model.h" // Sample, AddResult, findByHash
|
||||||
#include "bank_panel.h" // bankPanelRefresh
|
#include "bank_panel.h" // bankPanelRefresh
|
||||||
#include "capture_paths.h" // deriveBankPaths / projectDirOfRpp / hashWavContent
|
#include "capture_paths.h" // deriveBankPaths / projectDirOfRpp / hashWavContent
|
||||||
|
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
|
||||||
#include "instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId)
|
#include "instrument_drop.h" // pure buildInstrumentDropPreset (.vstpreset image for a sampleId)
|
||||||
#include "instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block)
|
#include "instrument_drop_win.h" // shell loadInstrumentOntoTrack (FX add+apply, no own undo block)
|
||||||
#include "persist.h" // ReaSamplerSession
|
#include "persist.h" // ReaSamplerSession
|
||||||
@@ -81,19 +82,8 @@ std::string currentProjectDir() {
|
|||||||
return projectDirOfRpp(std::string(buf.data()));
|
return projectDirOfRpp(std::string(buf.data()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads a whole file's bytes. Empty vector on any failure (missing / unreadable). Mirror
|
// Whole-file reads (source read + bank-copy validate/hash) go through the shared
|
||||||
// of capture.cpp's readFileBytes — used to read the source and validate/hash the bank copy.
|
// core/util readFileBytes (Q-W1, T2-03): empty on any failure (missing / unreadable).
|
||||||
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::streamsize n = f.tellg();
|
|
||||||
if (n <= 0) return {};
|
|
||||||
std::vector<std::uint8_t> bytes(static_cast<std::size_t>(n));
|
|
||||||
f.seekg(0);
|
|
||||||
f.read(reinterpret_cast<char*>(bytes.data()), n);
|
|
||||||
if (!f) return {};
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writes a byte buffer to a file. Returns true on success. The caller is responsible for
|
// Writes a byte buffer to a file. Returns true on success. The caller is responsible for
|
||||||
// ensuring the directory exists before calling.
|
// ensuring the directory exists before calling.
|
||||||
|
|||||||
+23
-225
@@ -1,19 +1,17 @@
|
|||||||
#include "owned_manifest.h"
|
#include "owned_manifest.h"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdio>
|
|
||||||
|
#include "core/json/json.h"
|
||||||
|
|
||||||
// owned_manifest implementation.
|
// owned_manifest implementation.
|
||||||
//
|
//
|
||||||
// JSON is hand-rolled and self-contained (project convention: the pure core is
|
// JSON rides on the shared core/json lexical layer (Q-W1, mirror of bank_model /
|
||||||
// dependency-free — no third-party JSON lib, mirror of bank_model / bank_book /
|
// bank_book / tail_control). The shape is a single object with one string array:
|
||||||
// tail_control). The shape is a single object with one string array:
|
|
||||||
//
|
//
|
||||||
// {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]}
|
// {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]}
|
||||||
//
|
//
|
||||||
// so a compact writer + a focused string-array parser is all it needs — far smaller
|
// so a compact writer + a focused string-array domain parse is all it needs.
|
||||||
// than bank_model's full recursive-descent parser, because there is exactly one key
|
|
||||||
// and one value kind.
|
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
@@ -56,237 +54,37 @@ bool OwnedFileManifest::contains(const std::string& relativePath) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JSON writer
|
// JSON writer (shared core/json escape — byte-identical to the prior local one)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
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 += '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::string OwnedFileManifest::serialize() const {
|
std::string OwnedFileManifest::serialize() const {
|
||||||
std::string out = "{\"owned\":[";
|
std::string out = "{\"owned\":[";
|
||||||
for (std::size_t i = 0; i < paths_.size(); ++i) {
|
for (std::size_t i = 0; i < paths_.size(); ++i) {
|
||||||
if (i) out += ',';
|
if (i) out += ',';
|
||||||
writeEscaped(out, paths_[i]);
|
json::writeEscaped(out, paths_[i]);
|
||||||
}
|
}
|
||||||
out += "]}";
|
out += "]}";
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// JSON parser (string-array only)
|
// JSON parser (string-array-only DOMAIN grammar over the shared core/json
|
||||||
|
// lexical layer). Tolerates unknown keys (forward-compat) and requires the
|
||||||
|
// "owned" value to be an array of strings.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class Parser {
|
bool parseManifest(json::Reader& r, OwnedFileManifest& out) {
|
||||||
public:
|
if (!r.consume('{')) return false;
|
||||||
explicit Parser(const std::string& s) : s_(s) {}
|
r.skipWs();
|
||||||
|
if (r.consume('}')) return true; // empty object -> empty manifest
|
||||||
// Parse the manifest object into `out`. Tolerates unknown keys (forward-compat)
|
|
||||||
// and requires the "owned" value to be an array of strings.
|
|
||||||
bool parseManifest(OwnedFileManifest& out);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
|
|
||||||
bool eof() const { return pos_ >= s_.size(); }
|
|
||||||
|
|
||||||
void skipWs() {
|
|
||||||
while (!eof()) {
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool consume(char c) {
|
|
||||||
skipWs();
|
|
||||||
if (eof() || s_[pos_] != c) return false;
|
|
||||||
++pos_;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool parseString(std::string& out);
|
|
||||||
bool parseStringArray(std::vector<std::string>& out);
|
|
||||||
bool skipValue(); // for forward-compat unknown keys
|
|
||||||
};
|
|
||||||
|
|
||||||
// Parses a JSON string literal (with the escapes our writer emits, plus \uXXXX for
|
|
||||||
// control chars). Positioned before the opening quote (skips leading whitespace).
|
|
||||||
bool Parser::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': {
|
|
||||||
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) {
|
|
||||||
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;
|
|
||||||
codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
|
|
||||||
} else if (hi >= 0xDC00 && hi <= 0xDFFF) {
|
|
||||||
return false; // unpaired low surrogate
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Parser::parseStringArray(std::vector<std::string>& out) {
|
|
||||||
if (!consume('[')) return false;
|
|
||||||
skipWs();
|
|
||||||
if (consume(']')) return true; // empty array
|
|
||||||
for (;;) {
|
|
||||||
std::string s;
|
|
||||||
if (!parseString(s)) return false;
|
|
||||||
out.push_back(std::move(s));
|
|
||||||
skipWs();
|
|
||||||
if (consume(',')) continue;
|
|
||||||
if (consume(']')) return true;
|
|
||||||
return false; // neither separator nor terminator — malformed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip a single JSON value (string / array / object / bare scalar) so an unknown key
|
|
||||||
// does not abort the parse. Minimal: enough for forward-compat siblings we don't know.
|
|
||||||
bool Parser::skipValue() {
|
|
||||||
skipWs();
|
|
||||||
if (eof()) return false;
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == '"') {
|
|
||||||
std::string tmp;
|
|
||||||
return parseString(tmp);
|
|
||||||
}
|
|
||||||
if (c == '[' || c == '{') {
|
|
||||||
// Balance nested brackets of either kind, ignoring bracket chars inside
|
|
||||||
// strings. Enough to step over an unknown nested value; not a full validator.
|
|
||||||
int depth = 0;
|
|
||||||
bool inStr = false;
|
|
||||||
while (!eof()) {
|
|
||||||
char d = s_[pos_];
|
|
||||||
if (inStr) {
|
|
||||||
if (d == '\\') { pos_ += 2; continue; }
|
|
||||||
if (d == '"') inStr = false;
|
|
||||||
++pos_;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (d == '"') { inStr = true; ++pos_; continue; }
|
|
||||||
if (d == '[' || d == '{') ++depth;
|
|
||||||
else if (d == ']' || d == '}') {
|
|
||||||
--depth;
|
|
||||||
if (depth == 0) { ++pos_; return true; }
|
|
||||||
}
|
|
||||||
++pos_;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// bare scalar (number / true / false / null) — read to the next structural char
|
|
||||||
while (!eof()) {
|
|
||||||
char d = s_[pos_];
|
|
||||||
if (d == ',' || d == '}' || d == ']' || d == ' ' || d == '\t' ||
|
|
||||||
d == '\n' || d == '\r')
|
|
||||||
break;
|
|
||||||
++pos_;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::parseManifest(OwnedFileManifest& out) {
|
|
||||||
if (!consume('{')) return false;
|
|
||||||
skipWs();
|
|
||||||
if (consume('}')) return true; // empty object -> empty manifest
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
std::string key;
|
std::string key;
|
||||||
if (!parseString(key)) return false;
|
if (!r.parseKey(key)) return false;
|
||||||
if (!consume(':')) return false;
|
|
||||||
if (key == "owned") {
|
if (key == "owned") {
|
||||||
std::vector<std::string> paths;
|
std::vector<std::string> paths;
|
||||||
if (!parseStringArray(paths)) return false;
|
if (!r.parseStringArray(paths)) return false;
|
||||||
for (auto& p : paths) {
|
for (auto& p : paths) {
|
||||||
// Feed through add() so the persisted invariants (dedup, reject
|
// Feed through add() so the persisted invariants (dedup, reject
|
||||||
// empty/absolute) are re-asserted on load — a hand-edited or corrupt
|
// empty/absolute) are re-asserted on load — a hand-edited or corrupt
|
||||||
@@ -294,21 +92,21 @@ bool Parser::parseManifest(OwnedFileManifest& out) {
|
|||||||
out.add(p);
|
out.add(p);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!skipValue()) return false; // forward-compat: tolerate unknown keys
|
if (!r.skipValue()) return false; // forward-compat: tolerate unknown keys
|
||||||
}
|
}
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume(',')) continue;
|
if (r.consume(',')) continue;
|
||||||
if (consume('}')) return true;
|
if (r.consume('}')) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::optional<OwnedFileManifest> OwnedFileManifest::deserialize(const std::string& json) {
|
std::optional<OwnedFileManifest> OwnedFileManifest::deserialize(const std::string& blob) {
|
||||||
OwnedFileManifest m;
|
OwnedFileManifest m;
|
||||||
Parser p(json);
|
json::Reader r(blob);
|
||||||
if (!p.parseManifest(m)) return std::nullopt;
|
if (!parseManifest(r, m)) return std::nullopt;
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-113
@@ -1,8 +1,8 @@
|
|||||||
#include "provenance.h"
|
#include "provenance.h"
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
|
||||||
#include <limits>
|
#include "core/wire/wire.h"
|
||||||
|
|
||||||
// provenance implementation — pure, self-contained (no third-party lib, mirror of
|
// provenance implementation — pure, self-contained (no third-party lib, mirror of
|
||||||
// bank_model's hand-rolled encoding discipline).
|
// bank_model's hand-rolled encoding discipline).
|
||||||
@@ -39,12 +39,12 @@ namespace {
|
|||||||
|
|
||||||
constexpr const char* kMagic = "rsprov1";
|
constexpr const char* kMagic = "rsprov1";
|
||||||
|
|
||||||
// Append one length-prefixed field: <decimal-len> ':' <bytes>
|
// The shared core/wire codec (Q-W1, T2-01b) carries the field grammar + the full
|
||||||
void putField(std::string& out, const std::string& field) {
|
// hardening (incl. the fixed fieldInt range check that closes the old strtol
|
||||||
out += std::to_string(field.size());
|
// silent-narrowing TODO). Only the %.17g double rendering stays local — it is
|
||||||
out += ':';
|
// this writer's convention, shared with the bank model's JSON doubles.
|
||||||
out += field;
|
using wire::putField;
|
||||||
}
|
using Cursor = wire::Cursor;
|
||||||
|
|
||||||
std::string dblToStr(double v) {
|
std::string dblToStr(double v) {
|
||||||
char buf[32];
|
char buf[32];
|
||||||
@@ -52,111 +52,6 @@ std::string dblToStr(double v) {
|
|||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor over the encoded string. All reads are bounds-checked; any short read
|
|
||||||
// fails the whole parse (ok_ latches false).
|
|
||||||
class Cursor {
|
|
||||||
public:
|
|
||||||
explicit Cursor(const std::string& s) : s_(s) {}
|
|
||||||
|
|
||||||
bool ok() const { return ok_; }
|
|
||||||
bool atEnd() const { return pos_ >= s_.size(); }
|
|
||||||
|
|
||||||
// Reads one length-prefixed field into `out`. Fails on a missing ':', an empty or
|
|
||||||
// non-numeric length, a length that overflows SIZE_MAX, or a length that runs past
|
|
||||||
// the end. Hardened form backported from the assignment_request / sample_usage
|
|
||||||
// siblings (Q-W0 T2-01a): the digit count is capped at 20 (the decimal width of
|
|
||||||
// SIZE_MAX on a 64-bit host) so a crafted 200-digit length cannot accumulate past
|
|
||||||
// SIZE_MAX via repeated multiply, and the bounds check is subtraction-first so a
|
|
||||||
// huge `len` cannot wrap `start + len` past the end test.
|
|
||||||
bool 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 fieldInt(int& out) {
|
|
||||||
std::string f;
|
|
||||||
if (!field(f)) return false;
|
|
||||||
return toInt(f, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A length-prefixed unsigned decimal (the GUID count). Hardened (Q-W0 T2-01a, the
|
|
||||||
// sample_usage fieldCount pattern): fails on empty, non-digit, a digit run past 20
|
|
||||||
// (SIZE_MAX's decimal width), or an accumulate that would overflow SIZE_MAX.
|
|
||||||
bool 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 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consumes an exact literal at the cursor (the magic tag). Fails if absent.
|
|
||||||
bool literal(const char* lit) {
|
|
||||||
if (!ok_) return false;
|
|
||||||
const std::string l(lit);
|
|
||||||
if (s_.compare(pos_, l.size(), l) != 0) return fail();
|
|
||||||
pos_ += l.size();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool fail() { ok_ = false; return false; }
|
|
||||||
// TODO(Q-W1): strtol does not check errno/range here, so an out-of-range field narrows
|
|
||||||
// silently to LONG_MAX (then truncates into `int`) instead of failing parse. Flagged for
|
|
||||||
// the Q-W1 wire-codec collapse rather than fixed in place.
|
|
||||||
static bool toInt(const std::string& f, int& out) {
|
|
||||||
const char* b = f.c_str();
|
|
||||||
char* end = nullptr;
|
|
||||||
long v = std::strtol(b, &end, 10);
|
|
||||||
if (end != b + f.size() || f.empty()) return false;
|
|
||||||
out = static_cast<int>(v);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
bool ok_ = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::string fxChainIdentity(const std::vector<FxIdentityEntry>& entries) {
|
std::string fxChainIdentity(const std::vector<FxIdentityEntry>& entries) {
|
||||||
|
|||||||
+10
-78
@@ -3,8 +3,8 @@
|
|||||||
#include "sample_usage.h"
|
#include "sample_usage.h"
|
||||||
|
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstddef>
|
|
||||||
#include <limits>
|
#include "core/wire/wire.h"
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
@@ -12,81 +12,13 @@ namespace {
|
|||||||
|
|
||||||
constexpr const char* kMagic = "rsusage1";
|
constexpr const char* kMagic = "rsusage1";
|
||||||
|
|
||||||
// Append one length-prefixed field: <decimal-len> ':' <bytes>. The same wire idiom as
|
// The shared core/wire codec (Q-W1, T2-01b) — one grammar across every
|
||||||
// assignment_request / provenance — one grammar across every ext-state seam.
|
// ext-state seam. The former local fieldCount (10-digit cap) is subsumed by the
|
||||||
void putField(std::string& out, const std::string& field) {
|
// codec's fieldSizeT (20-digit cap + overflow-guarded accumulate): every count
|
||||||
out += std::to_string(field.size());
|
// the old cap accepted decodes identically, and any larger count is rejected by
|
||||||
out += ':';
|
// the count-vs-wire-size sanity bound at the call site below.
|
||||||
out += field;
|
using wire::putField;
|
||||||
}
|
using Cursor = wire::Cursor;
|
||||||
|
|
||||||
// Bounds-checked cursor over the encoded string (the assignment_request Cursor, trimmed
|
|
||||||
// to the two field kinds this record needs). A short read latches ok_ false.
|
|
||||||
class Cursor {
|
|
||||||
public:
|
|
||||||
explicit Cursor(const std::string& s) : s_(s) {}
|
|
||||||
|
|
||||||
bool ok() const { return ok_; }
|
|
||||||
bool atEnd() const { return pos_ >= s_.size(); }
|
|
||||||
|
|
||||||
bool 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
|
|
||||||
if (colon - pos_ > 20u) return fail(); // SIZE_MAX is 20 decimal digits
|
|
||||||
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');
|
|
||||||
if (len > (std::numeric_limits<std::size_t>::max() - digit) / 10u)
|
|
||||||
return fail();
|
|
||||||
len = len * 10u + digit;
|
|
||||||
}
|
|
||||||
const std::size_t start = colon + 1;
|
|
||||||
if (start > s_.size() || len > s_.size() - start) return fail();
|
|
||||||
out.assign(s_, start, len);
|
|
||||||
pos_ = start + len;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consumes an exact literal at the cursor (the magic tag). Fails if absent.
|
|
||||||
bool 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A length-prefixed unsigned decimal (the hold count). Fails on empty, non-digit,
|
|
||||||
// or a value past a sane ceiling (a record cannot hold more entries than bytes).
|
|
||||||
bool fieldCount(std::size_t& out) {
|
|
||||||
std::string f;
|
|
||||||
if (!field(f)) return false;
|
|
||||||
if (f.empty() || f.size() > 10u) return fail();
|
|
||||||
std::size_t v = 0;
|
|
||||||
for (const char c : f) {
|
|
||||||
if (c < '0' || c > '9') return fail();
|
|
||||||
v = v * 10u + static_cast<std::size_t>(c - '0');
|
|
||||||
}
|
|
||||||
out = v;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool fail() {
|
|
||||||
ok_ = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
bool ok_ = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -115,7 +47,7 @@ std::optional<UsageRecord> decodeUsageRecord(const std::string& wire) {
|
|||||||
else if (unionedField == "0") rec.unioned = false;
|
else if (unionedField == "0") rec.unioned = false;
|
||||||
else return std::nullopt; // anything else is corruption -> reject whole
|
else return std::nullopt; // anything else is corruption -> reject whole
|
||||||
std::size_t count = 0;
|
std::size_t count = 0;
|
||||||
if (!c.fieldCount(count)) return std::nullopt;
|
if (!c.fieldSizeT(count)) return std::nullopt;
|
||||||
// Each hold needs at least 4 wire bytes ("0:0:"), so a count past wire.size()/4 is
|
// Each hold needs at least 4 wire bytes ("0:0:"), so a count past wire.size()/4 is
|
||||||
// provably bogus — reject before looping rather than iterating a crafted huge count.
|
// provably bogus — reject before looping rather than iterating a crafted huge count.
|
||||||
if (count > wire.size() / 4u + 1u) return std::nullopt;
|
if (count > wire.size() / 4u + 1u) return std::nullopt;
|
||||||
|
|||||||
+43
-44
@@ -3,10 +3,9 @@
|
|||||||
#include "tail_control.h"
|
#include "tail_control.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cerrno>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
#include "core/json/json.h"
|
||||||
|
|
||||||
namespace reasampler {
|
namespace reasampler {
|
||||||
|
|
||||||
@@ -51,13 +50,13 @@ std::string tailToggleLabel(const TailSetting& setting) {
|
|||||||
// JSON round-trip
|
// JSON round-trip
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
//
|
//
|
||||||
// The setting is a flat object of one enum + one double, so a compact hand-rolled
|
// The setting is a flat object of one enum + one double, riding the shared
|
||||||
// writer + a tolerant minimal reader is the simplest thing that works (mirroring
|
// core/json layer (Q-W1, T2-02: the former substring-scan valueAfterKey reader —
|
||||||
// bank_model's dependency-free JSON choice). manualMs is emitted with 17 significant
|
// the fifth hand-rolled JSON decoder — is retired). manualMs is emitted with 17
|
||||||
// digits (%.17g) — the shortest form that round-trips every IEEE-754 double exactly —
|
// significant digits (%.17g) — the shortest form that round-trips every IEEE-754
|
||||||
// so deserialize(serialize(x)) == x holds bit-for-bit. deserialize is deliberately
|
// double exactly — so deserialize(serialize(x)) == x holds bit-for-bit.
|
||||||
// forgiving: any parse failure returns nullopt so the caller falls back to a default,
|
// deserialize stays forgiving in outcome: any parse failure returns nullopt so
|
||||||
// exactly as an absent ext-state key does.
|
// the caller falls back to a default, exactly as an absent ext-state key does.
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -81,48 +80,48 @@ std::optional<TailMode> modeFromInt(int v) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the value token following `"key":` in `json`. Returns a pointer just past the
|
|
||||||
// colon (skipping whitespace) or nullptr if the key is absent. Minimal: the writer
|
|
||||||
// emits exactly one flat object with unique keys, so a substring search is sufficient
|
|
||||||
// and there is no nesting to confuse it.
|
|
||||||
const char* valueAfterKey(const std::string& json, const char* key) {
|
|
||||||
const std::string needle = std::string("\"") + key + "\"";
|
|
||||||
const std::size_t pos = json.find(needle);
|
|
||||||
if (pos == std::string::npos) return nullptr;
|
|
||||||
const char* p = json.c_str() + pos + needle.size();
|
|
||||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p;
|
|
||||||
if (*p != ':') return nullptr;
|
|
||||||
++p;
|
|
||||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') ++p;
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::string serializeTailSetting(const TailSetting& setting) {
|
std::string serializeTailSetting(const TailSetting& setting) {
|
||||||
char buf[128];
|
// Byte-identical to the former snprintf writer: {"mode":%d,"manualMs":%.17g}.
|
||||||
std::snprintf(buf, sizeof(buf), "{\"mode\":%d,\"manualMs\":%.17g}",
|
std::string out;
|
||||||
modeToInt(setting.mode), setting.manualMs);
|
{
|
||||||
return std::string(buf);
|
json::Writer w(out);
|
||||||
|
w.keyRaw("mode", json::numToStr(modeToInt(setting.mode)));
|
||||||
|
w.keyRaw("manualMs", json::numToStr(setting.manualMs));
|
||||||
|
} // Writer closes the object here (see bank_model's NRVO note)
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<TailSetting> deserializeTailSetting(const std::string& json) {
|
std::optional<TailSetting> deserializeTailSetting(const std::string& blob) {
|
||||||
const char* modeTok = valueAfterKey(json, "mode");
|
json::Reader r(blob);
|
||||||
const char* msTok = valueAfterKey(json, "manualMs");
|
if (!r.consume('{')) return std::nullopt;
|
||||||
if (!modeTok || !msTok) return std::nullopt; // absent key -> malformed -> default
|
|
||||||
|
|
||||||
char* end = nullptr;
|
int modeInt = 0;
|
||||||
errno = 0;
|
double ms = 0.0;
|
||||||
const long modeVal = std::strtol(modeTok, &end, 10);
|
bool haveMode = false, haveMs = false;
|
||||||
if (end == modeTok || errno != 0) return std::nullopt;
|
r.skipWs();
|
||||||
const std::optional<TailMode> mode = modeFromInt(static_cast<int>(modeVal));
|
if (!r.consume('}')) {
|
||||||
|
do {
|
||||||
|
std::string key;
|
||||||
|
if (!r.parseKey(key)) return std::nullopt;
|
||||||
|
if (key == "mode") {
|
||||||
|
if (!r.parseInt(modeInt)) return std::nullopt;
|
||||||
|
haveMode = true;
|
||||||
|
} else if (key == "manualMs") {
|
||||||
|
if (!r.parseDouble(ms)) return std::nullopt;
|
||||||
|
haveMs = true;
|
||||||
|
} else {
|
||||||
|
if (!r.skipValue()) return std::nullopt; // forward-compat
|
||||||
|
}
|
||||||
|
} while (r.consume(','));
|
||||||
|
if (!r.consume('}')) return std::nullopt;
|
||||||
|
}
|
||||||
|
if (!haveMode || !haveMs) return std::nullopt; // absent key -> malformed -> default
|
||||||
|
|
||||||
|
const std::optional<TailMode> mode = modeFromInt(modeInt);
|
||||||
if (!mode) return std::nullopt;
|
if (!mode) return std::nullopt;
|
||||||
|
|
||||||
end = nullptr;
|
|
||||||
errno = 0;
|
|
||||||
const double ms = std::strtod(msTok, &end);
|
|
||||||
if (end == msTok || errno != 0) return std::nullopt;
|
|
||||||
|
|
||||||
TailSetting out;
|
TailSetting out;
|
||||||
out.mode = *mode;
|
out.mode = *mode;
|
||||||
out.manualMs = ms;
|
out.manualMs = ms;
|
||||||
|
|||||||
+93
-343
@@ -2,19 +2,16 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cerrno>
|
|
||||||
#include <climits>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/json/json.h"
|
||||||
#include "lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention
|
#include "lane_keys.h" // laneNameForMode — the ONE durable managed-lane-key convention
|
||||||
|
|
||||||
// view_mode_model implementation.
|
// view_mode_model implementation.
|
||||||
//
|
//
|
||||||
// JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief:
|
// JSON rides on the shared core/json lexical layer (Q-W1), mirroring bank_model.
|
||||||
// keep the pure core dependency-free — no third-party JSON lib). A compact writer
|
// A compact writer
|
||||||
// plus a recursive-descent parser covers the field set: the mode registry, the
|
// plus a recursive-descent parser covers the field set: the mode registry, the
|
||||||
// GUID-keyed membership map, per-track snapshots (with a variable-length per-FX
|
// GUID-keyed membership map, per-track snapshots (with a variable-length per-FX
|
||||||
// offline vector), and the active mode. Ints are emitted plainly; strings are
|
// offline vector), and the active mode. Ints are emitted plainly; strings are
|
||||||
@@ -493,73 +490,12 @@ bool ViewModeModel::operator==(const ViewModeModel& o) const {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
void writeEscaped(std::string& out, const std::string& s) {
|
// Shared core/json emit helpers (Q-W1): same escape set + %d rendering as the
|
||||||
out += '"';
|
// prior file-local writer, so the emitted blob is byte-identical.
|
||||||
for (char c : s) {
|
using json::writeEscaped;
|
||||||
switch (c) {
|
using json::writeIntArray;
|
||||||
case '"': out += "\\\""; break;
|
std::string intToStr(int v) { return json::numToStr(v); }
|
||||||
case '\\': out += "\\\\"; break;
|
using ObjWriter = json::Writer;
|
||||||
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 intToStr(int v) {
|
|
||||||
char buf[16];
|
|
||||||
std::snprintf(buf, sizeof(buf), "%d", v);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 += intToStr(v[i]);
|
|
||||||
}
|
|
||||||
out += ']';
|
|
||||||
}
|
|
||||||
|
|
||||||
class ObjWriter {
|
|
||||||
public:
|
|
||||||
explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; }
|
|
||||||
~ObjWriter() { out_ += '}'; }
|
|
||||||
|
|
||||||
void keyRaw(const char* key, const std::string& rawValue) {
|
|
||||||
sep();
|
|
||||||
writeEscaped(out_, key);
|
|
||||||
out_ += ':';
|
|
||||||
out_ += rawValue;
|
|
||||||
}
|
|
||||||
void keyStr(const char* key, const std::string& value) {
|
|
||||||
sep();
|
|
||||||
writeEscaped(out_, key);
|
|
||||||
out_ += ':';
|
|
||||||
writeEscaped(out_, value);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -660,250 +596,64 @@ std::string ViewModeModel::serialize() const {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class Parser {
|
// The model DOMAIN grammar over the shared core/json lexical layer (Q-W1).
|
||||||
public:
|
|
||||||
explicit Parser(const std::string& s) : s_(s) {}
|
|
||||||
bool parseModel(ViewModeModel& out);
|
|
||||||
|
|
||||||
private:
|
|
||||||
const std::string& s_;
|
|
||||||
std::size_t pos_ = 0;
|
|
||||||
|
|
||||||
bool eof() const { return pos_ >= s_.size(); }
|
|
||||||
|
|
||||||
void skipWs() {
|
|
||||||
while (!eof()) {
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_;
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bool consume(char c) {
|
|
||||||
skipWs();
|
|
||||||
if (eof() || s_[pos_] != c) return false;
|
|
||||||
++pos_;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool parseString(std::string& out);
|
|
||||||
bool parseRawScalar(std::string& out);
|
|
||||||
bool parseInt(int& out);
|
|
||||||
bool parseBool(bool& out);
|
|
||||||
bool parseKey(std::string& key);
|
|
||||||
bool skipValue();
|
|
||||||
|
|
||||||
bool parseModes(ModeRegistry& reg);
|
|
||||||
bool parseMembership(MembershipIndex& idx);
|
|
||||||
bool parseSnapshots(std::map<std::string, TrackSnapshot>& snaps);
|
|
||||||
bool parseLanes(LaneOwnershipIndex& idx);
|
|
||||||
bool parseIntArray(std::vector<int>& out);
|
|
||||||
};
|
|
||||||
|
|
||||||
bool Parser::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': {
|
|
||||||
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) {
|
|
||||||
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;
|
|
||||||
codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
|
|
||||||
} else if (hi >= 0xDC00 && hi <= 0xDFFF) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::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 Parser::parseInt(int& 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;
|
|
||||||
if (v < INT_MIN || v > INT_MAX) return false;
|
|
||||||
out = static_cast<int>(v);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::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 Parser::parseKey(std::string& key) {
|
|
||||||
if (!parseString(key)) return false;
|
|
||||||
return consume(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::skipValue() {
|
|
||||||
skipWs();
|
|
||||||
if (eof()) return false;
|
|
||||||
char c = s_[pos_];
|
|
||||||
if (c == '"') { std::string tmp; return parseString(tmp); }
|
|
||||||
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; continue; }
|
|
||||||
if (d == open) ++depth;
|
|
||||||
else if (d == close) --depth;
|
|
||||||
++pos_;
|
|
||||||
}
|
|
||||||
return depth == 0;
|
|
||||||
}
|
|
||||||
std::string tmp;
|
|
||||||
return parseRawScalar(tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Parser::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(']');
|
|
||||||
}
|
|
||||||
|
|
||||||
// The registry starts seeded (Arrange + Design). Deserialization must reproduce the
|
// The registry starts seeded (Arrange + Design). Deserialization must reproduce the
|
||||||
// serialized set exactly, so we replace the seeded contents with the parsed ones —
|
// serialized set exactly, so we replace the seeded contents with the parsed ones —
|
||||||
// add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as
|
// add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as
|
||||||
// duplicates and the ordinals/names would not round-trip. We therefore parse into a
|
// duplicates and the ordinals/names would not round-trip. We therefore parse into a
|
||||||
// fresh vector and swap. `reg` is passed empty (see parseModel).
|
// fresh vector and swap. `reg` is passed empty (see parseModel).
|
||||||
bool Parser::parseModes(ModeRegistry& reg) {
|
bool parseModes(json::Reader& r, ModeRegistry& reg) {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume(']')) return true; // empty array (unusual, but valid)
|
if (r.consume(']')) return true; // empty array (unusual, but valid)
|
||||||
do {
|
do {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
Mode m;
|
Mode m;
|
||||||
bool haveId = false;
|
bool haveId = false;
|
||||||
do {
|
do {
|
||||||
std::string k;
|
std::string k;
|
||||||
if (!parseKey(k)) return false;
|
if (!r.parseKey(k)) return false;
|
||||||
if (k == "id") { if (!parseString(m.id)) return false; haveId = true; }
|
if (k == "id") { if (!r.parseString(m.id)) return false; haveId = true; }
|
||||||
else if (k == "displayName") { if (!parseString(m.displayName)) return false; }
|
else if (k == "displayName") { if (!r.parseString(m.displayName)) return false; }
|
||||||
else if (k == "ordinal") { if (!parseInt(m.ordinal)) return false; }
|
else if (k == "ordinal") { if (!r.parseInt(m.ordinal)) return false; }
|
||||||
else if (!skipValue()) return false;
|
else if (!r.skipValue()) return false;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
if (!haveId || !reg.add(m)) return false; // malformed / duplicate id
|
if (!haveId || !reg.add(m)) return false; // malformed / duplicate id
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
return consume(']');
|
return r.consume(']');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseMembership(MembershipIndex& idx) {
|
bool parseMembership(json::Reader& r, MembershipIndex& idx) {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume(']')) return true;
|
if (r.consume(']')) return true;
|
||||||
do {
|
do {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
std::string guid;
|
std::string guid;
|
||||||
Membership mem;
|
Membership mem;
|
||||||
bool haveGuid = false;
|
bool haveGuid = false;
|
||||||
do {
|
do {
|
||||||
std::string k;
|
std::string k;
|
||||||
if (!parseKey(k)) return false;
|
if (!r.parseKey(k)) return false;
|
||||||
if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; }
|
if (k == "guid") { if (!r.parseString(guid)) return false; haveGuid = true; }
|
||||||
else if (k == "modes") {
|
else if (k == "modes") {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!consume(']')) {
|
if (!r.consume(']')) {
|
||||||
do {
|
do {
|
||||||
std::string id;
|
std::string id;
|
||||||
if (!parseString(id)) return false;
|
if (!r.parseString(id)) return false;
|
||||||
mem.modeIds.insert(id);
|
mem.modeIds.insert(id);
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume(']')) return false;
|
if (!r.consume(']')) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (k == "showBoth") { if (!parseBool(mem.showBoth)) return false; }
|
else if (k == "showBoth") { if (!r.parseBool(mem.showBoth)) return false; }
|
||||||
else if (!skipValue()) return false;
|
else if (!r.skipValue()) return false;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
if (!haveGuid || guid.empty()) return false;
|
if (!haveGuid || guid.empty()) return false;
|
||||||
// Install the entry verbatim (tag() would clear a multi-mode set and drop
|
// Install the entry verbatim (tag() would clear a multi-mode set and drop
|
||||||
// show-both). A serialized entry is trusted to already satisfy the model's
|
// show-both). A serialized entry is trusted to already satisfy the model's
|
||||||
@@ -918,55 +668,55 @@ bool Parser::parseMembership(MembershipIndex& idx) {
|
|||||||
// mode that no longer exists has an immediate behavioral consequence, so it
|
// mode that no longer exists has an immediate behavioral consequence, so it
|
||||||
// is caught and the parse is rejected.
|
// is caught and the parse is rejected.
|
||||||
if (!idx.restore(guid, mem)) return false;
|
if (!idx.restore(guid, mem)) return false;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
return consume(']');
|
return r.consume(']');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseSnapshots(std::map<std::string, TrackSnapshot>& snaps) {
|
bool parseSnapshots(json::Reader& r, std::map<std::string, TrackSnapshot>& snaps) {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume(']')) return true;
|
if (r.consume(']')) return true;
|
||||||
do {
|
do {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
std::string guid;
|
std::string guid;
|
||||||
TrackSnapshot snap;
|
TrackSnapshot snap;
|
||||||
bool haveGuid = false;
|
bool haveGuid = false;
|
||||||
do {
|
do {
|
||||||
std::string k;
|
std::string k;
|
||||||
if (!parseKey(k)) return false;
|
if (!r.parseKey(k)) return false;
|
||||||
if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; }
|
if (k == "guid") { if (!r.parseString(guid)) return false; haveGuid = true; }
|
||||||
else if (k == "showInTcp") { if (!parseInt(snap.showInTcp)) return false; }
|
else if (k == "showInTcp") { if (!r.parseInt(snap.showInTcp)) return false; }
|
||||||
else if (k == "showInMixer") { if (!parseInt(snap.showInMixer)) return false; }
|
else if (k == "showInMixer") { if (!r.parseInt(snap.showInMixer)) return false; }
|
||||||
else if (k == "mainSend") { if (!parseInt(snap.mainSend)) return false; }
|
else if (k == "mainSend") { if (!r.parseInt(snap.mainSend)) return false; }
|
||||||
else if (k == "fxEnable") { if (!parseInt(snap.fxEnable)) return false; }
|
else if (k == "fxEnable") { if (!r.parseInt(snap.fxEnable)) return false; }
|
||||||
else if (k == "fxOffline") { if (!parseIntArray(snap.fxOffline)) return false; }
|
else if (k == "fxOffline") { if (!r.parseIntArray(snap.fxOffline)) return false; }
|
||||||
else if (!skipValue()) return false;
|
else if (!r.skipValue()) return false;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
if (!haveGuid || guid.empty()) return false;
|
if (!haveGuid || guid.empty()) return false;
|
||||||
snaps[guid] = snap;
|
snaps[guid] = snap;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
return consume(']');
|
return r.consume(']');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseLanes(LaneOwnershipIndex& idx) {
|
bool parseLanes(json::Reader& r, LaneOwnershipIndex& idx) {
|
||||||
if (!consume('[')) return false;
|
if (!r.consume('[')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume(']')) return true;
|
if (r.consume(']')) return true;
|
||||||
do {
|
do {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
std::string trackGuid, laneKey, mode;
|
std::string trackGuid, laneKey, mode;
|
||||||
bool haveTrack = false, haveLane = false, managed = false, haveManaged = false;
|
bool haveTrack = false, haveLane = false, managed = false, haveManaged = false;
|
||||||
do {
|
do {
|
||||||
std::string k;
|
std::string k;
|
||||||
if (!parseKey(k)) return false;
|
if (!r.parseKey(k)) return false;
|
||||||
if (k == "trackGuid") { if (!parseString(trackGuid)) return false; haveTrack = true; }
|
if (k == "trackGuid") { if (!r.parseString(trackGuid)) return false; haveTrack = true; }
|
||||||
else if (k == "laneKey") { if (!parseString(laneKey)) return false; haveLane = true; }
|
else if (k == "laneKey") { if (!r.parseString(laneKey)) return false; haveLane = true; }
|
||||||
else if (k == "managed") { if (!parseBool(managed)) return false; haveManaged = true; }
|
else if (k == "managed") { if (!r.parseBool(managed)) return false; haveManaged = true; }
|
||||||
else if (k == "mode") { if (!parseString(mode)) return false; }
|
else if (k == "mode") { if (!r.parseString(mode)) return false; }
|
||||||
else if (!skipValue()) return false;
|
else if (!r.skipValue()) return false;
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
// Both keys mandatory and non-empty (they form the lane's identity). A managed
|
// Both keys mandatory and non-empty (they form the lane's identity). A managed
|
||||||
// lane must carry a non-empty mode; a manual lane must not claim one. Enforcing
|
// lane must carry a non-empty mode; a manual lane must not claim one. Enforcing
|
||||||
// this on parse keeps a round-tripped index byte-for-byte identical to the
|
// this on parse keeps a round-tripped index byte-for-byte identical to the
|
||||||
@@ -980,14 +730,14 @@ bool Parser::parseLanes(LaneOwnershipIndex& idx) {
|
|||||||
if (!mode.empty()) return false; // manual lane must not carry a mode
|
if (!mode.empty()) return false; // manual lane must not carry a mode
|
||||||
if (!idx.setManual(trackGuid, laneKey)) return false;
|
if (!idx.setManual(trackGuid, laneKey)) return false;
|
||||||
}
|
}
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
return consume(']');
|
return r.consume(']');
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Parser::parseModel(ViewModeModel& out) {
|
bool parseModel(json::Reader& r, ViewModeModel& out) {
|
||||||
if (!consume('{')) return false;
|
if (!r.consume('{')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (consume('}')) return true; // lenient empty root ⇒ default-seeded model
|
if (r.consume('}')) return true; // lenient empty root ⇒ default-seeded model
|
||||||
|
|
||||||
ModeRegistry reg; // seeded default; REPLACED if a modes array is present
|
ModeRegistry reg; // seeded default; REPLACED if a modes array is present
|
||||||
bool haveModes = false;
|
bool haveModes = false;
|
||||||
@@ -999,33 +749,33 @@ bool Parser::parseModel(ViewModeModel& out) {
|
|||||||
|
|
||||||
do {
|
do {
|
||||||
std::string key;
|
std::string key;
|
||||||
if (!parseKey(key)) return false;
|
if (!r.parseKey(key)) return false;
|
||||||
if (key == "activeMode") {
|
if (key == "activeMode") {
|
||||||
if (!parseString(activeMode)) return false;
|
if (!r.parseString(activeMode)) return false;
|
||||||
haveActive = true;
|
haveActive = true;
|
||||||
} else if (key == "modes") {
|
} else if (key == "modes") {
|
||||||
ModeRegistry fresh = ModeRegistry::makeEmpty(); // parse into empty, then own
|
ModeRegistry fresh = ModeRegistry::makeEmpty(); // parse into empty, then own
|
||||||
if (!parseModes(fresh)) return false;
|
if (!parseModes(r, fresh)) return false;
|
||||||
reg = fresh;
|
reg = fresh;
|
||||||
haveModes = true;
|
haveModes = true;
|
||||||
} else if (key == "membership") {
|
} else if (key == "membership") {
|
||||||
if (!parseMembership(membership)) return false;
|
if (!parseMembership(r, membership)) return false;
|
||||||
} else if (key == "snapshots") {
|
} else if (key == "snapshots") {
|
||||||
if (!parseSnapshots(snaps)) return false;
|
if (!parseSnapshots(r, snaps)) return false;
|
||||||
} else if (key == "lanes") {
|
} else if (key == "lanes") {
|
||||||
if (!parseLanes(lanes)) return false;
|
if (!parseLanes(r, lanes)) return false;
|
||||||
} else {
|
} else {
|
||||||
// Unknown keys and the "version" field are skipped here.
|
// Unknown keys and the "version" field are skipped here.
|
||||||
// "version" is serialized as a forward-compat placeholder — there is no
|
// "version" is serialized as a forward-compat placeholder — there is no
|
||||||
// active version gate yet; all persisted data is parsed the same way
|
// active version gate yet; all persisted data is parsed the same way
|
||||||
// regardless of the value. A future gate would add a version branch here.
|
// regardless of the value. A future gate would add a version branch here.
|
||||||
if (!skipValue()) return false;
|
if (!r.skipValue()) return false;
|
||||||
}
|
}
|
||||||
} while (consume(','));
|
} while (r.consume(','));
|
||||||
|
|
||||||
if (!consume('}')) return false;
|
if (!r.consume('}')) return false;
|
||||||
skipWs();
|
r.skipWs();
|
||||||
if (!eof()) return false; // trailing garbage
|
if (!r.eof()) return false; // trailing garbage
|
||||||
|
|
||||||
if (haveModes) out.modes() = reg;
|
if (haveModes) out.modes() = reg;
|
||||||
out.membership() = membership;
|
out.membership() = membership;
|
||||||
@@ -1039,10 +789,10 @@ bool Parser::parseModel(ViewModeModel& out) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string& json) {
|
std::optional<ViewModeModel> ViewModeModel::deserialize(const std::string& blob) {
|
||||||
ViewModeModel vm;
|
ViewModeModel vm;
|
||||||
Parser p(json);
|
json::Reader r(blob);
|
||||||
if (!p.parseModel(vm)) return std::nullopt;
|
if (!parseModel(r, vm)) return std::nullopt;
|
||||||
return vm;
|
return vm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-14
@@ -3,27 +3,20 @@
|
|||||||
#include "bank_sync.h"
|
#include "bank_sync.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/wire/wire.h"
|
||||||
|
|
||||||
namespace reasampler::vst {
|
namespace reasampler::vst {
|
||||||
|
|
||||||
std::int64_t parseBankGeneration(const std::string& raw) {
|
std::int64_t parseBankGeneration(const std::string& raw) {
|
||||||
if (raw.empty()) return kBankGenerationAbsent;
|
// Whole-string, non-negative decimal parse WITHOUT exceptions or locale
|
||||||
|
// surprises — the shared core/wire accumulate (Q-W1, T2-01b). A leading
|
||||||
// Whole-string, non-negative decimal parse WITHOUT exceptions or locale surprises.
|
// '+' / '-', any non-digit, an empty string, or overflow past int64 max all
|
||||||
// A leading '+' / '-' , any non-digit, an empty digit run, or overflow past int64 max
|
// reject to the absent default (0); the guarded accumulate means a
|
||||||
// all reject to the absent default (0). Manual accumulation with an overflow guard so a
|
|
||||||
// pathologically long digit run can never wrap into a bogus small value.
|
// pathologically long digit run can never wrap into a bogus small value.
|
||||||
std::int64_t value = 0;
|
std::int64_t value = 0;
|
||||||
constexpr std::int64_t kMax = std::numeric_limits<std::int64_t>::max();
|
if (!wire::parseUnsignedDecimal(raw, value)) return kBankGenerationAbsent;
|
||||||
for (const char c : raw) {
|
|
||||||
if (c < '0' || c > '9') return kBankGenerationAbsent; // 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 kBankGenerationAbsent; // would overflow -> reject
|
|
||||||
value = value * 10 + digit;
|
|
||||||
}
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include "capture_browser.h"
|
#include "capture_browser.h"
|
||||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||||
#include "component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3)
|
#include "component_geometry.h" // KitBox — the kit text/fill draw box (Phase L, L3)
|
||||||
|
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
|
||||||
#include "curve_popup.h" // r11 centered curve-popup sheet geometry (FB1)
|
#include "curve_popup.h" // r11 centered curve-popup sheet geometry (FB1)
|
||||||
#include "draw_kit.h" // the L1 draw kit: fillSurface/drawButton/text/drawWaveform (L3)
|
#include "draw_kit.h" // the L1 draw kit: fillSurface/drawButton/text/drawWaveform (L3)
|
||||||
#include "editor_geometry.h" // Rect, contains
|
#include "editor_geometry.h" // Rect, contains
|
||||||
@@ -770,16 +771,8 @@ const std::vector<AudioSample>& ReaSamplerEditor::monoPcmFor(const std::string&
|
|||||||
if (!relativePath.empty()) {
|
if (!relativePath.empty()) {
|
||||||
const std::string projectDir = processor_->bridge().activeProjectDir();
|
const std::string projectDir = processor_->bridge().activeProjectDir();
|
||||||
const std::string abs = resolveBankFile(projectDir, relativePath);
|
const std::string abs = resolveBankFile(projectDir, relativePath);
|
||||||
std::vector<std::uint8_t> bytes;
|
// Shared core/util whole-file loader (Q-W1, T2-03): empty on any failure.
|
||||||
std::ifstream f(abs, std::ios::binary | std::ios::ate);
|
const std::vector<std::uint8_t> bytes = readFileBytes(abs);
|
||||||
if (f) {
|
|
||||||
const std::streamoff size = f.tellg();
|
|
||||||
if (size > 0) {
|
|
||||||
f.seekg(0, std::ios::beg);
|
|
||||||
bytes.resize(static_cast<std::size_t>(size));
|
|
||||||
if (!f.read(reinterpret_cast<char*>(bytes.data()), size)) bytes.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const WavLayout layout = parseWavLayout(bytes);
|
const WavLayout layout = parseWavLayout(bytes);
|
||||||
if (layout.valid) {
|
if (layout.valid) {
|
||||||
std::vector<AudioSample> interleaved =
|
std::vector<AudioSample> interleaved =
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "assignment_request.h" // decodeAssignmentRequest (S8 request wire parse)
|
#include "assignment_request.h" // decodeAssignmentRequest (S8 request wire parse)
|
||||||
#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
|
#include "bank_sync.h" // S9/S8 pure decisions: parseBankGeneration, consumeDecision
|
||||||
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
#include "capture_paths.h" // resolveBankFile (shared M4 path resolution)
|
||||||
|
#include "core/util/file_bytes.h" // shared whole-file loader (Q-W1, T2-03)
|
||||||
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey (shared wire contract)
|
#include "ext_keys.h" // kProjExtBanksKey / kProjExtBankGenKey / kProjExtAssignKey (shared wire contract)
|
||||||
#include "master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp)
|
#include "master_gain.h" // masterGainMaxLinear (FB1 post-mixer gain clamp)
|
||||||
#include "reasampler_editor.h"
|
#include "reasampler_editor.h"
|
||||||
@@ -69,19 +70,9 @@ std::string mintUsageInstanceGuid() {
|
|||||||
return std::string(buf);
|
return std::string(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read a whole file into a byte buffer. Off-thread only (blocking file I/O). Empty on
|
// Whole-file reads go through the shared core/util readFileBytes (Q-W1, T2-03).
|
||||||
// any failure — the caller treats an unreadable WAV as "nothing to play".
|
// Off-thread only (blocking file I/O). Empty on any failure — the caller treats
|
||||||
std::vector<std::uint8_t> readFileBytes(const std::string& path) {
|
// an unreadable WAV as "nothing to play".
|
||||||
std::vector<std::uint8_t> bytes;
|
|
||||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
|
||||||
if (!f) return bytes;
|
|
||||||
const std::streamoff size = f.tellg();
|
|
||||||
if (size <= 0) return bytes;
|
|
||||||
f.seekg(0, std::ios::beg);
|
|
||||||
bytes.resize(static_cast<std::size_t>(size));
|
|
||||||
if (!f.read(reinterpret_cast<char*>(bytes.data()), size)) bytes.clear();
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve a project-relative WAV path (the M4 way persist does), read + decode it (file
|
// Resolve a project-relative WAV path (the M4 way persist does), read + decode it (file
|
||||||
// I/O — off-thread only), and apply the S7 cross-mode channel policy for `mode`: mono mode
|
// I/O — off-thread only), and apply the S7 cross-mode channel policy for `mode`: mono mode
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Standalone tests for reasampler::readFileBytes — no REAPER, no framework.
|
||||||
|
// The ONE whole-file loader (Q-W1, T2-03) shared by both artifacts. Exercises
|
||||||
|
// the three-way contract: exact bytes back, empty on a missing file, empty on
|
||||||
|
// an empty file. Uses a scratch file in the test's working directory.
|
||||||
|
|
||||||
|
#include "../src/core/util/file_bytes.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
static const char* kScratch = "file_bytes_scratch.bin";
|
||||||
|
|
||||||
|
static void testReadsExactBytesBack() {
|
||||||
|
// Binary content incl. NUL and 0xFF — the loader must be byte-transparent.
|
||||||
|
const std::vector<std::uint8_t> payload = {0x00, 0x01, 0xFF, 0x7E, 0x00, 0x0A};
|
||||||
|
{
|
||||||
|
std::ofstream f(kScratch, std::ios::binary | std::ios::trunc);
|
||||||
|
f.write(reinterpret_cast<const char*>(payload.data()),
|
||||||
|
static_cast<std::streamsize>(payload.size()));
|
||||||
|
}
|
||||||
|
CHECK(readFileBytes(kScratch) == payload);
|
||||||
|
std::remove(kScratch);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testMissingFileIsEmpty() {
|
||||||
|
CHECK(readFileBytes("no_such_file_anywhere.bin").empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testEmptyFileIsEmpty() {
|
||||||
|
{ std::ofstream f(kScratch, std::ios::binary | std::ios::trunc); }
|
||||||
|
CHECK(readFileBytes(kScratch).empty());
|
||||||
|
std::remove(kScratch);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testReadsExactBytesBack();
|
||||||
|
testMissingFileIsEmpty();
|
||||||
|
testEmptyFileIsEmpty();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("file_bytes: all tests passed\n");
|
||||||
|
else std::printf("file_bytes: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
// Standalone tests for reasampler::json — no REAPER, no framework. The ONE
|
||||||
|
// lexical JSON layer (Q-W1) behind bank_model / bank_book / view_mode_model /
|
||||||
|
// owned_manifest / tail_control. The consumers' own suites prove the domain
|
||||||
|
// grammars; this suite pins the LEXICAL contract — the escape set, the number
|
||||||
|
// renderings (byte-exact), the parse tolerances, and the reject paths — so a
|
||||||
|
// change here is caught before it silently shifts five persisted-blob formats.
|
||||||
|
//
|
||||||
|
// NOTE: json::Reader BORROWS its input string, so every test binds a named
|
||||||
|
// std::string first — never a temporary.
|
||||||
|
|
||||||
|
#include "../src/core/json/json.h"
|
||||||
|
|
||||||
|
#include <climits>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// Convenience: parse helpers over a named buffer per call site.
|
||||||
|
static bool intFrom(const std::string& s, int& v) { json::Reader r(s); return r.parseInt(v); }
|
||||||
|
static bool int64From(const std::string& s, std::int64_t& v) { json::Reader r(s); return r.parseInt64(v); }
|
||||||
|
static bool doubleFrom(const std::string& s, double& v) { json::Reader r(s); return r.parseDouble(v); }
|
||||||
|
static bool boolFrom(const std::string& s, bool& v) { json::Reader r(s); return r.parseBool(v); }
|
||||||
|
static bool stringFrom(const std::string& s, std::string& v) { json::Reader r(s); return r.parseString(v); }
|
||||||
|
|
||||||
|
// --- emit: writeEscaped -------------------------------------------------------
|
||||||
|
|
||||||
|
static void testEscapeExactBytes() {
|
||||||
|
// The seven short escapes + \u00XX for remaining control chars, verbatim
|
||||||
|
// pass-through otherwise. Byte-exact: this is the persisted-blob format.
|
||||||
|
std::string out;
|
||||||
|
json::writeEscaped(out, "a\"b\\c\n\t\x01z");
|
||||||
|
CHECK(out == "\"a\\\"b\\\\c\\n\\t\\u0001z\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testEscapeUtf8PassesThrough() {
|
||||||
|
// Multi-byte UTF-8 passes through verbatim; only C0 controls are \u-escaped.
|
||||||
|
std::string out;
|
||||||
|
json::writeEscaped(out, "gr\xC3\xBC n"); // "grü n"
|
||||||
|
CHECK(out == "\"gr\xC3\xBC n\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- emit: numToStr -----------------------------------------------------------
|
||||||
|
|
||||||
|
static void testNumToStrIntForms() {
|
||||||
|
CHECK(json::numToStr(0) == "0");
|
||||||
|
CHECK(json::numToStr(-7) == "-7");
|
||||||
|
CHECK(json::numToStr(INT_MAX) == "2147483647");
|
||||||
|
CHECK(json::numToStr(static_cast<std::int64_t>(1) << 40) == "1099511627776");
|
||||||
|
CHECK(json::numToStr(2000.0) == "2000"); // %.17g drops the trailing .0
|
||||||
|
CHECK(json::numToStr(0.5) == "0.5");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testDoubleRoundTripsBitForBit() {
|
||||||
|
// %.17g is the shortest form that round-trips every IEEE-754 double.
|
||||||
|
const double v = 3141.592653589793;
|
||||||
|
double back = 0.0;
|
||||||
|
CHECK(doubleFrom(json::numToStr(v), back));
|
||||||
|
CHECK(back == v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- emit: Writer object grammar ----------------------------------------------
|
||||||
|
|
||||||
|
static void testWriterEmitsExactObjectBytes() {
|
||||||
|
std::string out;
|
||||||
|
{
|
||||||
|
json::Writer w(out);
|
||||||
|
w.keyRaw("a", json::numToStr(1));
|
||||||
|
w.keyStr("b", "x\"y");
|
||||||
|
w.keyBegin("c");
|
||||||
|
{
|
||||||
|
json::Writer nested(out);
|
||||||
|
nested.keyRaw("d", json::numToStr(2.5));
|
||||||
|
}
|
||||||
|
w.keyBegin("e");
|
||||||
|
json::writeStringArray(out, {"p", "q"});
|
||||||
|
w.keyBegin("f");
|
||||||
|
json::writeIntArray(out, {1, 2});
|
||||||
|
}
|
||||||
|
CHECK(out == "{\"a\":1,\"b\":\"x\\\"y\",\"c\":{\"d\":2.5},"
|
||||||
|
"\"e\":[\"p\",\"q\"],\"f\":[1,2]}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testEmptyArraysEmitBrackets() {
|
||||||
|
std::string s, i;
|
||||||
|
json::writeStringArray(s, {});
|
||||||
|
json::writeIntArray(i, {});
|
||||||
|
CHECK(s == "[]");
|
||||||
|
CHECK(i == "[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reader: strings ----------------------------------------------------------
|
||||||
|
|
||||||
|
static void testParseStringEscapes() {
|
||||||
|
std::string out;
|
||||||
|
CHECK(stringFrom(" \"a\\\"b\\\\c\\n\\u0041\"", out));
|
||||||
|
CHECK(out == "a\"b\\c\nA");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseStringSurrogatePairToUtf8() {
|
||||||
|
// \uD83D\uDE00 (grinning face) -> F0 9F 98 80.
|
||||||
|
std::string out;
|
||||||
|
CHECK(stringFrom("\"\\ud83d\\ude00\"", out));
|
||||||
|
CHECK(out == "\xF0\x9F\x98\x80");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseStringRejectsMalformed() {
|
||||||
|
std::string out;
|
||||||
|
CHECK(!stringFrom("\"unterminated", out));
|
||||||
|
CHECK(!stringFrom("\"bad\\qescape\"", out));
|
||||||
|
CHECK(!stringFrom("\"\\ud800 alone\"", out)); // unpaired high surrogate
|
||||||
|
CHECK(!stringFrom("\"\\udc00\"", out)); // unpaired low surrogate
|
||||||
|
CHECK(!stringFrom("noquote", out));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reader: numbers ----------------------------------------------------------
|
||||||
|
|
||||||
|
static void testParseIntAcceptsAndRejects() {
|
||||||
|
int v = 0;
|
||||||
|
CHECK(intFrom("42,", v)); CHECK(v == 42);
|
||||||
|
CHECK(intFrom("-7}", v)); CHECK(v == -7);
|
||||||
|
CHECK(intFrom("2147483647]", v)); CHECK(v == INT_MAX);
|
||||||
|
// Out of int range is REJECTED (the unified guard every consumer now shares).
|
||||||
|
CHECK(!intFrom("2147483648,", v));
|
||||||
|
CHECK(!intFrom("1.5,", v));
|
||||||
|
CHECK(!intFrom("x,", v));
|
||||||
|
CHECK(!intFrom("", v));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseInt64RangeAndReject() {
|
||||||
|
std::int64_t v = 0;
|
||||||
|
CHECK(int64From("9223372036854775807,", v));
|
||||||
|
CHECK(v == 9223372036854775807LL);
|
||||||
|
CHECK(!int64From("9223372036854775808,", v)); // ERANGE -> reject
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseDoubleRejectsRangeAndGarbage() {
|
||||||
|
double v = 0;
|
||||||
|
CHECK(!doubleFrom("1e999,", v)); // ERANGE
|
||||||
|
CHECK(!doubleFrom("1.5abc,", v)); // trailing bytes
|
||||||
|
CHECK(doubleFrom("2.5}", v)); CHECK(v == 2.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseBool() {
|
||||||
|
bool v = false;
|
||||||
|
CHECK(boolFrom("true,", v)); CHECK(v);
|
||||||
|
CHECK(boolFrom("false]", v)); CHECK(!v);
|
||||||
|
CHECK(!boolFrom("TRUE,", v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reader: structure --------------------------------------------------------
|
||||||
|
|
||||||
|
static void testExpectNullOr() {
|
||||||
|
{
|
||||||
|
const std::string s = "null,";
|
||||||
|
json::Reader r(s);
|
||||||
|
bool wasNull = false;
|
||||||
|
CHECK(r.expectNullOr(wasNull)); CHECK(wasNull); CHECK(r.consume(','));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string s = "\"x\"";
|
||||||
|
json::Reader r(s);
|
||||||
|
bool wasNull = true;
|
||||||
|
std::string v;
|
||||||
|
CHECK(r.expectNullOr(wasNull)); CHECK(!wasNull);
|
||||||
|
CHECK(r.parseString(v)); CHECK(v == "x");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string s;
|
||||||
|
json::Reader r(s);
|
||||||
|
bool wasNull = false;
|
||||||
|
CHECK(!r.expectNullOr(wasNull));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseKeyConsumesColon() {
|
||||||
|
const std::string s = " \"k\" : 1";
|
||||||
|
json::Reader r(s);
|
||||||
|
std::string k;
|
||||||
|
int v = 0;
|
||||||
|
CHECK(r.parseKey(k));
|
||||||
|
CHECK(k == "k");
|
||||||
|
CHECK(r.parseInt(v));
|
||||||
|
CHECK(v == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testParseArraysAppend() {
|
||||||
|
{
|
||||||
|
const std::string s = "[\"a\",\"b\"]";
|
||||||
|
json::Reader r(s);
|
||||||
|
std::vector<std::string> v;
|
||||||
|
CHECK(r.parseStringArray(v));
|
||||||
|
CHECK(v.size() == 2 && v[0] == "a" && v[1] == "b");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string s = "[]";
|
||||||
|
json::Reader r(s);
|
||||||
|
std::vector<std::string> v;
|
||||||
|
CHECK(r.parseStringArray(v)); CHECK(v.empty());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string s = "[1,2]"; // non-string element
|
||||||
|
json::Reader r(s);
|
||||||
|
std::vector<std::string> v;
|
||||||
|
CHECK(!r.parseStringArray(v));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string s = "[1,2,3]";
|
||||||
|
json::Reader r(s);
|
||||||
|
std::vector<int> v;
|
||||||
|
CHECK(r.parseIntArray(v));
|
||||||
|
CHECK(v.size() == 3 && v[2] == 3);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const std::string s = "[1,"; // truncated
|
||||||
|
json::Reader r(s);
|
||||||
|
std::vector<int> v;
|
||||||
|
CHECK(!r.parseIntArray(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testSkipValueOverNestedShapes() {
|
||||||
|
// Skips a nested object whose strings contain structural chars, then the
|
||||||
|
// cursor sits exactly on the next separator.
|
||||||
|
const std::string s = "{\"deep\":[\"}\",{\"x\":\"]\"}]},7";
|
||||||
|
json::Reader r(s);
|
||||||
|
CHECK(r.skipValue());
|
||||||
|
CHECK(r.consume(','));
|
||||||
|
int v = 0;
|
||||||
|
CHECK(r.parseInt(v));
|
||||||
|
CHECK(v == 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testCaptureValueVerbatim() {
|
||||||
|
const std::string s = " {\"a\":[1,\"{\"]} ,tail";
|
||||||
|
json::Reader r(s);
|
||||||
|
std::string raw;
|
||||||
|
CHECK(r.captureValue(raw));
|
||||||
|
CHECK(raw == "{\"a\":[1,\"{\"]}");
|
||||||
|
CHECK(r.consume(','));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testWriterOutputParsesBack() {
|
||||||
|
// The emitted object is consumable by the Reader — the seam the five
|
||||||
|
// consumers rely on (writer and reader agree on one dialect).
|
||||||
|
std::string out;
|
||||||
|
{
|
||||||
|
json::Writer w(out);
|
||||||
|
w.keyStr("name", "tab\there");
|
||||||
|
w.keyRaw("n", json::numToStr(-3));
|
||||||
|
}
|
||||||
|
json::Reader r(out);
|
||||||
|
CHECK(r.consume('{'));
|
||||||
|
std::string k1, v1;
|
||||||
|
CHECK(r.parseKey(k1) && k1 == "name");
|
||||||
|
CHECK(r.parseString(v1) && v1 == "tab\there");
|
||||||
|
CHECK(r.consume(','));
|
||||||
|
std::string k2;
|
||||||
|
int v2 = 0;
|
||||||
|
CHECK(r.parseKey(k2) && k2 == "n");
|
||||||
|
CHECK(r.parseInt(v2) && v2 == -3);
|
||||||
|
CHECK(r.consume('}'));
|
||||||
|
r.skipWs();
|
||||||
|
CHECK(r.eof());
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testEscapeExactBytes();
|
||||||
|
testEscapeUtf8PassesThrough();
|
||||||
|
testNumToStrIntForms();
|
||||||
|
testDoubleRoundTripsBitForBit();
|
||||||
|
testWriterEmitsExactObjectBytes();
|
||||||
|
testEmptyArraysEmitBrackets();
|
||||||
|
testParseStringEscapes();
|
||||||
|
testParseStringSurrogatePairToUtf8();
|
||||||
|
testParseStringRejectsMalformed();
|
||||||
|
testParseIntAcceptsAndRejects();
|
||||||
|
testParseInt64RangeAndReject();
|
||||||
|
testParseDoubleRejectsRangeAndGarbage();
|
||||||
|
testParseBool();
|
||||||
|
testExpectNullOr();
|
||||||
|
testParseKeyConsumesColon();
|
||||||
|
testParseArraysAppend();
|
||||||
|
testSkipValueOverNestedShapes();
|
||||||
|
testCaptureValueVerbatim();
|
||||||
|
testWriterOutputParsesBack();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("json: all tests passed\n");
|
||||||
|
else std::printf("json: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -135,6 +135,20 @@ static void testRoundTripAuto() {
|
|||||||
CHECK(back && settingsEqual(*back, s));
|
CHECK(back && settingsEqual(*back, s));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void testSerializeByteIdentity() {
|
||||||
|
// Q-W1 structural-dedupe guard: the core/json-backed writer must emit the
|
||||||
|
// EXACT bytes the former snprintf writer produced ({"mode":%d,"manualMs":%.17g})
|
||||||
|
// and re-serializing a round-tripped setting must be byte-identical — the blob
|
||||||
|
// lives in the .rpp, so a byte shift would dirty every saved project.
|
||||||
|
TailSetting s; // None + 2000.0 default
|
||||||
|
CHECK(serializeTailSetting(s) == "{\"mode\":0,\"manualMs\":2000}");
|
||||||
|
TailSetting man; man.mode = TailMode::Manual; man.manualMs = 3141.592653589793;
|
||||||
|
const std::string json = serializeTailSetting(man);
|
||||||
|
auto back = deserializeTailSetting(json);
|
||||||
|
CHECK(back.has_value());
|
||||||
|
CHECK(back && serializeTailSetting(*back) == json); // stable second round-trip
|
||||||
|
}
|
||||||
|
|
||||||
static void testDeserializeEmptyIsDefault() {
|
static void testDeserializeEmptyIsDefault() {
|
||||||
// An absent/empty stored value (older project) -> nullopt, so the caller falls
|
// An absent/empty stored value (older project) -> nullopt, so the caller falls
|
||||||
// back to the default. This is the graceful-old-project path the brief requires.
|
// back to the default. This is the graceful-old-project path the brief requires.
|
||||||
@@ -164,6 +178,7 @@ int main() {
|
|||||||
testRoundTripNoneDefault();
|
testRoundTripNoneDefault();
|
||||||
testRoundTripManualArbitraryMs();
|
testRoundTripManualArbitraryMs();
|
||||||
testRoundTripAuto();
|
testRoundTripAuto();
|
||||||
|
testSerializeByteIdentity();
|
||||||
testDeserializeEmptyIsDefault();
|
testDeserializeEmptyIsDefault();
|
||||||
testDeserializeMalformedIsDefault();
|
testDeserializeMalformedIsDefault();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Standalone tests for reasampler::wire — no REAPER, no framework. The ONE
|
||||||
|
// length-prefixed ext-state wire codec (Q-W1, T2-01b) behind provenance /
|
||||||
|
// assignment_request / sample_usage / bank_sync. The consumers' own suites
|
||||||
|
// prove their record grammars round-trip; this suite pins the CODEC contract —
|
||||||
|
// byte-exact encode, the full hardening (length caps, overflow guards,
|
||||||
|
// subtraction-first bounds), and the fixed fieldInt range rejection.
|
||||||
|
//
|
||||||
|
// NOTE: wire::Cursor BORROWS its input string, so every helper takes a named /
|
||||||
|
// reference-bound std::string — the Cursor never outlives its buffer.
|
||||||
|
|
||||||
|
#include "../src/core/wire/wire.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace reasampler;
|
||||||
|
|
||||||
|
static int g_fail = 0;
|
||||||
|
#define CHECK(cond) do { if(!(cond)) { \
|
||||||
|
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
|
||||||
|
|
||||||
|
// One length-prefixed field around `v` — the writer-side convention.
|
||||||
|
static std::string enc(const std::string& v) {
|
||||||
|
std::string out;
|
||||||
|
wire::putField(out, v);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single-field decode helpers (Cursor + buffer share the call's lifetime).
|
||||||
|
static bool fieldFrom(const std::string& s, std::string& out) {
|
||||||
|
wire::Cursor c(s);
|
||||||
|
return c.field(out);
|
||||||
|
}
|
||||||
|
static bool i64From(const std::string& s, std::int64_t& out) {
|
||||||
|
wire::Cursor c(s);
|
||||||
|
return c.fieldInt64(out);
|
||||||
|
}
|
||||||
|
static bool intFrom(const std::string& s, int& out) {
|
||||||
|
wire::Cursor c(s);
|
||||||
|
return c.fieldInt(out);
|
||||||
|
}
|
||||||
|
static bool sizeFrom(const std::string& s, std::size_t& out) {
|
||||||
|
wire::Cursor c(s);
|
||||||
|
return c.fieldSizeT(out);
|
||||||
|
}
|
||||||
|
static bool dblFrom(const std::string& s, double& out) {
|
||||||
|
wire::Cursor c(s);
|
||||||
|
return c.fieldDouble(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- putField: byte-exact encode ----------------------------------------------
|
||||||
|
|
||||||
|
static void testPutFieldExactBytes() {
|
||||||
|
std::string out;
|
||||||
|
wire::putField(out, "abc");
|
||||||
|
CHECK(out == "3:abc");
|
||||||
|
wire::putField(out, ""); // empty field is legal: "0:"
|
||||||
|
CHECK(out == "3:abc0:");
|
||||||
|
wire::putField(out, "a:b"); // ':' inside a value cannot shift the parse
|
||||||
|
CHECK(out == "3:abc0:3:a:b");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cursor: round-trip + literal ---------------------------------------------
|
||||||
|
|
||||||
|
static void testFieldRoundTripIncludingSeparators() {
|
||||||
|
std::string out = "magic";
|
||||||
|
wire::putField(out, "12:34"); // digits + colons in the value
|
||||||
|
wire::putField(out, "");
|
||||||
|
wire::putField(out, "tail");
|
||||||
|
wire::Cursor c(out);
|
||||||
|
std::string a, b, t;
|
||||||
|
CHECK(c.literal("magic"));
|
||||||
|
CHECK(c.field(a) && a == "12:34");
|
||||||
|
CHECK(c.field(b) && b.empty());
|
||||||
|
CHECK(c.field(t) && t == "tail");
|
||||||
|
CHECK(c.ok() && c.atEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testLiteralMismatchFails() {
|
||||||
|
const std::string good = "rsprov1x";
|
||||||
|
const std::string wrong = "rsprov0x";
|
||||||
|
const std::string truncated = "rspro";
|
||||||
|
{ wire::Cursor c(good); CHECK(c.literal("rsprov1")); }
|
||||||
|
{ wire::Cursor c(wrong); CHECK(!c.literal("rsprov1")); CHECK(!c.ok()); }
|
||||||
|
{ wire::Cursor c(truncated); CHECK(!c.literal("rsprov1")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cursor: field hardening ---------------------------------------------------
|
||||||
|
|
||||||
|
static void testFieldRejectsMalformedLengths() {
|
||||||
|
std::string f;
|
||||||
|
CHECK(!fieldFrom("abc", f)); // no colon
|
||||||
|
CHECK(!fieldFrom(":x", f)); // empty length
|
||||||
|
CHECK(!fieldFrom("2x:ab", f)); // non-digit length
|
||||||
|
CHECK(!fieldFrom("9:ab", f)); // runs past end
|
||||||
|
// A 200-digit length cannot accumulate past SIZE_MAX (digit-run cap).
|
||||||
|
CHECK(!fieldFrom(std::string(200, '9') + ":x", f));
|
||||||
|
// Exactly-20-digit values: SIZE_MAX itself passes the accumulate but fails the
|
||||||
|
// bounds check; one past SIZE_MAX trips the overflow guard.
|
||||||
|
CHECK(!fieldFrom("18446744073709551615:x", f));
|
||||||
|
CHECK(!fieldFrom("18446744073709551616:x", f));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFailureLatchesOk() {
|
||||||
|
// After one failed read every subsequent read fails too — the caller may
|
||||||
|
// check ok() once at the end (the "never a partial value" discipline).
|
||||||
|
const std::string s = "3:abc";
|
||||||
|
wire::Cursor c(s);
|
||||||
|
std::string f;
|
||||||
|
CHECK(!c.literal("nope"));
|
||||||
|
CHECK(!c.field(f));
|
||||||
|
CHECK(!c.ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cursor: fieldInt64 / fieldInt --------------------------------------------
|
||||||
|
|
||||||
|
static void testFieldInt64AcceptsAndRejects() {
|
||||||
|
std::int64_t v = 0;
|
||||||
|
CHECK(i64From(enc("12345"), v)); CHECK(v == 12345);
|
||||||
|
CHECK(i64From(enc("-42"), v)); CHECK(v == -42);
|
||||||
|
CHECK(i64From(enc("9223372036854775807"), v)); // INT64_MAX
|
||||||
|
CHECK(v == 9223372036854775807LL);
|
||||||
|
CHECK(!i64From(enc("9223372036854775808"), v)); // overflow
|
||||||
|
CHECK(!i64From(enc("12345678901234567890"), v)); // 20-digit cap
|
||||||
|
CHECK(!i64From(enc("-"), v)); // bare sign
|
||||||
|
CHECK(!i64From(enc("1a"), v)); // non-digit
|
||||||
|
CHECK(!i64From(enc(""), v)); // empty
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFieldIntRejectsOutOfIntRange() {
|
||||||
|
// The fixed form of the old provenance strtol TODO: an out-of-int-range field
|
||||||
|
// FAILS the parse instead of silently narrowing.
|
||||||
|
int v = 0;
|
||||||
|
CHECK(intFrom(enc("2147483647"), v)); CHECK(v == 2147483647);
|
||||||
|
CHECK(intFrom(enc("-2147483648"), v)); CHECK(v == -2147483647 - 1);
|
||||||
|
CHECK(!intFrom(enc("2147483648"), v));
|
||||||
|
CHECK(!intFrom(enc("3000000000"), v));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cursor: fieldSizeT / fieldDouble ------------------------------------------
|
||||||
|
|
||||||
|
static void testFieldSizeT() {
|
||||||
|
std::size_t v = 1;
|
||||||
|
CHECK(sizeFrom(enc("0"), v)); CHECK(v == 0);
|
||||||
|
CHECK(sizeFrom(enc("4096"), v)); CHECK(v == 4096);
|
||||||
|
CHECK(!sizeFrom(enc("-1"), v)); // sign = non-digit
|
||||||
|
CHECK(!sizeFrom(enc(std::string(21, '9')), v)); // 21-digit cap
|
||||||
|
CHECK(!sizeFrom(enc("18446744073709551616"), v)); // overflow guard
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testFieldDoubleRoundTrip() {
|
||||||
|
char buf[32];
|
||||||
|
std::snprintf(buf, sizeof(buf), "%.17g", 3141.592653589793);
|
||||||
|
double v = 0;
|
||||||
|
CHECK(dblFrom(enc(buf), v));
|
||||||
|
CHECK(v == 3141.592653589793);
|
||||||
|
CHECK(!dblFrom(enc("1.5x"), v)); // trailing bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- parseUnsignedDecimal (the bank_sync generation core) -----------------------
|
||||||
|
|
||||||
|
static void testParseUnsignedDecimal() {
|
||||||
|
std::int64_t v = 0;
|
||||||
|
CHECK(wire::parseUnsignedDecimal("0", v) && v == 0);
|
||||||
|
CHECK(wire::parseUnsignedDecimal("1721947293", v) && v == 1721947293);
|
||||||
|
CHECK(wire::parseUnsignedDecimal("9223372036854775807", v) && v == 9223372036854775807LL);
|
||||||
|
CHECK(!wire::parseUnsignedDecimal("", v));
|
||||||
|
CHECK(!wire::parseUnsignedDecimal("+5", v)); // sign rejected (non-digit)
|
||||||
|
CHECK(!wire::parseUnsignedDecimal("-5", v));
|
||||||
|
CHECK(!wire::parseUnsignedDecimal("12a", v));
|
||||||
|
CHECK(!wire::parseUnsignedDecimal("9223372036854775808", v)); // overflow
|
||||||
|
CHECK(!wire::parseUnsignedDecimal(std::string(40, '9'), v)); // long run cannot wrap
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testPutFieldExactBytes();
|
||||||
|
testFieldRoundTripIncludingSeparators();
|
||||||
|
testLiteralMismatchFails();
|
||||||
|
testFieldRejectsMalformedLengths();
|
||||||
|
testFailureLatchesOk();
|
||||||
|
testFieldInt64AcceptsAndRejects();
|
||||||
|
testFieldIntRejectsOutOfIntRange();
|
||||||
|
testFieldSizeT();
|
||||||
|
testFieldDoubleRoundTrip();
|
||||||
|
testParseUnsignedDecimal();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("wire: all tests passed\n");
|
||||||
|
else std::printf("wire: %d CHECK(s) FAILED\n", g_fail);
|
||||||
|
return g_fail == 0 ? 0 : 1;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user