- MString::read() now gives up after repeated I/O errors instead of looping forever and crashing via realloc() failure (root cause of a recurring barant.com production crash). - HTTPRequest::parse() no longer leaks lineParser/headerParser on a malformed request. - Log gained size-based rotation (config-driven) and a non-static-mutex fix; a separate fix stopped rotation from destroying the logs symlink on its first trigger. - File.cpp: symlink-preserving rotation support. - JString: fixed an array-index out-of-bounds and a setString issue. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
653 lines
20 KiB
C++
653 lines
20 KiB
C++
#include "JString.h"
|
|
#include "Exception.h"
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
|
|
namespace coreutils {
|
|
|
|
std::ostream &operator<<(std::ostream &os, const JString::Proxy &proxy) {
|
|
ZString tempkey = proxy.key;
|
|
ZString temp = proxy.jstring.find(tempkey);
|
|
os << temp;
|
|
return os;
|
|
}
|
|
|
|
MString &operator<<(MString &ms, const JString::Proxy &proxy) {
|
|
ZString tempkey = proxy.key;
|
|
ZString temp = proxy.jstring.find(tempkey);
|
|
ms << temp;
|
|
return ms;
|
|
}
|
|
|
|
JString::JString() : MString("{}") {}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Structural helpers - escape/type aware skipping so JSON-significant
|
|
// characters (", {, }, [, ]) appearing inside string literals never
|
|
// desync the parse position.
|
|
// ---------------------------------------------------------------------
|
|
|
|
JSONType JString::typeAtCursor() {
|
|
if (startsWith("\""))
|
|
return JSONType::STRING;
|
|
if (startsWith("{"))
|
|
return JSONType::OBJECT;
|
|
if (startsWith("["))
|
|
return JSONType::ARRAY;
|
|
if (startsWith("true") || startsWith("false"))
|
|
return JSONType::BOOLEAN;
|
|
if (startsWith("null"))
|
|
return JSONType::JNULL;
|
|
if (startsWithNumber())
|
|
return JSONType::NUMBER;
|
|
return JSONType::NOT_FOUND;
|
|
}
|
|
|
|
// Captures the raw (still-escaped) content between a pair of quotes, cursor
|
|
// must be positioned on the opening quote. Leaves the cursor just past the
|
|
// closing quote. Correctly treats \" and \\ as escaped, not structural, so
|
|
// a string containing an escaped quote is captured in full rather than
|
|
// truncated early.
|
|
ZString JString::captureString() {
|
|
if (!ifNext("\""))
|
|
throw coreutils::Exception("Expected '\"' at start of JSON string.");
|
|
char *start = getCursor();
|
|
while (!eod()) {
|
|
char ch = getChar();
|
|
if (ch == '\\') {
|
|
nextChar();
|
|
if (!eod())
|
|
nextChar();
|
|
continue;
|
|
}
|
|
if (ch == '"') {
|
|
ZString result(start, getCursor() - start);
|
|
nextChar();
|
|
return result;
|
|
}
|
|
nextChar();
|
|
}
|
|
throw coreutils::Exception("Unterminated JSON string.");
|
|
}
|
|
|
|
void JString::skipString() {
|
|
captureString();
|
|
}
|
|
|
|
ZString JString::captureContainer() {
|
|
char *start = getCursor();
|
|
skipContainer();
|
|
return ZString(start, getCursor() - start);
|
|
}
|
|
|
|
void JString::skipContainer() {
|
|
char open = getChar();
|
|
char close = (open == '{') ? '}' : ']';
|
|
int depth = 0;
|
|
do {
|
|
if (eod())
|
|
throw coreutils::Exception("Unterminated JSON object/array.");
|
|
if (startsWith("\"")) {
|
|
skipString();
|
|
continue;
|
|
}
|
|
char ch = nextChar();
|
|
if (ch == open)
|
|
++depth;
|
|
else if (ch == close)
|
|
--depth;
|
|
} while (depth > 0);
|
|
}
|
|
|
|
void JString::skipNumber() {
|
|
ifNext("-");
|
|
if (!((getChar() >= '0') && (getChar() <= '9')))
|
|
throw coreutils::Exception("Invalid JSON number.");
|
|
while (!eod() && (getChar() >= '0') && (getChar() <= '9'))
|
|
nextChar();
|
|
if (!eod() && (getChar() == '.')) {
|
|
nextChar();
|
|
while (!eod() && (getChar() >= '0') && (getChar() <= '9'))
|
|
nextChar();
|
|
}
|
|
if (!eod() && ((getChar() == 'e') || (getChar() == 'E'))) {
|
|
nextChar();
|
|
if (!eod() && ((getChar() == '+') || (getChar() == '-')))
|
|
nextChar();
|
|
while (!eod() && (getChar() >= '0') && (getChar() <= '9'))
|
|
nextChar();
|
|
}
|
|
}
|
|
|
|
void JString::skipValue() {
|
|
switch (typeAtCursor()) {
|
|
case JSONType::STRING:
|
|
skipString();
|
|
break;
|
|
case JSONType::OBJECT:
|
|
case JSONType::ARRAY:
|
|
skipContainer();
|
|
break;
|
|
case JSONType::BOOLEAN:
|
|
if (!ifNext("true") && !ifNext("false"))
|
|
throw coreutils::Exception("Invalid JSON boolean.");
|
|
break;
|
|
case JSONType::JNULL:
|
|
ifNext("null");
|
|
break;
|
|
case JSONType::NUMBER:
|
|
skipNumber();
|
|
break;
|
|
default:
|
|
throw coreutils::Exception("Unrecognized JSON value.");
|
|
}
|
|
}
|
|
|
|
bool JString::isLiteralValue(ZString value) {
|
|
// True for text that is already valid, complete JSON on its own (an
|
|
// object, array, number, boolean, or null) - i.e. text that should be
|
|
// inserted as-is rather than quoted and escaped as a JSON string.
|
|
if (value.startsWith("{") || value.startsWith("["))
|
|
return true;
|
|
if (value.equals("true") || value.equals("false") || value.equals("null"))
|
|
return true;
|
|
if (value.startsWithNumber())
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// JSON string escape / unescape
|
|
// ---------------------------------------------------------------------
|
|
|
|
std::string JString::jsonEscape(ZString value) {
|
|
std::string result;
|
|
ZString v = value;
|
|
v.reset();
|
|
while (!v.eod()) {
|
|
char ch = v.nextChar();
|
|
switch (ch) {
|
|
case '"': result += "\\\""; break;
|
|
case '\\': result += "\\\\"; break;
|
|
case '\b': result += "\\b"; break;
|
|
case '\f': result += "\\f"; break;
|
|
case '\n': result += "\\n"; break;
|
|
case '\r': result += "\\r"; break;
|
|
case '\t': result += "\\t"; break;
|
|
default:
|
|
if ((unsigned char)ch < 0x20) {
|
|
char buf[8];
|
|
snprintf(buf, sizeof(buf), "\\u%04x", (unsigned char)ch);
|
|
result += buf;
|
|
} else {
|
|
result += ch;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static void appendUTF8(std::string &out, unsigned int codepoint) {
|
|
if (codepoint <= 0x7F) {
|
|
out += (char)codepoint;
|
|
} else if (codepoint <= 0x7FF) {
|
|
out += (char)(0xC0 | (codepoint >> 6));
|
|
out += (char)(0x80 | (codepoint & 0x3F));
|
|
} else if (codepoint <= 0xFFFF) {
|
|
out += (char)(0xE0 | (codepoint >> 12));
|
|
out += (char)(0x80 | ((codepoint >> 6) & 0x3F));
|
|
out += (char)(0x80 | (codepoint & 0x3F));
|
|
} else {
|
|
out += (char)(0xF0 | (codepoint >> 18));
|
|
out += (char)(0x80 | ((codepoint >> 12) & 0x3F));
|
|
out += (char)(0x80 | ((codepoint >> 6) & 0x3F));
|
|
out += (char)(0x80 | (codepoint & 0x3F));
|
|
}
|
|
}
|
|
|
|
static unsigned int readHex4(ZString &v) {
|
|
if (v.getLength() < 4)
|
|
throw coreutils::Exception("Truncated \\u escape in JSON string.");
|
|
unsigned int value = 0;
|
|
for (int ix = 0; ix < 4; ++ix) {
|
|
char ch = v.nextChar();
|
|
value <<= 4;
|
|
if ((ch >= '0') && (ch <= '9')) value |= (ch - '0');
|
|
else if ((ch >= 'a') && (ch <= 'f')) value |= (ch - 'a' + 10);
|
|
else if ((ch >= 'A') && (ch <= 'F')) value |= (ch - 'A' + 10);
|
|
else throw coreutils::Exception("Invalid hex digit in \\u escape.");
|
|
}
|
|
return value;
|
|
}
|
|
|
|
std::string JString::jsonUnescape(ZString raw) {
|
|
std::string result;
|
|
ZString v = raw;
|
|
v.reset();
|
|
while (!v.eod()) {
|
|
char ch = v.nextChar();
|
|
if ((ch != '\\') || v.eod()) {
|
|
result += ch;
|
|
continue;
|
|
}
|
|
char esc = v.nextChar();
|
|
switch (esc) {
|
|
case '"': result += '"'; break;
|
|
case '\\': result += '\\'; break;
|
|
case '/': result += '/'; break;
|
|
case 'b': result += '\b'; break;
|
|
case 'f': result += '\f'; break;
|
|
case 'n': result += '\n'; break;
|
|
case 'r': result += '\r'; break;
|
|
case 't': result += '\t'; break;
|
|
case 'u': {
|
|
unsigned int codepoint = readHex4(v);
|
|
// High surrogate? consume the paired low surrogate and combine.
|
|
if ((codepoint >= 0xD800) && (codepoint <= 0xDBFF) &&
|
|
(v.getLength() >= 6) && (v.getChar() == '\\')) {
|
|
char *mark = v.getCursor();
|
|
v.nextChar();
|
|
if (v.getChar() == 'u') {
|
|
v.nextChar();
|
|
unsigned int low = readHex4(v);
|
|
if ((low >= 0xDC00) && (low <= 0xDFFF)) {
|
|
codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00);
|
|
} else {
|
|
v.setCursor(mark); // not a valid low surrogate, back out
|
|
}
|
|
} else {
|
|
v.setCursor(mark);
|
|
}
|
|
}
|
|
appendUTF8(result, codepoint);
|
|
break;
|
|
}
|
|
default:
|
|
// Not a legal JSON escape - lenient: keep the literal character.
|
|
result += esc;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Typed read access
|
|
// ---------------------------------------------------------------------
|
|
|
|
JSONType JString::getType(ZString path) {
|
|
reset();
|
|
if (!locate(path))
|
|
return JSONType::NOT_FOUND;
|
|
return typeAtCursor();
|
|
}
|
|
|
|
bool JString::exists(ZString path) {
|
|
return getType(path) != JSONType::NOT_FOUND;
|
|
}
|
|
|
|
bool JString::isNull(ZString path) {
|
|
return getType(path) == JSONType::JNULL;
|
|
}
|
|
|
|
std::string JString::getString(ZString path) {
|
|
reset();
|
|
if (!locate(path))
|
|
throw coreutils::Exception("Path not found.");
|
|
if (typeAtCursor() != JSONType::STRING)
|
|
throw coreutils::Exception("Value at path is not a JSON string.");
|
|
return jsonUnescape(captureString());
|
|
}
|
|
|
|
double JString::getNumber(ZString path) {
|
|
reset();
|
|
if (!locate(path))
|
|
throw coreutils::Exception("Path not found.");
|
|
if (typeAtCursor() != JSONType::NUMBER)
|
|
throw coreutils::Exception("Value at path is not a JSON number.");
|
|
char *start = getCursor();
|
|
skipNumber();
|
|
return atof(std::string(start, getCursor() - start).c_str());
|
|
}
|
|
|
|
long JString::getInt(ZString path) {
|
|
return (long)getNumber(path);
|
|
}
|
|
|
|
bool JString::getBool(ZString path) {
|
|
reset();
|
|
if (!locate(path))
|
|
throw coreutils::Exception("Path not found.");
|
|
if (ifNext("true"))
|
|
return true;
|
|
if (ifNext("false"))
|
|
return false;
|
|
throw coreutils::Exception("Value at path is not a JSON boolean.");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// find() - kept for callers that want the raw (still-escaped for strings)
|
|
// span rather than a fully typed/decoded value.
|
|
// ---------------------------------------------------------------------
|
|
|
|
ZString JString::find(ZString path) {
|
|
reset();
|
|
return findRelative(path);
|
|
}
|
|
|
|
// Same as find(), but searches from the cursor's current position rather than
|
|
// resetting to the start of the whole document first. Needed by the array
|
|
// selector logic in locate() below, which must check a subkey *within the
|
|
// array element currently under the cursor* - calling find() there (which
|
|
// always resets to the document root) would search the wrong object entirely.
|
|
ZString JString::findRelative(ZString path) {
|
|
if (!locate(path))
|
|
return ZString();
|
|
switch (typeAtCursor()) {
|
|
case JSONType::STRING:
|
|
return captureString();
|
|
case JSONType::ARRAY:
|
|
case JSONType::OBJECT:
|
|
return captureContainer();
|
|
default: {
|
|
char *start = getCursor();
|
|
skipValue();
|
|
return ZString(start, getCursor() - start);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// locate() - path navigation. Structurally unchanged except that skipping
|
|
// past a key's value (when it isn't the one being searched for) now goes
|
|
// through skipValue() so every JSON type is handled, not just
|
|
// string/array/object, and strings are skipped escape-aware.
|
|
// ---------------------------------------------------------------------
|
|
|
|
bool JString::locate(ZString path) {
|
|
path.getTokenExclude("[.");
|
|
notfirst = false;
|
|
if (ifNext("{")) {
|
|
while (startsWith("\"")) {
|
|
notfirst = true;
|
|
ZString label;
|
|
if (ifNext("\"")) {
|
|
label = getTokenExclude("\"");
|
|
ifNext("\"");
|
|
} else
|
|
throw coreutils::Exception("Labels must be contained in double quotes.");
|
|
if (ifNext(":")) {
|
|
if (label == path.parsed()) {
|
|
if (path.unparsed() == "") {
|
|
return true;
|
|
}
|
|
if (path.ifNext("."))
|
|
return locate(path.unparsed());
|
|
if (path.ifNext("[")) {
|
|
bool found = true;
|
|
if (path.startsWithNumber()) {
|
|
// Numeric index: label[N]
|
|
int index = path.asInteger();
|
|
if (!path.ifNext("]"))
|
|
throw coreutils::Exception("expecting ] after index value in array selector.");
|
|
if (!ifNext("["))
|
|
throw coreutils::Exception("Path is expecting an array for selector value.");
|
|
while (index > 0) {
|
|
skipValue();
|
|
// Reaching the array's closing bracket here means the
|
|
// caller asked for an index past the last real element -
|
|
// a legitimate "does this exist" probe (see exists()'s own
|
|
// doc comment), not malformed JSON, and the one real
|
|
// caller of this shape (BARANTMailConfig::getDkimEntries(),
|
|
// walking a "dkim" array of unknown length ascending until
|
|
// exists() first returns false) depends on getting a clean
|
|
// "not found" back rather than an uncaught exception.
|
|
// Confirmed live: querying one index past a real 2-element
|
|
// array threw this unconditionally before, crashing server
|
|
// startup outright instead of the config simply reporting
|
|
// "no more entries".
|
|
if (ifNext("]"))
|
|
return false;
|
|
if (!ifNext(","))
|
|
throw coreutils::Exception("expecting comma to seperate array elements.");
|
|
--index;
|
|
}
|
|
} else {
|
|
// Selector: label[key=value] - find the array element whose subkey
|
|
// matches the given value.
|
|
ZString key = path.getTokenExclude("=");
|
|
if (!path.ifNext("="))
|
|
throw coreutils::Exception("expecting '=' in array selector.");
|
|
ZString value = path.getTokenExclude("]");
|
|
if (!path.ifNext("]"))
|
|
throw coreutils::Exception("expecting ']' after array selector value.");
|
|
if (!ifNext("["))
|
|
throw coreutils::Exception("Path is expecting an array for selector value.");
|
|
found = false;
|
|
while (!ifNext("]")) {
|
|
push();
|
|
ZString check = findRelative(key);
|
|
pop();
|
|
if (check.equals(value)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
skipValue();
|
|
ifNext(",");
|
|
}
|
|
}
|
|
// Landed on the target array element (index or selector match) - if the
|
|
// path continues past it (e.g. "b[2].c" or "items[id=x].val"), descend
|
|
// into it; otherwise this element itself is the target. Previously the
|
|
// numeric-index case always returned true immediately after reaching the
|
|
// index, so any further path segment was silently ignored, and the
|
|
// selector case was never reachable from a nested path at all - both
|
|
// now share this same continuation logic.
|
|
if (!found)
|
|
return false;
|
|
if (path.eod())
|
|
return true;
|
|
if (path.ifNext("."))
|
|
return locate(path.unparsed());
|
|
throw coreutils::Exception("expecting '.' in path.");
|
|
}
|
|
}
|
|
skipValue();
|
|
ifNext(",");
|
|
} else
|
|
throw coreutils::Exception("Expected colon after label.");
|
|
}
|
|
} else if (ifNext("[")) {
|
|
ZString label = path.getTokenExclude("[");
|
|
path.ifNext("[");
|
|
ZString key = path.getTokenExclude("=");
|
|
path.ifNext("=");
|
|
ZString value = path.getTokenExclude("]");
|
|
while (!ifNext("]")) {
|
|
push();
|
|
ZString check = findRelative(key);
|
|
pop();
|
|
if (check.equals(value)) {
|
|
return locate(label);
|
|
} else {
|
|
skipValue();
|
|
ifNext(",");
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Writing values
|
|
// ---------------------------------------------------------------------
|
|
|
|
void JString::set(ZString &path, ZString &value) {
|
|
reset();
|
|
MString formatted;
|
|
if (isLiteralValue(value))
|
|
formatted << value;
|
|
else
|
|
formatted << "\"" << jsonEscape(value).c_str() << "\"";
|
|
setFormatted(path, formatted);
|
|
}
|
|
|
|
void JString::setFormatted(ZString path, MString formatted) {
|
|
reset();
|
|
if (locate(path)) {
|
|
int startOffset = offset();
|
|
skipValue();
|
|
int len = offset() - startOffset;
|
|
remove(startOffset, len);
|
|
insert(formatted, startOffset);
|
|
} else {
|
|
MString key;
|
|
if (notfirst)
|
|
key << ",";
|
|
key << "\"" << path << "\":" << formatted;
|
|
insert(key, offset());
|
|
}
|
|
}
|
|
|
|
void JString::setString(ZString path, std::string value) {
|
|
// Deliberately NOT routed through set() (which calls isLiteralValue()
|
|
// to guess whether the raw text should be quoted) - setString() is
|
|
// explicitly, unambiguously a string setter, so the value must
|
|
// ALWAYS be quoted+escaped regardless of what it looks like.
|
|
// isLiteralValue() classifies anything ZString::startsWithNumber()
|
|
// accepts as a bare numeric literal - true for "42", but ALSO true
|
|
// for a string that merely STARTS with digits, like a UUID
|
|
// ("00146df8-e42c-11e7-9c78-525400b869dc"). Going through set()
|
|
// wrote that UUID completely unquoted; skipValue() then only
|
|
// recognizes the JSON-number PREFIX ("00146") as the value's real
|
|
// span when locating it for a later update, so the next setString()
|
|
// call spliced its own new key/value pair in right after "00146",
|
|
// leaving "df8-e42c-...-869dc" as orphaned text outside the object
|
|
// entirely - malformed JSON, confirmed via a real corrupted session
|
|
// file (jet-2.0's own native $[@name] session variables, backed by
|
|
// this class - see Global.h). Every other setString() caller
|
|
// sitewide was silently exposed to the same risk for any string
|
|
// value that happens to start with a digit.
|
|
MString v(value);
|
|
MString formatted;
|
|
formatted << "\"" << jsonEscape(v).c_str() << "\"";
|
|
setFormatted(path, formatted);
|
|
}
|
|
|
|
void JString::setNumber(ZString path, double value) {
|
|
char buf[64];
|
|
snprintf(buf, sizeof(buf), "%g", value);
|
|
MString v(buf);
|
|
set(path, v);
|
|
}
|
|
|
|
void JString::setInt(ZString path, long value) {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%ld", value);
|
|
MString v(buf);
|
|
set(path, v);
|
|
}
|
|
|
|
void JString::setBool(ZString path, bool value) {
|
|
MString v(value ? "true" : "false");
|
|
set(path, v);
|
|
}
|
|
|
|
void JString::setNull(ZString path) {
|
|
MString v("null");
|
|
set(path, v);
|
|
}
|
|
|
|
JString & JString::operator=(coreutils:: ZString value) {
|
|
setSize(value.getLength());
|
|
memcpy(data, value.getData(), value.getLength());
|
|
removeWhitespace();
|
|
return *this;
|
|
}
|
|
|
|
void JString::removeWhitespace() {
|
|
reset();
|
|
while (!eod()) {
|
|
if (*cursor == ' ')
|
|
remove(1);
|
|
else if (*cursor == '\r')
|
|
remove(1);
|
|
else if (*cursor == '\t')
|
|
remove(1);
|
|
else if (*cursor == '\n')
|
|
remove(1);
|
|
else if (startsWith("\""))
|
|
skipString();
|
|
else
|
|
++cursor;
|
|
}
|
|
reset();
|
|
}
|
|
|
|
MString JString::pretty() {
|
|
MString output;
|
|
reset();
|
|
char ch;
|
|
int index = 0;
|
|
while (!eod()) {
|
|
ch = nextChar();
|
|
if (ch == '{') {
|
|
index += 3;
|
|
output << ch << '\n';
|
|
for (int ix = 0; ix < index; ++ix)
|
|
output << ' ';
|
|
}
|
|
else if (ch == '[') {
|
|
index += 3;
|
|
output << ch << '\n';
|
|
for (int ix = 0; ix < index; ++ix)
|
|
output << ' ';
|
|
}
|
|
else if (ch == ':') {
|
|
output << ": ";
|
|
}
|
|
else if (ch == '}') {
|
|
index -= 3;
|
|
output << '\n';
|
|
for (int ix = 0; ix < index; ++ix)
|
|
output << ' ';
|
|
output << ch;
|
|
}
|
|
else if (ch == ']') {
|
|
index -= 3;
|
|
output << '\n';
|
|
for (int ix = 0; ix < index; ++ix)
|
|
output << ' ';
|
|
output << ch;
|
|
}
|
|
else if (ch == ',') {
|
|
output << ch << '\n';
|
|
for (int ix = 0; ix < index; ++ix)
|
|
output << ' ';
|
|
}
|
|
else if (ch == '"') {
|
|
output << ch;
|
|
// Stream the rest of the string literal through untouched, including
|
|
// escapes, without the pretty-printer's structural-character logic
|
|
// firing on a { } [ ] , or : that happens to appear inside it.
|
|
while (!eod()) {
|
|
char sch = nextChar();
|
|
output << sch;
|
|
if (sch == '\\') {
|
|
if (!eod())
|
|
output << nextChar();
|
|
continue;
|
|
}
|
|
if (sch == '"')
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
output << ch;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
void JString::changed(ZString key, ZString value) {}
|
|
|
|
}
|