Files
CoreUtils/JString.h
T
Brad Arant 78c45d378c Fix File stat-before-open crash risk, JSON string-quoting bug, and MString buffer overflow
- File: stat() only after a successful open() (previously ran first with
  its result ignored, feeding a missing file's garbage st_size into a
  realloc - could crash the whole APIServer on a request for a
  nonexistent static asset)
- JString::setString(): always quote+escape the value instead of going
  through set()'s literal-value guess, which misclassified any string
  starting with digits (e.g. a UUID) as a bare numeric literal and wrote
  it unquoted, corrupting later edits to the same JSON document
- MString::remove() (both overloads): fixed an off-by-one that read one
  byte past the buffer on a completely full string (found via ASan under
  concurrent load)
- CGIFormData: added fields() so callers can iterate every parsed field
  without knowing names in advance (used to merge POST body into
  jet::Global::variables the same way query-string params already are)
2026-09-03 05:14:24 +00:00

245 lines
7.6 KiB
C++

#ifndef __JString_h__
#define __JString_h__
#include "MString.h"
#include <iostream>
#include <sstream>
namespace coreutils {
///
/// Use the JString object when you need a JSON interface to C++. JString uses
/// an MString to store a JSON string representing the object(s) contained.
/// The [] operator is overriden from the ZString and provides a method to
/// access the objects elements using a named path as the index to the items.
///
/// JString default constructor will create an empty JString object pointing
/// to an empty object ("{}").
///
/// Use a constructor or assignment operator to initialize a JString to an
/// initial JSON string.
///
/// Several constructors are available including a handle to a std::FILE
/// object to initialize the JSON string. On destruction the file is saved back.
///
/// The [] operator can be used to access elements within the JSON string.
/// Assigning a value dispatches on the C++ type of the right-hand side, so the
/// resulting JSON is correctly typed rather than always being a quoted string:
///
/// jstring["name"] = "value"; // JSON string, escaped correctly
/// jstring["count"] = 42; // JSON number (integer)
/// jstring["price"] = 19.95; // JSON number
/// jstring["active"] = true; // JSON boolean
/// jstring["notes"].setNull(); // JSON null
///
/// std::cout << jstring["name"];
///
/// Typed read access is available directly on the path (no need to go through
/// operator[] first):
///
/// std::string name = jstring.getString("name");
/// double price = jstring.getNumber("price");
/// bool active = jstring.getBool("active");
/// bool missing = !jstring.exists("notfound");
/// JSONType t = jstring.getType("count"); // JSONType::NUMBER
///
enum class JSONType {
STRING, NUMBER, BOOLEAN, JNULL, ARRAY, OBJECT, NOT_FOUND
};
class JString : virtual public MString {
public:
JString();
void set(ZString &path, ZString &value);
///
/// Inserts/replaces path with already-formatted, valid JSON text
/// (a quoted+escaped string, or a bare number/bool/null literal) -
/// the locate()/skipValue()/insert() mechanics set() and setString()
/// both share, factored out so setString() can force string quoting
/// unconditionally instead of going through set()'s own
/// isLiteralValue() guess (see setString()'s own comment in the
/// .cpp for why that guess is wrong for a string setter).
///
void setFormatted(ZString path, MString formatted);
///
/// Use the find method to look through an object for member specified by
/// the key in parameter 1. The cursor must be pointing to the { character
/// that begins the object's list. Returns the raw (still-escaped, for
/// strings) span of the value - use getString()/getNumber()/getBool()/
/// isNull() for decoded, typed access instead.
///
ZString find(ZString path);
bool locate(ZString path);
bool notfirst;
ZString path;
///
/// Type-aware access. All of these call locate() internally, so path uses
/// the same dotted/bracketed syntax as find()/set() (e.g. "a.b[2].c").
///
JSONType getType(ZString path);
bool exists(ZString path);
bool isNull(ZString path);
std::string getString(ZString path); ///< Escape-decoded string content.
double getNumber(ZString path);
long getInt(ZString path);
bool getBool(ZString path);
void setString(ZString path, std::string value); ///< Escapes value automatically.
void setNumber(ZString path, double value);
void setInt(ZString path, long value);
void setBool(ZString path, bool value);
void setNull(ZString path);
///
/// JSON string escape/unescape helpers (exposed as static utilities since
/// they operate on plain text, not on this object's own JSON document).
///
static std::string jsonEscape(ZString value);
static std::string jsonUnescape(ZString raw);
class Proxy {
public:
Proxy(JString &jstring, ZString key) : jstring(jstring), key(key) {}
friend std::ostream &operator<<(std::ostream &os, const Proxy &proxy);
///
/// Proxy offers several implicit conversion operators below (to
/// ZString, MString, std::string, double, bool) so a single $[...]-
/// style access can be used wherever any of those types is expected.
/// That flexibility makes "MString << Proxy" genuinely ambiguous on
/// its own - 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 has no way to prefer one path over another. This
/// exact-match overload (mirroring the ostream one above, which
/// solves the same problem for std::cout << proxy) resolves it by
/// giving MString a candidate that needs no conversion at all.
///
friend MString &operator<<(MString &ms, const Proxy &proxy);
operator coreutils::ZString () {
return jstring.find(key);
}
operator coreutils::MString () {
return jstring.find(key);
}
operator std::string () {
return jstring.getString(key);
}
operator double () {
return jstring.getNumber(key);
}
operator bool () {
return jstring.getBool(key);
}
Proxy& operator=(coreutils::MString value) {
jstring.setString(key, value.str());
jstring.changed(key, value);
return *this;
}
Proxy& operator=(const char *value) {
jstring.setString(key, std::string(value));
jstring.changed(key, ZString(value));
return *this;
}
Proxy& operator=(double value) {
jstring.setNumber(key, value);
return *this;
}
Proxy& operator=(int value) {
jstring.setInt(key, value);
return *this;
}
Proxy& operator=(long value) {
jstring.setInt(key, value);
return *this;
}
Proxy& operator=(bool value) {
jstring.setBool(key, value);
return *this;
}
void setNull() {
jstring.setNull(key);
}
private:
JString &jstring;
ZString key;
};
Proxy operator[](coreutils::ZString key) {
return Proxy(*this, key);
}
JString & operator=(coreutils:: ZString value);
MString pretty();
virtual void changed(ZString key, ZString value);
protected:
///
/// Strips whitespace between JSON structural tokens (indentation,
/// newlines, spaces after ':'/',') - the parser expects compact JSON
/// internally. operator=(ZString) already calls this for in-memory
/// assignment; JFile's constructor also calls it after loading a
/// file's raw bytes, which otherwise bypasses this entirely (see
/// JFile.cpp). Protected rather than private specifically so JFile
/// can reach it.
///
void removeWhitespace();
private:
///
/// Structural helpers used internally by locate()/set() to correctly skip
/// past a value of any JSON type without being confused by JSON-structural
/// characters (quotes, braces, brackets) appearing inside string literals.
///
ZString findRelative(ZString path);
JSONType typeAtCursor();
void skipValue();
void skipString();
void skipContainer();
void skipNumber();
ZString captureString();
ZString captureContainer();
bool isLiteralValue(ZString value);
};
}
#endif