#include "JString.h" #include "Exception.h" #include #include 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) {} }