fix(banks): enforce unique bank display names in-model + B3 review minors
This commit is contained in:
+45
-1
@@ -77,6 +77,43 @@ void BankBook::normalizeOrdinals() {
|
||||
banks_[i].ordinal = static_cast<int>(i);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Display-name uniqueness (trimmed + case-insensitive, ASCII)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// Folds a display name to its uniqueness key: strip leading/trailing ASCII
|
||||
// whitespace, lower-case ASCII letters. So "Drums", "drums", and " Drums " share one
|
||||
// key and cannot coexist. ASCII-only by design — the pure core carries no locale
|
||||
// facility and must not grow one; bank names are short user labels, not full Unicode
|
||||
// case-folding candidates.
|
||||
std::string nameKey(const std::string& s) {
|
||||
std::size_t b = 0, e = s.size();
|
||||
auto isWs = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
|
||||
while (b < e && isWs(s[b])) ++b;
|
||||
while (e > b && isWs(s[e - 1])) --e;
|
||||
std::string out;
|
||||
out.reserve(e - b);
|
||||
for (std::size_t i = b; i < e; ++i) {
|
||||
char c = s[i];
|
||||
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// True if any bank OTHER than `exceptId` already carries `name`'s uniqueness key. The
|
||||
// exception lets renameBank accept a bank keeping (or re-casing/-spacing) its own name.
|
||||
bool BankBook::displayNameTaken(const std::string& name, const std::string& exceptId) const {
|
||||
const std::string key = nameKey(name);
|
||||
for (const auto& b : banks_)
|
||||
if (b.id != exceptId && nameKey(b.displayName) == key) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bank lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -84,7 +121,10 @@ void BankBook::normalizeOrdinals() {
|
||||
bool BankBook::createBank(const std::string& id, const std::string& displayName) {
|
||||
if (id.empty()) return false; // ids key the registry
|
||||
if (id == kPoolBankId) return false; // reserved pool id
|
||||
if (bank(id) != nullptr) return false; // duplicate
|
||||
if (bank(id) != nullptr) return false; // duplicate id
|
||||
// Display names are unique (trimmed + case-insensitive); the pool's "Pool" is a
|
||||
// reserved name and is caught here like any other collision.
|
||||
if (displayNameTaken(displayName, /*exceptId=*/id)) return false;
|
||||
|
||||
Bank b;
|
||||
b.id = id;
|
||||
@@ -99,6 +139,10 @@ bool BankBook::renameBank(const std::string& id, const std::string& displayName)
|
||||
if (id == kPoolBankId) return false; // pool is un-renamable
|
||||
Bank* b = bank(id);
|
||||
if (b == nullptr) return false;
|
||||
// Reject a name already used by a DIFFERENT bank. Renaming a bank to its own
|
||||
// current name (or a case/space variant of it) is a no-op success, not a
|
||||
// rejection — exceptId=id excludes the bank itself from the collision scan.
|
||||
if (displayNameTaken(displayName, /*exceptId=*/id)) return false;
|
||||
b->displayName = displayName;
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user