- 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)
operator=(MString)/operator=(ZString)/operator=(std::string)/operator=(const
char*)/operator=(char) all replaced the buffer's content via setSize()+
memcpy() but never reset cursor back to the start - setSize() preserves
the *offset* the cursor was already at (correct for its own use, growing
a buffer while keeping a read position stable), so a full content
replacement left the cursor sitting at whatever offset it had into the
*previous* content, not the new content. operator=(double)/operator=(int)
already did `cursor = data;` after the same setSize()+memcpy() pattern,
confirming this was the intended behavior everywhere - just missing from
the other five overloads.
This is invisible to JetCore's interpreter, which never reassigns an
already-read-from MString (each tag is a freshly parsed C++ object every
time its container is scanned, so a keyword's MString is never both read
and then reassigned on the same instance). It surfaced in a prototype JET
compiler (JetCore/jetc) built this session, which intentionally reuses
one persistent keyword slot across loop iterations instead of
reconstructing everything from text every pass - exactly the case this
bug needed. A <while expr="..."> loop reusing a slot for its condition
worked for two iterations (matching a decoy pattern: the second
assignment happened to land the stale cursor at the exact end of the
new, same-length content) and threw "operand is not valid" on the third,
since a keyword reassigned inside the loop body left the cursor already
at eod() from the read.
Verified via jstring_test/mstring_test/zstring_test/parser_test (no
regressions), JetCore's tests/posttest.sh and the two-process JSON
session persistence test (no regressions), and the jetc prototype's two
demo templates - both now produce byte-identical output to jet-2.0's
interpreter, including a <while> loop that mutates and rereads the same
keyword slot every iteration.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
JFile.cpp: JFile and MFile both "virtual public MString" a single shared
base, so a virtual base is initialized only by the *most derived* class's
constructor. JFile's own constructor never mentions MString, so
JString's own "{}" default initializer never ran - a JFile backed by a
new or nonexistent file started as a raw empty string instead of a valid
empty JSON object. Every subsequent locate()/set() call then silently
misbehaved (no leading "{" for ifNext("{") to match, so writes landed at
offset 0 with no braces or separators - fields ended up prepended in
reverse order, concatenated with no delimiters at all). Fixed by seeding
"{}" via append (matching MFile's own load pattern) whenever the loaded
content is empty.
MString.cpp: several ZString-source copy paths (the ZString/MString copy
constructors, both assignment operators, operator<<(ZString), insert(),
write()) called zstring.getData() (the object's absolute start) paired
with zstring.getLength() (which is documented/implemented as "remaining
bytes from cursor to end", i.e. length - (cursor - data)). Pairing an
absolute start with a cursor-relative length silently corrupts any
cursor-advanced source: the copy starts from the wrong (too-early)
position but is truncated by exactly the amount the cursor had already
advanced, keeping unwanted leading bytes while dropping trailing ones.
Found via the CGIFormData-style resumability testing pattern used
elsewhere this session, triggered by JetCore's new "@name" session
variable feature stripping a sigil prefix via ifNext() before streaming
the remainder into an MString. Fixed by pairing zstring.getCursor()
(cursor-relative start) with zstring.getLength() consistently everywhere
this pattern appeared.
Verified via the existing jstring_test/mstring_test/zstring_test/
parser_test suites (all pass, no output changes) plus a new isolated
JFile round-trip test proving persistence across two separate process
instances sharing one file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- CGIFormData: fix broken setSize()+read() double-length bug that
corrupted every parsed body with a leading run of garbage bytes
(setSize(contentLength) pre-set length before read()/write()
appended on top of it); switch content-type checks from exact
equality to startsWith() since a real multipart Content-Type
always carries a boundary= parameter; implement the previously
empty multipart/form-data branch (ordinary fields only, file
uploads deferred to a separate upload mechanism per TODO.txt);
restore operator[] real lookup logic (was hardcoded to echo the
key back); default-initialize formdata to NULL; fix mismatched
include guard (leftover from an IMFFormData.h copy-paste); drop
the unused IMFHeader.h include that pulled in a broken,
unrelated StreamReader.h dependency.
- CGIFormattedData: parameterize the split delimiter (was hardcoded
to &, which silently broke cookie parsing since HTTP_COOKIE uses
; not &) and trim whitespace on each split token.
- MString::toCGI()/fromCGI(): were unimplemented stubs (always
returned an empty string); implemented proper CGI percent-encode
/ percent-decode + space plus-encoding, and fixed the one caller
that discarded fromCGIs return value instead of using it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Exception on the odd-length-input path was constructed but never
thrown (missing throw keyword) - the temporary was destroyed with no
effect, so execution fell through to an out-of-bounds nextChar() read
past the exhausted buffer instead of raising an error. Added the
missing throw.
Verified via full local compile: MString.cpp builds clean standalone;
a full-repo pass shows only the pre-existing, already-known
StreamReader.h-missing failures (7 files, unrelated to this change),
confirmed via each error log individually.