fix(ingest): convert-on-import to 32f WAV, undo-group bank+assign, overflow guards

Non-WAV sources decode via PCM_source::GetSamples and land as canonical 32f
RIFF/WAVE; hash taken post-conversion so re-imports dedup. Undo block covers
bank mutation + assign_request atomically. Overflow guards + adversarial tests.
This commit is contained in:
2026-07-26 21:56:09 -04:00
parent 8074e21057
commit 373948c18a
5 changed files with 378 additions and 78 deletions
+32 -5
View File
@@ -3,6 +3,7 @@
#include "assignment_request.h"
#include <cstddef>
#include <limits>
namespace reasampler {
@@ -29,27 +30,44 @@ public:
bool atEnd() const { return pos_ >= s_.size(); }
// Reads one length-prefixed field into `out`. Fails on a missing ':', an empty or
// non-numeric length, or a length that runs past the end.
// non-numeric length, a length that overflows SIZE_MAX, or a length that runs past
// the end. The digit count is capped at 20 (the decimal width of SIZE_MAX on a
// 64-bit host) so a crafted 200-digit length cannot accumulate past SIZE_MAX via
// repeated multiply. "never UB" promise from the header is upheld here.
bool field(std::string& out) {
if (!ok_) return false;
const std::size_t colon = s_.find(':', pos_);
if (colon == std::string::npos) return fail();
if (colon == pos_) return fail(); // empty length token
// Cap: SIZE_MAX fits in at most 20 decimal digits; a longer run is bogus.
if (colon - pos_ > 20u) return fail();
std::size_t len = 0;
for (std::size_t i = pos_; i < colon; ++i) {
const char c = s_[i];
if (c < '0' || c > '9') return fail();
len = len * 10 + static_cast<std::size_t>(c - '0');
const std::size_t digit = static_cast<std::size_t>(c - '0');
// Overflow guard: if len would exceed SIZE_MAX after multiply+add, fail.
if (len > (std::numeric_limits<std::size_t>::max() - digit) / 10u)
return fail();
len = len * 10u + digit;
}
const std::size_t start = colon + 1;
if (start + len > s_.size()) return fail();
// Guard: start may equal s_.size() (empty remainder), in which case only len==0
// is valid; start > s_.size() cannot happen (colon < s_.size() by find()).
// Use subtraction-first form to avoid start+len wrapping on a huge len.
if (start > s_.size() || len > s_.size() - start) return fail();
out.assign(s_, start, len);
pos_ = start + len;
return true;
}
// Reads a length-prefixed field and parses it as a signed 64-bit decimal (an
// optional leading '-'). Fails on empty, non-digit, or trailing bytes.
// optional leading '-'). Fails on empty, non-digit, trailing bytes, or a value
// that would overflow INT64_MAX / underflow INT64_MIN. The digit count is capped
// at 19 (the decimal width of INT64_MAX, plus 1 for the optional sign = 20
// characters maximum) so a crafted 21-digit field cannot accumulate UB. "never UB"
// promise from the header is upheld: all arithmetic is done on positive digits
// and capped before applying the sign.
bool fieldInt64(std::int64_t& out) {
std::string f;
if (!field(f)) return false;
@@ -61,11 +79,20 @@ public:
i = 1;
if (f.size() == 1) return fail(); // bare "-"
}
// Cap at 19 digits (INT64_MAX = 9223372036854775807 — 19 digits). A 20-digit
// positive value would overflow INT64_MAX; a 20-digit negative might be valid
// (INT64_MIN = -9223372036854775808) but we conservatively reject it too: the
// generation field is a unix timestamp, never near INT64 limits in practice.
if (f.size() - i > 19u) return fail();
std::int64_t v = 0;
for (; i < f.size(); ++i) {
const char c = f[i];
if (c < '0' || c > '9') return fail();
v = v * 10 + static_cast<std::int64_t>(c - '0');
const std::int64_t digit = static_cast<std::int64_t>(c - '0');
// Overflow guard: v * 10 + digit must not exceed INT64_MAX.
if (v > (std::numeric_limits<std::int64_t>::max() - digit) / 10)
return fail();
v = v * 10 + digit;
}
out = neg ? -v : v;
return true;