Fix audio-profile NVS persistence and whitespace-intolerant JSON parsers

- NvsAudioProfileStore used the key "audio_profile_json" (18 chars),
  exceeding NVS's 15-char key-name limit. Every nvs_set_str call failed
  silently with ESP_ERR_NVS_KEY_TOO_LONG (0x1109): applyProfile() always
  updated live DSP audio correctly, so the bug was invisible except as
  "store_failed" in the HTTP response and settings never surviving a
  reboot. Renamed to "audio_profile" (13 chars); confirmed live, EQ/mixer
  changes now persist across a reset.

- The hand-rolled JSON extractJsonString()/extractJsonBool() helpers
  (duplicated per-file: TunerJson, DspParamJson, StationListJson,
  WebRadioJson, WifiProvisionJson, AudioProfileJson, plus inline mac/name/
  save parsing in BluetoothJson) matched only the exact literal
  `"key":"value"` / `"key":true`, with no tolerance for a space after the
  colon. Standard JSON encoders (e.g. Swift's JSONEncoder in its default,
  non-compact mode) emit `"key": "value"`, which silently failed to parse
  as invalid_json/missing_field. Numeric fields were already fine
  (strtoul/strtof skip leading whitespace per the C standard); fixed only
  the string/bool extractors to skip whitespace after the colon before
  matching the value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 14:55:11 +02:00
