// sample_usage.cpp — see sample_usage.h. Pure: standard library only. #include "core/wire/sample_usage.h" #include #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 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& existing, const UsageRecord& mine) { UsagePublishPlan plan; UsageRecord cleanMine = mine; cleanMine.unioned = false; plan.wire = encodeUsageRecord(cleanMine); if (!existing || existing->empty()) { // NOTE (>16 MB gap): readReasamplerExtState returning nullopt for a value // larger than 16 MB is indistinguishable from "absent" here, so that narrow // case also takes this fresh-write branch rather than remint. return plan; // fresh key — write mine } const std::optional 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; } namespace { // The liveness rule, in one place so the path fold and the attribution fold can // never disagree about which records counted. `protectAll` is the caller's // zero-identified net (see usageHeldPaths). bool recordCounts(const UsageRecord& rec, const std::unordered_set& liveTrackGuids, bool anyInstanceLive, bool protectAll) { if (protectAll) return true; if (rec.trackGuid.empty()) return anyInstanceLive; return liveTrackGuids.count(rec.trackGuid) != 0; } } // namespace std::vector usageHeldPaths( const std::vector& records, const std::unordered_set& liveTrackGuids, bool anyInstanceLive) { std::vector out; std::unordered_set 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) { if (!recordCounts(rec, liveTrackGuids, anyInstanceLive, protectAll)) 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& decoded, const std::unordered_set& liveTrackGuids, bool anyInstanceLive) { UsageFoldResult result; for (const DecodedUsage& entry : decoded) { if (entry.record) continue; // Present-but-unreadable record: it may protect anything, so halt. result.abortPrune = true; result.offendingKeys.push_back(entry.key); } if (result.abortPrune) { // Belt-and-braces: 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. `counted` stays empty — // attribution is exactly what an unreadable record makes unknowable. std::unordered_set seen; for (const DecodedUsage& entry : decoded) { if (!entry.record) continue; for (const UsageHold& h : entry.record->holds) { if (h.relativePath.empty()) continue; if (seen.insert(h.relativePath).second) result.heldPaths.push_back(h.relativePath); } } return result; } std::vector records; records.reserve(decoded.size()); for (const DecodedUsage& entry : decoded) records.push_back(*entry.record); result.heldPaths = usageHeldPaths(records, liveTrackGuids, anyInstanceLive); const bool protectAll = !records.empty() && !anyInstanceLive; for (const DecodedUsage& entry : decoded) { if (!recordCounts(*entry.record, liveTrackGuids, anyInstanceLive, protectAll)) continue; result.counted.push_back(CountedUsage{entry.key, *entry.record}); } return result; } std::string toUpperAscii(const std::string& s) { std::string out = s; for (char& c : out) c = static_cast(std::toupper(static_cast(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