G2.0: StateStore (JSON state file + atomic write)
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
// native_bridge/src/StateStore.cpp
|
||||
#include "StateStore.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#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<char>(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user