co-authored by Claude Sonnet 5
parent 38e768edbb
commit 7cb25be3f3
8 changed files with 157 additions and 53 deletions
@@ -58,15 +58,24 @@ namespace {
std::string_view key, std::string_view key,
bool& out) bool& out)
{ {
const std::string trueNeedle = const std::string needle =
std::string("\"") + std::string(key) + "\":true"; std::string("\"") + std::string(key) + "\":";
const std::string falseNeedle = const std::size_t needlePos = json.find(needle);
std::string("\"") + std::string(key) + "\":false"; if (needlePos == std::string_view::npos) {
if (json.find(trueNeedle) != std::string_view::npos) { return false;
}
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
const std::string_view rest = json.substr(start);
if (rest.starts_with("true")) {
out = true; out = true;
return true; return true;
} }
if (json.find(falseNeedle) != std::string_view::npos) { if (rest.starts_with("false")) {
out = false; out = false;
return true; return true;
} }
+56 -20
View File
@@ -150,12 +150,21 @@ parseBluetoothConnectJson(std::string_view json)
if (json.find('{') == std::string_view::npos) { if (json.find('{') == std::string_view::npos) {
return std::unexpected(ParseError::InvalidJson); return std::unexpected(ParseError::InvalidJson);
} }
const std::string needle = "\"mac\":\""; const std::string needle = "\"mac\":";
const std::size_t start = json.find(needle); const std::size_t needlePos = json.find(needle);
if (start == std::string_view::npos) { if (needlePos == std::string_view::npos) {
return std::unexpected(ParseError::MissingField); return std::unexpected(ParseError::MissingField);
} }
const std::size_t valueStart = start + needle.size(); std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return std::unexpected(ParseError::InvalidJson);
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart); const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) { if (valueEnd == std::string_view::npos) {
return std::unexpected(ParseError::InvalidJson); return std::unexpected(ParseError::InvalidJson);
@@ -183,17 +192,35 @@ BluetoothConnectRequest parseBluetoothConnectRequest(std::string_view json)
if (auto mac = parseBluetoothConnectJson(json); mac) { if (auto mac = parseBluetoothConnectJson(json); mac) {
request.mac = std::move(*mac); request.mac = std::move(*mac);
} }
const std::string nameNeedle = "\"name\":\""; const std::string nameNeedle = "\"name\":";
const std::size_t nameStart = json.find(nameNeedle); const std::size_t nameNeedlePos = json.find(nameNeedle);
if (nameStart != std::string_view::npos) { if (nameNeedlePos != std::string_view::npos) {
const std::size_t valueStart = nameStart + nameNeedle.size(); std::size_t nameStart = nameNeedlePos + nameNeedle.size();
const std::size_t valueEnd = json.find('"', valueStart); while (nameStart < json.size()
if (valueEnd != std::string_view::npos) { && (json[nameStart] == ' ' || json[nameStart] == '\t'
request.name.assign(json.substr(valueStart, valueEnd - valueStart)); || json[nameStart] == '\r' || json[nameStart] == '\n')) {
++nameStart;
}
if (nameStart < json.size() && json[nameStart] == '"') {
const std::size_t valueStart = nameStart + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd != std::string_view::npos) {
request.name.assign(
json.substr(valueStart, valueEnd - valueStart));
}
} }
} }
request.save = json.find("\"save\":true") != std::string_view::npos const std::string saveNeedle = "\"save\":";
|| json.find("\"save\": true") != std::string_view::npos; const std::size_t saveNeedlePos = json.find(saveNeedle);
if (saveNeedlePos != std::string_view::npos) {
std::size_t saveStart = saveNeedlePos + saveNeedle.size();
while (saveStart < json.size()
&& (json[saveStart] == ' ' || json[saveStart] == '\t'
|| json[saveStart] == '\r' || json[saveStart] == '\n')) {
++saveStart;
}
request.save = json.substr(saveStart).starts_with("true");
}
return request; return request;
} }
@@ -219,13 +246,22 @@ parseBluetoothSpeakerJson(std::string_view json)
return std::unexpected(mac.error()); return std::unexpected(mac.error());
} }
BtSpeakerTarget target{.mac = *mac, .name = {}}; BtSpeakerTarget target{.mac = *mac, .name = {}};
const std::string nameNeedle = "\"name\":\""; const std::string nameNeedle = "\"name\":";
const std::size_t nameStart = json.find(nameNeedle); const std::size_t nameNeedlePos = json.find(nameNeedle);
if (nameStart != std::string_view::npos) { if (nameNeedlePos != std::string_view::npos) {
const std::size_t valueStart = nameStart + nameNeedle.size(); std::size_t nameStart = nameNeedlePos + nameNeedle.size();
const std::size_t valueEnd = json.find('"', valueStart); while (nameStart < json.size()
if (valueEnd != std::string_view::npos) { && (json[nameStart] == ' ' || json[nameStart] == '\t'
target.name.assign(json.substr(valueStart, valueEnd - valueStart)); || json[nameStart] == '\r' || json[nameStart] == '\n')) {
++nameStart;
}
if (nameStart < json.size() && json[nameStart] == '"') {
const std::size_t valueStart = nameStart + 1U;
const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd != std::string_view::npos) {
target.name.assign(
json.substr(valueStart, valueEnd - valueStart));
}
} }
} }
return target; return target;
+13 -4
View File
@@ -24,12 +24,21 @@ namespace {
std::string_view key) std::string_view key)
{ {
const std::string needle = const std::string needle =
std::string("\"") + std::string(key) + "\":\""; std::string("\"") + std::string(key) + "\":";
const std::size_t start = json.find(needle); const std::size_t needlePos = json.find(needle);
if (start == std::string_view::npos) { if (needlePos == std::string_view::npos) {
return {}; return {};
} }
const std::size_t valueStart = start + needle.size(); std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart); const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) { if (valueEnd == std::string_view::npos) {
return {}; return {};
@@ -24,12 +24,21 @@ namespace {
std::string_view key) std::string_view key)
{ {
const std::string needle = const std::string needle =
std::string("\"") + std::string(key) + "\":\""; std::string("\"") + std::string(key) + "\":";
const std::size_t start = json.find(needle); const std::size_t needlePos = json.find(needle);
if (start == std::string_view::npos) { if (needlePos == std::string_view::npos) {
return {}; return {};
} }
const std::size_t valueStart = start + needle.size(); std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart); const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) { if (valueEnd == std::string_view::npos) {
return {}; return {};
+13 -4
View File
@@ -24,12 +24,21 @@ namespace {
std::string_view key) std::string_view key)
{ {
const std::string needle = const std::string needle =
std::string("\"") + std::string(key) + "\":\""; std::string("\"") + std::string(key) + "\":";
const std::size_t start = json.find(needle); const std::size_t needlePos = json.find(needle);
if (start == std::string_view::npos) { if (needlePos == std::string_view::npos) {
return {}; return {};
} }
const std::size_t valueStart = start + needle.size(); std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart); const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) { if (valueEnd == std::string_view::npos) {
return {}; return {};
+28 -10
View File
@@ -24,12 +24,21 @@ constexpr std::string_view kHttpPrefix = "http://";
std::string_view key) std::string_view key)
{ {
const std::string needle = const std::string needle =
std::string("\"") + std::string(key) + "\":\""; std::string("\"") + std::string(key) + "\":";
const std::size_t start = json.find(needle); const std::size_t needlePos = json.find(needle);
if (start == std::string_view::npos) { if (needlePos == std::string_view::npos) {
return {}; return {};
} }
const std::size_t valueStart = start + needle.size(); std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart); const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) { if (valueEnd == std::string_view::npos) {
return {}; return {};
@@ -40,15 +49,24 @@ constexpr std::string_view kHttpPrefix = "http://";
[[nodiscard]] bool extractJsonBool(std::string_view json, [[nodiscard]] bool extractJsonBool(std::string_view json,
std::string_view key, bool& out) std::string_view key, bool& out)
{ {
const std::string trueNeedle = const std::string needle =
std::string("\"") + std::string(key) + "\":true"; std::string("\"") + std::string(key) + "\":";
const std::string falseNeedle = const std::size_t needlePos = json.find(needle);
std::string("\"") + std::string(key) + "\":false"; if (needlePos == std::string_view::npos) {
if (json.find(trueNeedle) != std::string_view::npos) { return false;
}
std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
const std::string_view rest = json.substr(start);
if (rest.starts_with("true")) {
out = true; out = true;
return true; return true;
} }
if (json.find(falseNeedle) != std::string_view::npos) { if (rest.starts_with("false")) {
out = false; out = false;
return true; return true;
} }
@@ -37,12 +37,21 @@ namespace {
[[nodiscard]] std::string_view extractJsonString(std::string_view json, [[nodiscard]] std::string_view extractJsonString(std::string_view json,
std::string_view key) std::string_view key)
{ {
const std::string needle = std::string("\"") + std::string(key) + "\":\""; const std::string needle = std::string("\"") + std::string(key) + "\":";
const std::size_t start = json.find(needle); const std::size_t needlePos = json.find(needle);
if (start == std::string_view::npos) { if (needlePos == std::string_view::npos) {
return {}; return {};
} }
const std::size_t valueStart = start + needle.size(); std::size_t start = needlePos + needle.size();
while (start < json.size()
&& (json[start] == ' ' || json[start] == '\t'
|| json[start] == '\r' || json[start] == '\n')) {
++start;
}
if (start >= json.size() || json[start] != '"') {
return {};
}
const std::size_t valueStart = start + 1U;
const std::size_t valueEnd = json.find('"', valueStart); const std::size_t valueEnd = json.find('"', valueStart);
if (valueEnd == std::string_view::npos) { if (valueEnd == std::string_view::npos) {
return {}; return {};
@@ -26,7 +26,12 @@ namespace secure_store {
namespace { namespace {
constexpr char kTag[] = "NvsAudioProfileStore"; constexpr char kTag[] = "NvsAudioProfileStore";
constexpr char kNamespace[] = "digiradio"; constexpr char kNamespace[] = "digiradio";
constexpr char kProfileKey[] = "audio_profile_json"; // NVS key names are capped at 15 chars (NVS_KEY_NAME_MAX_SIZE=16 incl. NUL);
// the previous "audio_profile_json" (18 chars) made every nvs_set_str call
// fail with ESP_ERR_NVS_KEY_TOO_LONG (0x1109), silently -- applyProfile()
// always updated live audio correctly but persistProfile() never actually
// wrote anything, so nothing survived a reboot.
constexpr char kProfileKey[] = "audio_profile";
} // namespace } // namespace
bool NvsAudioProfileStore::hasProfile() const bool NvsAudioProfileStore::hasProfile() const