From 3e6e410fe84fc453701a3cc6330c05e0d408c43e Mon Sep 17 00:00:00 2001 From: Brad Arant Date: Sun, 23 Aug 2026 18:44:59 -0700 Subject: [PATCH] Fix JFile empty-document seed and MString's ZString-source pointer/length mismatch 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 --- JFile.cpp | 15 ++++++++++++++- MString.cpp | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/JFile.cpp b/JFile.cpp index 4cfac59..ed64f43 100644 --- a/JFile.cpp +++ b/JFile.cpp @@ -4,7 +4,20 @@ namespace coreutils { - JFile::JFile(ZString filename) : MFile(filename) {} + // MFile and JString share a single virtual MString base (the diamond: + // both "MFile : virtual public MString" and "JString : virtual public + // MString"), and a virtual base is initialized only by the *most + // derived* class's constructor - so JString's own "{}" default + // initializer never runs here. Left alone, a JFile backed by a new or + // empty file starts as a raw empty string instead of a valid empty JSON + // object, and every subsequent locate()/set() call silently misbehaves + // (no leading "{" for ifNext("{") to match, so writes end up inserted + // at offset 0 with no braces or separators at all). Detect that case + // here and seed valid JSON explicitly. + JFile::JFile(ZString filename) : MFile(filename) { + if(getLength() == 0) + *this << "{}"; + } } diff --git a/MString.cpp b/MString.cpp index 360f91f..303a590 100644 --- a/MString.cpp +++ b/MString.cpp @@ -48,13 +48,13 @@ namespace coreutils { MString::MString(const ZString &zstring) { setSize(zstring.getLength()); - memcpy(data, zstring.getData(), zstring.getLength()); - cursor = data; - } + memcpy(data, zstring.getCursor(), zstring.getLength()); + cursor = data; + } MString::MString(ZString &zstring) { setSize(zstring.getLength()); - memcpy(data, zstring.getData(), zstring.getLength()); + memcpy(data, zstring.getCursor(), zstring.getLength()); cursor = data; } @@ -66,7 +66,7 @@ namespace coreutils { MString::MString(MString &mstring) { setSize(mstring.getLength()); - memcpy(data, mstring.getData(), mstring.getLength()); + memcpy(data, mstring.getCursor(), mstring.getLength()); cursor = data; } @@ -106,7 +106,7 @@ namespace coreutils { MString &MString::operator=(coreutils::MString value) { setSize(value.getLength()); - memcpy(data, value.getData(), value.getLength()); + memcpy(data, value.getCursor(), value.getLength()); length = value.getLength(); onChange(); return *this; @@ -114,7 +114,7 @@ namespace coreutils { MString &MString::operator=(coreutils::ZString value) { setSize(value.getLength()); - memcpy(data, value.getData(), value.getLength()); + memcpy(data, value.getCursor(), value.getLength()); length = value.getLength(); onChange(); return *this; @@ -195,7 +195,7 @@ namespace coreutils { int temp = length; int len = length + zstring.getLength(); setSize(len); - memcpy(data + temp, zstring.getData(), zstring.getLength()); + memcpy(data + temp, zstring.getCursor(), zstring.getLength()); onChange(); return *this; } @@ -240,7 +240,7 @@ namespace coreutils { setSize(length + value.getLength()); for (int ix = length; ix >= offset; ix--) getData()[ix] = getData()[ix - value.getLength()]; - std::memcpy(data + offset, value.getData(), value.getLength()); + std::memcpy(data + offset, value.getCursor(), value.getLength()); onChange(); } @@ -285,7 +285,7 @@ namespace coreutils { MString &MString::write(ZString value) { int len = length; setSize(length + value.getLength()); - memcpy(data + len, value.getData(), value.getLength()); + memcpy(data + len, value.getCursor(), value.getLength()); onChange(); return *this; }