fix(banks): enforce unique bank display names in-model + B3 review minors

This commit is contained in:
2026-07-25 01:29:47 -04:00
parent cbfc13c4e0
commit ccf65415c9
6 changed files with 169 additions and 22 deletions
+45 -1
View File
@@ -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;
}