Merge branch 'beta' into dev

This commit is contained in:
Jesse Hills
2026-10-09 15:42:24 +13:00
@@ -107,6 +107,89 @@ std::optional<size_t> parse_record_slot(const std::string &key) {
} // namespace
namespace {
// The reason passed to on_pairing_failed automations, in the protocol's own spelling.
StringRef pair_abort_reason_to_string(sendspin::SendspinPairAbortReason reason) {
using sendspin::SendspinPairAbortReason;
switch (reason) {
case SendspinPairAbortReason::ATTEMPT_TIMEOUT:
return StringRef::from_lit("attempt_timeout");
case SendspinPairAbortReason::CONCURRENT_ATTEMPT:
return StringRef::from_lit("concurrent_attempt");
case SendspinPairAbortReason::METHOD_NOT_SUPPORTED:
return StringRef::from_lit("method_not_supported");
case SendspinPairAbortReason::PAIRING_CODE_MISMATCH:
return StringRef::from_lit("pairing_code_mismatch");
case SendspinPairAbortReason::USER_CANCELLED:
return StringRef::from_lit("user_cancelled");
case SendspinPairAbortReason::UNKNOWN:
break;
}
return StringRef::from_lit("unknown");
}
// Zeroes through a volatile pointer so dead-store elimination cannot drop it.
void secure_wipe(void *data, size_t len) {
volatile auto *p = static_cast<volatile uint8_t *>(data);
for (size_t i = 0; i < len; ++i) {
p[i] = 0;
}
}
// Call once per key, from setup().
ESPPreferenceObject make_blob_pref(const std::string &name, size_t size) {
return global_preferences->make_preference(size, fnv1a_hash("sendspin_" + name));
}
// Loads straight into the returned buffer, which the library wipes after use. A failed load can
// leave part of a stored private key or PSK behind, so that path wipes it here. This only covers
// this buffer: the preference backend keeps its own copy of a pending write until it syncs.
std::optional<std::vector<uint8_t>> read_blob(ESPPreferenceObject &pref, size_t size) {
std::vector<uint8_t> out(size);
if (!pref.load(out.data(), out.size())) {
secure_wipe(out.data(), out.size());
return std::nullopt;
}
return out;
}
// The library always writes a key's fixed size; any other length is misuse and is not stored.
bool write_blob(ESPPreferenceObject &pref, const char *key, size_t size, const uint8_t *data, size_t len) {
if (len != size) {
ESP_LOGW(TAG, "\"%s\" blob of %zu bytes does not match its size of %zu; rejecting write", key, len, size);
return false;
}
if (!pref.save(data, len)) {
ESP_LOGW(TAG, "Failed to persist \"%s\" blob (%zu bytes)", key, len);
return false;
}
return true;
}
// "rec_<n>" to n; nullopt for any other key or a slot with no storage here.
std::optional<size_t> parse_record_slot(const std::string &key) {
const char *const prefix = sendspin::persistence_keys::RECORD_SLOT_PREFIX;
const size_t prefix_len = std::strlen(prefix);
if (key.size() <= prefix_len || key.compare(0, prefix_len, prefix) != 0) {
return std::nullopt;
}
size_t slot = 0;
for (size_t i = prefix_len; i < key.size(); i++) {
const char c = key[i];
if (c < '0' || c > '9') {
return std::nullopt;
}
slot = slot * 10 + static_cast<size_t>(c - '0');
if (slot >= SENDSPIN_RECORD_SLOTS) {
return std::nullopt;
}
}
return slot;
}
} // namespace
#ifdef USE_SENDSPIN_ARTWORK
// Indexed by the library enums, which start at zero and are contiguous.
static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"};