36270e2064
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.
136 lines
5.6 KiB
C++
136 lines
5.6 KiB
C++
#include "capture_paths.h"
|
|
|
|
#include <cassert>
|
|
|
|
namespace reasampler {
|
|
|
|
std::string normalizeSlashes(const std::string& path) {
|
|
std::string out = path;
|
|
for (char& c : out) {
|
|
if (c == '\\') c = '/';
|
|
}
|
|
// Strip a single trailing slash so joins do not double up. Preserve a lone
|
|
// "/" (root) — stripping it would turn root into empty.
|
|
if (out.size() > 1 && out.back() == '/') {
|
|
out.pop_back();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string sanitizeStem(const std::string& baseName) {
|
|
std::string out;
|
|
out.reserve(baseName.size());
|
|
for (unsigned char c : baseName) {
|
|
const bool keep = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '.' || c == '_' ||
|
|
c == '-';
|
|
out.push_back(keep ? static_cast<char>(c) : '_');
|
|
}
|
|
// Collapse to a stable default if nothing usable survived (e.g. all spaces).
|
|
// A stem of only separators ('.', '_', '-') is also unhelpful as a name.
|
|
bool hasAlnum = false;
|
|
for (unsigned char c : out) {
|
|
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9')) {
|
|
hasAlnum = true;
|
|
break;
|
|
}
|
|
}
|
|
if (out.empty() || !hasAlnum) {
|
|
return "capture";
|
|
}
|
|
return out;
|
|
}
|
|
|
|
BankPaths deriveBankPaths(const std::string& projectDir,
|
|
const std::string& baseName,
|
|
const std::string& uniqueTag) {
|
|
const std::string dir = normalizeSlashes(projectDir);
|
|
|
|
std::string stem = sanitizeStem(baseName);
|
|
if (!uniqueTag.empty()) {
|
|
stem += "_" + sanitizeStem(uniqueTag);
|
|
}
|
|
const std::string fileName = stem + ".wav";
|
|
|
|
// Precondition: the capture shell must resolve a non-empty project directory
|
|
// before calling this function. An empty projectDir would produce a bare
|
|
// relative "reasampler_bank" path — the silent default-location fallback this
|
|
// tool explicitly forbids. Assert in debug; leave absoluteDir empty in release
|
|
// so any caller that ignores the precondition fails loudly at the render/stat
|
|
// step rather than silently writing to CWD.
|
|
assert(!dir.empty() && "deriveBankPaths: projectDir must not be empty");
|
|
|
|
BankPaths p;
|
|
p.fileStem = stem; // stem only — REAPER appends extension
|
|
p.fileName = fileName;
|
|
p.relativePath = std::string(kBankSubfolder) + "/" + fileName;
|
|
// absoluteDir intentionally omits a trailing slash (RENDER_FILE wants the
|
|
// directory itself; RENDER_PATTERN supplies the file name separately).
|
|
// Empty when precondition is violated (dir empty) — caller must not proceed.
|
|
p.absoluteDir = dir.empty() ? std::string{}
|
|
: dir + "/" + kBankSubfolder;
|
|
return p;
|
|
}
|
|
|
|
std::string resolveBankFile(const std::string& projectDir,
|
|
const std::string& relativePath) {
|
|
// No default-location fallback (CLAUDE.md invariant): an empty project dir or
|
|
// relative path yields empty, not a bare relative path resolved against CWD.
|
|
if (projectDir.empty() || relativePath.empty()) {
|
|
return {};
|
|
}
|
|
const std::string dir = normalizeSlashes(projectDir);
|
|
const std::string rel = normalizeSlashes(relativePath);
|
|
if (dir.empty() || rel.empty()) {
|
|
return {};
|
|
}
|
|
return dir + "/" + rel;
|
|
}
|
|
|
|
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
|
|
const std::string& newProjectDir) {
|
|
BankRelocation r;
|
|
if (oldProjectDir.empty() || newProjectDir.empty()) {
|
|
return r; // needed=false, empty dirs — nothing to relocate
|
|
}
|
|
const std::string oldDir = normalizeSlashes(oldProjectDir);
|
|
const std::string newDir = normalizeSlashes(newProjectDir);
|
|
|
|
r.oldBankDir = oldDir + "/" + kBankSubfolder;
|
|
r.newBankDir = newDir + "/" + kBankSubfolder;
|
|
// A Save (in place) leaves the project dir unchanged — nothing to relocate.
|
|
// Only a Save-As to a different directory needs the bank moved.
|
|
r.needed = (oldDir != newDir);
|
|
return r;
|
|
}
|
|
|
|
ProjectTransition classifyProjectTransition(bool sameProjectObject,
|
|
const std::string& lastGuid,
|
|
const std::string& lastPath,
|
|
const std::string& currentGuid,
|
|
const std::string& currentPath) {
|
|
// A DIFFERENT project object is a tab-switch / open / recycled pointer — load
|
|
// ITS index; NEVER relocate a bank. This is the load-bearing safety fix: it
|
|
// holds even when currentGuid == lastGuid, which is exactly the forked-sibling
|
|
// case (Save-As copied our GUID, so two distinct projects share it on disk).
|
|
// The GUID is deliberately NOT consulted here — the object identity alone
|
|
// decides, and it cannot be fooled by a copied GUID.
|
|
(void)lastGuid;
|
|
(void)currentGuid;
|
|
if (!sameProjectObject) {
|
|
return ProjectTransition::Load;
|
|
}
|
|
|
|
// Same object from here on: identity is PROVEN by the pointer. A path change is
|
|
// a Save-As (or a first save, when the old path was empty); an unchanged path
|
|
// is Save-in-place / idle. Note SaveAsRelocate is safe even for a first save:
|
|
// the old project dir is empty, so deriveRelocationPlan makes `needed` false
|
|
// and nothing is physically relocated (empty-GUID safety preserved), while
|
|
// poll() still mints a GUID on that branch.
|
|
return (currentPath == lastPath) ? ProjectTransition::NoOp
|
|
: ProjectTransition::SaveAsRelocate;
|
|
}
|
|
|
|
} // namespace reasampler
|