63f35fa58d
Pure OwnedFileManifest (relative paths, dedup, JSON round-trip) persisted under sibling owned_files ext-state key; both capture commit paths record; joins the R-B undo-reload set. Phase R prune consumes it later.
316 lines
11 KiB
C++
316 lines
11 KiB
C++
#include "owned_manifest.h"
|
|
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
|
|
// owned_manifest implementation.
|
|
//
|
|
// JSON is hand-rolled and self-contained (project convention: the pure core is
|
|
// dependency-free — no third-party JSON lib, mirror of bank_model / bank_book /
|
|
// tail_control). The shape is a single object with one string array:
|
|
//
|
|
// {"owned":["reasampler_bank/a.wav","reasampler_bank/b.wav"]}
|
|
//
|
|
// so a compact writer + a focused string-array parser is all it needs — far smaller
|
|
// than bank_model's full recursive-descent parser, because there is exactly one key
|
|
// and one value kind.
|
|
|
|
namespace reasampler {
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// path invariant (mirror of bank_model's isAbsolutePath)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
|
|
// Any leading '/' or '\' (POSIX root / UNC), or a leading <alpha>: (Windows drive,
|
|
// incl. drive-relative "C:foo") is absolute. Same rejection bank_model applies to
|
|
// Sample.relativePath — the manifest holds the SAME kind of path, so the invariant
|
|
// must match exactly (a path the index accepts must be recordable, and vice versa).
|
|
bool isAbsolutePath(const std::string& p) {
|
|
if (p.empty()) return false;
|
|
if (p[0] == '/' || p[0] == '\\') return true;
|
|
if (p.size() >= 2 && std::isalpha(static_cast<unsigned char>(p[0])) && p[1] == ':')
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// mutation / query
|
|
// ---------------------------------------------------------------------------
|
|
|
|
ManifestAddResult OwnedFileManifest::add(const std::string& relativePath) {
|
|
if (relativePath.empty()) return ManifestAddResult::RejectedEmptyPath;
|
|
if (isAbsolutePath(relativePath)) return ManifestAddResult::RejectedAbsolutePath;
|
|
if (contains(relativePath)) return ManifestAddResult::AlreadyPresent;
|
|
paths_.push_back(relativePath);
|
|
return ManifestAddResult::Added;
|
|
}
|
|
|
|
bool OwnedFileManifest::contains(const std::string& relativePath) const {
|
|
for (const auto& p : paths_)
|
|
if (p == relativePath) return true;
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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<unsigned char>(c) < 0x20) {
|
|
char buf[8];
|
|
std::snprintf(buf, sizeof(buf), "\\u%04x",
|
|
static_cast<unsigned char>(c));
|
|
out += buf;
|
|
} else {
|
|
out += c;
|
|
}
|
|
}
|
|
}
|
|
out += '"';
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string OwnedFileManifest::serialize() const {
|
|
std::string out = "{\"owned\":[";
|
|
for (std::size_t i = 0; i < paths_.size(); ++i) {
|
|
if (i) out += ',';
|
|
writeEscaped(out, paths_[i]);
|
|
}
|
|
out += "]}";
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// JSON parser (string-array only)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
|
|
class Parser {
|
|
public:
|
|
explicit Parser(const std::string& s) : s_(s) {}
|
|
|
|
// Parse the manifest object into `out`. Tolerates unknown keys (forward-compat)
|
|
// and requires the "owned" value to be an array of strings.
|
|
bool parseManifest(OwnedFileManifest& 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 parseStringArray(std::vector<std::string>& out);
|
|
bool skipValue(); // for forward-compat unknown keys
|
|
};
|
|
|
|
// Parses a JSON string literal (with the escapes our writer emits, plus \uXXXX for
|
|
// control chars). Positioned before the opening quote (skips leading whitespace).
|
|
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<unsigned>(h - '0');
|
|
else if (h >= 'a' && h <= 'f') cp |= static_cast<unsigned>(h - 'a' + 10);
|
|
else if (h >= 'A' && h <= 'F') cp |= static_cast<unsigned>(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; // unpaired low surrogate
|
|
}
|
|
|
|
if (codePoint <= 0x7F) {
|
|
out += static_cast<char>(codePoint);
|
|
} else if (codePoint <= 0x7FF) {
|
|
out += static_cast<char>(0xC0 | (codePoint >> 6));
|
|
out += static_cast<char>(0x80 | (codePoint & 0x3F));
|
|
} else if (codePoint <= 0xFFFF) {
|
|
out += static_cast<char>(0xE0 | (codePoint >> 12));
|
|
out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
|
|
out += static_cast<char>(0x80 | (codePoint & 0x3F));
|
|
} else {
|
|
out += static_cast<char>(0xF0 | (codePoint >> 18));
|
|
out += static_cast<char>(0x80 | ((codePoint >> 12) & 0x3F));
|
|
out += static_cast<char>(0x80 | ((codePoint >> 6) & 0x3F));
|
|
out += static_cast<char>(0x80 | (codePoint & 0x3F));
|
|
}
|
|
break;
|
|
}
|
|
default: return false;
|
|
}
|
|
} else {
|
|
out += c;
|
|
}
|
|
}
|
|
return false; // unterminated string
|
|
}
|
|
|
|
bool Parser::parseStringArray(std::vector<std::string>& out) {
|
|
if (!consume('[')) return false;
|
|
skipWs();
|
|
if (consume(']')) return true; // empty array
|
|
for (;;) {
|
|
std::string s;
|
|
if (!parseString(s)) return false;
|
|
out.push_back(std::move(s));
|
|
skipWs();
|
|
if (consume(',')) continue;
|
|
if (consume(']')) return true;
|
|
return false; // neither separator nor terminator — malformed
|
|
}
|
|
}
|
|
|
|
// Skip a single JSON value (string / array / object / bare scalar) so an unknown key
|
|
// does not abort the parse. Minimal: enough for forward-compat siblings we don't know.
|
|
bool Parser::skipValue() {
|
|
skipWs();
|
|
if (eof()) return false;
|
|
char c = s_[pos_];
|
|
if (c == '"') {
|
|
std::string tmp;
|
|
return parseString(tmp);
|
|
}
|
|
if (c == '[' || c == '{') {
|
|
// Balance nested brackets of either kind, ignoring bracket chars inside
|
|
// strings. Enough to step over an unknown nested value; not a full validator.
|
|
int depth = 0;
|
|
bool inStr = false;
|
|
while (!eof()) {
|
|
char d = s_[pos_];
|
|
if (inStr) {
|
|
if (d == '\\') { pos_ += 2; continue; }
|
|
if (d == '"') inStr = false;
|
|
++pos_;
|
|
continue;
|
|
}
|
|
if (d == '"') { inStr = true; ++pos_; continue; }
|
|
if (d == '[' || d == '{') ++depth;
|
|
else if (d == ']' || d == '}') {
|
|
--depth;
|
|
if (depth == 0) { ++pos_; return true; }
|
|
}
|
|
++pos_;
|
|
}
|
|
return false;
|
|
}
|
|
// bare scalar (number / true / false / null) — read to the next structural char
|
|
while (!eof()) {
|
|
char d = s_[pos_];
|
|
if (d == ',' || d == '}' || d == ']' || d == ' ' || d == '\t' ||
|
|
d == '\n' || d == '\r')
|
|
break;
|
|
++pos_;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Parser::parseManifest(OwnedFileManifest& out) {
|
|
if (!consume('{')) return false;
|
|
skipWs();
|
|
if (consume('}')) return true; // empty object -> empty manifest
|
|
for (;;) {
|
|
std::string key;
|
|
if (!parseString(key)) return false;
|
|
if (!consume(':')) return false;
|
|
if (key == "owned") {
|
|
std::vector<std::string> paths;
|
|
if (!parseStringArray(paths)) return false;
|
|
for (auto& p : paths) {
|
|
// Feed through add() so the persisted invariants (dedup, reject
|
|
// empty/absolute) are re-asserted on load — a hand-edited or corrupt
|
|
// blob cannot smuggle an absolute or duplicate path into the manifest.
|
|
out.add(p);
|
|
}
|
|
} else {
|
|
if (!skipValue()) return false; // forward-compat: tolerate unknown keys
|
|
}
|
|
skipWs();
|
|
if (consume(',')) continue;
|
|
if (consume('}')) return true;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::optional<OwnedFileManifest> OwnedFileManifest::deserialize(const std::string& json) {
|
|
OwnedFileManifest m;
|
|
Parser p(json);
|
|
if (!p.parseManifest(m)) return std::nullopt;
|
|
return m;
|
|
}
|
|
|
|
} // namespace reasampler
|