From 7cb25be3f3dcf9963b4bbccfdf5ca65d05c14d82 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Mon, 24 Aug 2026 14:55:11 +0200 Subject: [PATCH] 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 --- .../components/core/src/AudioProfileJson.cpp | 21 +++-- .../components/core/src/BluetoothJson.cpp | 76 ++++++++++++++----- Software/components/core/src/DspParamJson.cpp | 17 ++++- .../components/core/src/StationListJson.cpp | 17 ++++- Software/components/core/src/TunerJson.cpp | 17 ++++- Software/components/core/src/WebRadioJson.cpp | 38 +++++++--- .../components/core/src/WifiProvisionJson.cpp | 17 ++++- .../secure_store/src/NvsAudioProfileStore.cpp | 7 +- 8 files changed, 157 insertions(+), 53 deletions(-) diff --git a/Software/components/core/src/AudioProfileJson.cpp b/Software/components/core/src/AudioProfileJson.cpp index 3edcb72..89e3511 100644 --- a/Software/components/core/src/AudioProfileJson.cpp +++ b/Software/components/core/src/AudioProfileJson.cpp @@ -58,15 +58,24 @@ namespace { std::string_view key, bool& out) { - const std::string trueNeedle = - std::string("\"") + std::string(key) + "\":true"; - const std::string falseNeedle = - std::string("\"") + std::string(key) + "\":false"; - if (json.find(trueNeedle) != std::string_view::npos) { + const std::string needle = + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == 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; return true; } - if (json.find(falseNeedle) != std::string_view::npos) { + if (rest.starts_with("false")) { out = false; return true; } diff --git a/Software/components/core/src/BluetoothJson.cpp b/Software/components/core/src/BluetoothJson.cpp index a126130..4f53950 100644 --- a/Software/components/core/src/BluetoothJson.cpp +++ b/Software/components/core/src/BluetoothJson.cpp @@ -150,12 +150,21 @@ parseBluetoothConnectJson(std::string_view json) if (json.find('{') == std::string_view::npos) { return std::unexpected(ParseError::InvalidJson); } - const std::string needle = "\"mac\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + const std::string needle = "\"mac\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { 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); if (valueEnd == std::string_view::npos) { return std::unexpected(ParseError::InvalidJson); @@ -183,17 +192,35 @@ BluetoothConnectRequest parseBluetoothConnectRequest(std::string_view json) if (auto mac = parseBluetoothConnectJson(json); mac) { request.mac = std::move(*mac); } - const std::string nameNeedle = "\"name\":\""; - const std::size_t nameStart = json.find(nameNeedle); - if (nameStart != std::string_view::npos) { - const std::size_t valueStart = nameStart + nameNeedle.size(); - const std::size_t valueEnd = json.find('"', valueStart); - if (valueEnd != std::string_view::npos) { - request.name.assign(json.substr(valueStart, valueEnd - valueStart)); + const std::string nameNeedle = "\"name\":"; + const std::size_t nameNeedlePos = json.find(nameNeedle); + if (nameNeedlePos != std::string_view::npos) { + std::size_t nameStart = nameNeedlePos + nameNeedle.size(); + while (nameStart < json.size() + && (json[nameStart] == ' ' || json[nameStart] == '\t' + || 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 - || json.find("\"save\": true") != std::string_view::npos; + const std::string saveNeedle = "\"save\":"; + 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; } @@ -219,13 +246,22 @@ parseBluetoothSpeakerJson(std::string_view json) return std::unexpected(mac.error()); } BtSpeakerTarget target{.mac = *mac, .name = {}}; - const std::string nameNeedle = "\"name\":\""; - const std::size_t nameStart = json.find(nameNeedle); - if (nameStart != std::string_view::npos) { - const std::size_t valueStart = nameStart + nameNeedle.size(); - const std::size_t valueEnd = json.find('"', valueStart); - if (valueEnd != std::string_view::npos) { - target.name.assign(json.substr(valueStart, valueEnd - valueStart)); + const std::string nameNeedle = "\"name\":"; + const std::size_t nameNeedlePos = json.find(nameNeedle); + if (nameNeedlePos != std::string_view::npos) { + std::size_t nameStart = nameNeedlePos + nameNeedle.size(); + while (nameStart < json.size() + && (json[nameStart] == ' ' || json[nameStart] == '\t' + || 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; diff --git a/Software/components/core/src/DspParamJson.cpp b/Software/components/core/src/DspParamJson.cpp index 4868f6c..00be98e 100644 --- a/Software/components/core/src/DspParamJson.cpp +++ b/Software/components/core/src/DspParamJson.cpp @@ -24,12 +24,21 @@ namespace { std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { 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); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/core/src/StationListJson.cpp b/Software/components/core/src/StationListJson.cpp index a46287c..faba0fc 100644 --- a/Software/components/core/src/StationListJson.cpp +++ b/Software/components/core/src/StationListJson.cpp @@ -24,12 +24,21 @@ namespace { std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { 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); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/core/src/TunerJson.cpp b/Software/components/core/src/TunerJson.cpp index 84297ad..b69e1fc 100644 --- a/Software/components/core/src/TunerJson.cpp +++ b/Software/components/core/src/TunerJson.cpp @@ -24,12 +24,21 @@ namespace { std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { 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); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/core/src/WebRadioJson.cpp b/Software/components/core/src/WebRadioJson.cpp index fee4b2f..5f4c870 100644 --- a/Software/components/core/src/WebRadioJson.cpp +++ b/Software/components/core/src/WebRadioJson.cpp @@ -24,12 +24,21 @@ constexpr std::string_view kHttpPrefix = "http://"; std::string_view key) { const std::string needle = - std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { 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); if (valueEnd == std::string_view::npos) { return {}; @@ -40,15 +49,24 @@ constexpr std::string_view kHttpPrefix = "http://"; [[nodiscard]] bool extractJsonBool(std::string_view json, std::string_view key, bool& out) { - const std::string trueNeedle = - std::string("\"") + std::string(key) + "\":true"; - const std::string falseNeedle = - std::string("\"") + std::string(key) + "\":false"; - if (json.find(trueNeedle) != std::string_view::npos) { + const std::string needle = + std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == 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; return true; } - if (json.find(falseNeedle) != std::string_view::npos) { + if (rest.starts_with("false")) { out = false; return true; } diff --git a/Software/components/core/src/WifiProvisionJson.cpp b/Software/components/core/src/WifiProvisionJson.cpp index f3590e0..1739dc9 100644 --- a/Software/components/core/src/WifiProvisionJson.cpp +++ b/Software/components/core/src/WifiProvisionJson.cpp @@ -37,12 +37,21 @@ namespace { [[nodiscard]] std::string_view extractJsonString(std::string_view json, std::string_view key) { - const std::string needle = std::string("\"") + std::string(key) + "\":\""; - const std::size_t start = json.find(needle); - if (start == std::string_view::npos) { + const std::string needle = std::string("\"") + std::string(key) + "\":"; + const std::size_t needlePos = json.find(needle); + if (needlePos == std::string_view::npos) { 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); if (valueEnd == std::string_view::npos) { return {}; diff --git a/Software/components/secure_store/src/NvsAudioProfileStore.cpp b/Software/components/secure_store/src/NvsAudioProfileStore.cpp index 01c2b5a..9cacbea 100644 --- a/Software/components/secure_store/src/NvsAudioProfileStore.cpp +++ b/Software/components/secure_store/src/NvsAudioProfileStore.cpp @@ -26,7 +26,12 @@ namespace secure_store { namespace { constexpr char kTag[] = "NvsAudioProfileStore"; 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 bool NvsAudioProfileStore::hasProfile() const