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

163 lines
6.3 KiB
C++

#include "core/model/provenance.h"
#include <cstdio>
#include "core/wire/wire.h"
// provenance implementation — pure, self-contained (no third-party lib, mirror of
// bank_model's hand-rolled encoding discipline).
//
// ENCODING (the fingerprint string): a length-prefixed, field-ordered format so it
// is unambiguous and forge-proof (a value containing the separator cannot shift
// the parse). Grammar:
//
// "rsprov1" -- magic + version tag
// then, in fixed order, each field as <len>':'<bytes>
//
// Every field — including numbers — is emitted as its decimal / %.17g text then
// length-prefixed, so the parser never has to guess a field boundary. A trailing
// field is the track-GUID count followed by that many length-prefixed GUIDs, then
// the folded fxChainIdentity. Numbers use the SAME %.17g the bank model uses so a
// double round-trips bit-for-bit. Any deviation (wrong magic, short read, bad
// number) -> parseFingerprint returns nullopt.
//
// The fxChainIdentity fold is itself length-prefixed per entry field, so it is
// injection-proof on its own and can be embedded whole as one more length-prefixed
// field of the fingerprint.
namespace reasampler::model {
bool CaptureRecipe::operator==(const CaptureRecipe& o) const {
return scope == o.scope && sourceMode == o.sourceMode &&
startSeconds == o.startSeconds && endSeconds == o.endSeconds &&
tailMode == o.tailMode && tailMs == o.tailMs &&
sampleRate == o.sampleRate && channelCount == o.channelCount &&
trackGuids == o.trackGuids && fxChainIdentity == o.fxChainIdentity;
}
namespace {
constexpr const char* kMagic = "rsprov1";
// The shared core/wire codec (Q-W1, T2-01b) carries the field grammar + the full
// hardening (incl. the fixed fieldInt range check that closes the old strtol
// silent-narrowing TODO). Only the %.17g double rendering stays local — it is
// this writer's convention, shared with the bank model's JSON doubles.
using wire::putField;
using Cursor = wire::Cursor;
std::string dblToStr(double v) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%.17g", v);
return buf;
}
} // namespace
std::string fxChainIdentity(const std::vector<FxIdentityEntry>& entries) {
std::string out;
// Count first, then each entry's three fields length-prefixed. Order is part of
// identity (chain order matters), so we emit in the given vector order.
putField(out, std::to_string(entries.size()));
for (const FxIdentityEntry& e : entries) {
putField(out, e.name);
putField(out, e.guid);
putField(out, e.enabled ? "1" : "0");
}
return out;
}
std::string combineChainIdentities(const std::vector<std::string>& perTrack) {
std::string out;
putField(out, std::to_string(perTrack.size()));
for (const std::string& id : perTrack) putField(out, id);
return out;
}
std::string buildFingerprint(const CaptureRecipe& r) {
std::string out(kMagic);
putField(out, std::to_string(static_cast<int>(r.scope)));
putField(out, std::to_string(r.sourceMode));
putField(out, dblToStr(r.startSeconds));
putField(out, dblToStr(r.endSeconds));
putField(out, std::to_string(r.tailMode));
putField(out, dblToStr(r.tailMs));
putField(out, std::to_string(r.sampleRate));
putField(out, std::to_string(r.channelCount));
putField(out, std::to_string(r.trackGuids.size()));
for (const std::string& g : r.trackGuids) putField(out, g);
putField(out, r.fxChainIdentity);
return out;
}
std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint) {
Cursor c(fingerprint);
if (!c.literal(kMagic)) return std::nullopt;
CaptureRecipe r;
int scopeInt = 0;
if (!c.fieldInt(scopeInt)) return std::nullopt;
if (scopeInt != static_cast<int>(ProvenanceScope::Item) &&
scopeInt != static_cast<int>(ProvenanceScope::Track))
return std::nullopt;
r.scope = static_cast<ProvenanceScope>(scopeInt);
if (!c.fieldInt(r.sourceMode)) return std::nullopt;
if (!c.fieldDouble(r.startSeconds)) return std::nullopt;
if (!c.fieldDouble(r.endSeconds)) return std::nullopt;
if (!c.fieldInt(r.tailMode)) return std::nullopt;
if (!c.fieldDouble(r.tailMs)) return std::nullopt;
if (!c.fieldInt(r.sampleRate)) return std::nullopt;
if (!c.fieldInt(r.channelCount)) return std::nullopt;
std::size_t guidCount = 0;
if (!c.fieldSizeT(guidCount)) return std::nullopt;
// Q-W0 T2-01a (the sample_usage count-sanity pattern): each GUID field costs at least
// 2 wire bytes ("0:"), so a count past size/2 is provably bogus — reject BEFORE the
// reserve, so a corrupt/crafted persisted fingerprint can never drive reserve(huge)
// into std::length_error / bad_alloc through the shell.
if (guidCount > fingerprint.size() / 2u + 1u) return std::nullopt;
r.trackGuids.reserve(guidCount);
for (std::size_t i = 0; i < guidCount; ++i) {
std::string g;
if (!c.field(g)) return std::nullopt;
r.trackGuids.push_back(std::move(g));
}
if (!c.field(r.fxChainIdentity)) return std::nullopt;
// Trailing garbage means the string was not produced by our writer -> reject,
// so a corrupt/extended blob never silently drives a partial re-capture.
if (!c.ok() || !c.atEnd()) return std::nullopt;
return r;
}
std::optional<std::string> detectParent(
const std::vector<std::string>& sourceItemFiles,
const std::vector<BankFileRef>& bankFiles) {
if (sourceItemFiles.empty()) return std::nullopt;
std::optional<std::string> parent; // the single bank sample all sources point at
for (const std::string& src : sourceItemFiles) {
// Resolve this source file against the bank by exact normalized path.
const std::string* matchedId = nullptr;
for (const BankFileRef& ref : bankFiles) {
if (!ref.absolutePath.empty() && ref.absolutePath == src) {
matchedId = &ref.sampleId;
break;
}
}
if (matchedId == nullptr)
return std::nullopt; // a source item is NOT a bank file -> not a resample
if (!parent) {
parent = *matchedId;
} else if (*parent != *matchedId) {
return std::nullopt; // sources span >1 bank sample -> ambiguous, no parent
}
}
return parent;
}
} // namespace reasampler::model