Files
reasampler/src/core/wire/sample_usage.cpp
T

198 lines
7.6 KiB
C++

// sample_usage.cpp — see sample_usage.h. Pure: standard library only.
#include "core/wire/sample_usage.h"
#include <cctype>
#include "core/wire/wire.h"
namespace reasampler::wire {
namespace {
constexpr const char* kMagic = "rsusage1";
using wire::putField;
using Cursor = wire::Cursor;
} // 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.fieldSizeT(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;
UsageRecord cleanMine = mine;
cleanMine.unioned = false;
plan.wire = encodeUsageRecord(cleanMine);
if (!existing || existing->empty()) {
return plan; // fresh key — write mine
}
const std::optional<UsageRecord> theirs = decodeUsageRecord(*existing);
if (!theirs) {
// Corrupt value under my key: remint rather than overwrite. Overwriting
// would clear the prune-side abort currently protecting a same-key
// sibling's (possibly unprotected) holds; leaving the corrupt key in
// place keeps that abort firing until the sibling republishes.
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 last and it was never unioned
// by another writer — content is provably all mine.
if (plan.wire == *existing) plan.skipWrite = true; // idle reload tick
return plan;
}
if (theirs->trackGuid == mine.trackGuid || (nonceMatch && theirs->unioned)) {
// Same-track sibling, my own last-session record, or an already-unioned
// record — no hold in it may be dropped. Union, existing-first, de-duped,
// poisoned unioned=true so a future clean replace can never drop it.
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 only
// flip ownerNonce; skip. A false->true unioned flip is NEVER skipped.
plan.skipWrite = true;
}
plan.wire = encodeUsageRecord(merged);
return plan;
}
// Foreign value from another track: a cross-track copy or move. 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) {
// Present-but-unreadable record: it may protect anything, so halt.
// Belt-and-braces: also return the protect-all set (every readable
// record's paths, bypassing the liveness filter) so the fail-safe
// holds even if a future caller forgets to check abortPrune first.
result.abortPrune = true;
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);
// Class-UID byte-order in fx_ident is unverified on Windows COM layout,
// hence the two name fallbacks below (see header for the protect-all net).
if (!uidHexUpper.empty() && up.find(uidHexUpper) != std::string::npos) return true;
if (!outputNameUpper.empty() && up.find(outputNameUpper) != std::string::npos)
return true;
return !nameUpper.empty() && up.find(nameUpper) != std::string::npos;
}
} // namespace reasampler::wire