fix: escape control characters and count JSON string lengths correctly

calcEscapedLength looped from strlen(raw) down to 1, counting the
terminator instead of the first character.  A value starting with a
character that needs escaping was counted one byte short, so the fit
check could let it be written one byte past the buffer.  Control
characters other than \b\f\n\r\t were copied raw, which isn't valid
JSON.

Both now go through one escapeChar helper, which also escapes the
remaining control characters as \u00XX.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-11 13:30:42 +02:00
parent 48676b05c3
commit b2294ced34
2 changed files with 31 additions and 51 deletions

View file

@ -94,59 +94,36 @@ void JsonFormatter::_safecat(const char *val, bool escape) {
if(escape) strcat(this->buff, "\""); if(escape) strcat(this->buff, "\"");
} }
void JsonFormatter::_appendNumber(const char *name) { this->appendElem(name); this->_safecat(this->_numbuff); } void JsonFormatter::_appendNumber(const char *name) { this->appendElem(name); this->_safecat(this->_numbuff); }
size_t JsonFormatter::escapeChar(char c, char *out) {
const char *esc = nullptr;
switch(c) {
case '"': esc = "\\\""; break;
case '/': esc = "\\/"; break;
case '\b': esc = "\\b"; break;
case '\f': esc = "\\f"; break;
case '\n': esc = "\\n"; break;
case '\r': esc = "\\r"; break;
case '\t': esc = "\\t"; break;
case '\\': esc = "\\\\"; break;
default: break;
}
if(esc) {
strcpy(out, esc);
return 2;
}
// Any other control character is not allowed raw in a JSON string.
if((unsigned char)c < 0x20) return snprintf(out, 7, "\\u%04x", (unsigned char)c);
out[0] = c;
out[1] = 0x00;
return 1;
}
uint32_t JsonFormatter::calcEscapedLength(const char *raw) { uint32_t JsonFormatter::calcEscapedLength(const char *raw) {
char tmp[7];
uint32_t len = 0; uint32_t len = 0;
for(size_t i = strlen(raw); i > 0; i--) { for(size_t i = 0; raw[i]; i++) len += escapeChar(raw[i], tmp);
switch(raw[i]) {
case '"':
case '/':
case '\b':
case '\f':
case '\n':
case '\r':
case '\t':
case '\\':
len += 2;
break;
default:
len++;
break;
}
}
return len; return len;
} }
void JsonFormatter::escapeString(const char *raw, char *escaped) { void JsonFormatter::escapeString(const char *raw, char *escaped) {
for(uint32_t i = 0; i < strlen(raw); i++) { char *out = escaped + strlen(escaped);
switch(raw[i]) { for(size_t i = 0; raw[i]; i++) out += escapeChar(raw[i], out);
case '"':
strcat(escaped, "\\\"");
break;
case '/':
strcat(escaped, "\\/");
break;
case '\b':
strcat(escaped, "\\b");
break;
case '\f':
strcat(escaped, "\\f");
break;
case '\n':
strcat(escaped, "\\n");
break;
case '\r':
strcat(escaped, "\\r");
break;
case '\t':
strcat(escaped, "\\t");
break;
case '\\':
strcat(escaped, "\\\\");
break;
default:
size_t len = strlen(escaped);
escaped[len] = raw[i];
escaped[len+1] = 0x00;
break;
}
}
} }

View file

@ -16,6 +16,9 @@ class JsonFormatter {
virtual void _safecat(const char *val, bool escape = false); virtual void _safecat(const char *val, bool escape = false);
void _appendNumber(const char *name); void _appendNumber(const char *name);
public: public:
// 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);
void escapeString(const char *raw, char *escaped); void escapeString(const char *raw, char *escaped);
uint32_t calcEscapedLength(const char *raw); uint32_t calcEscapedLength(const char *raw);
void beginObject(const char *name = nullptr); void beginObject(const char *name = nullptr);