fix(pS-usage): fail-safe prune protection — in-wire owner nonce + sticky union poison, protect-all on zero identified, abort on unreadable record, rsusage_ prefix
This commit is contained in:
+107
-91
@@ -18,16 +18,17 @@
|
||||
|
||||
#include "usage_scan.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "app_version.h" // vstPluginName (channel display-name fallback match)
|
||||
#include "app_version.h" // vstPluginName / vstOutputName (channel name needles)
|
||||
#include "ext_keys.h" // kProjExtNamespace / kProjExtUsageKeyPrefix
|
||||
#include "instrument_drop.h" // vstClassIdHex — the frozen channel class-UID hex
|
||||
#include "sample_usage.h" // decodeUsageRecord, usageHeldPaths (the pure decisions)
|
||||
#include "sample_usage.h" // identityMatches, foldUsageRecords (the pure decisions)
|
||||
#include "track_guid.h" // guidString — the ONE canonical GUID key formatter
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
@@ -52,28 +53,53 @@ namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string toUpperAscii(const std::string& s) {
|
||||
std::string out = s;
|
||||
for (char& c : out)
|
||||
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
return out;
|
||||
// The three UPPERCASED channel needles identityMatches (pure, sample_usage) checks
|
||||
// every FX identity string against. One instance drives the whole scan.
|
||||
struct FxIdentityNeedles {
|
||||
std::string uidHexUpper; // 32-hex VST3 class UID (may not appear on all builds)
|
||||
std::string outputNameUpper; // "REASAMPLER_9000" — the .vst3 filename base fx_ident embeds
|
||||
std::string nameUpper; // "REASAMPLER 9000" — factory display name
|
||||
};
|
||||
|
||||
// A named-config-parm getter abstracted over the FX-chain kind: track FX and take FX
|
||||
// share the identical identity walk (fx_ident + original_name + container recursion),
|
||||
// differing only in which REAPER getter reads the parm.
|
||||
using FxParmGetter =
|
||||
std::function<std::string(int fxId, const char* parm)>;
|
||||
|
||||
// True if any FX in the (possibly container-nested) sub-chain rooted at `fxId` is a
|
||||
// ReaSampler 9000. BOTH fx_ident and original_name are checked on BOTH chain kinds (a
|
||||
// renamed instance keeps its original_name; fx_ident carries the module path — the
|
||||
// review's take-path gap is closed by sharing this one walk). Containers are walked via
|
||||
// the documented container_count / container_item.X addressing (v7.06+); on a chain
|
||||
// kind or REAPER version without containers the parm read returns empty and recursion
|
||||
// is a no-op. `depth` bounds pathological nesting. fx_ident is queried per FX — chain
|
||||
// enumeration is chunk-level, so OFFLINE instances match too (load-bearing: a
|
||||
// Design-View-parked instance must keep protecting its holds).
|
||||
bool fxSubtreeHasInstance(const FxParmGetter& parm, int fxId,
|
||||
const FxIdentityNeedles& id, int depth) {
|
||||
if (identityMatches(parm(fxId, "fx_ident"), id.uidHexUpper, id.nameUpper,
|
||||
id.outputNameUpper) ||
|
||||
identityMatches(parm(fxId, "original_name"), id.uidHexUpper, id.nameUpper,
|
||||
id.outputNameUpper))
|
||||
return true;
|
||||
if (depth <= 0) return false;
|
||||
const std::string countStr = parm(fxId, "container_count");
|
||||
if (countStr.empty()) return false; // not a container
|
||||
const int n = std::atoi(countStr.c_str());
|
||||
for (int k = 0; k < n; ++k) {
|
||||
const std::string item =
|
||||
parm(fxId, ("container_item." + std::to_string(k)).c_str());
|
||||
if (item.empty()) continue;
|
||||
const int childId = std::atoi(item.c_str());
|
||||
if (childId <= 0) continue;
|
||||
if (fxSubtreeHasInstance(parm, childId, id, depth - 1)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does this FX identity string name a ReaSampler 9000 of THIS channel? Primary match:
|
||||
// fx_ident contains the channel's 32-hex class UID (REAPER renders VST3 idents with the
|
||||
// UID hex embedded; case varies, so compare uppercased). Fallback: the identity carries
|
||||
// the channel display name ("ReaSampler 9000" / "ReaSampler 9000 beta") — belt and
|
||||
// braces for an fx_ident rendering that omits the hex. A false positive here only
|
||||
// widens the protected set (fail-safe direction); it can never cause a delete.
|
||||
bool identityMatches(const std::string& identity, const std::string& uidHexUpper,
|
||||
const std::string& nameUpper) {
|
||||
if (identity.empty()) return false;
|
||||
const std::string up = toUpperAscii(identity);
|
||||
if (!uidHexUpper.empty() && up.find(uidHexUpper) != std::string::npos) return true;
|
||||
return !nameUpper.empty() && up.find(nameUpper) != std::string::npos;
|
||||
}
|
||||
constexpr int kMaxContainerDepth = 8;
|
||||
|
||||
// Read one named config parm of a track FX into a string ("" on failure/absence).
|
||||
std::string trackFxParm(MediaTrack* tr, int fxId, const char* parm) {
|
||||
char buf[2048] = {0};
|
||||
if (!TrackFX_GetNamedConfigParm(tr, fxId, parm, buf, static_cast<int>(sizeof(buf))))
|
||||
@@ -81,47 +107,26 @@ std::string trackFxParm(MediaTrack* tr, int fxId, const char* parm) {
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// True if any FX in the (possibly container-nested) sub-chain rooted at `fxId` is a
|
||||
// ReaSampler 9000. Containers are walked via the documented container_count /
|
||||
// container_item.X addressing (v7.06+); `depth` bounds pathological nesting. fx_ident
|
||||
// is queried per FX — chain enumeration is chunk-level, so OFFLINE instances match too
|
||||
// (load-bearing: a Design-View-parked instance must keep protecting its holds).
|
||||
bool trackFxSubtreeHasInstance(MediaTrack* tr, int fxId,
|
||||
const std::string& uidHexUpper,
|
||||
const std::string& nameUpper, int depth) {
|
||||
if (identityMatches(trackFxParm(tr, fxId, "fx_ident"), uidHexUpper, nameUpper) ||
|
||||
identityMatches(trackFxParm(tr, fxId, "original_name"), uidHexUpper, nameUpper))
|
||||
return true;
|
||||
if (depth <= 0) return false;
|
||||
const std::string countStr = trackFxParm(tr, fxId, "container_count");
|
||||
if (countStr.empty()) return false; // not a container
|
||||
const int n = std::atoi(countStr.c_str());
|
||||
for (int k = 0; k < n; ++k) {
|
||||
const std::string item =
|
||||
trackFxParm(tr, fxId, ("container_item." + std::to_string(k)).c_str());
|
||||
if (item.empty()) continue;
|
||||
const int childId = std::atoi(item.c_str());
|
||||
if (childId <= 0) continue;
|
||||
if (trackFxSubtreeHasInstance(tr, childId, uidHexUpper, nameUpper, depth - 1))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
std::string takeFxParm(MediaItem_Take* take, int fxId, const char* parm) {
|
||||
char buf[2048] = {0};
|
||||
if (!TakeFX_GetNamedConfigParm(take, fxId, parm, buf, static_cast<int>(sizeof(buf))))
|
||||
return {};
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
// True if `tr` hosts >= 1 ReaSampler 9000 anywhere: normal chain, record/input chain
|
||||
// (index | 0x1000000), containers recursively.
|
||||
bool trackHasInstance(MediaTrack* tr, const std::string& uidHexUpper,
|
||||
const std::string& nameUpper) {
|
||||
constexpr int kMaxContainerDepth = 8;
|
||||
bool trackHasInstance(MediaTrack* tr, const FxIdentityNeedles& id) {
|
||||
const FxParmGetter parm = [tr](int fxId, const char* p) {
|
||||
return trackFxParm(tr, fxId, p);
|
||||
};
|
||||
const int n = TrackFX_GetCount(tr);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (trackFxSubtreeHasInstance(tr, i, uidHexUpper, nameUpper, kMaxContainerDepth))
|
||||
return true;
|
||||
if (fxSubtreeHasInstance(parm, i, id, kMaxContainerDepth)) return true;
|
||||
}
|
||||
const int rec = TrackFX_GetRecCount(tr);
|
||||
for (int i = 0; i < rec; ++i) {
|
||||
if (trackFxSubtreeHasInstance(tr, 0x1000000 + i, uidHexUpper, nameUpper,
|
||||
kMaxContainerDepth))
|
||||
if (fxSubtreeHasInstance(parm, 0x1000000 + i, id, kMaxContainerDepth))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -129,50 +134,53 @@ bool trackHasInstance(MediaTrack* tr, const std::string& uidHexUpper,
|
||||
|
||||
// True if any take FX on `item` is a ReaSampler 9000 (all takes, not just active — a
|
||||
// non-active take's instance still exists in the project and reactivates with the
|
||||
// take). No container recursion here: take chains are queried flat, and a sampler
|
||||
// nested in a take-FX container is exotic enough that the empty-trackGuid any-instance
|
||||
// fallback (sample_usage liveness rule) is the documented safety net.
|
||||
bool itemHasInstance(MediaItem* item, const std::string& uidHexUpper,
|
||||
const std::string& nameUpper) {
|
||||
// take). The SAME identity walk as the track path: fx_ident + original_name + container
|
||||
// recursion (an unrecognized exotic still lands in the pure protect-all net — records
|
||||
// with zero identified instances protect everything rather than nothing).
|
||||
bool itemHasInstance(MediaItem* item, const FxIdentityNeedles& id) {
|
||||
const int takes = CountTakes(item);
|
||||
for (int t = 0; t < takes; ++t) {
|
||||
MediaItem_Take* take = GetMediaItemTake(item, t);
|
||||
if (!take) continue;
|
||||
const FxParmGetter parm = [take](int fxId, const char* p) {
|
||||
return takeFxParm(take, fxId, p);
|
||||
};
|
||||
const int n = TakeFX_GetCount(take);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
char buf[2048] = {0};
|
||||
if (TakeFX_GetNamedConfigParm(take, i, "fx_ident", buf,
|
||||
static_cast<int>(sizeof(buf))) &&
|
||||
identityMatches(buf, uidHexUpper, nameUpper))
|
||||
return true;
|
||||
if (fxSubtreeHasInstance(parm, i, id, kMaxContainerDepth)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Growing GetProjExtState read (the persist.cpp idiom): the usage record scales with
|
||||
// the hold count, so a fixed buffer risks a truncated decode — and an undecodable
|
||||
// record protects nothing, which is the DANGEROUS direction here. Empty on absence.
|
||||
std::string readExtStateValue(ReaProject* proj, const char* key) {
|
||||
// the hold count, so a fixed buffer risks a truncated decode. Returns nullopt when the
|
||||
// key cannot be read WHOLE — absent-after-enumeration (rv <= 0) or pathologically large
|
||||
// (> 16 MB give-up). The caller only queries keys the enumeration just listed, so a
|
||||
// nullopt here is a PRESENT-BUT-UNREADABLE record: it folds to abortPrune (fail-safe —
|
||||
// silently reduced protection is the delete direction).
|
||||
std::optional<std::string> readExtStateValue(ReaProject* proj, const char* key) {
|
||||
for (int cap = 1 << 12; cap <= (1 << 24); cap <<= 2) {
|
||||
std::vector<char> buf(static_cast<std::size_t>(cap), '\0');
|
||||
const int rv = GetProjExtState(proj, kProjExtNamespace(), key, buf.data(), cap);
|
||||
if (rv <= 0) return {};
|
||||
if (rv <= 0) return std::nullopt;
|
||||
std::string s(buf.data());
|
||||
if (static_cast<int>(s.size()) + 1 < cap) return s;
|
||||
// else possibly truncated -> grow and retry
|
||||
}
|
||||
return {};
|
||||
return std::nullopt; // > 16 MB — unreadable whole, never "absent"
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::string> liveInstanceHeldPaths(void* projOpaque) {
|
||||
UsageScanResult liveInstanceHeldPaths(void* projOpaque) {
|
||||
ReaProject* proj = static_cast<ReaProject*>(projOpaque);
|
||||
UsageScanResult result;
|
||||
|
||||
// 1. Enumerate the usage_* keys and decode each record. Key names first (values via
|
||||
// the growing reader — EnumProjExtState's fixed val buffer could truncate a large
|
||||
// record, and a truncated record decodes to nothing = protects nothing).
|
||||
// 1. Enumerate the rsusage_* keys and read+decode each record. Key names first
|
||||
// (values via the growing reader — EnumProjExtState's fixed val buffer could
|
||||
// truncate a large record). A nullopt element = present-but-unreadable/
|
||||
// undecodable -> the pure fold ABORTS the prune.
|
||||
std::vector<std::string> usageKeys;
|
||||
{
|
||||
char keyBuf[256];
|
||||
@@ -186,26 +194,30 @@ std::vector<std::string> liveInstanceHeldPaths(void* projOpaque) {
|
||||
if (key.compare(0, prefix.size(), prefix) == 0) usageKeys.push_back(key);
|
||||
}
|
||||
}
|
||||
std::vector<UsageRecord> records;
|
||||
records.reserve(usageKeys.size());
|
||||
for (const std::string& key : usageKeys) {
|
||||
const std::string value = readExtStateValue(proj, key.c_str());
|
||||
if (value.empty()) continue;
|
||||
if (std::optional<UsageRecord> rec = decodeUsageRecord(value)) {
|
||||
records.push_back(std::move(*rec));
|
||||
}
|
||||
}
|
||||
if (records.empty()) return {}; // no instance ever published — skip the FX scan
|
||||
if (usageKeys.empty()) return result; // no instance ever published — skip the scan
|
||||
|
||||
// 2. Enumerate live ReaSampler 9000 hosts. One channel-frozen identity pair drives
|
||||
std::vector<std::optional<UsageRecord>> decoded;
|
||||
decoded.reserve(usageKeys.size());
|
||||
for (const std::string& key : usageKeys) {
|
||||
const std::optional<std::string> value = readExtStateValue(proj, key.c_str());
|
||||
if (!value) {
|
||||
decoded.push_back(std::nullopt); // unreadable -> abort (pure fold)
|
||||
continue;
|
||||
}
|
||||
decoded.push_back(decodeUsageRecord(*value)); // undecodable -> nullopt -> abort
|
||||
}
|
||||
|
||||
// 2. Enumerate live ReaSampler 9000 hosts. One channel-frozen needle set drives
|
||||
// every match; a track needs only ONE instance to keep all its records live.
|
||||
const std::string uidHexUpper = toUpperAscii(vstClassIdHex());
|
||||
const std::string nameUpper = toUpperAscii(vstPluginName());
|
||||
FxIdentityNeedles id;
|
||||
id.uidHexUpper = toUpperAscii(vstClassIdHex());
|
||||
id.outputNameUpper = toUpperAscii(vstOutputName());
|
||||
id.nameUpper = toUpperAscii(vstPluginName());
|
||||
std::unordered_set<std::string> liveTrackGuids;
|
||||
bool anyLive = false;
|
||||
|
||||
if (MediaTrack* master = GetMasterTrack(proj)) {
|
||||
if (trackHasInstance(master, uidHexUpper, nameUpper)) {
|
||||
if (trackHasInstance(master, id)) {
|
||||
liveTrackGuids.insert(guidString(master));
|
||||
anyLive = true;
|
||||
}
|
||||
@@ -214,7 +226,7 @@ std::vector<std::string> liveInstanceHeldPaths(void* projOpaque) {
|
||||
for (int i = 0; i < trackCount; ++i) {
|
||||
MediaTrack* tr = GetTrack(proj, i);
|
||||
if (!tr) continue;
|
||||
if (trackHasInstance(tr, uidHexUpper, nameUpper)) {
|
||||
if (trackHasInstance(tr, id)) {
|
||||
liveTrackGuids.insert(guidString(tr));
|
||||
anyLive = true;
|
||||
}
|
||||
@@ -225,7 +237,7 @@ std::vector<std::string> liveInstanceHeldPaths(void* projOpaque) {
|
||||
for (int i = 0; i < itemCount; ++i) {
|
||||
MediaItem* item = GetMediaItem(proj, i);
|
||||
if (!item) continue;
|
||||
if (itemHasInstance(item, uidHexUpper, nameUpper)) {
|
||||
if (itemHasInstance(item, id)) {
|
||||
if (MediaTrack* tr = GetMediaItemTrack(item)) {
|
||||
liveTrackGuids.insert(guidString(tr));
|
||||
}
|
||||
@@ -233,8 +245,12 @@ std::vector<std::string> liveInstanceHeldPaths(void* projOpaque) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. The pure liveness fold decides which records count.
|
||||
return usageHeldPaths(records, liveTrackGuids, anyLive);
|
||||
// 3. The pure fold decides: abort on any unreadable record; protect-all when zero
|
||||
// instances were identified; otherwise the per-record liveness rule.
|
||||
const UsageFoldResult fold = foldUsageRecords(decoded, liveTrackGuids, anyLive);
|
||||
result.abortPrune = fold.abortPrune;
|
||||
result.heldPaths = fold.heldPaths;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user