diff --git a/src/JsonFormatter.cpp b/src/JsonFormatter.cpp index 3a4c20d..87b7a20 100644 --- a/src/JsonFormatter.cpp +++ b/src/JsonFormatter.cpp @@ -7,13 +7,14 @@ void BufferedJsonFormatter::setBuffer(char *b, size_t sz) { this->buff = b; this->buffSize = sz; this->buff[0] = 0; + this->_len = 0; this->_nocomma = true; this->_overflow = false; this->_objects = 0; this->_arrays = 0; } size_t BufferedJsonFormatter::length() const { - return strlen(this->buff); + return this->_len; } void JsonFormatter::beginObject(const char *name) { if(name && strlen(name) > 0) this->appendElem(name); @@ -87,17 +88,29 @@ void JsonFormatter::addElem(const char *name, uint64_t lval) { sprintf(this->_nu void JsonFormatter::addElem(const char *name, bool bval) { strcpy(this->_numbuff, bval ? "true" : "false"); this->_appendNumber(name); } 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) { + const size_t vlen = escape ? this->calcEscapedLength(val) + 2 : strlen(val); + if(this->_len + vlen >= 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, "\""); - if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); - else strcat(this->buff, val); - if(escape) strcat(this->buff, "\""); + this->_appendAt(val, escape, vlen); +} +void JsonFormatter::_appendAt(const char *val, bool escape, size_t vlen) { + char *p = this->buff + this->_len; + if(escape) { + *p++ = '"'; + *p = 0; + this->escapeString(val, p); + p += vlen - 2; + *p++ = '"'; + } + else { + memcpy(p, val, vlen); + p += vlen; + } + *p = 0; + this->_len += vlen; } void JsonFormatter::_appendNumber(const char *name) { this->appendElem(name); this->_safecat(this->_numbuff); } size_t JsonFormatter::escapeChar(char c, char *out) { diff --git a/src/JsonFormatter.h b/src/JsonFormatter.h index 54f8927..3f93025 100644 --- a/src/JsonFormatter.h +++ b/src/JsonFormatter.h @@ -8,6 +8,8 @@ class JsonFormatter { protected: char *buff; size_t buffSize; + // What is in buff, kept so appending never has to strlen() the buffer. + size_t _len = 0; bool _headersSent = false; uint8_t _objects = 0; uint8_t _arrays = 0; @@ -16,6 +18,9 @@ class JsonFormatter { bool _overflow = false; char _numbuff[25] = {0}; virtual void _safecat(const char *val, bool escape = false); + // Appends val at buff + _len. vlen is what it takes, including the quotes + // when escaped; the caller has made sure it fits. + void _appendAt(const char *val, bool escape, size_t vlen); void _appendNumber(const char *name); public: bool overflowed() const { return _overflow; } diff --git a/src/WResp.cpp b/src/WResp.cpp index 1d4dcde..7d00372 100644 --- a/src/WResp.cpp +++ b/src/WResp.cpp @@ -12,12 +12,13 @@ void JsonSockEvent::beginEvent(AsyncWebSocket *server, const char *evt, char *bu this->_nocomma = true; this->_closed = false; this->_overflow = false; - snprintf(this->buff, buffSize, "42[%s,", evt); + const int n = snprintf(this->buff, buffSize, "42[%s,", evt); + this->_len = n < 0 ? 0 : ((size_t)n < buffSize ? (size_t)n : buffSize - 1); } void JsonSockEvent::closeEvent() { if(!this->_closed) { // The ] and the terminator both have to fit. - if(strlen(this->buff) + 1 < buffSize) strcat(this->buff, "]"); + if(this->_len + 1 < buffSize) this->_appendAt("]", false, 1); else this->_overflow = true; } this->_nocomma = true; @@ -37,49 +38,47 @@ void JsonSockEvent::endEvent(uint32_t clientId) { const uint32_t us = timer.us(); if(us < EMIT_SLOW_MS * 1000) return; char what[80]; - if(clientId == 0) snprintf(what, sizeof(what), "Socket emit %s (%u bytes) to all", this->_evt, (unsigned)strlen(this->buff)); - else snprintf(what, sizeof(what), "Socket emit %s (%u bytes) to %lu", this->_evt, (unsigned)strlen(this->buff), (unsigned long)clientId); + if(clientId == 0) snprintf(what, sizeof(what), "Socket emit %s (%u bytes) to all", this->_evt, (unsigned)this->_len); + else snprintf(what, sizeof(what), "Socket emit %s (%u bytes) to %lu", this->_evt, (unsigned)this->_len, (unsigned long)clientId); logElapsed(TAG, what, us); } 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) { + const size_t vlen = escape ? this->calcEscapedLength(val) + 2 : strlen(val); + if(this->_len + vlen >= this->buffSize) { this->_overflow = true; return; } - if(escape) strcat(this->buff, "\""); - if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); - else strcat(this->buff, val); - if(escape) strcat(this->buff, "\""); + this->_appendAt(val, escape, vlen); } void AsyncJsonResp::beginResponse(AsyncWebServerRequest *request, char *buff, size_t buffSize) { this->_request = request; this->buff = buff; this->buffSize = buffSize; this->buff[0] = 0x00; + this->_len = 0; this->_nocomma = true; this->_headersSent = false; this->_overflow = false; this->_stream = request->beginResponseStream("application/json"); } void AsyncJsonResp::endResponse() { - if(strlen(this->buff)) this->flush(); + if(this->_len) this->flush(); if(this->_request && this->_stream) { this->_request->send(this->_stream); } } void AsyncJsonResp::flush() { - if(this->_stream && strlen(this->buff) > 0) { - this->_stream->print(this->buff); + if(this->_stream && this->_len > 0) { + this->_stream->write((const uint8_t *)this->buff, this->_len); this->buff[0] = 0x00; + this->_len = 0; } } void AsyncJsonResp::_safecat(const char *val, bool escape) { const size_t vlen = escape ? this->calcEscapedLength(val) + 2 : strlen(val); - if(vlen + strlen(this->buff) >= this->buffSize) { + if(vlen + this->_len >= this->buffSize) { this->flush(); - if(vlen + strlen(this->buff) >= this->buffSize) { + if(vlen + this->_len >= this->buffSize) { // Bigger than the whole buffer so stream it straight out. if(!this->_stream) { this->_overflow = true; @@ -99,8 +98,5 @@ void AsyncJsonResp::_safecat(const char *val, bool escape) { return; } } - if(escape) strcat(this->buff, "\""); - if(escape) this->escapeString(val, &this->buff[strlen(this->buff)]); - else strcat(this->buff, val); - if(escape) strcat(this->buff, "\""); + this->_appendAt(val, escape, vlen); } diff --git a/src/Web.cpp b/src/Web.cpp index f8e9414..4cc1276 100644 --- a/src/Web.cpp +++ b/src/Web.cpp @@ -555,7 +555,7 @@ private: serializeGitVersion(fmt); fmt.endObject(); fmt.beginArray("rooms"); - unitLen = strlen(unit); + unitLen = fmt.length(); section = S_ROOMS; idx = 0; firstInArray = true; @@ -577,7 +577,7 @@ private: fmt.beginObject(); serializeRoom(&somfy.rooms[idx], fmt); fmt.endObject(); - unitLen = pos + strlen(unit + pos); + unitLen = pos + fmt.length(); idx++; firstInArray = false; return; @@ -599,7 +599,7 @@ private: fmt.beginObject(); serializeShade(&somfy.shades[idx], fmt); fmt.endObject(); - unitLen = pos + strlen(unit + pos); + unitLen = pos + fmt.length(); idx++; firstInArray = false; return; @@ -622,7 +622,7 @@ private: fmt.beginObject(); serializeGroupRef(g, fmt); fmt.beginArray("linkedShades"); - unitLen = pos + strlen(unit + pos); + unitLen = pos + fmt.length(); lsIdx = 0; firstInArray = false; firstInLSArray = true; @@ -650,7 +650,7 @@ private: fmt.beginObject(); serializeShadeRef(lshade, fmt); fmt.endObject(); - unitLen = pos + strlen(unit + pos); + unitLen = pos + fmt.length(); lsIdx++; firstInLSArray = false; return; diff --git a/test/test_json/test_json.cpp b/test/test_json/test_json.cpp index 4b21266..1dfa190 100644 --- a/test/test_json/test_json.cpp +++ b/test/test_json/test_json.cpp @@ -159,6 +159,42 @@ static void test_setBuffer_clears_overflow() { TEST_ASSERT_FALSE(fmt.overflowed()); } +/********************************************************************* + * Tracked length + ********************************************************************/ +static void test_length_matches_what_was_written() { + fmt.beginObject(); + fmt.addElem("name", "Living \"Room\"\n"); + fmt.addElem("pos", 41.5f); + fmt.beginArray("ids"); + fmt.addElem((uint8_t)1); + fmt.addElem((int32_t)-2); + fmt.endArray(); + fmt.addElem("on", true); + fmt.endObject(); + TEST_ASSERT_EQUAL_UINT32(strlen(storage), fmt.length()); + TEST_ASSERT_EQUAL_STRING("{\"name\":\"Living \\\"Room\\\"\\n\",\"pos\":41.5000,\"ids\":[1,-2],\"on\":true}", storage); +} +static void test_refused_append_leaves_length_alone() { + useBuffer(16); + fmt.beginObject(); + fmt.addElem("name", "fits"); + const size_t before = fmt.length(); + fmt.addElem("x", "this value does not fit"); + TEST_ASSERT_TRUE(fmt.overflowed()); + // The comma and key still fit; the value was refused whole. + TEST_ASSERT_EQUAL_UINT32(strlen(storage), fmt.length()); + TEST_ASSERT_TRUE(fmt.length() >= before); + TEST_ASSERT_TRUE(fmt.length() < 16); +} +static void test_setBuffer_resets_length() { + fmt.addElem("something"); + TEST_ASSERT_TRUE(fmt.length() > 0); + useBuffer(BUFF_SIZE); + TEST_ASSERT_EQUAL_UINT32(0, fmt.length()); + TEST_ASSERT_EQUAL_STRING("", storage); +} + int main(int argc, char **argv) { (void)argc; (void)argv; @@ -177,5 +213,8 @@ int main(int argc, char **argv) { RUN_TEST(test_value_needing_one_byte_too_many_is_refused); RUN_TEST(test_overflow_is_flagged); RUN_TEST(test_setBuffer_clears_overflow); + RUN_TEST(test_length_matches_what_was_written); + RUN_TEST(test_refused_append_leaves_length_alone); + RUN_TEST(test_setBuffer_resets_length); return UNITY_END(); }