#include "view_mode_model.h" #include #include #include #include #include // view_mode_model implementation. // // JSON is hand-rolled and self-contained, mirroring bank_model's approach (brief: // keep the pure core dependency-free — no third-party JSON lib). A compact writer // plus a recursive-descent parser covers the field set: the mode registry, the // GUID-keyed membership map, per-track snapshots (with a variable-length per-FX // offline vector), and the active mode. Ints are emitted plainly; strings are // escaped identically to bank_model so control chars and unicode survive. namespace reasampler { // --------------------------------------------------------------------------- // equality // --------------------------------------------------------------------------- bool Mode::operator==(const Mode& o) const { return id == o.id && displayName == o.displayName && ordinal == o.ordinal; } // --------------------------------------------------------------------------- // ModeRegistry // --------------------------------------------------------------------------- ModeRegistry::ModeRegistry() { modes_.push_back(Mode{kArrangeModeId, "Arrange", 0}); modes_.push_back(Mode{kDesignModeId, "Design", 1}); } bool ModeRegistry::add(const Mode& mode) { if (mode.id.empty()) return false; if (query(mode.id) != nullptr) return false; // ids are unique modes_.push_back(mode); // Keep ordinal order stable; std::stable_sort so equal ordinals keep insertion // order (the tie-break documented in the header). std::stable_sort(modes_.begin(), modes_.end(), [](const Mode& a, const Mode& b) { return a.ordinal < b.ordinal; }); return true; } const Mode* ModeRegistry::query(const std::string& id) const { for (const auto& m : modes_) if (m.id == id) return &m; return nullptr; } // --------------------------------------------------------------------------- // MembershipIndex // --------------------------------------------------------------------------- bool MembershipIndex::tag(const std::string& guid, const std::string& modeId) { if (guid.empty() || modeId.empty()) return false; Membership& m = entries_[guid]; m.modeIds.clear(); // a leaf lives in exactly one mode (show-both aside) m.modeIds.insert(modeId); return true; } bool MembershipIndex::untag(const std::string& guid) { return entries_.erase(guid) > 0; } bool MembershipIndex::setShowBoth(const std::string& guid, bool showBoth) { if (guid.empty()) return false; entries_[guid].showBoth = showBoth; // creates an Arrange-default entry if new return true; } bool MembershipIndex::restore(const std::string& guid, const Membership& membership) { if (guid.empty()) return false; entries_[guid] = membership; return true; } const Membership* MembershipIndex::query(const std::string& guid) const { auto it = entries_.find(guid); return it == entries_.end() ? nullptr : &it->second; } std::set MembershipIndex::modesOf(const std::string& guid) const { const Membership* m = query(guid); return m ? m->modeIds : std::set{}; } // --------------------------------------------------------------------------- // planner helpers // --------------------------------------------------------------------------- TrackPlan makeParkPlan(const std::string& guid, int fxCount) { // Parking contract: hide both panels, out of the mix, FX bypassed, every FX // offline. All fixed zeros — park never consults a snapshot. TrackPlan p; p.flags = { {guid, Flag::ShowInTcp, 0}, {guid, Flag::ShowInMixer, 0}, {guid, Flag::MainSend, 0}, {guid, Flag::FxEnable, 0}, }; for (int i = 0; i < fxCount; ++i) p.fxOffline.push_back({guid, i, true}); return p; } TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) { // Restore contract: every driven flag returns to its SNAPSHOTTED value — never // a hardcoded "on"/default. A flag captured at 0 restores to 0. TrackPlan p; p.flags = { {guid, Flag::ShowInTcp, snap.showInTcp}, {guid, Flag::ShowInMixer, snap.showInMixer}, {guid, Flag::MainSend, snap.mainSend}, {guid, Flag::FxEnable, snap.fxEnable}, }; for (std::size_t i = 0; i < snap.fxOffline.size(); ++i) p.fxOffline.push_back({guid, static_cast(i), snap.fxOffline[i] != 0}); return p; } // --------------------------------------------------------------------------- // ViewModeModel // --------------------------------------------------------------------------- ViewModeModel::ViewModeModel() : activeModeId_(kArrangeModeId) {} bool ViewModeModel::setActiveMode(const std::string& modeId) { if (!modes_.contains(modeId)) return false; activeModeId_ = modeId; return true; } void ViewModeModel::storeSnapshot(const std::string& guid, const TrackSnapshot& snap) { snapshots_[guid] = snap; } void ViewModeModel::clearSnapshot(const std::string& guid) { snapshots_.erase(guid); } const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const { auto it = snapshots_.find(guid); return it == snapshots_.end() ? nullptr : &it->second; } bool ViewModeModel::leafBelongsToMode(const std::string& guid, const std::string& modeId) const { const Membership* m = membership_.query(guid); if (!m) return modeId == kArrangeModeId; // untagged ⇒ Arrange default if (m->showBoth) return true; // show-both ⇒ every mode if (m->modeIds.empty()) return modeId == kArrangeModeId; // show-both-cleared, no mode return m->modeIds.count(modeId) > 0; } std::set ViewModeModel::visibleTracks(const FolderTree& tree, const std::string& modeId) const { std::set visible; // Pass 1: every leaf that belongs to the mode is visible. for (const auto& node : tree.nodes) { if (node.isParent) continue; // parents derived in pass 2 if (leafBelongsToMode(node.guid, modeId)) visible.insert(node.guid); } // Pass 2: a parent is visible if any descendant leaf is visible. Walk each // visible leaf up its parent chain and mark ancestors. Parent chains are read // from the supplied tree only (no REAPER access). A cycle-guard bounds the walk // in case a malformed tree links a node to itself. std::map parentOf; for (const auto& node : tree.nodes) parentOf[node.guid] = node.parentGuid; // Snapshot the leaf-visible set so we don't re-walk parents we add mid-loop. const std::vector seeds(visible.begin(), visible.end()); for (const auto& leaf : seeds) { auto it = parentOf.find(leaf); std::size_t guard = 0; while (it != parentOf.end() && !it->second.empty() && guard++ < parentOf.size()) { const std::string& parent = it->second; if (!visible.insert(parent).second) break; // already marked ⇒ chain done it = parentOf.find(parent); } } return visible; } TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string& targetMode) const { TogglePlan plan; // Index the tree so we can classify each tagged GUID (leaf vs parent vs stale). std::map byGuid; for (const auto& node : tree.nodes) byGuid[node.guid] = &node; for (const auto& [guid, m] : membership_.all()) { auto it = byGuid.find(guid); if (it == byGuid.end()) continue; // stale GUID: prune-safe, ignore if (it->second->isParent) continue; // parents are derived, never parked if (m.showBoth) continue; // show-both leaves are never parked const bool active = leafBelongsToMode(guid, targetMode); if (active) { // Returning to visibility: restore from snapshot if we have one. No // snapshot ⇒ the track was never parked, nothing to restore. if (const TrackSnapshot* snap = snapshot(guid)) plan.restore.push_back(makeRestorePlan(guid, *snap)); } else { // Inactive tagged leaf ⇒ park. fxOffline is intentionally empty here: // the D2 shell expands per-FX offline writes using TrackFX_GetCount. // The pure model has no access to REAPER FX counts at plan time; // makeParkPlan(guid, 0) emits only the scalar flags as a result. plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0)); } } return plan; } bool ViewModeModel::operator==(const ViewModeModel& o) const { return modes_ == o.modes_ && membership_ == o.membership_ && activeModeId_ == o.activeModeId_ && snapshots_ == o.snapshots_; } // =========================================================================== // JSON — writer // =========================================================================== namespace { void writeEscaped(std::string& out, const std::string& s) { out += '"'; for (char c : s) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\b': out += "\\b"; break; case '\f': out += "\\f"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (static_cast(c) < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast(c)); out += buf; } else { out += c; } } } out += '"'; } std::string intToStr(int v) { char buf[16]; std::snprintf(buf, sizeof(buf), "%d", v); return buf; } void writeIntArray(std::string& out, const std::vector& v) { out += '['; for (std::size_t i = 0; i < v.size(); ++i) { if (i) out += ','; out += intToStr(v[i]); } out += ']'; } class ObjWriter { public: explicit ObjWriter(std::string& out) : out_(out) { out_ += '{'; } ~ObjWriter() { out_ += '}'; } void keyRaw(const char* key, const std::string& rawValue) { sep(); writeEscaped(out_, key); out_ += ':'; out_ += rawValue; } void keyStr(const char* key, const std::string& value) { sep(); writeEscaped(out_, key); out_ += ':'; writeEscaped(out_, value); } void keyBegin(const char* key) { sep(); writeEscaped(out_, key); out_ += ':'; } private: void sep() { if (first_) first_ = false; else out_ += ','; } std::string& out_; bool first_ = true; }; } // namespace std::string ViewModeModel::serialize() const { std::string out; { ObjWriter root(out); root.keyRaw("version", intToStr(1)); root.keyStr("activeMode", activeModeId_); // modes root.keyBegin("modes"); out += '['; { const auto& all = modes_.all(); for (std::size_t i = 0; i < all.size(); ++i) { if (i) out += ','; ObjWriter m(out); m.keyStr("id", all[i].id); m.keyStr("displayName", all[i].displayName); m.keyRaw("ordinal", intToStr(all[i].ordinal)); } } out += ']'; // membership: array of { guid, modes[], showBoth } root.keyBegin("membership"); out += '['; { bool first = true; for (const auto& [guid, mem] : membership_.all()) { if (!first) out += ','; first = false; ObjWriter e(out); e.keyStr("guid", guid); e.keyBegin("modes"); out += '['; { bool mf = true; for (const auto& id : mem.modeIds) { if (!mf) out += ','; mf = false; writeEscaped(out, id); } } out += ']'; e.keyRaw("showBoth", mem.showBoth ? "true" : "false"); } } out += ']'; // snapshots: array of { guid, showInTcp, showInMixer, mainSend, fxEnable, fxOffline[] } root.keyBegin("snapshots"); out += '['; { bool first = true; for (const auto& [guid, snap] : snapshots_) { if (!first) out += ','; first = false; ObjWriter e(out); e.keyStr("guid", guid); e.keyRaw("showInTcp", intToStr(snap.showInTcp)); e.keyRaw("showInMixer", intToStr(snap.showInMixer)); e.keyRaw("mainSend", intToStr(snap.mainSend)); e.keyRaw("fxEnable", intToStr(snap.fxEnable)); e.keyBegin("fxOffline"); writeIntArray(out, snap.fxOffline); } } out += ']'; } // root closes here (see bank_model note on NRVO + deferred close) return out; } // =========================================================================== // JSON — parser (recursive descent; false on any malformed input, never UB) // =========================================================================== namespace { class Parser { public: explicit Parser(const std::string& s) : s_(s) {} bool parseModel(ViewModeModel& out); private: const std::string& s_; std::size_t pos_ = 0; bool eof() const { return pos_ >= s_.size(); } void skipWs() { while (!eof()) { char c = s_[pos_]; if (c == ' ' || c == '\t' || c == '\n' || c == '\r') ++pos_; else break; } } bool consume(char c) { skipWs(); if (eof() || s_[pos_] != c) return false; ++pos_; return true; } bool parseString(std::string& out); bool parseRawScalar(std::string& out); bool parseInt(int& out); bool parseBool(bool& out); bool parseKey(std::string& key); bool skipValue(); bool parseModes(ModeRegistry& reg); bool parseMembership(MembershipIndex& idx); bool parseSnapshots(std::map& snaps); bool parseIntArray(std::vector& out); }; bool Parser::parseString(std::string& out) { skipWs(); if (eof() || s_[pos_] != '"') return false; ++pos_; out.clear(); while (!eof()) { char c = s_[pos_++]; if (c == '"') return true; if (c == '\\') { if (eof()) return false; char e = s_[pos_++]; switch (e) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; case 'b': out += '\b'; break; case 'f': out += '\f'; break; case 'n': out += '\n'; break; case 'r': out += '\r'; break; case 't': out += '\t'; break; case 'u': { auto readHex4 = [&](unsigned int& cp) -> bool { if (pos_ + 4 > s_.size()) return false; cp = 0; for (int i = 0; i < 4; ++i) { char h = s_[pos_++]; cp <<= 4; if (h >= '0' && h <= '9') cp |= static_cast(h - '0'); else if (h >= 'a' && h <= 'f') cp |= static_cast(h - 'a' + 10); else if (h >= 'A' && h <= 'F') cp |= static_cast(h - 'A' + 10); else return false; } return true; }; unsigned int hi = 0; if (!readHex4(hi)) return false; unsigned int codePoint = hi; if (hi >= 0xD800 && hi <= 0xDBFF) { if (pos_ + 6 > s_.size()) return false; if (s_[pos_] != '\\' || s_[pos_ + 1] != 'u') return false; pos_ += 2; unsigned int lo = 0; if (!readHex4(lo)) return false; if (lo < 0xDC00 || lo > 0xDFFF) return false; codePoint = 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00); } else if (hi >= 0xDC00 && hi <= 0xDFFF) { return false; } if (codePoint <= 0x7F) { out += static_cast(codePoint); } else if (codePoint <= 0x7FF) { out += static_cast(0xC0 | (codePoint >> 6)); out += static_cast(0x80 | (codePoint & 0x3F)); } else if (codePoint <= 0xFFFF) { out += static_cast(0xE0 | (codePoint >> 12)); out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); out += static_cast(0x80 | (codePoint & 0x3F)); } else { out += static_cast(0xF0 | (codePoint >> 18)); out += static_cast(0x80 | ((codePoint >> 12) & 0x3F)); out += static_cast(0x80 | ((codePoint >> 6) & 0x3F)); out += static_cast(0x80 | (codePoint & 0x3F)); } break; } default: return false; } } else { out += c; } } return false; // unterminated } bool Parser::parseRawScalar(std::string& out) { skipWs(); std::size_t start = pos_; while (!eof()) { char c = s_[pos_]; if (c == ',' || c == '}' || c == ']' || c == ' ' || c == '\t' || c == '\n' || c == '\r') break; ++pos_; } if (pos_ == start) return false; out.assign(s_, start, pos_ - start); return true; } bool Parser::parseInt(int& out) { std::string tok; if (!parseRawScalar(tok)) return false; const char* b = tok.c_str(); char* end = nullptr; errno = 0; long long v = std::strtoll(b, &end, 10); if (end != b + tok.size()) return false; if (errno == ERANGE) return false; if (v < INT_MIN || v > INT_MAX) return false; out = static_cast(v); return true; } bool Parser::parseBool(bool& out) { std::string tok; if (!parseRawScalar(tok)) return false; if (tok == "true") { out = true; return true; } if (tok == "false") { out = false; return true; } return false; } bool Parser::parseKey(std::string& key) { if (!parseString(key)) return false; return consume(':'); } bool Parser::skipValue() { skipWs(); if (eof()) return false; char c = s_[pos_]; if (c == '"') { std::string tmp; return parseString(tmp); } if (c == '{' || c == '[') { char open = c, close = (c == '{') ? '}' : ']'; ++pos_; int depth = 1; while (!eof() && depth > 0) { char d = s_[pos_]; if (d == '"') { std::string tmp; if (!parseString(tmp)) return false; continue; } if (d == open) ++depth; else if (d == close) --depth; ++pos_; } return depth == 0; } std::string tmp; return parseRawScalar(tmp); } bool Parser::parseIntArray(std::vector& out) { if (!consume('[')) return false; skipWs(); if (consume(']')) return true; do { int v = 0; if (!parseInt(v)) return false; out.push_back(v); } while (consume(',')); return consume(']'); } // The registry starts seeded (Arrange + Design). Deserialization must reproduce the // serialized set exactly, so we replace the seeded contents with the parsed ones — // add() dedups by id, so a serialized Arrange/Design would otherwise be rejected as // duplicates and the ordinals/names would not round-trip. We therefore parse into a // fresh vector and swap. `reg` is passed empty (see parseModel). bool Parser::parseModes(ModeRegistry& reg) { if (!consume('[')) return false; skipWs(); if (consume(']')) return true; // empty array (unusual, but valid) do { if (!consume('{')) return false; Mode m; bool haveId = false; do { std::string k; if (!parseKey(k)) return false; if (k == "id") { if (!parseString(m.id)) return false; haveId = true; } else if (k == "displayName") { if (!parseString(m.displayName)) return false; } else if (k == "ordinal") { if (!parseInt(m.ordinal)) return false; } else if (!skipValue()) return false; } while (consume(',')); if (!consume('}')) return false; if (!haveId || !reg.add(m)) return false; // malformed / duplicate id } while (consume(',')); return consume(']'); } bool Parser::parseMembership(MembershipIndex& idx) { if (!consume('[')) return false; skipWs(); if (consume(']')) return true; do { if (!consume('{')) return false; std::string guid; Membership mem; bool haveGuid = false; do { std::string k; if (!parseKey(k)) return false; if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; } else if (k == "modes") { if (!consume('[')) return false; skipWs(); if (!consume(']')) { do { std::string id; if (!parseString(id)) return false; mem.modeIds.insert(id); } while (consume(',')); if (!consume(']')) return false; } } else if (k == "showBoth") { if (!parseBool(mem.showBoth)) return false; } else if (!skipValue()) return false; } while (consume(',')); if (!consume('}')) return false; if (!haveGuid || guid.empty()) return false; // Install the entry verbatim (tag() would clear a multi-mode set and drop // show-both). A serialized entry is trusted to already satisfy the model's // invariants. // // Deliberate tolerance: we do NOT validate that membership modeIds reference // registered modes, and we do not validate snapshot GUIDs against the index. // Stale-GUID and stale-mode tolerance is a stated invariant of this model — // a deserialized entry is treated as trusted data, not as live cross-checked // state. Rejecting stale entries here would violate that invariant. The one // exception is activeMode (validated below in parseModel): a persisted active // mode that no longer exists has an immediate behavioral consequence, so it // is caught and the parse is rejected. if (!idx.restore(guid, mem)) return false; } while (consume(',')); return consume(']'); } bool Parser::parseSnapshots(std::map& snaps) { if (!consume('[')) return false; skipWs(); if (consume(']')) return true; do { if (!consume('{')) return false; std::string guid; TrackSnapshot snap; bool haveGuid = false; do { std::string k; if (!parseKey(k)) return false; if (k == "guid") { if (!parseString(guid)) return false; haveGuid = true; } else if (k == "showInTcp") { if (!parseInt(snap.showInTcp)) return false; } else if (k == "showInMixer") { if (!parseInt(snap.showInMixer)) return false; } else if (k == "mainSend") { if (!parseInt(snap.mainSend)) return false; } else if (k == "fxEnable") { if (!parseInt(snap.fxEnable)) return false; } else if (k == "fxOffline") { if (!parseIntArray(snap.fxOffline)) return false; } else if (!skipValue()) return false; } while (consume(',')); if (!consume('}')) return false; if (!haveGuid || guid.empty()) return false; snaps[guid] = snap; } while (consume(',')); return consume(']'); } bool Parser::parseModel(ViewModeModel& out) { if (!consume('{')) return false; skipWs(); if (consume('}')) return true; // lenient empty root ⇒ default-seeded model ModeRegistry reg; // seeded default; REPLACED if a modes array is present bool haveModes = false; std::string activeMode; bool haveActive = false; MembershipIndex membership; std::map snaps; do { std::string key; if (!parseKey(key)) return false; if (key == "activeMode") { if (!parseString(activeMode)) return false; haveActive = true; } else if (key == "modes") { ModeRegistry fresh = ModeRegistry::makeEmpty(); // parse into empty, then own if (!parseModes(fresh)) return false; reg = fresh; haveModes = true; } else if (key == "membership") { if (!parseMembership(membership)) return false; } else if (key == "snapshots") { if (!parseSnapshots(snaps)) return false; } else { // Unknown keys and the "version" field are skipped here. // "version" is serialized as a forward-compat placeholder — there is no // active version gate yet; all persisted data is parsed the same way // regardless of the value. A future gate would add a version branch here. if (!skipValue()) return false; } } while (consume(',')); if (!consume('}')) return false; skipWs(); if (!eof()) return false; // trailing garbage if (haveModes) out.modes() = reg; out.membership() = membership; for (const auto& [guid, snap] : snaps) out.storeSnapshot(guid, snap); if (haveActive) { if (!out.setActiveMode(activeMode)) return false; // active mode must exist } return true; } } // namespace std::optional ViewModeModel::deserialize(const std::string& json) { ViewModeModel vm; Parser p(json); if (!p.parseModel(vm)) return std::nullopt; return vm; } } // namespace reasampler