Multi-day accumulated fixes: tag dispatch, session/pooling correctness, SPA rollout, jetc compiler

JetCore engine (Tag.cpp/.h, Global.cpp/.h, __*.cpp tag classes):
- Numerous __mysql.cpp connection-pooling correctness fixes (stale
  result sets, double-indirection sessionid bugs, field caching,
  pool keying) and a session-registration-before-connection-confirmed
  ordering fix (dangling pointer on a failed connect)
- Tag.cpp: large body of fixes to the tag-dispatch/container-scanning
  logic (bare-name fallback, database aliasing, single-quote handling,
  @-prefix session variable resolution, TAGDEF caching, nested split-tag
  scanning for if/while/until/ifrow so an inner one doesn't corrupt an
  outer one's own boundary scan) plus the process-first rendering
  order fix (a <process> now runs before <checksession>/page
  resolution, fixing a real "blank page after successful login" bug)
- Global.h/.cpp: added imapSessions (parallel to the existing __mysql*
  session map) for the new <imap> tag family
- __whiledir.cpp/__include.cpp: caching and directory-name-aliasing
  fixes
- __call.cpp/.h: fixed a dangling-pointer bug
- __write.cpp: fixed an O_WRONLY-only open that couldn't create a new
  file
- __ifrow.cpp/__sql.cpp: fixed the same sessionid double-indirection
  bug __mysql.cpp had (resolveKeyword's result was being used as a map
  key lookup instead of the resolved value itself)

jetc compiler (jetc/JetParser.cpp/.h, JetCodegen.cpp/.h, JetAST.h,
jetc.cpp, build-shared.sh, build-compiled.sh):
- Parser fixes: inline <include>, nested-quote handling, void-tag
  recognition
- Codegen fixes and expanded AST support
- build-shared.sh: atomic rename into place for a freshly-built .jtc
  (a live server dlopen()ing the same path mid-rebuild was corrupting
  its own already-mapped view of the old file) and a staleness check
  so a cached JetCore .o doesn't silently keep serving yesterday's
  fixes forever
- Added -lssl to both build scripts' link lines (needed once the new
  <imap> tag's client-side TLS code existed - previously nothing in
  this codebase used OpenSSL's SSL/TLS client API, only libcrypto)

Site content (jetc/barant-site/): SPA (fetch-and-swap navigation)
rollout across essentially every .page/.jet/.section/.tag/.proc/.method
file - dynamic-content fixes, fragment-mode stripping, native
$[@name] session variables replacing older mechanisms, and this
session's own checkout.page redesign, login start-param redirect fix,
and per-page side-menu (menu.bar/$[PAGE].menu) refresh support.
Completed the earlier main.css/control.css move from assets/ to
assets/images/ (git still showed the old paths as tracked-but-deleted
until now).

Deliberately left out of this commit: dozens of .pre-<description>-
<date> manual backup files (this session's and earlier ones' own
before/after snapshots, not meant for permanent history), compiled
.jtc build outputs, ad hoc test binaries and scripts under jetc/
(login_asan, wcwc_test, optest2_bin, etc.), and jetc/__pycache__/ -
none of these are meant to be tracked; a .gitignore covering them
would be worth adding separately.
This commit is contained in:
Brad Arant committed 2026-09-03 05:20:48 +00:00
1 parent 54b6e522f9
commit b0cadc3f08
162 files changed
+3567 -2887

No files matched your search

+201 -3
View File
@@ -6,6 +6,8 @@
#include <time.h>
#include <chrono>
#include <cmath>
#include <vector>
#include <random>
namespace jet {
@@ -27,8 +29,73 @@ namespace jet {
string = op.string;
doubleValue = op.doubleValue;
isNumber = op.isNumber;
// A parenthesized sub-expression's own comparison result was never
// propagated to the wrapping Operand - only string/doubleValue/
// isNumber were copied, leaving this Operand's own `boolean`
// genuinely uninitialized (Operand.h declares it with no default
// value). Confirmed via debug tracing: printed as garbage stack
// bytes, which "and"/"or" then combined instead of the real
// true/false result.
boolean = op.boolean;
if(!in.ifNext(")"))
throw coreutils::Exception("expected ) in expression.");
} else if(in.ifNextIgnoreCase("SUBSTRING_INDEX")) {
// MySQL SUBSTRING_INDEX(str, delim, count): with a positive count,
// returns everything before the count-th occurrence of delim
// (counting from the left); with a negative count, everything
// after the |count|-th occurrence (counting from the right). If
// delim doesn't occur that many times, the whole string is
// returned unchanged. Genuinely missing (like LOCATE was) - real
// content (index2.cgi's own URI-parsing logic, ported as part of
// the /var/www/barant jet-2.0 conversion) relies on it to peel a
// path apart one segment at a time:
// substring_index(substring_index('$[REQUEST_URI]','?',1),'/',2).
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for SUBSTRING_INDEX parameters.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in SUBSTRING_INDEX expression.");
Operand parm2(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in SUBSTRING_INDEX expression.");
Operand parm3(in, tag);
if(in.ifNext(")")) {
coreutils::MString haystack = parm1.string;
haystack.reset();
coreutils::MString delim = parm2.string;
delim.reset();
int count = parm3.string.asInteger();
int delimLen = (int)delim.getLength();
std::vector<int> positions;
if(delimLen > 0) {
int base = 0;
while(true) {
coreutils::MString scan = haystack.substring(base);
scan.reset();
int pos = scan.find(delim);
if(pos == -1) break;
positions.push_back(base + pos);
base += pos + delimLen;
}
}
if(count >= 0) {
size_t n = (size_t)count;
if(n == 0 || positions.size() < n) {
string = haystack.substring(0);
} else {
string = haystack.substring(0, positions[n - 1]);
}
} else {
size_t n = (size_t)(-count);
if(positions.size() < n) {
string = haystack.substring(0);
} else {
int idx = positions[positions.size() - n];
string = haystack.substring(idx + delimLen);
}
}
} else
throw coreutils::Exception("Expecting ) at end of SUBSTRING_INDEX expression.");
} else if(in.ifNextIgnoreCase("SUBSTRING")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for SUBSTRING parameters.");
@@ -58,6 +125,58 @@ namespace jet {
} else
throw coreutils::Exception("Expecting ) at end of substring expression.");
}
} else if(in.ifNextIgnoreCase("LOCATE")) {
// MySQL-style LOCATE(needle, haystack): 1-based position of the
// first occurrence of needle within haystack, or 0 if not found -
// a genuinely missing function (like <eval> was a genuinely
// missing tag). Real 1.4 content depends on it for exactly this
// "is #[name] present in the space-padded $[errfield] list"
// pattern (barant.tag's own <field> definition:
// locate(' #[name] ','$[errfield]') != 0), used throughout for
// marking a field's input as erroring after a failed validation.
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for LOCATE parameters.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in LOCATE expression.");
Operand parm2(in, tag);
if(in.ifNext(")")) {
coreutils::MString haystack = parm2.string;
haystack.reset();
int pos = haystack.find(parm1.string);
doubleValue = (pos == -1) ? 0 : (pos + 1);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else {
// Optional 3rd start_pos argument (MySQL LOCATE(substr, str,
// pos)): search begins at the given 1-based position instead of
// the start of the string. Needed by index2.cgi's own real
// URI-parsing logic (walking a path one '/' at a time), ported
// as part of the /var/www/barant jet-2.0 conversion.
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in LOCATE expression.");
Operand parm3(in, tag);
if(in.ifNext(")")) {
coreutils::MString haystack = parm2.string;
haystack.reset();
int totalLen = (int)haystack.getLength();
int startPos = parm3.string.asInteger() - 1;
if(startPos < 0) startPos = 0;
if(startPos >= totalLen) {
doubleValue = 0;
} else {
coreutils::MString sub = haystack.substring(startPos);
sub.reset();
int pos = sub.find(parm1.string);
doubleValue = (pos == -1) ? 0 : (startPos + pos + 1);
}
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of LOCATE expression.");
}
} else if(in.ifNextIgnoreCase("LEFT")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for LEFT parameters.");
@@ -384,7 +503,10 @@ namespace jet {
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of FLOOR expression.");
} else if(in.ifNextIgnoreCase("UNIXTIME")) {
} else if(in.ifNextIgnoreCase("UNIXTIME") || in.ifNextIgnoreCase("UNIX_TIMESTAMP")) {
// Real content (cart.page's own <set expr="unix_timestamp()">)
// calls MySQL's real function name, not this codebase's own
// "UNIXTIME" - same value, just accept both spellings.
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for UNIXTIME.");
if(in.ifNext(")")) {
@@ -396,6 +518,59 @@ namespace jet {
} else
throw coreutils::Exception("Expecting ) at end of UNIXTIME.");
}
else if(in.ifNextIgnoreCase("NOW")) {
// MySQL's now() - real content (SESSIONCTL.tag's own
// createsession definition: <set name="_timestamp" expr="now()">,
// embedded directly into a DATETIME column via a raw SQL literal:
// timestamp='$[_timestamp]') expects a MySQL DATETIME-formatted
// string ("YYYY-MM-DD HH:MM:SS"), not a Unix timestamp (that's
// UNIX_TIMESTAMP, just above).
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for NOW.");
if(in.ifNext(")")) {
std::time_t t = std::time(nullptr);
std::tm tmVal;
gmtime_r(&t, &tmVal);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tmVal);
string = buf;
isNumber = false;
} else
throw coreutils::Exception("Expecting ) at end of NOW.");
}
else if(in.ifNextIgnoreCase("UUID")) {
// MySQL's uuid() - real content (SESSIONCTL.tag's own
// createsession definition: <set name="_uuid" expr="uuid()">)
// uses this to mint a new session id, stored as sessions.uuid and
// handed back to the browser as the _session_id cookie. A real
// random (v4) UUID, not MySQL's actual v1 (time+MAC-derived)
// algorithm - the schema only ever treats this as an opaque
// unique string, nothing depends on v1's specific bit layout.
// Seeded from std::random_device (real entropy), not RANDOM's
// existing clock()-seeded rand_r() above - this value doubles as
// a session token, so it needs to be unguessable, not just unique.
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for UUID.");
if(in.ifNext(")")) {
static thread_local std::mt19937_64 rng(std::random_device{}());
std::uniform_int_distribution<int> hexDigit(0, 15);
const char *hex = "0123456789abcdef";
char buf[37];
int p = 0;
for(int i = 0; i < 36; i++) {
if(i == 8 || i == 13 || i == 18 || i == 23) { buf[p++] = '-'; continue; }
int v;
if(i == 14) v = 4; // version 4
else if(i == 19) v = 8 + hexDigit(rng) % 4; // variant 10xx
else v = hexDigit(rng);
buf[p++] = hex[v];
}
buf[p] = '\0';
string = buf;
isNumber = false;
} else
throw coreutils::Exception("Expecting ) at end of UUID.");
}
else if(in.ifNextIgnoreCase("DATEDIFF")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for DATEDIFF.");
@@ -437,8 +612,19 @@ namespace jet {
string = std::format("{:.12f}", doubleValue);
isNumber = true;
} else if(in.ifNext("'")) {
string = in.getTokenExclude("'");
coreutils::MString raw = in.getTokenExclude("'");
in.ifNext("'");
coreutils::MString resolved;
raw.reset();
while(!raw.eod()) {
if(raw.startsWith("$[") || raw.startsWith("#["))
resolved.write(tag.getVariable(raw));
else {
resolved.write(raw.charAt(0));
raw.nextChar();
}
}
string = resolved;
isNumber = false;
} else
throw coreutils::Exception("operand is not valid.");
@@ -645,7 +831,19 @@ namespace jet {
throw coreutils::Exception("operand is not a number.");
} else
throw coreutils::Exception("operand is not a number.");
}
}
in.skipWhitespace();
if(in.ifNextIgnoreCase("and")) {
Operand op(in, tag);
boolean = boolean && op.boolean;
isNumber = false;
string = boolean ? "true" : "false";
} else if(in.ifNextIgnoreCase("or")) {
Operand op(in, tag);
boolean = boolean || op.boolean;
isNumber = false;
string = boolean ? "true" : "false";
}
return true;
}