#include "owned_manifest.h" #include #include // 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 : (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(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(c) < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", static_cast(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& 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(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; // unpaired low surrogate } 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 string } bool Parser::parseStringArray(std::vector& 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 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::deserialize(const std::string& json) { OwnedFileManifest m; Parser p(json); if (!p.parseManifest(m)) return std::nullopt; return m; } } // namespace reasampler