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.
851 lines
30 KiB
C++
851 lines
30 KiB
C++
#include "Operand.h"
|
|
#include "Exception.h"
|
|
#include <format>
|
|
#include <iostream>
|
|
#include <algorithm>
|
|
#include <time.h>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <vector>
|
|
#include <random>
|
|
|
|
namespace jet {
|
|
|
|
Operand::Operand(coreutils::ZString &in, Tag &tag, bool stop) : in(in), tag(tag) {
|
|
|
|
doubleValue = 0;
|
|
|
|
in.skipWhitespace();
|
|
|
|
if(in.startsWith("$[") || in.startsWith("#[")) {
|
|
string = tag.getVariable(in);
|
|
doubleValue = string.asDouble();
|
|
isNumber = string.eod();
|
|
string.reset();
|
|
if((string == "false") || (string == "true"))
|
|
boolean = (string == "true");
|
|
} else if(in.ifNext("(")) {
|
|
Operand op(in, tag);
|
|
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.");
|
|
Operand parm1(in, tag);
|
|
if(!in.ifNext(","))
|
|
throw coreutils::Exception("Expecting , in SUBSTRING expression.");
|
|
Operand parm2(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.substring(parm2.string.asInteger());
|
|
} else {
|
|
// Real, pre-existing bug fixed here: the 3-arg-only branch below
|
|
// used to run unconditionally (missing braces around this
|
|
// else-if), so even after the 2-arg form above already
|
|
// succeeded, it fell through into trying to parse a third
|
|
// argument from whatever was left (nothing, since the closing
|
|
// ")" was already consumed) - always throwing "operand is not
|
|
// valid." right after computing the correct 2-arg result.
|
|
// Confirmed via real debug tracing showing an extra Operand
|
|
// constructed on empty input immediately after a successful
|
|
// 2-arg substring(str, start) call - the 2-arg form was
|
|
// completely unusable before this fix.
|
|
if(!in.ifNext(","))
|
|
throw coreutils::Exception("Expecting , in SUBSTRING expression.");
|
|
Operand parm3(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.substring(parm2.string.asInteger(), parm3.string.asInteger());
|
|
} 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.");
|
|
Operand parm1(in, tag);
|
|
if(!in.ifNext(","))
|
|
throw coreutils::Exception("Expecting , in LEFT expression.");
|
|
Operand parm2(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.substring(0, parm2.string.asInteger());
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of LEFT expression.");
|
|
} else if(in.ifNextIgnoreCase("EXPR")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for EXPR parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
Operand op(parm1.string, tag);
|
|
string = op.string;
|
|
isNumber = op.isNumber;
|
|
if(isNumber) {
|
|
doubleValue = op.doubleValue;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
}
|
|
boolean = op.boolean;
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of EXPR expression.");
|
|
} else if(in.ifNextIgnoreCase("RIGHT")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for RIGHT parameters.");
|
|
Operand parm1(in, tag);
|
|
if(!in.ifNext(","))
|
|
throw coreutils::Exception("Expecting , in RIGHT expression.");
|
|
Operand parm2(in, tag);
|
|
if(in.ifNext(")")) {
|
|
int len = parm1.string.getLength();
|
|
int size = parm2.string.asInteger();
|
|
int start = len - size;
|
|
string = parm1.string.substring(start, size);
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of RIGHT expression.");
|
|
} else if(in.ifNextIgnoreCase("TRIM")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for TRIM parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.trim();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of TRIM expression.");
|
|
} else if(in.ifNextIgnoreCase("TOUPPER")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for TOUPPER parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.toUpper();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of TOUPPER expression.");
|
|
} else if(in.ifNextIgnoreCase("TOLOWER")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for TOLOWER parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.toLower();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of TOLOWER expression.");
|
|
} else if(in.ifNextIgnoreCase("REVERSE")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for REVERSE parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
std::string reversed = parm1.string.str();
|
|
std::reverse(reversed.begin(), reversed.end());
|
|
string = reversed;
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of REVERSE expression.");
|
|
} else if(in.ifNextIgnoreCase("CONCAT")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for CONCAT parameters.");
|
|
Operand parm1(in, tag);
|
|
string = parm1.string;
|
|
while(in.ifNext(",")) {
|
|
Operand parm(in, tag);
|
|
string << parm.string;
|
|
}
|
|
if(!in.ifNext(")"))
|
|
throw coreutils::Exception("Expecting ) at end of CONCAT expression.");
|
|
} else if(in.ifNextIgnoreCase("INTEGER")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for INTEGER parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
string = parm1.string.asInteger();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of INTEGER expression.");
|
|
} else if(in.ifNextIgnoreCase("ROUND")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for ROUND parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = round(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of ROUND expression.");
|
|
} else if(in.ifNextIgnoreCase("RANDOM")) {
|
|
unsigned int seed = (unsigned int)clock();
|
|
doubleValue = (double) rand_r(&seed) / (RAND_MAX + 1.0);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else if(in.ifNextIgnoreCase("ABS")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for ABS parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = abs(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of ABS expression.");
|
|
} else if(in.ifNextIgnoreCase("MAX")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for MAX parameters.");
|
|
Operand *parm = new Operand(in, tag);
|
|
string = parm->string;
|
|
doubleValue = parm->doubleValue;
|
|
isNumber = parm->isNumber;
|
|
delete parm;
|
|
while(in.ifNext(",")) {
|
|
parm = new Operand(in, tag);
|
|
if(isNumber != parm->isNumber)
|
|
throw coreutils::Exception("All parameters in MAX expression must be same type.");
|
|
if(isNumber) {
|
|
if(parm->doubleValue > doubleValue) {
|
|
doubleValue = parm->doubleValue;
|
|
string = parm->string;
|
|
}
|
|
} else {
|
|
if(parm->string > string) {
|
|
doubleValue = parm->doubleValue;
|
|
string = parm->string;
|
|
}
|
|
}
|
|
delete parm;
|
|
}
|
|
if(!in.ifNext(")")) {
|
|
throw coreutils::Exception("Expecting ) at end of MAX expression.");
|
|
}
|
|
string.removeTrailingZeros();
|
|
} else if(in.ifNextIgnoreCase("MIN")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for MIN parameters.");
|
|
Operand *parm = new Operand(in, tag);
|
|
string = parm->string;
|
|
doubleValue = parm->doubleValue;
|
|
isNumber = parm->isNumber;
|
|
delete parm;
|
|
while(in.ifNext(",")) {
|
|
parm = new Operand(in, tag);
|
|
if(isNumber != parm->isNumber)
|
|
throw coreutils::Exception("All parameters in MIN expression must be same type.");
|
|
if(isNumber) {
|
|
if(parm->doubleValue < doubleValue) {
|
|
doubleValue = parm->doubleValue;
|
|
string = parm->string;
|
|
}
|
|
} else {
|
|
if(parm->string < string) {
|
|
doubleValue = parm->doubleValue;
|
|
string = parm->string;
|
|
}
|
|
}
|
|
delete parm;
|
|
}
|
|
if(!in.ifNext(")")) {
|
|
throw coreutils::Exception("Expecting ) at end of MIN expression.");
|
|
}
|
|
string.removeTrailingZeros();
|
|
} else if(in.ifNextIgnoreCase("POW")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for POW parameters.");
|
|
Operand parm1(in, tag);
|
|
if(!in.ifNext(","))
|
|
throw coreutils::Exception("Expecting , in POW expression.");
|
|
Operand parm2(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = pow(parm1.doubleValue, parm2.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of substring expression.");
|
|
} else if(in.ifNextIgnoreCase("SIN")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for SIN parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = sin(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of SIN expression.");
|
|
} else if(in.ifNextIgnoreCase("ASIN")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for ASIN parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = asin(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of ASIN expression.");
|
|
} else if(in.ifNextIgnoreCase("COS")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for COS parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = cos(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of COS expression.");
|
|
} else if(in.ifNextIgnoreCase("ACOS")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for ACOS parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = acos(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of ACOS expression.");
|
|
} else if(in.ifNextIgnoreCase("ATAN")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for ATAN parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = atan(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of ATAN expression.");
|
|
} else if(in.ifNextIgnoreCase("SQRT")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for SQRT parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = sqrt(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of SQRT expression.");
|
|
} else if(in.ifNextIgnoreCase("DEG")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for DEG parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = parm1.doubleValue * 180 / 3.14159;
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of DEG expression.");
|
|
} else if(in.ifNextIgnoreCase("RAD")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for RAD parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = parm1.doubleValue * 3.14159 / 180;
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of RAD expression.");
|
|
} else if(in.ifNextIgnoreCase("TAN")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for TAN parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = tan(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of TAN expression.");
|
|
} else if(in.ifNextIgnoreCase("TRUNC")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for TRUNC parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = trunc(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of TRUNC expression.");
|
|
} else if(in.ifNextIgnoreCase("CEIL")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for CEIL parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = ceil(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of CEIL expression.");
|
|
} else if(in.ifNextIgnoreCase("FLOOR")) {
|
|
if(!in.ifNext("("))
|
|
throw coreutils::Exception("Expecting ( for FLOOR parameters.");
|
|
Operand parm1(in, tag);
|
|
if(in.ifNext(")")) {
|
|
doubleValue = floor(parm1.doubleValue);
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of FLOOR expression.");
|
|
} 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(")")) {
|
|
auto now = std::chrono::system_clock::now();
|
|
auto duration_since_epoch = now.time_since_epoch();
|
|
auto seconds_since_epoch = std::chrono::duration_cast<std::chrono::seconds>(duration_since_epoch);
|
|
int unixtime = seconds_since_epoch.count();
|
|
string << unixtime;
|
|
} 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.");
|
|
Operand parm1(in, tag);
|
|
if(!in.ifNext(","))
|
|
throw coreutils::Exception("Expecting , in DATEDIFF expression.");
|
|
Operand parm2(in, tag);
|
|
if(in.ifNext(")")) {
|
|
// Matches MySQL's DATEDIFF(expr1, expr2): expr1 - expr2 expressed
|
|
// as a whole number of days, ignoring any time-of-day component.
|
|
// Dates are parsed as YYYY-MM-DD (a leading date portion of a
|
|
// longer "YYYY-MM-DD HH:MM:SS" value parses fine too, since
|
|
// strptime only consumes what the format string asks for) and
|
|
// interpreted as UTC via timegm() (not mktime(), which is
|
|
// local-timezone-dependent) so the result doesn't shift with the
|
|
// server's own timezone setting.
|
|
struct tm tm1 = {};
|
|
struct tm tm2 = {};
|
|
if(!strptime(parm1.string.c_str(), "%Y-%m-%d", &tm1))
|
|
throw coreutils::Exception("DATEDIFF: first argument is not a valid date (expected YYYY-MM-DD).");
|
|
if(!strptime(parm2.string.c_str(), "%Y-%m-%d", &tm2))
|
|
throw coreutils::Exception("DATEDIFF: second argument is not a valid date (expected YYYY-MM-DD).");
|
|
time_t t1 = timegm(&tm1);
|
|
time_t t2 = timegm(&tm2);
|
|
doubleValue = difftime(t1, t2) / 86400.0;
|
|
isNumber = true;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("Expecting ) at end of DATEDIFF expression.");
|
|
} else if(in.ifNextIgnoreCase("true")) {
|
|
boolean = true;
|
|
string = "true";
|
|
} else if(in.ifNextIgnoreCase("false")) {
|
|
boolean = false;
|
|
string = "false";
|
|
} else if(in.startsWithNumber()) {
|
|
doubleValue = in.asDouble();
|
|
string = std::format("{:.12f}", doubleValue);
|
|
isNumber = true;
|
|
} else if(in.ifNext("'")) {
|
|
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.");
|
|
|
|
in.skipWhitespace();
|
|
|
|
if(!stop)
|
|
parseOperator();
|
|
|
|
return;
|
|
}
|
|
|
|
int Operand::parseOperator() {
|
|
|
|
if(in.ifNext("!=") || in.ifNext("<>")) {
|
|
Operand op(in, tag);
|
|
if(isNumber && op.isNumber) {
|
|
if(doubleValue != op.doubleValue) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
} else if(!isNumber && !op.isNumber) {
|
|
if(string != op.string) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
}
|
|
}
|
|
if(in.ifNext("<=")) {
|
|
Operand op(in, tag);
|
|
if(isNumber && op.isNumber) {
|
|
if(doubleValue <= op.doubleValue) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
} else if(!isNumber && !op.isNumber) {
|
|
if(string <= op.string) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
}
|
|
}
|
|
if(in.ifNext(">=")) {
|
|
Operand op(in, tag);
|
|
if(isNumber && op.isNumber) {
|
|
if(doubleValue >= op.doubleValue) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
} else if(!isNumber && !op.isNumber) {
|
|
if(string >= op.string) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
}
|
|
}
|
|
if(in.ifNext("=")) {
|
|
Operand op(in, tag);
|
|
if(isNumber && op.isNumber) {
|
|
if(doubleValue == op.doubleValue) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
} else if(!isNumber && !op.isNumber) {
|
|
if(string == op.string) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
}
|
|
}
|
|
if(in.ifNext("<")) {
|
|
Operand op(in, tag);
|
|
if(isNumber && op.isNumber) {
|
|
if(doubleValue < op.doubleValue) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
} else if(!isNumber && !op.isNumber) {
|
|
if(string < op.string) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
}
|
|
}
|
|
if(in.ifNext(">")) {
|
|
Operand op(in, tag);
|
|
if(isNumber && op.isNumber) {
|
|
if(doubleValue > op.doubleValue) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
} else if(!isNumber && !op.isNumber) {
|
|
if(string > op.string) {
|
|
boolean = true;
|
|
isNumber = false;
|
|
string = "true";
|
|
} else {
|
|
boolean = false;
|
|
isNumber = false;
|
|
string = "false";
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if(in.ifNext("+")) {
|
|
if(isNumber) {
|
|
Operand op(in, tag);
|
|
if(op.isNumber) {
|
|
doubleValue += op.doubleValue;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("operand is not a number.");
|
|
} else
|
|
throw coreutils::Exception("operand is not a number.");
|
|
} else if(in.ifNext("-")) {
|
|
if(isNumber) {
|
|
Operand op(in, tag);
|
|
if(op.isNumber) {
|
|
doubleValue -= op.doubleValue;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
} else
|
|
throw coreutils::Exception("operand is not a number.");
|
|
} else
|
|
throw coreutils::Exception("operand is not a number.");
|
|
} else if(in.ifNext("*")) {
|
|
if(isNumber) {
|
|
Operand op(in, tag, true);
|
|
if(op.isNumber) {
|
|
doubleValue *= op.doubleValue;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
parseOperator();
|
|
} else
|
|
throw coreutils::Exception("operand is not a number.");
|
|
} else
|
|
throw coreutils::Exception("operand is not a number.");
|
|
} else if(in.ifNext("/")) {
|
|
if(isNumber) {
|
|
Operand op(in, tag, true);
|
|
if(op.isNumber) {
|
|
doubleValue /= op.doubleValue;
|
|
string = std::format("{:.12f}", doubleValue);
|
|
string.removeTrailingZeros();
|
|
parseOperator();
|
|
} else
|
|
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;
|
|
}
|
|
|
|
}
|