fix(persist): use project-object identity to stop forked-bank cross-contamination
M4's GUID-only classifier read a tab-switch between two Save-As forks (shared copied GUID, different paths) as a Save-As and clobbered a bank. Thread sameProjectObject into classifyProjectTransition: a different object always Loads, never relocates; a forked sibling gets re-GUID'd to diverge.
This commit is contained in:
+45
-24
@@ -89,42 +89,63 @@ struct BankRelocation {
|
||||
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
|
||||
const std::string& newProjectDir);
|
||||
|
||||
// --- Project-identity transition (M4 defect fix) ----------------------------
|
||||
// --- Project-identity transition (W10 forked-project fix) --------------------
|
||||
//
|
||||
// What the persist timer must do on each tick, decided purely from the LAST
|
||||
// observed identity and the CURRENT one. Identity is CONTENT-BASED: a project
|
||||
// GUID we mint and store in our ext state (REAPER exposes no stable per-project
|
||||
// GUID). The raw ReaProject* is deliberately NOT part of this decision — REAPER
|
||||
// recycles pointer addresses across project close/open, and keying Save-As off
|
||||
// the pointer let a project switch masquerade as a Save-As and clobber a bank.
|
||||
// What the persist timer must do on each tick. Identity now rests on TWO facts,
|
||||
// not the GUID alone:
|
||||
// 1. sameProjectObject — did the same live ReaProject* stay active across the
|
||||
// two ticks (computed in poll() as `proj == lastProject_`)? This is what a
|
||||
// genuine Save-As looks like: ONE project object saved to a new path. A
|
||||
// tab-switch or open is a DIFFERENT object.
|
||||
// 2. the minted GUID — content-based identity stored in ext state, kept to
|
||||
// survive pointer *reuse* (REAPER recycles a closed project's address).
|
||||
//
|
||||
// The pointer was dropped in M4 (GUID-only), which broke FORKED projects: Save-As
|
||||
// copies the whole .rpp incl. our stored GUID, so a fork and its parent share a
|
||||
// GUID on disk. Tab-switching between two forked siblings (same GUID, different
|
||||
// paths) then read as a Save-As and clobbered one bank with the other's — the
|
||||
// data-integrity defect this fix closes. The pointer is the ONLY signal that
|
||||
// separates "same object saved elsewhere" (Save-As) from "different object that
|
||||
// happens to share a forked GUID" (a switch).
|
||||
//
|
||||
// The load-bearing rule: a DIFFERENT project object NEVER relocates a bank.
|
||||
enum class ProjectTransition {
|
||||
NoOp, // same project, same location — nothing to do
|
||||
NoOp, // same object, same location — nothing to do
|
||||
Load, // a different project is active — load ITS index from ext state
|
||||
SaveAsRelocate, // same project, new .rpp location — relocate the bank folder
|
||||
SaveAsRelocate, // SAME object, new .rpp location — relocate the bank folder
|
||||
};
|
||||
|
||||
// Classifies what a poll tick observed.
|
||||
// sameProjectObject : true iff the SAME ReaProject* stayed active across the two
|
||||
// ticks (poll() computes `proj == lastProject_`). The pure
|
||||
// classifier takes the bool, not the raw pointer, to stay
|
||||
// REAPER-free and testable.
|
||||
// lastGuid : the GUID of the project persist last acted on ("" if none/unsaved)
|
||||
// lastPath : that project's .rpp path when last seen ("" if unsaved)
|
||||
// currentGuid : the GUID stored in the now-active project's ext state ("" if
|
||||
// unsaved or never written)
|
||||
// currentPath : the now-active project's .rpp path ("" if unsaved)
|
||||
//
|
||||
// Rules (GUID is the identity; path only distinguishes Save vs Save-As within
|
||||
// the SAME identity):
|
||||
// * currentGuid != lastGuid -> Load (a different project)
|
||||
// * same non-empty GUID, currentPath == lastPath -> NoOp (Save in place / idle)
|
||||
// * same non-empty GUID, currentPath != lastPath -> SaveAsRelocate
|
||||
// * both GUIDs empty, same path -> NoOp (idle unsaved project)
|
||||
// * both GUIDs empty, different path -> Load (can't PROVE same
|
||||
// project without a GUID — a
|
||||
// first-save or a switch
|
||||
// between unsaved projects;
|
||||
// never a relocate)
|
||||
// The both-empty/different-path -> Load rule is what makes the recycled-pointer
|
||||
// bug impossible: absent GUID corroboration, a path change is treated as a new
|
||||
// project (safe: load), never a relocate (destructive: copy-over).
|
||||
ProjectTransition classifyProjectTransition(const std::string& lastGuid,
|
||||
// Rules:
|
||||
// * sameProjectObject == false -> Load (a different project
|
||||
// object — tab-switch / open /
|
||||
// recycled pointer; NEVER a
|
||||
// relocate, even if the GUID
|
||||
// matches a forked sibling)
|
||||
// * same object, non-empty GUID, path unchanged -> NoOp (Save in place / idle)
|
||||
// * same object, non-empty GUID, path changed -> SaveAsRelocate
|
||||
// * same object, empty GUID, path unchanged -> NoOp (idle unsaved project)
|
||||
// * same object, empty GUID, path changed -> SaveAsRelocate (first save;
|
||||
// the relocation plan no-ops on
|
||||
// the empty old dir, so nothing
|
||||
// is physically relocated —
|
||||
// the empty-GUID safety holds —
|
||||
// and poll() mints a GUID)
|
||||
// A different object never yields SaveAsRelocate: that is the whole fix. The empty-
|
||||
// GUID safety (unsaved projects never physically relocate) is preserved because an
|
||||
// empty old project dir makes deriveRelocationPlan's `needed` false.
|
||||
ProjectTransition classifyProjectTransition(bool sameProjectObject,
|
||||
const std::string& lastGuid,
|
||||
const std::string& lastPath,
|
||||
const std::string& currentGuid,
|
||||
const std::string& currentPath);
|
||||
|
||||
Reference in New Issue
Block a user