diff --git a/src/JsonFormatter.cpp b/src/JsonFormatter.cpp index d54d81c..3a4c20d 100644 --- a/src/JsonFormatter.cpp +++ b/src/JsonFormatter.cpp @@ -1,10 +1,14 @@ #include "JsonFormatter.h" +#include "esp_log.h" + +static const char *TAG = "JSON"; void BufferedJsonFormatter::setBuffer(char *b, size_t sz) { this->buff = b; this->buffSize = sz; this->buff[0] = 0; this->_nocomma = true; + this->_overflow = false; this->_objects = 0; this->_arrays = 0; } @@ -86,6 +90,8 @@ void JsonFormatter::_safecat(const char *val, bool escape) { size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); if(escape) len += 2; if(len >= this->buffSize) { + if(!this->_overflow) ESP_LOGE(TAG, "JSON exceeded buffer size %u", (unsigned)this->buffSize); + this->_overflow = true; return; } if(escape) strcat(this->buff, "\""); diff --git a/src/JsonFormatter.h b/src/JsonFormatter.h index 6554870..54f8927 100644 --- a/src/JsonFormatter.h +++ b/src/JsonFormatter.h @@ -12,10 +12,13 @@ class JsonFormatter { uint8_t _objects = 0; uint8_t _arrays = 0; bool _nocomma = true; + // Something didn't fit and was left out so the JSON is incomplete. + bool _overflow = false; char _numbuff[25] = {0}; virtual void _safecat(const char *val, bool escape = false); void _appendNumber(const char *name); public: + bool overflowed() const { return _overflow; } // Writes c as it must appear inside a JSON string (out needs 7 bytes) and // returns its length. static size_t escapeChar(char c, char *out); diff --git a/src/WResp.cpp b/src/WResp.cpp index 64fda3b..1fb8aa4 100644 --- a/src/WResp.cpp +++ b/src/WResp.cpp @@ -8,18 +8,25 @@ void JsonSockEvent::beginEvent(AsyncWebSocket *server, const char *evt, char *bu this->buffSize = buffSize; this->_nocomma = true; this->_closed = false; + this->_overflow = false; snprintf(this->buff, buffSize, "42[%s,", evt); } void JsonSockEvent::closeEvent() { if(!this->_closed) { - if(strlen(this->buff) < buffSize) strcat(this->buff, "]"); - else this->buff[buffSize - 1] = ']'; + // The ] and the terminator both have to fit. + if(strlen(this->buff) + 1 < buffSize) strcat(this->buff, "]"); + else this->_overflow = true; } this->_nocomma = true; this->_closed = true; } void JsonSockEvent::endEvent(uint32_t clientId) { this->closeEvent(); + // Part of it is missing so clients would only fail to parse it. + if(this->_overflow) { + ESP_LOGE(TAG, "Socket event too large for %u bytes, not sent: %.32s", (unsigned)this->buffSize, this->buff); + return; + } if(clientId == 0) this->server->textAll(this->buff); else this->server->text(clientId, this->buff); } @@ -27,7 +34,7 @@ void JsonSockEvent::_safecat(const char *val, bool escape) { size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); if(escape) len += 2; if(len >= this->buffSize) { - ESP_LOGE(TAG, "Socket exceeded buffer size %d - %d: %s", this->buffSize, len, this->buff); + this->_overflow = true; return; } if(escape) strcat(this->buff, "\""); @@ -42,6 +49,7 @@ void AsyncJsonResp::beginResponse(AsyncWebServerRequest *request, char *buff, si this->buff[0] = 0x00; this->_nocomma = true; this->_headersSent = false; + this->_overflow = false; this->_stream = request->beginResponseStream("application/json"); } void AsyncJsonResp::endResponse() { @@ -57,10 +65,28 @@ void AsyncJsonResp::flush() { } } void AsyncJsonResp::_safecat(const char *val, bool escape) { - size_t len = (escape ? this->calcEscapedLength(val) : strlen(val)) + strlen(this->buff); - if(escape) len += 2; - if(len >= this->buffSize) { + const size_t vlen = escape ? this->calcEscapedLength(val) + 2 : strlen(val); + if(vlen + strlen(this->buff) >= this->buffSize) { this->flush(); + if(vlen + strlen(this->buff) >= this->buffSize) { + // Bigger than the whole buffer so stream it straight out. + if(!this->_stream) { + this->_overflow = true; + return; + } + if(!escape) { + this->_stream->print(val); + return; + } + char esc[7]; + this->_stream->print("\""); + for(size_t i = 0; val[i]; i++) { + escapeChar(val[i], esc); + this->_stream->print(esc); + } + this->_stream->print("\""); + return; + } } if(escape) strcat(this->buff, "\""); if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); diff --git a/test/test_json/test_json.cpp b/test/test_json/test_json.cpp index 4172b63..4b21266 100644 --- a/test/test_json/test_json.cpp +++ b/test/test_json/test_json.cpp @@ -140,6 +140,24 @@ static void test_value_needing_one_byte_too_many_is_refused() { TEST_ASSERT_TRUE(strlen(storage) < 11); assertCanary(11); } +static void test_overflow_is_flagged() { + useBuffer(16); + fmt.beginObject(); + fmt.addElem("name", "fits"); + TEST_ASSERT_FALSE(fmt.overflowed()); + fmt.addElem("long", "this value does not fit"); + TEST_ASSERT_TRUE(fmt.overflowed()); + // What was already written (here up to the comma) stays intact and terminated. + TEST_ASSERT_EQUAL_STRING("{\"name\":\"fits\",", storage); + assertCanary(16); +} +static void test_setBuffer_clears_overflow() { + useBuffer(4); + fmt.addElem("too long"); + TEST_ASSERT_TRUE(fmt.overflowed()); + useBuffer(BUFF_SIZE); + TEST_ASSERT_FALSE(fmt.overflowed()); +} int main(int argc, char **argv) { (void)argc; @@ -157,5 +175,7 @@ int main(int argc, char **argv) { RUN_TEST(test_escaped_length); RUN_TEST(test_escaped_length_counts_first_character); RUN_TEST(test_value_needing_one_byte_too_many_is_refused); + RUN_TEST(test_overflow_is_flagged); + RUN_TEST(test_setBuffer_clears_overflow); return UNITY_END(); }