Q-W5: persist → session/ext_state_io/prune_fs (deletion authority concentrated); one GetProjExtState grow-loop in bridge_marshal (T2-04, ×3 rewired); bank_book JSON codec → bank_book_json via private static nameKey; persist.h stays umbrella. 61/61 green.

This commit is contained in:
2026-07-29 12:56:06 -04:00
parent bbbb69ee55
commit 75aa93f913
16 changed files with 1899 additions and 1514 deletions
+60
View File
@@ -21,6 +21,8 @@
#include <optional>
#include <string>
#include <utility>
#include <vector>
namespace reasampler::instrument::map {
@@ -34,4 +36,62 @@ namespace reasampler::instrument::map {
std::optional<std::string> decodeGetProjExtState(int apiReturn,
const std::string& buffer);
// ---------------------------------------------------------------------------
// The GetProjExtState GROW-LOOP retry policy (Q-W5 rider, T2-04).
// ---------------------------------------------------------------------------
// GetProjExtState writes into a caller-supplied buffer with no documented
// query-the-size call, so a large value (bank blob, usage record) must be read by
// growing a buffer until the value fits strictly inside it. Three shells carried
// hand-rolled copies of that loop (persist's ext-state reads, usage_scan's
// prune-safety-adjacent record read, reaper_bridge's VST-side bank read); the ONE
// policy now lives here so the retry/termination rules cannot drift. The fiddly
// part is the termination taxonomy, which each caller folds differently:
//
// * Absent — the API returned <= 0 on some attempt: the key holds no value.
// (persist -> "" empty bank; usage_scan / bridge -> nullopt)
// * Complete — the written C string fits STRICTLY inside the buffer (size+1 <
// cap), so it cannot have been clipped: `value` is the whole value.
// * Overflow — the value never fit under the 16 MB ceiling: it is unreadable
// WHOLE, which is NOT the same as absent. (persist warns on the
// console; usage_scan folds it to the prune fail-safe abort)
//
// `read` is one GetProjExtState-shaped attempt: int read(char* buf, int cap),
// returning the API's int. A template, statically dispatched per call site — no
// virtual calls, no std::function (the §3 performance guardrail); the caller binds
// the project/namespace/key (or a resolved function pointer, VST side) in a lambda.
struct GrowingExtStateRead {
enum class Status { Absent, Complete, Overflow };
Status status = Status::Absent;
int apiReturn = 0; // the FINAL attempt's return (<= 0 iff Absent); feeds
// decodeGetProjExtState on the bridge path unchanged
std::string value; // the whole value; meaningful only when Complete
};
template <class ReadFn>
GrowingExtStateRead readProjExtStateGrowing(ReadFn&& read) {
// Start generous; grow ×4 if REAPER reports the value may have been clipped
// (the return is the value length; equal-to-capacity-minus-NUL is ambiguous,
// so only a strict fit terminates). Ceiling 16 MB — give up rather than loop
// forever on a pathological value.
GrowingExtStateRead result;
for (int cap = 1 << 16; cap <= (1 << 24); cap <<= 2) {
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
const int rv = read(buf.data(), cap);
result.apiReturn = rv;
if (rv <= 0) {
result.status = GrowingExtStateRead::Status::Absent;
return result;
}
std::string s(buf.data());
if (static_cast<int>(s.size()) + 1 < cap) {
result.status = GrowingExtStateRead::Status::Complete;
result.value = std::move(s);
return result;
}
// else: possibly truncated -> grow and retry.
}
result.status = GrowingExtStateRead::Status::Overflow;
return result;
}
} // namespace reasampler::instrument::map