302 lines
13 KiB
C++
302 lines
13 KiB
C++
// sample_usage.cpp — see sample_usage.h. Pure: standard library only.
|
|
|
|
#include "sample_usage.h"
|
|
|
|
#include <cctype>
|
|
#include <cstddef>
|
|
#include <limits>
|
|
|
|
namespace reasampler {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kMagic = "rsusage1";
|
|
|
|
// Append one length-prefixed field: <decimal-len> ':' <bytes>. The same wire idiom as
|
|
// assignment_request / provenance — one grammar across every ext-state seam.
|
|
void putField(std::string& out, const std::string& field) {
|
|
out += std::to_string(field.size());
|
|
out += ':';
|
|
out += field;
|
|
}
|
|
|
|
// 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
|
|
|
|
std::string encodeUsageRecord(const UsageRecord& rec) {
|
|
std::string out = kMagic;
|
|
putField(out, rec.trackGuid);
|
|
putField(out, rec.ownerNonce);
|
|
putField(out, rec.unioned ? "1" : "0");
|
|
putField(out, std::to_string(rec.holds.size()));
|
|
for (const UsageHold& h : rec.holds) {
|
|
putField(out, h.sampleId);
|
|
putField(out, h.relativePath);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::optional<UsageRecord> decodeUsageRecord(const std::string& wire) {
|
|
Cursor c(wire);
|
|
if (!c.literal(kMagic)) return std::nullopt;
|
|
UsageRecord rec;
|
|
if (!c.field(rec.trackGuid)) return std::nullopt;
|
|
if (!c.field(rec.ownerNonce)) return std::nullopt;
|
|
std::string unionedField;
|
|
if (!c.field(unionedField)) return std::nullopt;
|
|
if (unionedField == "1") rec.unioned = true;
|
|
else if (unionedField == "0") rec.unioned = false;
|
|
else return std::nullopt; // anything else is corruption -> reject whole
|
|
std::size_t count = 0;
|
|
if (!c.fieldCount(count)) return std::nullopt;
|
|
// 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.
|
|
if (count > wire.size() / 4u + 1u) return std::nullopt;
|
|
rec.holds.reserve(count);
|
|
for (std::size_t i = 0; i < count; ++i) {
|
|
UsageHold h;
|
|
if (!c.field(h.sampleId)) return std::nullopt;
|
|
if (!c.field(h.relativePath)) return std::nullopt;
|
|
rec.holds.push_back(std::move(h));
|
|
}
|
|
if (!c.ok() || !c.atEnd()) return std::nullopt; // trailing garbage -> reject whole
|
|
return rec;
|
|
}
|
|
|
|
UsagePublishPlan planUsagePublish(const std::optional<std::string>& existing,
|
|
const UsageRecord& mine) {
|
|
UsagePublishPlan plan;
|
|
// The written form of "just mine": mine's identity + holds, unioned=false (the plan
|
|
// computes the flag; a sole-writer record is un-poisoned).
|
|
UsageRecord cleanMine = mine;
|
|
cleanMine.unioned = false;
|
|
plan.wire = encodeUsageRecord(cleanMine);
|
|
|
|
if (!existing || existing->empty()) {
|
|
// Fresh key — write mine.
|
|
return plan;
|
|
}
|
|
const std::optional<UsageRecord> theirs = decodeUsageRecord(*existing);
|
|
if (!theirs) {
|
|
// Undecodable existing value under MY key: corruption (a sibling sharing
|
|
// this key via copy always writes decodable records). REMINT rather than
|
|
// overwrite: writing mine over the corrupt key would clear the prune-side
|
|
// abort, but a same-key sibling B's holds would then be unprotected until
|
|
// B publishes again. Leaving the corrupt key in place keeps the prune-side
|
|
// abort firing (foldUsageRecords.abortPrune) so the window where B's holds
|
|
// might be unprotected can never resolve toward delete. Mine is published
|
|
// under the new key that remint produces.
|
|
// NOTE (>16 MB gap): readReasamplerExtState returning nullopt for a value
|
|
// larger than 16 MB is indistinguishable from "absent" at the publish site;
|
|
// that narrow case takes the fresh-write branch above rather than remint.
|
|
// Both outcomes are safe (fresh write is also correct for a truly absent key);
|
|
// the gap is documented in the header's fail-safe list.
|
|
plan.remint = true;
|
|
return plan;
|
|
}
|
|
|
|
const bool nonceMatch =
|
|
!mine.ownerNonce.empty() && theirs->ownerNonce == mine.ownerNonce;
|
|
|
|
if (nonceMatch && !theirs->unioned) {
|
|
// Exactly THIS incarnation wrote the key (the per-lifetime nonce is the exact
|
|
// ownership proof — a same-track sibling's byte-identical hold set can NOT pass
|
|
// this test, its nonce differs) AND no other writer has ever unioned into it,
|
|
// so the content is provably all mine. Clean replace: released holds drop.
|
|
if (plan.wire == *existing) plan.skipWrite = true; // idle reload tick
|
|
return plan;
|
|
}
|
|
|
|
if (theirs->trackGuid == mine.trackGuid || (nonceMatch && theirs->unioned)) {
|
|
// A foreign writer on MY OWN track (a same-track copy-sibling, or my own
|
|
// last-session record — indistinguishable by construction), or a record I
|
|
// wrote last but that carries unioned holds from an earlier multi-writer
|
|
// merge. Either way no hold in it may be dropped by me — union, existing-
|
|
// first, de-duped, and the record is (or stays) POISONED unioned=true so no
|
|
// future nonce-matching write can clean-replace a sibling's holds away.
|
|
UsageRecord merged;
|
|
merged.trackGuid = mine.trackGuid;
|
|
merged.ownerNonce = mine.ownerNonce;
|
|
merged.unioned = true;
|
|
merged.holds = theirs->holds;
|
|
for (const UsageHold& h : mine.holds) {
|
|
bool dup = false;
|
|
for (const UsageHold& e : merged.holds) {
|
|
if (e == h) { dup = true; break; }
|
|
}
|
|
if (!dup) merged.holds.push_back(h);
|
|
}
|
|
if (theirs->unioned && merged.holds == theirs->holds) {
|
|
// Already poisoned and the union adds nothing — the write would flip only
|
|
// the ownerNonce. Skip the redundant ext-state churn. (A false->true
|
|
// unioned flip is NEVER skipped: it is the poison that protects the other
|
|
// writer's holds from the last writer's future clean replace.)
|
|
plan.skipWrite = true;
|
|
}
|
|
plan.wire = encodeUsageRecord(merged);
|
|
return plan;
|
|
}
|
|
|
|
// Foreign value from ANOTHER track: this instance is a cross-track copy (or was
|
|
// moved). Take a fresh identity; never overwrite the other's record.
|
|
plan.remint = true;
|
|
return plan;
|
|
}
|
|
|
|
std::vector<std::string> usageHeldPaths(
|
|
const std::vector<UsageRecord>& records,
|
|
const std::unordered_set<std::string>& liveTrackGuids,
|
|
bool anyInstanceLive) {
|
|
std::vector<std::string> out;
|
|
std::unordered_set<std::string> seen;
|
|
// FAIL-SAFE NET: records exist but not one instance was identified live anywhere —
|
|
// indistinguishable from an identity-matcher failure, so protect EVERY record's
|
|
// paths rather than none (zero-identified must never degrade toward delete).
|
|
const bool protectAll = !records.empty() && !anyInstanceLive;
|
|
for (const UsageRecord& rec : records) {
|
|
const bool live = protectAll ||
|
|
(rec.trackGuid.empty()
|
|
? anyInstanceLive
|
|
: (liveTrackGuids.count(rec.trackGuid) != 0));
|
|
if (!live) continue;
|
|
for (const UsageHold& h : rec.holds) {
|
|
if (h.relativePath.empty()) continue;
|
|
if (seen.insert(h.relativePath).second) out.push_back(h.relativePath);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
UsageFoldResult foldUsageRecords(
|
|
const std::vector<std::optional<UsageRecord>>& decoded,
|
|
const std::unordered_set<std::string>& liveTrackGuids,
|
|
bool anyInstanceLive) {
|
|
UsageFoldResult result;
|
|
std::vector<UsageRecord> records;
|
|
records.reserve(decoded.size());
|
|
for (const std::optional<UsageRecord>& rec : decoded) {
|
|
if (!rec) {
|
|
// A present-but-unreadable record: it may protect ANYTHING, so the prune
|
|
// must halt outright. Belt-and-braces: return the PROTECT-ALL set (all
|
|
// readable records' paths) so the fail-safe holds even under a future
|
|
// caller that forgets to check abortPrune before using heldPaths. The
|
|
// abort flag is still the authoritative signal; heldPaths is the
|
|
// maximum-protection fallback.
|
|
result.abortPrune = true;
|
|
// Collect EVERY path from EVERY readable record, bypassing the liveness
|
|
// filter entirely (on abort the protected set is unknowable, so every
|
|
// decoded hold must be included regardless of track-guid membership).
|
|
std::unordered_set<std::string> seen;
|
|
for (const std::optional<UsageRecord>& r : decoded) {
|
|
if (!r) continue;
|
|
for (const UsageHold& h : r->holds) {
|
|
if (h.relativePath.empty()) continue;
|
|
if (seen.insert(h.relativePath).second)
|
|
result.heldPaths.push_back(h.relativePath);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
records.push_back(*rec);
|
|
}
|
|
result.heldPaths = usageHeldPaths(records, liveTrackGuids, anyInstanceLive);
|
|
return result;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool identityMatches(const std::string& identity, const std::string& uidHexUpper,
|
|
const std::string& nameUpper,
|
|
const std::string& outputNameUpper) {
|
|
if (identity.empty()) return false;
|
|
const std::string up = toUpperAscii(identity);
|
|
// Primary: the 32-hex class UID embedded in REAPER's fx_ident rendering. Not
|
|
// guaranteed on every platform/REAPER build (byte-order of the rendered FUID vs
|
|
// REAPER's hex is unverified on Windows COM layout), hence the two name nets below
|
|
// — and the protect-all fold above them (see usageHeldPaths).
|
|
if (!uidHexUpper.empty() && up.find(uidHexUpper) != std::string::npos) return true;
|
|
// The module filename base ("REASAMPLER_9000") — fx_ident carries the .vst3 module
|
|
// path, so this is the alternative that works in the common case (the display name
|
|
// "REASAMPLER 9000", space-separated, can never match the filename form).
|
|
if (!outputNameUpper.empty() && up.find(outputNameUpper) != std::string::npos)
|
|
return true;
|
|
// The factory display name — matches original_name / renamed-instance renderings.
|
|
// Beta-substring over-protect is deliberate (see the header note): stable needles
|
|
// are substrings of beta ones, widening protection only — never a delete.
|
|
return !nameUpper.empty() && up.find(nameUpper) != std::string::npos;
|
|
}
|
|
|
|
} // namespace reasampler
|