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