#include "core/model/owned_manifest.h" #include #include "core/json/json.h" // owned_manifest implementation. // // JSON rides on the shared core/json lexical layer (Q-W1, 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 domain parse is all it needs. namespace reasampler::model { // --------------------------------------------------------------------------- // 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 (shared core/json escape — byte-identical to the prior local one) // --------------------------------------------------------------------------- std::string OwnedFileManifest::serialize() const { std::string out = "{\"owned\":["; for (std::size_t i = 0; i < paths_.size(); ++i) { if (i) out += ','; json::writeEscaped(out, paths_[i]); } out += "]}"; return out; } // --------------------------------------------------------------------------- // JSON parser (string-array-only DOMAIN grammar over the shared core/json // lexical layer). Tolerates unknown keys (forward-compat) and requires the // "owned" value to be an array of strings. // --------------------------------------------------------------------------- namespace { bool parseManifest(json::Reader& r, OwnedFileManifest& out) { if (!r.consume('{')) return false; r.skipWs(); if (r.consume('}')) return true; // empty object -> empty manifest for (;;) { std::string key; if (!r.parseKey(key)) return false; if (key == "owned") { std::vector paths; if (!r.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 (!r.skipValue()) return false; // forward-compat: tolerate unknown keys } r.skipWs(); if (r.consume(',')) continue; if (r.consume('}')) return true; return false; } } } // namespace std::optional OwnedFileManifest::deserialize(const std::string& blob) { OwnedFileManifest m; json::Reader r(blob); if (!parseManifest(r, m)) return std::nullopt; return m; } } // namespace reasampler::model