// bake_wire.cpp — see bake_wire.h. Pure: standard library only. #include "core/wire/bake_wire.h" #include #include "core/version/app_version.h" #include "core/wire/wire.h" namespace reasampler::wire { namespace { constexpr const char* kRequestMagic = "rsbakereq1"; constexpr const char* kOutcomeMagic = "rsbakeout1"; using wire::putField; using Cursor = wire::Cursor; // An unrecognized integer is `Failed`, not a parse error: the two artifacts ship // independently, and a newer extension naming a failure this build has no word for must // still read as a failure rather than as Ok (which is what the numeric default would be). BakeStatus statusFromWire(int raw) { switch (static_cast(raw)) { case BakeStatus::Ok: case BakeStatus::Failed: case BakeStatus::NoProject: case BakeStatus::StagedMissing: case BakeStatus::NoSource: case BakeStatus::IndexRejected: case BakeStatus::WrongProject: return static_cast(raw); } return BakeStatus::Failed; } } // namespace std::string bakeActionLookupName() { return "_" + version::channelCommandId(kBakeActionSuffix); } bool BakeRequest::operator==(const BakeRequest& o) const { return instanceGuid == o.instanceGuid && stagedFilePath == o.stagedFilePath && sourceSampleId == o.sourceSampleId && sourceRelativePath == o.sourceRelativePath && sourceDisplayName == o.sourceDisplayName && ownUsageKey == o.ownUsageKey && rootNote == o.rootNote && generation == o.generation; } bool BakeOutcome::operator==(const BakeOutcome& o) const { return status == o.status && sampleId == o.sampleId && relativePath == o.relativePath && displayName == o.displayName && rootNote == o.rootNote && channelCount == o.channelCount && replaced == o.replaced && message == o.message && generation == o.generation; } std::string encodeBakeRequest(const BakeRequest& req) { std::string out = kRequestMagic; putField(out, req.instanceGuid); putField(out, req.stagedFilePath); putField(out, req.sourceSampleId); putField(out, req.sourceRelativePath); putField(out, req.sourceDisplayName); putField(out, req.ownUsageKey); putField(out, std::to_string(req.rootNote)); putField(out, std::to_string(req.generation)); return out; } std::optional decodeBakeRequest(const std::string& wire) { Cursor cur(wire); if (!cur.literal(kRequestMagic)) return std::nullopt; BakeRequest req; if (!cur.field(req.instanceGuid)) return std::nullopt; if (!cur.field(req.stagedFilePath)) return std::nullopt; if (!cur.field(req.sourceSampleId)) return std::nullopt; if (!cur.field(req.sourceRelativePath)) return std::nullopt; if (!cur.field(req.sourceDisplayName)) return std::nullopt; if (!cur.field(req.ownUsageKey)) return std::nullopt; if (!cur.fieldInt(req.rootNote)) return std::nullopt; if (!cur.fieldInt64(req.generation)) return std::nullopt; if (!cur.ok() || !cur.atEnd()) return std::nullopt; return req; } std::string encodeBakeOutcome(const BakeOutcome& outcome) { std::string out = kOutcomeMagic; putField(out, std::to_string(static_cast(outcome.status))); putField(out, outcome.sampleId); putField(out, outcome.relativePath); putField(out, outcome.displayName); putField(out, std::to_string(outcome.rootNote)); putField(out, std::to_string(outcome.channelCount)); putField(out, outcome.replaced ? "1" : "0"); putField(out, outcome.message); putField(out, std::to_string(outcome.generation)); return out; } std::optional decodeBakeOutcome(const std::string& wire) { Cursor cur(wire); if (!cur.literal(kOutcomeMagic)) return std::nullopt; BakeOutcome outcome; int rawStatus = 0; std::string replaced; if (!cur.fieldInt(rawStatus)) return std::nullopt; if (!cur.field(outcome.sampleId)) return std::nullopt; if (!cur.field(outcome.relativePath)) return std::nullopt; if (!cur.field(outcome.displayName)) return std::nullopt; if (!cur.fieldInt(outcome.rootNote)) return std::nullopt; if (!cur.fieldInt(outcome.channelCount)) return std::nullopt; if (!cur.field(replaced)) return std::nullopt; if (!cur.field(outcome.message)) return std::nullopt; if (!cur.fieldInt64(outcome.generation)) return std::nullopt; if (!cur.ok() || !cur.atEnd()) return std::nullopt; if (replaced != "0" && replaced != "1") return std::nullopt; outcome.status = statusFromWire(rawStatus); outcome.replaced = (replaced == "1"); return outcome; } BakeAnswer classifyBakeAnswer(const std::optional& raw, const BakeRequest& sent) { BakeAnswer answer; // `raw` folds three distinct bridge outcomes into one — absent, explicitly // cleared, and an unreadable/oversized read — because the bridge cannot label // which occurred. A caller's message for `Cleared` must not claim more than that. if (!raw || raw->empty()) { answer.kind = BakeAnswerKind::Cleared; return answer; } if (std::optional outcome = decodeBakeOutcome(*raw)) { answer.kind = outcome->generation == sent.generation ? BakeAnswerKind::Answered : BakeAnswerKind::ForeignOutcome; answer.outcome = std::move(outcome); return answer; } // Still a request: whether it is OURS is what separates "nothing read this key" from // "another instance overwrote it" — a persisted instanceGuid is copyable, so two // instances CAN name one key. if (const std::optional req = decodeBakeRequest(*raw)) { answer.kind = *req == sent ? BakeAnswerKind::Unanswered : BakeAnswerKind::ForeignRequest; return answer; } answer.kind = BakeAnswerKind::Undecodable; return answer; } const BakeOutcome* answeredOutcome(const BakeAnswer& answer) { if (answer.kind != BakeAnswerKind::Answered || !answer.outcome) return nullptr; return &*answer.outcome; } BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanKey& key, std::int64_t nowSec) { // Not a request: an outcome the writing instance has not collected yet, or a value // from a build we do not read. The writer owns clearing its own key. if (!key.decoded) return BakeScanVerdict::Ignore; // Either direction, so a clock moved backwards is caught too. const std::int64_t age = nowSec - key.generation; if (age > kMaxRequestAgeSeconds || age < -kMaxRequestAgeSeconds) return BakeScanVerdict::ClearStale; // Two facts have to agree before anything may land: the loaded project is the one a // persist would write into (loadedProjectIsActive), and this key's tab IS that loaded // project (matchesLoadedProject — false by construction whenever no project is // loaded, which is what folds the former three-way check into two). const bool landable = session.loadedProjectIsActive && key.matchesLoadedProject; return landable ? BakeScanVerdict::Land : BakeScanVerdict::RefuseWrongProject; } } // namespace reasampler::wire