Tighten RSBK package-format validation for review remediation

Reject NUL/control bytes and Windows-hostile names in entry names, relax
the over-broad ".." substring ban to component-only, close the
trailing-garbage gap on empty manifests, and relocate the package
CMake subdirectory to its ladder home.
This commit is contained in:
2026-08-02 07:56:45 -04:00
parent 043558a54d
commit e0b4ec2e21
10 changed files with 161 additions and 56 deletions
+32 -32
View File
@@ -5,11 +5,6 @@
#include "core/json/json.h"
#include "core/package/package_format.h"
// Duplicate entry names are rejected in both directions: two payloads landing
// on one destination name is incoherent, and on import it would be a silent
// overwrite. Sample-id rules (remap, collision) are deliberately NOT enforced
// here — they are the import plan's decisions, not the codec's.
namespace reasampler::package {
namespace {
@@ -17,6 +12,8 @@ namespace {
using json::numToStr;
using ObjWriter = json::Writer;
// Shared by serializeManifest and deserializeManifest — see this directory's
// CLAUDE.md for why duplicate names are rejected both ways.
bool duplicateName(const std::vector<PackageEntry>& entries) {
for (std::size_t i = 0; i < entries.size(); ++i)
for (std::size_t j = i + 1; j < entries.size(); ++j)
@@ -155,37 +152,40 @@ bool parseEntry(json::Reader& r, PackageEntry& e) {
bool parseManifest(json::Reader& r, PackageManifest& m) {
if (!r.consume('{')) return false;
r.skipWs();
if (r.consume('}')) return true; // empty object: a valid empty manifest
bool haveEntries = false;
if (!r.consume('}')) { // not the empty-object shortcut: parse the members
do {
std::string key;
if (!r.parseKey(key)) return false;
do {
std::string key;
if (!r.parseKey(key)) return false;
if (key == "bankName") {
if (!r.parseString(m.bankDisplayName)) return false;
} else if (key == "exported") {
if (!r.parseInt64(m.exportTimestamp)) return false;
} else if (key == "entries") {
if (!r.consume('[')) return false;
r.skipWs();
if (!r.consume(']')) {
do {
PackageEntry e;
if (!parseEntry(r, e)) return false;
m.entries.push_back(std::move(e));
} while (r.consume(','));
if (!r.consume(']')) return false;
if (key == "bankName") {
if (!r.parseString(m.bankDisplayName)) return false;
} else if (key == "exported") {
if (!r.parseInt64(m.exportTimestamp)) return false;
} else if (key == "entries") {
if (haveEntries) return false; // a repeated key must not accumulate
haveEntries = true;
if (!r.consume('[')) return false;
r.skipWs();
if (!r.consume(']')) {
do {
PackageEntry e;
if (!parseEntry(r, e)) return false;
m.entries.push_back(std::move(e));
} while (r.consume(','));
if (!r.consume(']')) return false;
}
} else if (key == "slots") {
if (!parseSlots(r, m.slots)) return false;
} else {
if (!r.skipValue()) return false; // forward-compat unknown keys
}
} else if (key == "slots") {
if (!parseSlots(r, m.slots)) return false;
} else {
if (!r.skipValue()) return false; // forward-compat unknown keys
}
} while (r.consume(','));
} while (r.consume(','));
if (!r.consume('}')) return false;
if (!r.consume('}')) return false;
}
r.skipWs();
if (!r.eof()) return false; // trailing garbage
if (!r.eof()) return false; // trailing garbage — even after an empty object
return !duplicateName(m.entries);
}