// native_bridge/src/StateStore.cpp #include "StateStore.h" #include #include #include #include #include #include #include #ifdef _WIN32 #include #endif namespace { // Escape a string for JSON output (keeps UTF-8 bytes >= 0x20 as-is). std::string jsonEscape(const std::string& s) { std::string out; out.reserve(s.size() + 16); for (unsigned char c : s) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (c < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", c); out += buf; } else { out += static_cast(c); } } } return out; } // Create the parent directory of `path` (all missing levels). Returns false // only on a hard error (permission, invalid path). bool ensureParentDir(const std::string& path) { const size_t slash = path.find_last_of("/\\"); if (slash == std::string::npos) return true; const std::string dir = path.substr(0, slash); if (dir.empty()) return true; #ifdef _WIN32 // CreateDirectoryA creates one level at a time - walk the chain so // %APPDATA%\SonicForgeDAW\state (or a custom SF_STATE_FILE dir) is // created even when none of it exists yet. for (size_t i = 0; i < dir.size(); ++i) { if ((dir[i] == '/' || dir[i] == '\\') && i > 0) CreateDirectoryA(dir.substr(0, i).c_str(), nullptr); } if (CreateDirectoryA(dir.c_str(), nullptr)) return true; return GetLastError() == ERROR_ALREADY_EXISTS; #else for (size_t i = 1; i < dir.size(); ++i) if (dir[i] == '/') mkdir(dir.substr(0, i).c_str(), 0755); return mkdir(dir.c_str(), 0755) == 0 || errno == EEXIST; #endif } // Skip whitespace; returns pointer at first non-ws char. const char* skipWs(const char* p) { while (p && *p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) ++p; return p; } // Find the value after `"key":` starting at `from`. Returns pointer at the // first value char (after ws), or nullptr. const char* findValue(const std::string& s, const char* key, size_t from = 0) { size_t keyLen = std::strlen(key); size_t pos = s.find(key, from); while (pos != std::string::npos) { if (pos > 0 && s[pos - 1] == '"') { size_t colon = s.find(':', pos + keyLen); if (colon != std::string::npos) return skipWs(s.c_str() + colon + 1); } pos = s.find(key, pos + 1); } return nullptr; } // Parse a quoted JSON string starting at *pp (which must point at '"'). // Advances *pp past the closing quote. Returns false on malformed input. bool parseString(const char** pp, std::string& out) { const char* p = *pp; if (!p || *p != '"') return false; ++p; out.clear(); while (*p && *p != '"') { if (*p == '\\') { ++p; switch (*p) { case '"': out += '"'; ++p; break; case '\\': out += '\\'; ++p; break; case '/': out += '/'; ++p; break; case 'n': out += '\n'; ++p; break; case 'r': out += '\r'; ++p; break; case 't': out += '\t'; ++p; break; case 'u': { unsigned v = 0; for (int i = 0; i < 4; ++i) { ++p; if (!*p) return false; char c = *p; v <<= 4; if (c >= '0' && c <= '9') v |= (unsigned)(c - '0'); else if (c >= 'a' && c <= 'f') v |= (unsigned)(c - 'a' + 10); else if (c >= 'A' && c <= 'F') v |= (unsigned)(c - 'A' + 10); else return false; } out += (char)(v & 0xFF); break; } default: return false; } } else { out += *p; ++p; } } if (*p != '"') return false; *pp = p + 1; return true; } // Parse an integer until delimiter , } ] ws. Returns false on empty. bool parseIntAt(const char** pp, long& out) { const char* p = skipWs(*pp); if (!p || !*p) return false; long sign = 1; if (*p == '-') { sign = -1; ++p; } if (*p < '0' || *p > '9') return false; long v = 0; while (*p >= '0' && *p <= '9') { v = v * 10 + (*p - '0'); ++p; } out = sign * v; *pp = p; return true; } // Parse a double until delimiter , } ] ws. bool parseDoubleAt(const char** pp, double& out) { const char* p = skipWs(*pp); const char* start = p; while (*p && *p != ',' && *p != '}' && *p != ']' && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') ++p; if (p == start) return false; out = std::atof(std::string(start, p - start).c_str()); *pp = p; return true; } } // namespace std::string StateStore::defaultPath() { #ifdef _WIN32 const char* appData = std::getenv("APPDATA"); std::string base = appData && *appData ? appData : "."; return base + "\\SonicForgeDAW\\state\\bridge_state.json"; #else const char* home = std::getenv("HOME"); std::string base = home && *home ? home : "."; return base + "/.sonicforge/state/bridge_state.json"; #endif } bool StateStore::save(const BridgeStateSnapshot& snap, const std::string& path) { if (!ensureParentDir(path)) return false; std::string json; json.reserve(256 + snap.instruments.size() * 192); json += "{\n"; json += " \"version\": 1,\n"; json += " \"playing\": "; json += snap.playing ? "true" : "false"; json += ",\n"; json += " \"playhead\": "; char buf[64]; std::snprintf(buf, sizeof(buf), "%.6f", snap.playhead); json += buf; json += ",\n"; json += " \"instruments\": [\n"; for (size_t i = 0; i < snap.instruments.size(); ++i) { const InstrumentStateEntry& e = snap.instruments[i]; json += " {\n"; json += " \"channel\": " + std::to_string(e.channel) + ",\n"; json += " \"type\": " + std::to_string(e.type) + ",\n"; json += " \"path\": \"" + jsonEscape(e.path) + "\",\n"; json += " \"presetBase64\": \"" + jsonEscape(e.presetBase64) + "\",\n"; json += " \"bank\": " + std::to_string(e.bank) + ",\n"; json += " \"program\": " + std::to_string(e.program) + "\n"; json += " }"; if (i + 1 < snap.instruments.size()) json += ","; json += "\n"; } json += " ]\n"; json += "}\n"; // Atomic write: temp file in same dir, then rename over the target. const std::string tmp = path + ".tmp"; { std::ofstream f(tmp, std::ios::binary | std::ios::trunc); if (!f) return false; f.write(json.data(), (std::streamsize)json.size()); f.flush(); if (!f) return false; f.close(); } #ifdef _WIN32 if (!MoveFileExA(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING)) { // Fallback: direct write if rename failed (e.g. AV lock on target). std::ofstream f(path, std::ios::binary | std::ios::trunc); if (!f) return false; f.write(json.data(), (std::streamsize)json.size()); return (bool)f; } #else if (std::rename(tmp.c_str(), path.c_str()) != 0) return false; #endif return true; } bool StateStore::load(BridgeStateSnapshot& out, const std::string& path) { std::ifstream f(path, std::ios::binary); if (!f) return false; std::ostringstream ss; ss << f.rdbuf(); const std::string json = ss.str(); if (json.empty()) return false; BridgeStateSnapshot snap; const char* p; if ((p = findValue(json, "playing")) != nullptr) { p = skipWs(p); if (std::strncmp(p, "true", 4) == 0) snap.playing = true; else if (std::strncmp(p, "false", 5) == 0) snap.playing = false; else return false; } if ((p = findValue(json, "playhead")) != nullptr) { double v = 0.0; if (!parseDoubleAt(&p, v)) return false; snap.playhead = v; } // Parse the instruments array object by object. const char* arr = findValue(json, "instruments"); if (arr && *arr == '[') { const char* q = arr + 1; while (*q && *q != ']') { q = skipWs(q); if (*q == ',') { ++q; continue; } if (*q != '{') { if (*q == ']') break; return false; } // Our writer never nests braces inside an entry. const char* objEnd = std::strchr(q, '}'); if (!objEnd) return false; std::string obj(q, objEnd - q + 1); q = objEnd + 1; InstrumentStateEntry e; if ((p = findValue(obj, "channel"))) { long v; if (parseIntAt(&p, v)) e.channel = (uint32_t)v; } if ((p = findValue(obj, "type"))) { long v; if (parseIntAt(&p, v)) e.type = (int)v; } if ((p = findValue(obj, "path"))) { std::string s; if (parseString(&p, s)) e.path = s; } if ((p = findValue(obj, "presetBase64"))){ std::string s; if (parseString(&p, s)) e.presetBase64 = s; } if ((p = findValue(obj, "bank"))) { long v; if (parseIntAt(&p, v)) e.bank = (uint32_t)v; } if ((p = findValue(obj, "program"))) { long v; if (parseIntAt(&p, v)) e.program = (uint32_t)v; } snap.instruments.push_back(e); } } out = snap; return true; } bool StateStore::clear(const std::string& path) { #ifdef _WIN32 bool ok = DeleteFileA(path.c_str()) != 0; DeleteFileA((path + ".tmp").c_str()); return ok; #else bool ok = std::remove(path.c_str()) == 0; std::remove((path + ".tmp").c_str()); return ok; #endif }