Files
CoreUtils/JString.cpp
T
Brad ArantandClaude Sonnet 5 5b6d4ea8ab Fix ambiguous MString << JString::Proxy overload resolution
JString::Proxy offers several implicit conversion operators (ZString,
MString, std::string, double, bool) so a single $[...]-style access can
be used wherever any of those types is expected. That flexibility made
"MString << Proxy" genuinely ambiguous: operator ZString() and
operator MString() (via its own implicit upcast to ZString) both satisfy
MString::operator<<(ZString), and operator double()/operator bool() both
satisfy MString::operator<<(int) via a standard numeric conversion, so
the compiler had no basis to prefer one conversion path over another.

Fixed the same way Proxy already solves this exact problem for
std::cout << proxy - a dedicated friend operator<<(MString&, const
Proxy&) that needs no conversion at all, so it's always the preferred
candidate. Verified by actually running testing/jstring_test.cpp (not
just compiling it) - the line that used to fail to compile now correctly
prints "this is a test: this is a comment".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:11:56 -07:00

615 lines
17 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();
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() << "\"";
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) {
MString v(value);
set(path, v);
}
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) {}
}