289 lines
9.9 KiB
C++
289 lines
9.9 KiB
C++
// native_bridge/src/StateStore.cpp
|
|
#include "StateStore.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
#include <cerrno>
|
|
#include <sys/stat.h>
|
|
|
|
#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;
|
|
}
|
|
|
|
// 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
|
|
}
|