Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user