Files
reasampler/src/core/model/owned_manifest.cpp
T

113 lines
4.1 KiB
C++

#include "core/model/owned_manifest.h"
#include <cctype>
#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 <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 (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<std::string> 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> OwnedFileManifest::deserialize(const std::string& blob) {
OwnedFileManifest m;
json::Reader r(blob);
if (!parseManifest(r, m)) return std::nullopt;
return m;
}
} // namespace reasampler::model