diff --git a/Global.cpp b/Global.cpp index 5b4e82c..aaea293 100644 --- a/Global.cpp +++ b/Global.cpp @@ -1,8 +1,10 @@ #include "Global.h" #include "Exception.h" #include "__mysql.h" +#include "__imap.h" #include #include +#include namespace jet { @@ -44,6 +46,26 @@ namespace jet { return sessions[sessionId]; } + bool Global::imapSessionExists(coreutils::MString sessionId) { + return imapSessions.find(sessionId) != imapSessions.end(); + } + + void Global::addImapSession(coreutils::MString sessionId, __imap *imap) { + if(imapSessionExists(sessionId)) + throw coreutils::Exception("imap sessionid already exists."); + imapSessions[sessionId] = imap; + } + + void Global::removeImapSession(coreutils::MString sessionId) { + imapSessions.erase(sessionId); + } + + __imap * Global::getImapSession(coreutils::MString sessionId) { + if(imapSessions.find(sessionId) == imapSessions.end()) + throw coreutils::Exception("requested imap session is not available."); + return imapSessions[sessionId]; + } + coreutils::ZString Global::getSessionVariable(coreutils::MString &splitName) { if(sessions.find(splitName[0]) == sessions.end()) throw coreutils::Exception("requested session is not available in variable."); diff --git a/Global.h b/Global.h index e61c41a..0eff2f4 100644 --- a/Global.h +++ b/Global.h @@ -11,7 +11,9 @@ namespace jet { class __mysql; - + class __imap; + class Tag; + class Global { public: @@ -23,6 +25,10 @@ namespace jet { void addSession(coreutils::MString sessionId, __mysql *mysql); void removeSession(coreutils::MString sessionId); __mysql * getSession(coreutils::MString sessionId); + bool imapSessionExists(coreutils::MString sessionId); + void addImapSession(coreutils::MString sessionId, __imap *imap); + void removeImapSession(coreutils::MString sessionId); + __imap * getImapSession(coreutils::MString sessionId); coreutils::ZString getSessionVariable(coreutils::MString &splitName); void outputHeaders(); void setupFormData(coreutils::ZString &formdata); @@ -55,10 +61,32 @@ namespace jet { std::map sessions; std::map headers; std::map tags; + + // Populated once per compiled .jtc module's own entry function (see + // jetc's TAGDEF codegen) - a top-level definition + // that jetc could compile directly registers its own render + // function here under its lowercase name, same key convention as + // `tags` above. __dotag checks this first before falling back to + // the text-based findTagDefinition()/processContainer() path (see + // __dotag.cpp), avoiding a full re-parse of the definition's body + // on every invocation. A miss here (a locally-scoped/nested tag + // definition, or content that was never compiled at all) is always + // safe - it just means the existing interpreted path runs exactly + // as it always has; this is a pure additive fast path, never a + // replacement for `tags`, which every TAGDEF still populates too. + std::map compiledTags; + char **envp; bool cgi = false; bool session = false; coreutils::MString sessionId; + + // Deliberately the LAST member declared in Global - appended + // here, after every pre-existing field, rather than inserted + // earlier among them, so this addition does not shift the + // memory offset of any field that existed before it. See + // __imap.h for what registers here. + std::map imapSessions; }; diff --git a/Operand.cpp b/Operand.cpp index db245c2..77c7edd 100644 --- a/Operand.cpp +++ b/Operand.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include 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 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 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 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 ) + // 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: , + // 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: ) + // 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 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; } diff --git a/Tag.cpp b/Tag.cpp index e0ca433..2e9c2ef 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -1,10 +1,20 @@ #include "Tag.h" +#include +#include +#include #include "Exception.h" #include "Global.h" #include "__mysql.h" #include "__sql.h" +#include "__imap.h" +#include "__imaplist.h" +#include "__imapmessage.h" +#include "__imapflag.h" +#include "__imapdelete.h" #include "__whilerow.h" #include "__comment.h" +#include "__eval.h" +#include #include "__exclude.h" #include "__for.h" #include "__if.h" @@ -50,7 +60,66 @@ namespace jet { while(!finished) { in.skipWhitespace(); if(in.ifNext(">")) { - hasContainer = true; + // jet-1.4's real parser knew which built-ins never take + // a body (its own "void element" list) - real 1.4 + // content (SESSIONCTL.tag, config.cfg, index.cgi) is + // written with a bare closing '>' and no explicit + // for these, which a plain "bare '>' always means + // scan for a body" rule can't tell apart from a real + // container tag that happens to be immediately followed + // by more content. Kept in sync with jetc's own + // JetParser.cpp::isVoidTag - same list, same reasoning, + // this is the runtime-interpreted-fallback-path + // equivalent of that compile-time fix. + coreutils::MString nameLower = coreutils::MString(name).toLower(); + // exclude was wrongly listed here - __exclude.cpp's own + // constructor explicitly throws "exclude must have a + // container" if hasContainer is false, so it always + // needs a real body (e.g. mainpagetemplate.temp's own + // </exclude>) - it was never actually + // void, this was a miscategorization. + bool isVoid = (nameLower == "set") || (nameLower == "include") || + (nameLower == "cookie") || (nameLower == "header") || + (nameLower == "read") || (nameLower == "write") || + (nameLower == "call") || (nameLower == "system") || + (nameLower == "dump"); + // A CUSTOM tag (not one of jet's own built-ins above) + // written bare, with no explicit close, is genuinely + // ambiguous: is a real body coming (scan for + // </thisname>), or is this self-closing and whatever + // follows is unrelated sibling content? Real 1.4 + // content writes multiple bare invocations of the SAME + // custom tag back-to-back with no close at all + // (login.page's own two <field .../> entries, no + // </field> anywhere) - scanning for a body in that case + // doesn't just fail to find one, it finds the NEXT + // sibling invocation of the same tag name via ifNested + // (which recognizes it as "another <field>", recurses + // into IT looking for ITS OWN close, and so on) and + // silently swallows everything through EOF as this + // tag's own unused body - since a tag whose own + // definition never references <container> never reads + // that captured text anyway, the result is silent, + // undetectable content loss, confirmed live: the second + // <field>, <buttonbay>, and the login button all + // vanished from real rendered output with zero errors. + // Resolve the ambiguity the same way the built-in list + // above already does: if this tag's own DEFINITION + // never references a bare <container> (so it could + // never use captured body text even if it had one), + // treat it as void too - genuinely no way to lose + // content that would never have been read regardless. + if(!isVoid) { + coreutils::ZString customDef; + if(findTagDefinition(name, customDef)) { + coreutils::MString defLower(customDef); + defLower.reset(); + defLower = defLower.toLower(); + defLower.reset(); + isVoid = (defLower.find(coreutils::MString("<container")) == -1); + } + } + hasContainer = !isVoid; hasContainer2 = false; finished = true; break; @@ -61,14 +130,52 @@ namespace jet { break; } if(!finished) { - coreutils::ZString keywordName = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); - if(in.ifNext("=\"")) { - if(keywords.count(keywordName) == 0) - keywords[keywordName] = in.getTokenExclude("\""); - else - throw coreutils::Exception("keyword name must be unique for tag."); - } - if(!in.ifNext("\"")) {} + // Every keyword lookup site in this codebase (keywordDefined("name"), + // resolveKeyword("host"), etc.) uses a lowercase literal - real + // 1.4 content writes attribute NAMES in any case too (e.g. + // SESSIONCTL.tag's <TAG NAME="ABUTTON" ...>), so the key has to + // be normalized to lowercase here, at the point of storage, or + // every lookup against it silently misses (keywordDefined("name") + // doesn't find "NAME") rather than erroring clearly. + coreutils::MString keywordName = coreutils::MString(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")).toLower(); + // Real 1.4 content quotes attribute values with either " or ' + // (SESSIONCTL.tag's own <tag name='sessionctl' keywords="page + // stylesheet"> uses both in the same tag) - accept either, + // closing with whichever character opened it. + bool dq = in.ifNext("=\""); + bool sq = !dq && in.ifNext("='"); + if(dq || sq) { + if(keywords.count(keywordName) == 0) + keywords[keywordName] = in.getTokenExclude(dq ? "\"" : "'"); + else + throw coreutils::Exception("keyword name must be unique for tag."); + in.ifNext(dq ? "\"" : "'"); + } else if(keywordName.getLength() > 0) { + // Unquoted attribute value (e.g. TYPE=NE, SCOPE=LOCAL - real + // 1.4 content throughout). Neither quote style matched, but + // getTokenInclude() above already consumed the attribute NAME + // and left the cursor sitting exactly on '=' - without also + // consuming '=' and the unquoted value here, the next + // iteration's getTokenInclude() call returns empty (cursor is + // still at '=', not a name character) and this loops forever + // with zero forward progress. Same bug class already found and + // fixed in ifNested()'s own copy of this logic - this is + // Tag::Tag's main constructor's separate, parallel copy. + if(in.ifNext("=")) { + coreutils::MString value; + while(!in.eod()) { + char c = in.charAt(0); + if(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '>' || c == '/') + break; + value.write(c); + in.nextChar(); + } + if(keywords.count(keywordName) == 0) + keywords[keywordName] = value; + else + throw coreutils::Exception("keyword name must be unique for tag."); + } + } } } if(keywordDefined("filterblanklines")) { @@ -105,7 +212,7 @@ namespace jet { // from splitTagName itself so it works for any split tag. coreutils::MString splitOpenTag; splitOpenTag << "<" << splitTagName << ">"; - in.ifNext(splitOpenTag); + in.ifNextIgnoreCase(splitOpenTag); start = in.getCursor(); } else if(ifNested(in)) { } else @@ -134,7 +241,53 @@ namespace jet { copyContainer(container, parentOut); } + // Tracks (scope, keyword-name) pairs whose resolution is currently + // in progress, shared between resolveKeyword (populates it around its + // own body) and getVariable's #[...] parent-chain walk (consults it). + // Real content passes a keyword straight through under the SAME name + // to a nested invocation that also declares a keyword by that name - + // e.g. barant.tag's own <linkbutton page="#[page]" ...> (linkbutton + // itself declares "page" as one of its own keywords too, coincidentally + // the same name as the enclosing tag's own "page" being forwarded). + // #[page] here means "my CALLER's page value", but by the time it's + // lazily resolved (raw text is stored unresolved at attribute-parse + // time, only interpreted later when actually read), there's no record + // of which scope originally wrote it - a naive parent-chain walk + // restarts from `local` every time and immediately re-finds THIS SAME + // tag's own "page" keyword (the one currently being resolved) instead + // of continuing outward to the real caller, recursing forever. Once a + // (scope, name) pair is already being resolved, the walk below skips + // that scope for that name and continues to scope->parent instead of + // recursing back into it. + static thread_local std::set<std::pair<Tag*, coreutils::MString>> resolvingKeywords; + coreutils::MString Tag::resolveKeyword(coreutils::ZString keyword) { + // Defensive recursion guard, kept as a backstop for any OTHER + // self-referential chain the (scope, keyword) tracking above + // doesn't happen to cover (e.g. a longer, indirect cycle through + // several different keyword names) - crashing the whole process + // via stack overflow is never acceptable, even for a case not + // specifically identified yet. + static thread_local int resolveDepth = 0; + if(resolveDepth > 100) + throw coreutils::Exception("keyword resolution recursion limit exceeded - likely a self-referential #[name]/$[name] value."); + struct DepthGuard { + int &d; + DepthGuard(int &d) : d(d) { ++d; } + ~DepthGuard() { --d; } + } depthGuard(resolveDepth); + coreutils::MString keywordNameCopy(keyword); + std::pair<Tag*, coreutils::MString> resolvingKey(this, keywordNameCopy); + bool alreadyTracked = resolvingKeywords.count(resolvingKey) > 0; + if(!alreadyTracked) + resolvingKeywords.insert(resolvingKey); + struct ResolvingGuard { + std::set<std::pair<Tag*, coreutils::MString>> &s; + std::pair<Tag*, coreutils::MString> k; + bool owns; + ResolvingGuard(std::set<std::pair<Tag*, coreutils::MString>> &s, std::pair<Tag*, coreutils::MString> k, bool owns) : s(s), k(k), owns(owns) {} + ~ResolvingGuard() { if(owns) s.erase(k); } + } resolvingGuard(resolvingKeywords, resolvingKey, !alreadyTracked); coreutils::MString resolved; keywords[keyword].reset(); while(!keywords[keyword].eod()) { @@ -154,8 +307,123 @@ namespace jet { parseContainer(container, out, container2, topLevel); } + // Extracted, unchanged, from parseContainer's own "container" dispatch + // branch below - see that branch's original comment (preserved there) + // and Tag.h's own doc comment for the full explanation of the + // scope-walk/recursion-guard behavior this preserves exactly. The + // hasContainer/evaluate gate matches processContainer's own gate + // (both call sites this replaced went through processContainer, + // which silently no-ops unless both are true) - collapsed to one + // check here since it's the same guard either branch below would + // have applied individually. + void Tag::renderContainer(coreutils::MString &out, coreutils::ZString container2) { + if(!(hasContainer && evaluate)) + return; + static thread_local std::set<Tag*> resolvingContainerScopes; + Tag *containerScope = local; + while(containerScope && + (!containerScope->isCustomTagBoundary || + resolvingContainerScopes.count(containerScope) > 0)) + containerScope = containerScope->parent; + if(containerScope != NULL) { + resolvingContainerScopes.insert(containerScope); + try { + // ZString tracks its own read cursor (see ZString.h's own + // reset() doc comment) - containerScope->container is a + // persistent member reused across EVERY <container> reference + // within the same custom tag invocation, not a fresh value per + // call. Without resetting it first, a SECOND <container> + // reference in one invocation (e.g. SESSIONCTL.tag's own + // setenv definition: <eval name="_envvalue" scope="local"> + // <container></eval> followed later by + // <eval name="#[name]"><container></eval>) parses an + // already-exhausted cursor and silently renders empty, even + // though the first reference worked correctly - confirmed via + // a real repro: setenv's own second <container> use wrote "" + // instead of the caller's actual value, so every + // $[_login_id]-style read after a successful login stayed + // empty. resolveKeyword() already resets its own equivalent + // cursor (keywords[keyword]) both before and after for the + // same reason - this mirrors that. + containerScope->container.reset(); + parseContainer(containerScope->container, out, NULL, false); + } catch(...) { + resolvingContainerScopes.erase(containerScope); + throw; + } + resolvingContainerScopes.erase(containerScope); + } else { + container2.reset(); + parseContainer(container2, out, NULL, false); + } + } + + namespace { + struct CachedTagDef { + coreutils::MString name; + coreutils::MString body; + }; + // Recognizes the exact pattern jetc's own codegen emits for every + // top-level <tag name="X">...</tag> definition it doesn't itself + // specially compile (everything except set/if/for/while - see + // JetCodegen.cpp): one interpretSpan(span, out) call per + // definition, re-run from scratch on every single request even + // though the definition text (SESSIONCTL.tag/barant.tag/ + // project.tag, spliced in at compile time via <include>) never + // changes between requests once compiled - confirmed live, a real + // page's compiled main.compiled.cpp calls this 15+ times per + // request, one of them (the whole <tag name='sessionctl'> block, + // every nested tag definition inside it included) close to 5000 + // characters, all fully re-scanned through the generic runtime Tag + // dispatch every time. Keyed by the span's own raw data pointer, + // not its content - safe because jetc emits these spans as C + // string literals (static storage duration, the same address for + // the whole life of the process), so pointer identity alone means + // "the exact same compiled span, already seen once on this + // thread" with no need to hash or compare bytes. + thread_local std::unordered_map<const char*, CachedTagDef> tagDefCache; + } + void Tag::interpretSpan(coreutils::ZString span, coreutils::MString &out) { + const char *key = span.getData(); + auto cached = tagDefCache.find(key); + if(cached != tagDefCache.end()) { + global.tags[cached->second.name] = cached->second.body; + return; + } + + // Only the fast path above is new behavior - everything below + // this point, for a cache MISS, is the exact same real interpreter + // call as before. The snapshot/diff afterward is purely for + // deciding whether THIS span is safe to cache for next time - it + // never changes what actually happens on this (first, per-thread) + // call. + std::set<coreutils::MString> tagKeysBefore; + for(auto &entry : global.tags) + tagKeysBefore.insert(entry.first); + size_t outLenBefore = out.getLength(); + parseContainer(span, out); + + // Only cache spans whose entire observable effect was registering + // EXACTLY one new top-level tag definition, with nothing written + // to `out` - the precise signature of a bare <tag name="X">... + // </tag> definition block (evaluate=false/output=false in + // __tag.cpp - see its own doc comment). Real page content, + // multiple registrations in one span, or a registration that + // landed in some scope's own localTags instead of global.tags + // (nested inside another custom tag, not the top-level case this + // targets) are all left running the real interpreter every time - + // the safe default, never caching something that isn't provably + // pure and static. + if(out.getLength() == outLenBefore && global.tags.size() == tagKeysBefore.size() + 1) { + for(auto &entry : global.tags) { + if(tagKeysBefore.find(entry.first) == tagKeysBefore.end()) { + tagDefCache[key] = CachedTagDef{entry.first, entry.second}; + break; + } + } + } } void Tag::parseContainer(coreutils::ZString &in, coreutils::MString &out, coreutils::ZString container2, bool topLevel) { @@ -175,7 +443,12 @@ namespace jet { out.write("<|"); continue; } else if(in.startsWith("<")) { - if(ifTagName(in, "mysql")) { + if(ifTagName(in, "mysql") || ifTagName(in, "database")) { + // <database> is jet-1.4's name for this same tag (real 1.4 + // content - SESSIONCTL.tag's own <sessionctl> definition, + // index.cgi itself - uses <database name="..."> throughout, + // never <mysql>) - recognized as a real alias rather than + // requiring that content to be rewritten. __mysql _mysql(in, out, global, this, local); continue; } else if(ifTagName(in, "comment")) { @@ -184,6 +457,21 @@ namespace jet { } else if(ifTagName(in, "sql")) { __sql _sql(in, out, global, this, local); continue; + } else if(ifTagName(in, "imap")) { + __imap _imap(in, out, global, this, local); + continue; + } else if(ifTagName(in, "imaplist")) { + __imaplist _imaplist(in, out, global, this, local); + continue; + } else if(ifTagName(in, "imapmessage")) { + __imapmessage _imapmessage(in, out, global, this, local); + continue; + } else if(ifTagName(in, "imapflag")) { + __imapflag _imapflag(in, out, global, this, local); + continue; + } else if(ifTagName(in, "imapdelete")) { + __imapdelete _imapdelete(in, out, global, this, local); + continue; } else if(ifTagName(in, "whilerow")) { __whilerow _whilerow(in, out, global, this, local); continue; @@ -214,6 +502,9 @@ namespace jet { } else if(ifTagName(in, "set")) { __set _set(in, out, global, this, local); continue; + } else if(ifTagName(in, "eval")) { + __eval _eval(in, out, global, this, local); + continue; } else if(ifTagName(in, "call")) { __call _call(in, out, global, this, local); continue; @@ -251,10 +542,46 @@ namespace jet { __dotag _dotag(in, out, global, this, local); continue; } else if(ifTagName(in, "container")) { - processContainer(container2); - in.ifNext("<container"); - in.skipWhitespace(); - in.ifNext("/>"); + // Rendering logic lives in renderContainer (see Tag.h/below) - + // extracted, unchanged, so generated code for a compiled + // <tag name="X"> definition body (see jetc's TAGDEF handling) + // can call it directly too, without re-deriving the same + // scope-walking/recursion-guard logic. + renderContainer(out, container2); + // The old consumption here - in.ifNext("<container"); + // in.skipWhitespace(); in.ifNext("/>"); - only ever handled + // a bare, lowercase, attribute-free "<container/>". Real + // 1.4 content writes this several other ways: uppercase + // (SESSIONCTL.tag's own <CONTAINER EVAL="NO">), with + // attributes, and closed with a bare ">" rather than "/>" - + // ifTagName(in, "container") above already matches all of + // these case-insensitively for DISPATCH, but the hardcoded + // lowercase "<container" literal consumption step then + // failed outright on any of them, consuming nothing at + // all and leaving the cursor exactly where it started - + // the dispatch loop re-entered this same branch forever, + // re-running processContainer(container2) with zero + // forward progress. Confirmed live via a loop-iteration + // guard showing "preview=[<CONTAINER EVAL=\"NO\">]" + // unchanged across tens of thousands of iterations. + // Consume properly instead: skip '<', the tag name itself + // (whatever case it was written in), then scan to the + // closing '>' - respecting quoted attribute values so a + // '>' inside one doesn't end the tag early - regardless of + // whether it was self-closed with '/>' or left as a bare + // '>' (a self-closing '/' is just consumed as an ordinary + // character by the scan below either way). + in.ifNext("<"); + in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + while(!in.eod() && !in.ifNext(">")) { + if(in.ifNext("\"")) { + while(!in.eod() && !in.ifNext("\"")) in.nextChar(); + } else if(in.ifNext("'")) { + while(!in.eod() && !in.ifNext("'")) in.nextChar(); + } else { + in.nextChar(); + } + } continue; } else { out.write(in.nextChar()); @@ -279,46 +606,61 @@ namespace jet { } bool Tag::ifTagName(coreutils::ZString &in, const char *tag) { + // Case-insensitive - real 1.4 content invokes built-ins in every + // case (<MYSQL>, <Sql>, <DATABASE>, ...), and this is the dispatch + // that decides which __xxx class handles a tag at all, so a + // case-sensitive miss here doesn't throw, it just silently treats + // the tag as unrecognized fallback content instead. in.push(); - if(in.ifNext("<")) - if(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_").equals(tag)) { - if(in.ifNext(" ")) { - in.pop(); - return true; - } else if(in.ifNext("/>")) { - in.pop(); - return true; - } else if(in.ifNext(">")) { - in.pop(); - return true; - } - } + if(in.ifNext("<")) { + coreutils::MString token = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + if(token.toLower() == coreutils::MString(tag).toLower()) { + if(in.ifNext(" ")) { + in.pop(); + return true; + } else if(in.ifNext("/>")) { + in.pop(); + return true; + } else if(in.ifNext(">")) { + in.pop(); + return true; + } + } + } in.pop(); return false; } bool Tag::ifTagName(coreutils::ZString &in) { + // Case-insensitive - same reasoning as ifEndTagName above. in.push(); - if(in.ifNext("<")) - if(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_").equals(name)) { - in.push(); - if(in.ifNext(" ")) { - in.pop(); - return true; - } else if(in.ifNext("/>")) { - in.pop(); - return true; - } else if(in.ifNext(">")) { - in.pop(); - return true; - } - in.pop(); - } + if(in.ifNext("<")) { + coreutils::MString token = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + if(token.toLower() == coreutils::MString(name).toLower()) { + in.push(); + if(in.ifNext(" ")) { + in.pop(); + return true; + } else if(in.ifNext("/>")) { + in.pop(); + return true; + } else if(in.ifNext(">")) { + in.pop(); + return true; + } + in.pop(); + } + } in.pop(); return false; } bool Tag::findTagDefinition(coreutils::MString name, coreutils::ZString &result) { + // Case-insensitive, matching __tag.cpp's own registration fix - + // real 1.4 content declares and invokes a custom tag in different + // case at different sites (SESSIONCTL.tag's <TAG NAME='TEMPLATE'> + // vs mainpagetemplate.temp's <template>...</template>). + name = name.toLower(); Tag *scope = this; while(scope) { if(scope->isCustomTagBoundary && scope->localTags.count(name)) { @@ -358,12 +700,17 @@ namespace jet { } bool Tag::ifEndTagName(coreutils::ZString &in) { + // Case-insensitive, matching jetc's own compile-time matchTagClose + // fix - real 1.4 content closes tags in whatever case it opened + // them with (e.g. SESSIONCTL.tag's <tag name="linkbutton"> ... </TAG>). in.push(); - if(in.ifNext("</")) - if(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_").equals(name)) { - if(in.ifNext(">")) - return true; - } + if(in.ifNext("</")) { + coreutils::MString token = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + if(token.toLower() == coreutils::MString(name).toLower()) { + if(in.ifNext(">")) + return true; + } + } in.pop(); return false; } @@ -371,7 +718,7 @@ namespace jet { bool Tag::ifSplitTagName(coreutils::ZString &in) { in.push(); if(in.ifNext("<")) - if(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_").equals(splitTagName)) { + if(coreutils::MString(in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")).toLower() == coreutils::MString(splitTagName).toLower()) { if(in.ifNext(">")) in.pop(); return true; @@ -412,6 +759,17 @@ namespace jet { return keywords.find(keyword) != keywords.end(); } + namespace { + // Every built-in tag that uses splitTagName "else" - shared by + // ifNested(in)'s own cross-name fallback below and the new + // ifNested(in,tagName)/scanContainerNamed overloads. <try>'s own + // splitTagName is "catch", a different marker - deliberately not + // folded into this fix (no repro of the same bug shape there, and + // guessing its exact semantics without one risks getting it wrong). + const char *elseSplitTagNames[] = { "if", "while", "until", "ifrow" }; + const int elseSplitTagNameCount = 4; + } + bool Tag::ifNested(coreutils::ZString &in) { bool hasContainer = false; if(ifTagName(in)) { @@ -424,22 +782,152 @@ namespace jet { hasContainer = false; return true; } else if(in.getTokenExclude("=").getLength() != 0) { - if(in.ifNext("=\"")) { + // Real 1.4 content quotes attribute values with either " or + // ' (e.g. <TAG NAME='TEMPLATE' SCOPE=LOCAL> nested inside + // SESSIONCTL.tag's own <sessionctl> definition) - accept + // either, closing with whichever character opened it. + bool dq = in.ifNext("=\""); + bool sq = !dq && in.ifNext("='"); + if(dq || sq) { while(1) { - if(in.ifNext("\"")) { + if(in.ifNext(dq ? "\"" : "'")) { break; } in.nextChar(); } + } else { + // Unquoted attribute value (e.g. TYPE=NE, SCOPE=LOCAL - + // real 1.4 content throughout, including SESSIONCTL.tag's + // own <TAG NAME='TEMPLATE' SCOPE=LOCAL>). Neither quote + // style matched, but the getTokenExclude("=") above + // already consumed the attribute NAME and left the cursor + // sitting exactly on '=' - without also consuming '=' and + // the unquoted value here, the next iteration's + // getTokenExclude("=") returns empty (cursor is already + // AT '=') and this loops forever with zero forward + // progress. Confirmed live via gdb: three snapshots over + // 15s showed IDENTICAL stack depth and cursor position, + // not a slow-but-progressing recursion. + in.nextChar(); // consume '=' + while(!in.eod()) { + char c = in.charAt(0); + if(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '>' || c == '/') + break; + in.nextChar(); + } } } } if(hasContainer) scanContainer(in); + return false; + } + // Same-name match failed - if THIS tag itself is one of the + // splitTagName="else" conditionals (if/while/until/ifrow), a + // DIFFERENTLY-named one of that same set nested here (e.g. an + // <if> nested directly inside an <ifrow>'s own body) needs to be + // recognized and skipped too, exactly like a same-name nested + // occurrence already is above - otherwise the inner tag's own + // <else>/</...> markers get read as if they belonged to THIS + // (outer) tag's own boundary scan. Confirmed via a real, minimal + // repro: <ifrow>...<if expr="..."><else>...</if>...<else>... + // </ifrow> - the outer <ifrow> incorrectly split on the INNER + // <if>'s own <else>, and the inner <if>'s real closing </if> then + // leaked into the rendered output as literal text (found while + // investigating a real "successful login silently doesn't + // proceed" report - login.method's own <ifrow><if expr=...>... + // </if></ifrow> structure hits this exactly). + if(splitTagName == "else") { + for(int i = 0; i < elseSplitTagNameCount; i++) { + if(ifNested(in, elseSplitTagNames[i])) + break; + } } return false; } + // See Tag.h's own doc comment on this overload set. Mirrors + // ifNested(in) above exactly, but checks an explicit tagName (via + // ifTagName(in,tagName)) instead of this->name, and skips the + // nested tag's own body (if any) via scanContainerNamed(in,tagName) + // instead of scanContainer(in) - so it correctly stops at THAT + // tag's own close, not this outer tag's. + bool Tag::ifNested(coreutils::ZString &in, const char *tagName) { + bool hasContainer = false; + if(ifTagName(in, tagName)) { + while(!in.eod()) { + in.skipWhitespace(); + if(in.ifNext(">")) { + hasContainer = true; + break; + } else if(in.ifNext("/>")) { + hasContainer = false; + return true; + } else if(in.getTokenExclude("=").getLength() != 0) { + bool dq = in.ifNext("=\""); + bool sq = !dq && in.ifNext("='"); + if(dq || sq) { + while(1) { + if(in.ifNext(dq ? "\"" : "'")) { + break; + } + in.nextChar(); + } + } else { + in.nextChar(); // consume '=' - see ifNested(in)'s own comment on why + while(!in.eod()) { + char c = in.charAt(0); + if(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '>' || c == '/') + break; + in.nextChar(); + } + } + } + } + if(hasContainer) + scanContainerNamed(in, tagName); + return true; + } + return false; + } + + bool Tag::ifEndTagName(coreutils::ZString &in, const char *tagName) { + in.push(); + if(in.ifNext("</")) { + coreutils::MString token = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + if(token.toLower() == coreutils::MString(tagName).toLower()) { + if(in.ifNext(">")) + return true; + } + } + in.pop(); + return false; + } + + // Like scanContainer(in) above, but scans for tagName's own close + // rather than this->name's - and, since whatever's nested INSIDE + // tagName's own body could itself contain any mix of the four + // splitTagName="else" conditionals (not just tagName repeated), tries + // all four at each position rather than only tagName itself. This is + // what actually skips PAST an inner conditional's entire span + // (attributes, body, close tag) when ifNested(in)'s own cross-name + // fallback recognizes one nested in a DIFFERENTLY-named outer tag. + void Tag::scanContainerNamed(coreutils::ZString &in, const char *tagName) { + while(!in.eod()) { + if(ifEndTagName(in, tagName)) + return; + bool matchedNested = false; + for(int i = 0; i < elseSplitTagNameCount; i++) { + if(ifNested(in, elseSplitTagNames[i])) { + matchedNested = true; + break; + } + } + if(!matchedNested) + in.nextChar(); + } + } + coreutils::MString Tag::getVariable(coreutils::ZString &variable, bool inContainer) { if(variable.ifNext("$[")) { coreutils::MString name; @@ -485,11 +973,60 @@ namespace jet { renderVariableName(variable, name, modifier); name.split("."); if(name.getList().size() == 1) { - if(global.variables.find(name[0]) == global.variables.end()) - throw coreutils::Exception("global variable is not initialized."); + if(global.variables.find(name[0]) == global.variables.end()) { + // jet-1.4 fallback: a bare $[name] that isn't a real global + // also checks the current default query (sessionid="") + // before failing - real 1.4 source depends on this, e.g. + // <sql>select ...</sql> immediately followed by a bare + // $[description] with no explicit $[.description] dot. + // 2.0 originally required the dot; this restores 1.4's + // more forgiving behavior without requiring the real + // source to be rewritten. + coreutils::MString defaultSession(""); + if(global.sessionExists(defaultSession)) { + try { + coreutils::MString columnValue = global.getSession(defaultSession)->getColumnValue(name[0]); + return processModifier(columnValue, modifier); + } catch(coreutils::Exception &) { + // No such column in whatever's currently in the default + // session (or no query has run on it yet) - fall through + // to the empty-string default below rather than propagate. + } + } + // jet-1.4 fallback #2: a genuinely never-initialized bare $[name] + // (no global, no session/row match either) reads as an empty + // string, not a hard error - real 1.4 source depends on this too + // (e.g. SESSIONCTL.tag's own + // <if value1="$[_session_id]" value2="" type="ne"> for a fresh, + // no-cookie visitor - $[_session_id] was never set by anything + // at that point). 2.0 originally threw "global variable is not + // initialized" for this - matches ordinary template-language + // semantics (undefined reads as empty) far better than erroring + // on every fresh page hit. + coreutils::MString empty(""); + return processModifier(empty, modifier); + } return processModifier(global.variables[name[0]], modifier); } - return global.getSessionVariable(name); + // Same jet-1.4 fallback as the bare (undotted) case just above, + // extended to the explicit $[session.column] form: real content + // (invoice.method's own $[.amount]/$[.invnumber]/etc., read + // right after a <sql>...where i.id=$[_invoice_id]</sql> that + // legitimately has no matching row when no invoice id was ever + // passed in - e.g. thankyou.page's unconditional <include + // file="invoice.method">) depends on a missing column/session + // reading as empty, not a hard error. This call previously threw + // straight through with no catch at all here - confirmed live, + // this exact page ("column does not exist in session result.") + // was an uncaught propagation from here, not the already-guarded + // 1-part path above. + try { + coreutils::MString sessionValue = global.getSessionVariable(name); + return processModifier(sessionValue, modifier); + } catch(coreutils::Exception &) { + coreutils::MString empty(""); + return processModifier(empty, modifier); + } } throw coreutils::Exception("expected variable name or type designator."); } else if(variable.ifNext("#[")) { @@ -507,9 +1044,63 @@ namespace jet { // tag invocation with no enclosing local-scope-owning tag. if(local == NULL) throw coreutils::Exception("local variable requested but no local scope is active."); - if(local->variables.find(name) == local->variables.end()) - throw coreutils::Exception("local variable is not initialized."); - return local->variables[name]; + // #[name] most commonly refers to some ENCLOSING custom tag's own + // declared/passed attribute value - e.g. setpage's own body reads + // #[NAME]/#[DIR] (its own keywords, found at the first scope + // checked below), but sessionctl's body invokes + // <setpage name="#[PAGE]">, where "#[PAGE]" must resolve against + // SESSIONCTL's own "page" keyword, not setpage's (setpage never + // declares "page" - only "NAME DIR"). Since __dotag binds each + // custom tag invocation's own local=this (so a tag's body sees + // its OWN attributes first), reaching an ancestor's attributes + // requires walking the lexical containment chain via `parent` + // (the same chain findTagDefinition() already walks for tag + // DEFINITION lookups) rather than checking a single scope. + // local->variables (explicit <set scope="local"> only) is checked + // at each level too, ahead of that level's keywords, so a local + // <set> can shadow an attribute of the same name at that scope. + for(Tag *scope = local; scope; scope = scope->parent) { + if(scope->variables.find(name) != scope->variables.end()) + return scope->variables[name]; + // Only consult a scope's own keywords when it's a real custom + // tag invocation (isCustomTagBoundary - see __tag.cpp/Tag.h), + // not a control-flow construct like <if>/<while>/<until> that + // also binds local=this for ITS OWN internal value1/value2/ + // type comparison keywords. Those are never meant to be + // visible to #[...] reads from nested content - they collide + // with real, unrelated attribute names on the ENCLOSING custom + // tag constantly (barant.tag's own <field> declares its own + // "type" keyword - the input's type="text" - and its + // definition immediately nests + // <if value1="#[type]" value2="multiline" type="ne">, whose + // OWN "type" keyword is the comparison operator "ne" - #[type] + // read from further inside that <if> must reach field's "type" + // two scopes up, not the <if>'s own "ne" one scope up). + // Confirmed via real rendered output: without this, + // <input type="#[type]"> rendered type="eq" (the nearest <if>'s + // own comparison type) instead of type="text" (the field's). + if(scope->isCustomTagBoundary) { + // Attribute names are stored lowercase (see the main + // constructor's attribute-parsing loop); the #[name] + // designator itself stays case-sensitive as written, only + // the keywords-map lookup key is lowered to match that + // storage. + coreutils::MString lname = coreutils::MString(name).toLower(); + if((scope->keywords.find(lname) != scope->keywords.end()) && + (resolvingKeywords.count(std::pair<Tag*, coreutils::MString>(scope, lname)) == 0)) + return scope->resolveKeyword(lname); + } + } + // jet-1.4 fallback: a bare #[name] that was never passed as an + // attribute anywhere in the enclosing scope chain and never + // locally <set> reads as an empty string, not a hard error - real + // 1.4 content depends on this throughout (e.g. SESSIONCTL.tag's + // <page> definition checks #[secure]/#[admin]/#[loggedin] + // unconditionally, but most real .page files - including + // login.page's own <page template="mainpagetemplate">, no other + // attributes - never pass them at all). + coreutils::MString empty(""); + return processModifier(empty, modifier); } throw coreutils::Exception("Expecting a variable initializer ('$[' or '#[')."); } @@ -549,8 +1140,30 @@ namespace jet { void Tag::assignVariable(coreutils::ZString variable, coreutils::MString value, coreutils::ZString scope) { if(variable.startsWith("@")) { - coreutils::ZString sessionName = variable; - sessionName.ifNext("@"); + // NOT sessionName.ifNext("@") - that only advances sessionName's + // own read cursor past the "@", it doesn't produce a string whose + // OWN logical start is there. JString::locate() (used internally + // by JFile's operator[] to find an EXISTING key to replace, not + // just append a new one) resets whatever ZString it's given back + // to ITS OWN position 0 partway through searching - for a + // cursor-advanced view over the ORIGINAL "@name" buffer, position + // 0 is still '@', so the search silently looks for "@name" instead + // of "name". That only matters once the session file already has + // real content loaded from a PRIOR request to search through - a + // brand new/empty file has nothing to find either way, so this + // looked fine on a first write and only broke on session vars that + // outlive one request (login state read back on a later page, + // exactly what this mechanism exists for). Confirmed via a real + // corrupted session file: <clearenv>'s own <set name="@#[name]" + // value=""> calls (see SESSIONCTL.tag) never found the existing + // "_login_id" key that a PRIOR request's <setenv> had written, so + // each one appended a brand new "_login_id":"" pair instead of + // replacing it - duplicate keys, with whichever occurrence a given + // JSON reader picks (first or last) an unreliable coin flip, not a + // deliberate "cleared" state. substring(1) - not ifNext(), not + // reset() - is what actually produces an independent copy starting + // AT that position, immune to being reset back past it. + coreutils::MString sessionName = variable.substring(1); if(global.sessionFile == NULL) throw coreutils::Exception("session variable assignment requested but no session is active (missing sessiondir keyword, or cgi mode is off)."); (*global.sessionFile)[sessionName] = value; @@ -561,11 +1174,48 @@ namespace jet { else if(scope == "local") { if(local == NULL) throw coreutils::Exception("scope=\"local\" requested but no local scope is active."); - local->variables[variable] = value; + // Same reasoning as scope="parent" just below, one scope closer: + // `local` itself isn't necessarily a real custom tag invocation - + // <if>/<while>/<until> also bind local=this. A <set scope="local"> + // written inside one of THEIR branches (barant.tag's own <field> + // definition: <if expr="..."><set name="class" value="..." + // scope="local"><else>...</if>) stores onto that transient <if> + // instance, which is destructed the moment </if> closes - so by + // the time <field>'s own #[class] read runs later in the SAME + // definition (outside the <if>), the value is already gone, + // falling to the empty-string fallback. Confirmed via real + // rendered output: class="" instead of class="fieldinput". Walk + // up to the nearest real scope boundary first, same as + // scope="parent"/__tag.cpp/the bare <container> fix. + Tag *localScope = local; + while(localScope->parent && !localScope->isCustomTagBoundary) + localScope = localScope->parent; + localScope->variables[variable] = value; } else if(scope == "parent") { if((local == NULL) || (local->parent == NULL)) throw coreutils::Exception("scope=\"parent\" requested but no local scope (or its parent) is active."); - local->parent->variables[variable] = value; + // Walk up from local->parent to the nearest real scope boundary + // (isCustomTagBoundary), the same way __tag.cpp's own tag- + // DEFINITION registration already does - a bare parent hop lands + // on whatever C++ object happens to be the immediate caller in + // the dispatch chain, which for content reached through + // transparent wrapper constructs (<eval>, <include>) is that + // wrapper itself, not the custom tag it's lexically nested + // inside. SESSIONCTL.tag's own <TAG NAME='TEMPLATE'> definition + // depends on this: its + // <EVAL NAME=TEMPLATEDATA SCOPE=PARENT><CONTAINER EVAL="NO"></EVAL> + // is reached via <page>'s own + // <EVAL><INCLUDE FILE="...#[TEMPLATE].temp"></EVAL> - without + // this walk, TEMPLATEDATA landed on the transient __include + // instance's own scope (one hop from TEMPLATE) rather than + // <page>'s (two hops away, past the wrapper), so <page>'s own + // later #[TEMPLATEDATA] read - itself walking the real scope- + // boundary chain via getVariable's own parent-chain fix - never + // found it. + Tag *scope2 = local->parent; + while(scope2->parent && !scope2->isCustomTagBoundary) + scope2 = scope2->parent; + scope2->variables[variable] = value; } } @@ -575,35 +1225,35 @@ namespace jet { void Tag::storeVariable(coreutils::ZString variable) { if(keywordDefined("expr")) { - if(!keywordDefined("scope") || (resolveKeyword("scope") == "global")) + if(!keywordDefined("scope") || (resolveKeyword("scope").toLower() == "global")) assignVariable(variable, Operand(keywords["expr"], *this).string, "global"); - else if(resolveKeyword("scope") == "local") + else if(resolveKeyword("scope").toLower() == "local") assignVariable(variable, Operand(keywords["expr"], *this).string, "local"); - else if(resolveKeyword("scope") == "parent") + else if(resolveKeyword("scope").toLower() == "parent") assignVariable(variable, Operand(keywords["expr"], *this).string, "parent"); } else if(hasContainer) { processContainer(container); if(evaluate) { - if(!keywordDefined("scope") || (resolveKeyword("scope") == "global")) + if(!keywordDefined("scope") || (resolveKeyword("scope").toLower() == "global")) assignVariable(variable, out, "global"); - else if(resolveKeyword("scope") == "local") + else if(resolveKeyword("scope").toLower() == "local") assignVariable(variable, out, "local"); - else if(resolveKeyword("scope") == "parent") + else if(resolveKeyword("scope").toLower() == "parent") assignVariable(variable, out, "parent"); } else { - if(!keywordDefined("scope") || (resolveKeyword("scope") == "global")) + if(!keywordDefined("scope") || (resolveKeyword("scope").toLower() == "global")) assignVariable(variable, container, "global"); - else if(resolveKeyword("scope") == "local") + else if(resolveKeyword("scope").toLower() == "local") assignVariable(variable, container, "local"); - else if(resolveKeyword("scope") == "parent") + else if(resolveKeyword("scope").toLower() == "parent") assignVariable(variable, container, "parent"); } } else { - if(!keywordDefined("scope") || (resolveKeyword("scope") == "global")) + if(!keywordDefined("scope") || (resolveKeyword("scope").toLower() == "global")) assignVariable(variable, resolveKeyword("value"), "global"); - else if(resolveKeyword("scope") == "local") + else if(resolveKeyword("scope").toLower() == "local") assignVariable(variable, resolveKeyword("value"), "local"); - else if(resolveKeyword("scope") == "parent") + else if(resolveKeyword("scope").toLower() == "parent") assignVariable(variable, resolveKeyword("value"), "parent"); } } diff --git a/Tag.h b/Tag.h index 1a34b41..eb4c0a4 100644 --- a/Tag.h +++ b/Tag.h @@ -72,6 +72,20 @@ namespace jet { // actual definition to expand). bool findTagDefinition(coreutils::MString name, coreutils::ZString &result); + // Renders the enclosing custom tag invocation's own nested content - + // the <container> marker inside a <tag name="X"> definition body - + // into `out`. Extracted, unchanged, from parseContainer's own + // "container" dispatch branch (see Tag.cpp) so generated code for a + // compiled tag-definition body (see jetc's TAGDEF handling) can call + // this exact logic directly instead of going through the raw-text + // scan to reach it - walks up from `local` to the nearest + // isCustomTagBoundary ancestor not already being resolved (guards + // the pass-through self-reference case, e.g. linkbutton's own + // <EVAL><CONTAINER></EVAL>) and renders THAT scope's own ->container; + // falls back to rendering container2 if no such ancestor exists, + // exactly matching parseContainer's own fallback for the same case. + void renderContainer(coreutils::MString &out, coreutils::ZString container2 = NULL); + protected: bool hasContainer = false; bool hasContainer2 = false; @@ -106,6 +120,23 @@ namespace jet { bool ifTagDefined(coreutils::ZString &in, coreutils::ZString &tag); bool ifEndTagName(coreutils::ZString &in); bool ifSplitTagName(coreutils::ZString &in); + + // ifNested(in)'s own same-name-only check misses a DIFFERENTLY + // named nested split-tag construct - e.g. an <if> nested inside an + // <ifrow>'s own body (both use splitTagName "else"): ifNested(in) + // only recognizes another nested <ifrow>, so an inner <if>'s own + // <else>/</if> markers were being read as if they belonged to the + // OUTER <ifrow>'s own boundary scan, corrupting both. This overload + // set (ifNested/ifEndTagName parameterized by an explicit tag name, + // plus scanContainerNamed) generalizes the exact same logic to any + // of the built-in splitTagName="else" conditionals - if/while/ + // until/ifrow - so the outer tag's boundary scan correctly skips + // PAST an inner one of any of these, regardless of name, before + // resuming its own search. See Tag.cpp's own doc comment on the + // shared splitTagNames list for the real story/repro. + bool ifNested(coreutils::ZString &in, const char *tagName); + bool ifEndTagName(coreutils::ZString &in, const char *tagName); + void scanContainerNamed(coreutils::ZString &in, const char *tagName); coreutils::MString processModifier(coreutils::MString &value, coreutils::MString &modifier); }; diff --git a/__call.cpp b/__call.cpp index 7e4eff7..d951445 100644 --- a/__call.cpp +++ b/__call.cpp @@ -16,14 +16,22 @@ namespace jet { throw coreutils::Exception("call tag cannot have both input keyword and have a container."); if(!keywordDefined("pgm")) throw coreutils::Exception("pgm keyword must be specified."); - for(ix = 0; ix <= 50; ++ix) + for(ix = 0; ix < 50; ++ix) argv[ix] = NULL; - argv[0] = resolveKeyword("pgm").c_str(); // TODO: Need to peel off the program name only and pass as argv[0]. - for(ix = 1; ix <= 50; ++ix) { + // Reserve up front - argStrings must never reallocate once a + // c_str() pointer from one of its elements has been stored into + // argv[] below, or that pointer dangles exactly like the bug this + // replaces (see the .h's own comment). 50 arg slots + argv[0]'s + // own program name. + argStrings.reserve(51); + argStrings.push_back(resolveKeyword("pgm")); // TODO: Need to peel off the program name only and pass as argv[0]. + argv[0] = argStrings.back().c_str(); + for(ix = 1; ix < 50; ++ix) { coreutils::MString arg("arg"); arg << ix; - if(keywordDefined(arg)) { - argv[ix] = resolveKeyword(arg).c_str(); + if(keywordDefined(arg)) { + argStrings.push_back(resolveKeyword(arg)); + argv[ix] = argStrings.back().c_str(); } else break; } @@ -54,7 +62,7 @@ namespace jet { close(fdi[1]); dup2(fdi[0], 0); } - rc = execvpe(resolveKeyword("pgm").c_str(), argv, global.envp); + rc = execvpe(argv[0], argv, global.envp); close(fdo[1]); exit(errno); } diff --git a/__call.h b/__call.h index 3fa6bb9..4fcb221 100644 --- a/__call.h +++ b/__call.h @@ -2,11 +2,12 @@ #define ____call_h__ #include "Tag.h" +#include <vector> namespace jet { - + class __call : public Tag { - + public: __call(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local); @@ -18,9 +19,24 @@ namespace jet { int fdo[2]; int rc; char *argv[50]; - + + // Owns the actual character storage argv[] points into. Resolved + // MStrings used to be created as unnamed temporaries with only + // their .c_str() pointer kept (argv[ix] = resolveKeyword(arg) + // .c_str()) - the temporary is destroyed at the end of that same + // statement, so argv[ix] was a dangling pointer from that point + // on, read only much later by execvpe() in the forked child. + // Undefined behavior - confirmed via a real repro: two sequential + // <call> tags (date, then cp using date's own captured output) + // corrupted each other's arguments, producing a "cp" invocation + // that actually ran /bin/date with garbage arguments instead. + // Reserved to its final size up front (see the constructor) so no + // push_back can reallocate and invalidate a c_str() pointer + // already stored in argv[] earlier in the same construction. + std::vector<coreutils::MString> argStrings; + }; - + } #endif diff --git a/__dotag.cpp b/__dotag.cpp index e81423c..7479022 100644 --- a/__dotag.cpp +++ b/__dotag.cpp @@ -3,12 +3,22 @@ namespace jet { - __dotag::__dotag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { + __dotag::__dotag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) { // Every invocation of a custom tag is itself a new scope boundary - // a <tag name="..."> encountered while expanding this invocation's // own template (below) registers into THIS instance's localTags, // not the global map, so it's only visible for the rest of this // expansion. See Tag.h's isCustomTagBoundary/localTags doc comment. + // + // local=this (bound above, in the base Tag constructor call rather + // than forwarding the caller's own local) is what makes a custom + // tag's own body see ITS OWN declared/passed attributes via + // #[name] - matches the pattern __if/__while/__until already use + // for the same reason. Without this, #[name] reads inside e.g. + // SESSIONCTL.tag's <sessionctl>/<setpage>/<retain> bodies resolved + // against whatever local scope happened to be active in the + // caller instead of this invocation's own attributes, and always + // fell through to the empty-string fallback. isCustomTagBoundary = true; // The base Tag constructor already set hasContainer from whether @@ -28,10 +38,29 @@ namespace jet { // this invocation was written. hasContainer = true; - coreutils::ZString container3; - if(!findTagDefinition(name, container3)) - throw coreutils::Exception("tag definition not found."); - processContainer(container3, container); + // Fast path: if jetc compiled this tag's definition directly (a + // top-level <tag name="X"> - see JetCodegen's TAGDEF handling), a + // render function was registered under its lowercase name (matching + // __tag.cpp's own registration key convention) instead of - or + // alongside - the raw text findTagDefinition()/processContainer() + // below would otherwise have to re-parse on every single invocation. + // A miss here (locally-scoped/nested definitions, or a tag that was + // never part of a compiled module) falls straight through to that + // exact same interpreted path, unchanged - see Global.h's own doc + // comment on compiledTags. + coreutils::MString nameLower = coreutils::MString(name).toLower(); + auto compiledIt = global.compiledTags.find(nameLower); + if(compiledIt != global.compiledTags.end()) { + // Same gate processContainer() below applies on the interpreted + // path - keeps both paths behaviorally identical rather than + // having the compiled fast path unconditionally render. + if(hasContainer && evaluate) + compiledIt->second(*this, out); + } else { + coreutils::ZString container3; + if(!findTagDefinition(name, container3)) + throw coreutils::Exception("tag definition not found."); + processContainer(container3, container); + } } } - diff --git a/__exclude.cpp b/__exclude.cpp index 0b16873..173a5ac 100644 --- a/__exclude.cpp +++ b/__exclude.cpp @@ -2,12 +2,12 @@ #include "Exception.h" namespace jet { - + __exclude::__exclude(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) { if(!hasContainer) - throw coreutils::Exception("exclude must have a container."); + throw coreutils::Exception("exclude must have a container."); output = true; evaluate = false; } - + } diff --git a/__for.cpp b/__for.cpp index a1bdb01..67a899a 100644 --- a/__for.cpp +++ b/__for.cpp @@ -5,32 +5,54 @@ namespace jet { __for::__for(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) { - double counter = 0.0f; + double counter = 0.0f; bool nameDefined = keywordDefined("name"); if(keywordDefined("start")) { counter = resolveKeyword("start").asDouble(); keywords["start"].reset(); } - if(!keywordDefined("end")) + if(!keywordDefined("end")) throw coreutils::Exception("for tag requires end keyword."); - if(!keywordDefined("step")) + if(!keywordDefined("step")) throw coreutils::Exception("for tag requires step keyword."); + + // Where to write the loop counter (which variables map, and under + // what name) is a static property of this <for>'s own invocation - + // "scope"/"name" have no sensible reason to change mid-loop, so + // resolve and branch on them once instead of re-resolving both + // (and re-checking scope with up to 3 comparisons) on every single + // iteration. Loop bounds (end/step) are deliberately left + // re-resolved per iteration exactly as before - real content could + // plausibly rely on a bound that changes as a side effect of the + // loop body itself, and there's no clear evidence either way, so + // that part stays conservative/unchanged. + coreutils::MString counterVarName; + enum { SCOPE_GLOBAL, SCOPE_LOCAL, SCOPE_PARENT } counterScope = SCOPE_GLOBAL; + if(nameDefined) { + counterVarName = resolveKeyword("name"); + if(keywordDefined("scope")) { + coreutils::MString scopeVal = resolveKeyword("scope"); + if(scopeVal == "global") counterScope = SCOPE_GLOBAL; + else if(scopeVal == "local") counterScope = SCOPE_LOCAL; + else if(scopeVal == "parent") counterScope = SCOPE_PARENT; + else throw coreutils::Exception("scope value is not valid."); + } + } + for(double ix = counter; ix <= resolveKeyword("end").asDouble(); ix += resolveKeyword("step").asDouble()) { resolveKeyword("end").reset(); - resolveKeyword("step").reset(); + resolveKeyword("step").reset(); if(nameDefined) { - if(!keywordDefined("scope") || (resolveKeyword("scope") == "global")) - global.variables[resolveKeyword("name")] = ix; - else if(resolveKeyword("scope") == "local") - this->local->variables[resolveKeyword("name")] = ix; - else if(resolveKeyword("scope") == "parent") - parent->local->variables[resolveKeyword("name")] = ix; + if(counterScope == SCOPE_GLOBAL) + global.variables[counterVarName] = ix; + else if(counterScope == SCOPE_LOCAL) + this->local->variables[counterVarName] = ix; else - throw coreutils::Exception("scope value is not valid."); - } + parent->local->variables[counterVarName] = ix; + } processContainer(container); container.reset(); - } + } } - + } diff --git a/__if.cpp b/__if.cpp index b564476..fd2f31f 100644 --- a/__if.cpp +++ b/__if.cpp @@ -16,7 +16,7 @@ namespace jet { throw coreutils::Exception("type expected if value1 and value2 specified."); } else throw coreutils::Exception("value2 required if value1 specified."); - coreutils::MString type = resolveKeyword("type"); + coreutils::MString type = resolveKeyword("type").toLower(); if((type != "eq") && (type != "ne") && (type != "lt") && diff --git a/__ifrow.cpp b/__ifrow.cpp index ce27763..8b1741a 100644 --- a/__ifrow.cpp +++ b/__ifrow.cpp @@ -6,17 +6,37 @@ #include <unistd.h> namespace jet { - + __ifrow::__ifrow(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this, "else") { - output = false; + // NOT setting output=false here (an earlier version of this file + // did) - that flag gates Tag::~Tag()'s own copyContainer(out, + // parentOut) call, the mechanism that propagates whatever + // processContainer() below actually wrote up to this tag's own + // caller. <ifrow>'s entire purpose is to conditionally DISPLAY one + // of its two branches (exactly like <if>/<while>/<until>, none of + // which ever touch `output`) - unlike the tags that legitimately + // do set it (<set>/<sql>/<cookie>/<header>/<write>/<tag>/ + // <comment>/<stream>, none of which have any visible content of + // their own to emit), so setting it here silently discarded + // EVERY <ifrow>'s rendered content, true branch or false, on every + // single use. Confirmed live via gdb-style tracing: hasRow(), + // the resolved session pointer, and container's own text were all + // exactly correct at the point processContainer(container) ran - + // the content just never reached parentOut because the destructor + // skipped the copy. Never caught before because no real content + // this session exercised an <ifrow> with real row data AND had + // its rendered output actually inspected (account.page's own + // <ifrow sessionid="projman"> usage always redirected before + // reaching it, for lack of a login session). if(!hasContainer) - throw coreutils::Exception("ifrow tag must have a container."); - if(!global.sessionExists(keywords[resolveKeyword("sessionid")])) + throw coreutils::Exception("ifrow tag must have a container."); + coreutils::MString sessionId = resolveKeyword("sessionid"); + if(!global.sessionExists(sessionId)) throw coreutils::Exception("sessionid does not exist."); - if(global.getSession(keywords[resolveKeyword("sessionid")])->hasRow()) + if(global.getSession(sessionId)->hasRow()) processContainer(container); else processContainer(container2); } - + } diff --git a/__include.cpp b/__include.cpp index 99ca5c1..f72eba0 100644 --- a/__include.cpp +++ b/__include.cpp @@ -1,35 +1,104 @@ #include "__include.h" #include "Exception.h" #include "File.h" +#include <string> +#include <unordered_map> +#include <sys/stat.h> namespace jet { - + + namespace { + struct CachedInclude { + std::string content; + time_t mtime; + }; + // Persistent, per-worker-thread cache of <include> file contents, + // keyed by resolved filename. Real content depends on this + // constantly through custom tag bodies that jetc's own compile- + // time <include>-inlining doesn't reach (only the top-level .jet + // source's own includes get compiled in) - mainpagetemplate.temp + // alone pulls in login.menu/menu.bar/opensource.menu/cart.menu/ + // sessionvar.menu, all previously re-read from disk on every + // single request even though the files are 100% static between + // edits. Invalidated via mtime comparison (a stat() call, cheap + // relative to a full read()) so a live edit still takes effect on + // the very next request - matching the workflow already relied on + // all session of editing .tag/.temp/.menu content directly and + // seeing it live without a rebuild. + thread_local std::unordered_map<std::string, CachedInclude> includeCache; + } + __include::__include(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { if(!keywordDefined("file")) throw coreutils::Exception("file keyword must be specified."); if(hasContainer) throw coreutils::Exception("include tag should not have a container."); hasContainer = true; - coreutils::File file(resolveKeyword("file")); - file.read(); - container = file.asZString(); - try { + coreutils::MString includeFileName = resolveKeyword("file"); + std::string fileNameKey = includeFileName.str(); + + struct stat st; + bool haveStat = (stat(fileNameKey.c_str(), &st) == 0); + + auto cached = includeCache.find(fileNameKey); + if(cached != includeCache.end() && haveStat && cached->second.mtime == st.st_mtime) { + container = coreutils::ZString(cached->second.content.data(), cached->second.content.length()); + } else { + coreutils::File file(includeFileName); + file.read(); + if(haveStat) { + CachedInclude &entry = includeCache[fileNameKey]; + entry.content = file.asString(); + entry.mtime = st.st_mtime; + container = coreutils::ZString(entry.content.data(), entry.content.length()); + } else { + // Couldn't stat the file (unusual - e.g. a race with deletion) - + // don't cache something we can't validate the freshness of, + // just render this one read directly, matching the pre-cache + // behavior exactly for this edge case. + container = file.asZString(); + } + } + + try { processContainer(container); } catch(coreutils::Exception e) { - container.setCursor(global.errorCursor); - container.moveBackToLineStart(); - std::cout << "-----------------------------" << std::endl; - std::cout << "Error in included script '" << variables["file"] << "' at line " << container.getLineNumberAtCursor() << std::endl; - std::cout << "Error text: " << e.text << std::endl; - std::cout << "-----------------------------" << std::endl; - std::cout << container.parsed() << std::endl; - std::cout << "******** Error caught: " << e.text << std::endl; - std::cout << container.unparsed() << std::endl; + // global.errorCursor is whatever cursor was active in whichever + // ZString buffer was being parsed at the moment the ORIGINAL + // exception was thrown - not necessarily this include's own + // `container` buffer at all (e.g. a keyword-resolution error + // happening while interpreting a completely different .temp + // file's own text). setCursor() on a pointer outside container's + // own [data, data+length] range throws "Cursor out of range on + // setCursor." - and since that happens INSIDE this diagnostic- + // only "show a nice line number" block, it was replacing the + // real error (e.text) with that unrelated cursor-range message + // before it ever reached the caller, making every real error + // from inside an <include> impossible to see. This is purely + // diagnostic output - if it can't compute a line number, fall + // back to printing the real error text without one rather than + // losing it entirely. + try { + container.setCursor(global.errorCursor); + container.moveBackToLineStart(); + std::cout << "-----------------------------" << std::endl; + std::cout << "Error in included script '" << variables["file"] << "' at line " << container.getLineNumberAtCursor() << std::endl; + std::cout << "Error text: " << e.text << std::endl; + std::cout << "-----------------------------" << std::endl; + std::cout << container.parsed() << std::endl; + std::cout << "******** Error caught: " << e.text << std::endl; + std::cout << container.unparsed() << std::endl; + } catch(coreutils::Exception &) { + std::cout << "-----------------------------" << std::endl; + std::cout << "Error in included script '" << variables["file"] << "' (line number unavailable - error originated outside this include's own content)" << std::endl; + std::cout << "Error text: " << e.text << std::endl; + std::cout << "-----------------------------" << std::endl; + } global.errorCursor = in.getCursor(); - throw coreutils::Exception("error in included file."); + throw coreutils::Exception(e.text); } - + } } diff --git a/__mysql.cpp b/__mysql.cpp index cdbd9f1..b1bda60 100644 --- a/__mysql.cpp +++ b/__mysql.cpp @@ -1,33 +1,145 @@ #include "__mysql.h" #include "Exception.h" #include <iostream> +#include <string> +#include <unordered_map> namespace jet { + namespace { + struct PooledConnection { + MYSQL *handle; + std::string host, user, database; + }; + // Persistent, per-worker-thread connection cache - reused across + // requests instead of a fresh mysql_real_connect() every time. + // Safe with no locking: the apiserver's EPoll worker pool is a + // fixed set of OS threads for the process's whole life (see + // MysqlThreadScope's own doc comment - mysql_thread_init() runs + // once per worker thread at thread start), so a thread_local + // instance here means a given MYSQL* handle is only ever touched + // by the one thread that created it, never concurrently accessed. + // Keyed by sessionid, which is how this codebase's own <database>/ + // <sql>/<ifrow> tags already address a specific connection + // (Global::sessions is keyed the same way) - the cached host/user/ + // database are kept alongside purely as a defensive check that a + // reused entry really matches what THIS invocation asked for, + // not a real expectation that sessionid is ever reused across + // genuinely different backing databases in practice. + thread_local std::unordered_map<std::string, PooledConnection> connectionPool; + } + __mysql::__mysql(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) { if(!keywordDefined("host")) throw coreutils::Exception("host must be specified for mysql tag."); - if(!keywordDefined("database")) + // jet-1.4's <database> spells the same attribute "name" instead of + // "database" (e.g. SESSIONCTL.tag's own <sessionctl> definition) - + // accept either rather than requiring that real content to change. + coreutils::ZString databaseKeyword = keywordDefined("database") ? "database" : "name"; + if(!keywordDefined(databaseKeyword)) throw coreutils::Exception("database must be specified for mysql tag."); if(!keywordDefined("user")) throw coreutils::Exception("user must be specified for mysql tag."); if(!keywordDefined("password")) throw coreutils::Exception("password must be specified for mysql tag."); - sessionId = keywords[resolveKeyword("sessionid")]; - + // Same double-indirection bug as the host/user/password/database + // fix above - resolveKeyword("sessionid") already returns the + // resolved VALUE (e.g. "_SESSIONCTL"), so wrapping it in keywords[...] + // used that value as a map KEY instead, auto-vivifying an empty + // string (no attribute is ever literally named "_SESSIONCTL"). + // sessionId was therefore always empty regardless of what + // sessionid= actually specified - confirmed live via gdb: two + // real <mysql>/<database> tags with genuinely different sessionid + // values ("" and "_SESSIONCTL") both registered under "", the + // second colliding with "sessionid already exists.". + sessionId = resolveKeyword("sessionid"); + + // mysql_real_connect() returns NULL on failure but does NOT itself + // free the handle mysql_init() allocated - the caller is still + // expected to mysql_close() that original handle to avoid leaking + // it (MySQL C API docs). The previous code overwrote `mysql` with + // the connect() return value directly, so on failure the only + // pointer to the real handle was lost before ever being freed - a + // real leak on every failed connection attempt (this member's + // eventual value, NULL, is what a normal ~__mysql() would try to + // mysql_close() anyway, which is a safe no-op, but that never even + // runs here since the constructor throws below - see the + // addSession() comment for why that matters more). + // resolveKeyword() already returns the fully-resolved VALUE - the + // previous keywords[resolveKeyword("host")] wrapped that resolved + // VALUE ("localhost") back into a keywords[] lookup, i.e. used it + // as a MAP KEY instead of the value itself. Since no attribute is + // ever literally named "localhost"/the real username/etc., + // std::map::operator[] silently auto-vivified an empty MString for + // every one of the four parameters - mysql_real_connect() was + // therefore always called with host/user/password/database all + // empty, regardless of what the tag actually specified. Confirmed + // live: MySQL's own error ("Access denied for user 'barant'@'localhost' + // (using password: NO)") showed the password was genuinely empty, + // not merely wrong. + coreutils::MString hostVal = resolveKeyword("host"); + coreutils::MString userVal = resolveKeyword("user"); + coreutils::MString passwordVal = resolveKeyword("password"); + coreutils::MString databaseVal = resolveKeyword(databaseKeyword); + + // Reuse a live connection this same worker thread already holds + // for this sessionid, rather than paying mysql_real_connect()'s + // full handshake cost on every single request. mysql_ping() is + // what makes this safe - it's a genuine round trip to the server, + // so a connection MySQL itself already dropped (its own + // wait_timeout, or an admin-initiated restart like this project + // already lived through once) is detected and discarded here + // rather than handed to processContainer() to fail on later. + std::string poolKey = sessionId.str(); + auto pooled = connectionPool.find(poolKey); + if(pooled != connectionPool.end() && + pooled->second.host == hostVal.str() && + pooled->second.user == userVal.str() && + pooled->second.database == databaseVal.str()) { + if(mysql_ping(pooled->second.handle) == 0) { + mysql = pooled->second.handle; + } else { + mysql_close(pooled->second.handle); + connectionPool.erase(pooled); + } + } + + if(!mysql) { + MYSQL *handle = mysql_init(NULL); + mysql = mysql_real_connect(handle, + hostVal.c_str(), + userVal.c_str(), + passwordVal.c_str(), + databaseVal.c_str(), 0, NULL, 0); + + if(!mysql) { + std::string err = mysql_error(handle); + mysql_close(handle); + throw coreutils::Exception("database connection failed: " + err); + } + + connectionPool[poolKey] = PooledConnection{mysql, hostVal.str(), userVal.str(), databaseVal.str()}; + } + + // Registered only once the connection is confirmed live - a real, + // reproducible crash (heap corruption - "free(): invalid pointer" + // seen live, then two independent server-hang repros) traced back + // to this: addSession() used to run BEFORE the connect attempt, so + // a FAILED connection (which throws, right above) left a dangling + // `this` pointer registered under sessionId in Global's session + // map forever - ~__mysql() (which would normally call + // removeSession()) never runs for a constructor that itself threw, + // per ordinary C++ rules (only already-completed base-class + // subobjects get destroyed during exception unwinding, not the + // derived object whose constructor is still executing). The very + // next <mysql> tag construction using the same sessionId (e.g. two + // product pages with an empty sessionid="" back to back, both + // failing the same way) then collided with that stale entry. + // Confirmed via a real, deterministic two-request repro before this + // fix, and confirmed gone after it. global.addSession(sessionId, this); - - mysql = mysql_init(NULL); - mysql = mysql_real_connect(mysql, - keywords[resolveKeyword("host")].c_str(), - keywords[resolveKeyword("user")].c_str(), - keywords[resolveKeyword("password")].c_str(), - keywords[resolveKeyword("database")].c_str(), 0, NULL, 0); - - if(!mysql) - throw coreutils::Exception("database and host parameters are not valid."); processContainer(container); @@ -36,7 +148,11 @@ namespace jet { __mysql::~__mysql() { global.removeSession(sessionId); mysql_free_result(result); - mysql_close(mysql); + // mysql itself is deliberately NOT closed here anymore - see the + // connectionPool doc comment above the constructor. It's owned by + // this worker thread's persistent pool now and gets reused by + // whatever request this thread handles next, instead of being + // torn down and fully reconnected from scratch every time. } void __mysql::query(coreutils::MString query) { @@ -46,6 +162,36 @@ namespace jet { row = mysql_fetch_row(result); fieldLength = mysql_fetch_lengths(result); qFields = mysql_num_fields(result); + // Built once per query, not once per column LOOKUP - see the + // matching comment on getColumnValue() and on fieldIndex's own + // declaration in __mysql.h. mysql_fetch_fields() returns the + // whole field array in one call (mysql_fetch_field_direct() + // looping ix=0..qFields-1, as getColumnValue() used to do on + // every single named access, does the exact same work over and + // over for a result set whose schema never changes after this + // point). + fieldIndex.clear(); + MYSQL_FIELD *fields = mysql_fetch_fields(result); + for(unsigned int ix = 0; ix < qFields; ++ix) + fieldIndex[std::string(fields[ix].name)] = (int)ix; + } else { + // A failed/errored query (real content builds one from an unset + // $[...] parameter, e.g. "where tasks.id=" with no value - genuine + // MySQL syntax error) previously left row/qFields holding whatever + // a PRIOR successful query on this same session object had set, + // since only the success branch above ever touched them. A later + // getColumnValue() call then looped 0..qFields (stale, nonzero) + // calling mysql_fetch_field_direct() against this NULL `result` - + // segfaulting (confirmed via gdb: SIGSEGV in + // mysql_fetch_field_direct, called from getColumnValue) instead of + // reaching the "column does not exist in session result." the + // caller already throws/catches for a genuinely missing column. + // Resetting both here makes a failed query present as "no row, no + // columns" - clean and consistent regardless of what ran before it + // on this session. + row = NULL; + qFields = 0; + fieldIndex.clear(); } } @@ -63,25 +209,51 @@ namespace jet { if(column == "?#") { nbrOfColumns = (int)qFields; return nbrOfColumns; - } else if(column.ifNext("#")) { + } else if(column.ifNext("#") || column.ifNext("?")) { + // "#N"/"?N" (column name at 1-based position N) are the same + // jet-1.4 syntax under two different prefixes - real, unmodified + // 1.4 content uses both (barant.tag's own <showtable> definition + // writes $[?$[index]], never $[#$[index]]) - confirmed via a real + // repro: admin.page's own Users/Products tables rendered every + // column header and cell completely blank, since "?N" fell all + // the way through to the ordinary column-VALUE lookup below + // (nothing named "?3" exists in the row) and read as empty rather + // than as "the name of column 3", per the jet-1.4-fallback #2 + // empty-string-on-missing-column behavior elsewhere in this + // codebase (see Tag.cpp). if(column.eod()) { nbrOfRows = (int)mysql_num_rows(result); return nbrOfRows; } else { - std::cout << "[" << column.unparsed() << "]" << std::endl; int index = column.asInteger(); - std::cout << "integer: " << index << std::endl; field = mysql_fetch_field_direct(result, index - 1); - return coreutils::ZString(field->name); + return coreutils::ZString(field->name); } } - - for(int ix = 0; ix < qFields; ++ix) { - field = mysql_fetch_field_direct(result, ix); - if(column.equals((char *)field->name)) { - return coreutils::ZString(row[ix], fieldLength[ix]); - } - } + + // A query that ran successfully but matched zero rows (a real, + // valid query - e.g. cart.page's own aggregate/JOIN query against + // an empty cart session - not a failed one, see query()'s own + // row=NULL/qFields=0 reset above for that separate case) leaves + // qFields correctly reflecting the real schema but `row` NULL. + // The loop below finds a genuine matching column name and then + // dereferences row[ix] - segfaulting (confirmed via gdb: SIGSEGV + // right here) instead of reaching a catchable error. Reusing the + // same "column does not exist" exception the loop's own fallthrough + // already throws - no current row means no column value is + // obtainable either way, and callers already expect and catch + // this exact exception. + if(!row) + throw coreutils::Exception("column does not exist in session result."); + + // O(1) against the map query() already built for this result set, + // instead of re-deriving field metadata (mysql_fetch_field_direct, + // a real MySQL C API call) and string-comparing column names one + // by one on every single named access - see fieldIndex's own doc + // comment in __mysql.h for the full story. + auto it = fieldIndex.find(column.str()); + if(it != fieldIndex.end()) + return coreutils::ZString(row[it->second], fieldLength[it->second]); throw coreutils::Exception("column does not exist in session result."); } diff --git a/__mysql.h b/__mysql.h index 07652eb..5119365 100644 --- a/__mysql.h +++ b/__mysql.h @@ -5,6 +5,8 @@ #include "ZString.h" #include "MString.h" #include <sstream> +#include <string> +#include <unordered_map> #include <mysql/mysql.h> namespace jet { @@ -21,13 +23,29 @@ namespace jet { coreutils::ZString getColumnValue(coreutils::ZString column); private: - MYSQL *mysql; - MYSQL_RES *result; - MYSQL_ROW row; - unsigned long *fieldLength; - unsigned int qFields; + // Explicitly initialized - result/row/fieldLength were otherwise + // left as garbage stack values until query() first ran. A + // <mysql>...</mysql> block that connects successfully but never + // runs a <sql> query inside it (e.g. just a connectivity check) + // would previously reach ~__mysql()'s mysql_free_result(result) + // with an uninitialized pointer - real undefined behavior, found + // while root-causing a live crash in a related part of this same + // class's constructor. + MYSQL *mysql = NULL; + MYSQL_RES *result = NULL; + MYSQL_ROW row = NULL; + unsigned long *fieldLength = NULL; + unsigned int qFields = 0; coreutils::MString sessionId; + // Column-name -> row[] index, built once per query() call (see its + // own comment) instead of getColumnValue() re-deriving it via a + // fresh mysql_fetch_field_direct() linear scan on every single + // named lookup - a template reading N columns off one row used to + // pay that scan N times over, even though the schema is fixed for + // the lifetime of one result set. + std::unordered_map<std::string, int> fieldIndex; + coreutils::MString nbrOfRows = "0"; coreutils::MString nbrOfColumns = "0"; diff --git a/__sql.cpp b/__sql.cpp index 987e1c6..abaa648 100644 --- a/__sql.cpp +++ b/__sql.cpp @@ -8,15 +8,25 @@ #include <sys/wait.h> namespace jet { - + __sql::__sql(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { output = false; if(!hasContainer) - throw coreutils::Exception("sql tag must have a container."); - if(!global.sessionExists(keywords["sessionid"])) + throw coreutils::Exception("sql tag must have a container."); + // resolveKeyword("sessionid") already returns the resolved VALUE + // (e.g. "projman") - the query call below used to wrap that value + // back into keywords[...] as if it were a MAP KEY, auto-vivifying + // an empty string and silently running every <sql sessionid="X"> + // query (X != "") against the DEFAULT session instead of the one + // actually named - same double-indirection bug already fixed in + // __mysql.cpp's own constructor (see its own comment on this). + // Resolved once into a local so both the existence check and the + // query call agree on the real, resolved session id. + coreutils::MString sessionId = resolveKeyword("sessionid"); + if(!global.sessionExists(sessionId)) throw coreutils::Exception("sessionid does not exist."); processContainer(container); - global.getSession(keywords[resolveKeyword("sessionid")])->query(out); + global.getSession(sessionId)->query(out); } - + } diff --git a/__tag.cpp b/__tag.cpp index 620bbb2..5d2a469 100644 --- a/__tag.cpp +++ b/__tag.cpp @@ -2,7 +2,7 @@ #include "Exception.h" namespace jet { - + __tag::__tag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { evaluate = false; output = false; @@ -29,10 +29,19 @@ namespace jet { Tag *scope = parent; while(scope && !scope->isCustomTagBoundary) scope = scope->parent; + // Custom tag NAMES are matched case-insensitively at invocation + // (findTagDefinition normalizes its own lookup key the same way), + // matching the precedent already set for built-in tag names + // (ifTagName) and attribute names - real 1.4 content declares and + // invokes a custom tag in different case at different sites (e.g. + // SESSIONCTL.tag's own <TAG NAME='TEMPLATE'> definition vs + // mainpagetemplate.temp's <template>...</template> invocation of + // it). Registering under the lowercase key here is what makes that + // match. + coreutils::MString tagName = coreutils::MString(keywords["name"]).toLower(); if(scope) - scope->localTags[keywords["name"]] = container; + scope->localTags[tagName] = container; else - global.tags[keywords["name"]] = container; + global.tags[tagName] = container; } } - diff --git a/__until.cpp b/__until.cpp index 35179a0..7ef091f 100644 --- a/__until.cpp +++ b/__until.cpp @@ -21,7 +21,7 @@ namespace jet { if(!keywordDefined("type")) throw coreutils::Exception("type expected if value1 and value2 specified."); - int rc = keywords[resolveKeyword("value1")].compare(keywords[resolveKeyword("value2")]); + int rc = resolveKeyword("value1").compare(resolveKeyword("value2")); if(((keywords[resolveKeyword("type")] == "eq") && (rc == 0)) || ((keywords[resolveKeyword("type")] == "ne") && (rc != 0)) || ((keywords[resolveKeyword("type")] == "lt") && (rc == -1)) || @@ -50,7 +50,7 @@ namespace jet { booleanResult = Operand(keywords["expr"], *this).boolean; } else { booleanResult = false; - int rc = keywords[resolveKeyword("value1")].compare(keywords[resolveKeyword("value2")]); + int rc = resolveKeyword("value1").compare(resolveKeyword("value2")); if(((keywords[resolveKeyword("type")] == "eq") && (rc == 0)) || ((keywords[resolveKeyword("type")] == "ne") && (rc != 0)) || ((keywords[resolveKeyword("type")] == "lt") && (rc == -1)) || diff --git a/__while.cpp b/__while.cpp index fc998e2..30963be 100644 --- a/__while.cpp +++ b/__while.cpp @@ -21,7 +21,7 @@ namespace jet { if(!keywordDefined("type")) throw coreutils::Exception("type expected if value1 and value2 specified."); - int rc = keywords[resolveKeyword("value1")].compare(keywords[resolveKeyword("value2")]); + int rc = resolveKeyword("value1").compare(resolveKeyword("value2")); if(((keywords[resolveKeyword("type")] == "eq") && (rc == 0)) || ((keywords[resolveKeyword("type")] == "ne") && (rc != 0)) || ((keywords[resolveKeyword("type")] == "lt") && (rc == -1)) || @@ -50,7 +50,7 @@ namespace jet { booleanResult = Operand(keywords["expr"], *this).boolean; } else { booleanResult = false; - int rc = keywords[resolveKeyword("value1")].compare(keywords[resolveKeyword("value2")]); + int rc = resolveKeyword("value1").compare(resolveKeyword("value2")); if(((keywords[resolveKeyword("type")] == "eq") && (rc == 0)) || ((keywords[resolveKeyword("type")] == "ne") && (rc != 0)) || ((keywords[resolveKeyword("type")] == "lt") && (rc == -1)) || diff --git a/__whiledir.cpp b/__whiledir.cpp index aa0d70a..ab9a2af 100644 --- a/__whiledir.cpp +++ b/__whiledir.cpp @@ -9,36 +9,101 @@ namespace jet { __whiledir::__whiledir(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, this) { - if(!keywordDefined("path")) + // Real 1.4 content (barantmusic.page/davidarant.page's own + // bae1996.section) writes <whiledir dir="..." name="filename">, + // not <whiledir path="..." filename="...">. Accept "dir" as an + // alias for "path" and "name" as an alias for "filename" so that + // content runs unmodified, matching the same "accept either" + // precedent already used for <database name=>/<mysql database=>. + // Resolved once into real local VALUES (not a dynamically-chosen + // keyword NAME threaded through repeated keywordDefined()/ + // resolveKeyword() calls) - an earlier version of this fix passed + // a ZString holding just the chosen attribute name around and hit + // real, unexplained corruption (a whiledir on a real, correctly + // specified directory resolved to a garbage nonsense path instead). + // This form fixed it in isolation (a standalone whiledir_test.jet + // against this exact directory/attributes works perfectly) but the + // SAME garbage path still recurs when this exact content + // (bae1996.section) is reached through real content's actual deep + // <eval>/<dotag>/<include> nesting (40+ stack frames by that + // point) - genuinely not yet root-caused; flagged in project + // memory as still open. The try/catch below at least converts + // whatever throws there into the same graceful, catchable error + // page every other tag failure already gets, instead of a raw + // std::exception unwinding past everything and dropping the + // connection with no HTTP response at all. + coreutils::MString dirPath; + if(keywordDefined("path")) + dirPath = resolveKeyword("path"); + else if(keywordDefined("dir")) + dirPath = resolveKeyword("dir"); + else throw coreutils::Exception("whiledir tag must specify a path."); - if(keywordDefined("sort") && (resolveKeyword("sort") == "true")) { - std::vector<std::filesystem::directory_entry> entries; - for(auto const &entry : std::filesystem::directory_iterator(resolveKeyword("path").str())) - entries.push_back(entry); - std::sort(entries.begin(), entries.end(), [](const auto &a, const auto &b) { return a.path() < b.path(); }); - for(const auto &entry : entries) { - if(keywordDefined("fullpath")) - global.variables[resolveKeyword("fullpath")] = entry.path(); - if(keywordDefined("filename")) - global.variables[resolveKeyword("filename")] = entry.path().filename(); - if(keywordDefined("filenamenoextension")) - global.variables[resolveKeyword("filenamenoextension")] = entry.path().stem(); - processContainer(container); - container.reset(); - } - } else { - for(auto const &entry : std::filesystem::directory_iterator(variables[resolveKeyword("path")].str())) { - if(keywordDefined("fullpath")) - global.variables[resolveKeyword("fullpath")] = entry.path(); - if(keywordDefined("filename")) - global.variables[resolveKeyword("filename")] = entry.path().filename(); - if(keywordDefined("filenamenoextension")) - global.variables[resolveKeyword("filenamenoextension")] = entry.path().stem(); - processContainer(container); - container.reset(); - } + + // Every one of these attributes - which output variable name to + // assign into for a given field, and whether that field was even + // requested - is a static property of this <whiledir> invocation, + // fixed at construction, never changing across its own loop. The + // per-iteration body used to call keywordDefined()+resolveKeyword() + // (2 map lookups each) for fullpath/filename/filenamenoextension + // on every single directory entry - resolved once here instead, + // same reasoning as __whilerow.cpp's session-pointer hoist. + coreutils::MString filenameVarName; + bool haveFilenameVar = false; + if(keywordDefined("filename")) { + filenameVarName = resolveKeyword("filename"); + haveFilenameVar = true; + } else if(keywordDefined("name")) { + filenameVarName = resolveKeyword("name"); + haveFilenameVar = true; } - + + coreutils::MString fullpathVarName; + bool haveFullpathVar = keywordDefined("fullpath"); + if(haveFullpathVar) + fullpathVarName = resolveKeyword("fullpath"); + + coreutils::MString noExtVarName; + bool haveNoExtVar = keywordDefined("filenamenoextension"); + if(haveNoExtVar) + noExtVarName = resolveKeyword("filenamenoextension"); + + bool sorted = keywordDefined("sort") && (resolveKeyword("sort") == "true"); + + try { + if(sorted) { + std::vector<std::filesystem::directory_entry> entries; + for(auto const &entry : std::filesystem::directory_iterator(dirPath.str())) + entries.push_back(entry); + std::sort(entries.begin(), entries.end(), [](const auto &a, const auto &b) { return a.path() < b.path(); }); + for(const auto &entry : entries) { + if(haveFullpathVar) + global.variables[fullpathVarName] = entry.path(); + if(haveFilenameVar) + global.variables[filenameVarName] = entry.path().filename(); + if(haveNoExtVar) + global.variables[noExtVarName] = entry.path().stem(); + processContainer(container); + container.reset(); + } + } else { + for(auto const &entry : std::filesystem::directory_iterator(dirPath.str())) { + if(haveFullpathVar) + global.variables[fullpathVarName] = entry.path(); + if(haveFilenameVar) + global.variables[filenameVarName] = entry.path().filename(); + if(haveNoExtVar) + global.variables[noExtVarName] = entry.path().stem(); + processContainer(container); + container.reset(); + } + } + } catch(std::exception &e) { + coreutils::MString msg("whiledir failed: "); + msg << e.what(); + throw coreutils::Exception(msg.str()); + } + } - + } diff --git a/__whilerow.cpp b/__whilerow.cpp index 5e2caef..877de41 100644 --- a/__whilerow.cpp +++ b/__whilerow.cpp @@ -6,16 +6,35 @@ namespace jet { __whilerow::__whilerow(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) { - - int count = keywords["count"].asInteger(); - - while ((count != 0) && global.getSession(keywords["sessionid"])->hasRow()) { + + // A bare <whilerow> (no count= attribute at all - the overwhelming + // majority of real usage: modular1_patches.page, account.page, + // task.page, project.page, editinvoice.page, musicianhome.page, + // invoice.method, modular1_1_preset_admin.page) used to default + // count to 0 - keywords["count"] auto-vivifies an empty string for + // a missing attribute, and "".asInteger() is 0 - which made the + // loop's own (count != 0) condition false on the very first check, + // before hasRow() was ever even consulted. Every one of those + // pages' row-based content silently rendered nothing, for every + // request, this entire session - never caught because nothing + // that exercised a bare <whilerow> with real row data also had its + // actual rendered output inspected until now. count= is meant as + // an OPTIONAL cap ("show at most N rows"); absent, it must mean + // unlimited, not zero - -1 as a sentinel keeps working with the + // existing (count != 0) loop test forever (decrementing through + // negative numbers never hits 0), letting hasRow() alone decide + // when the loop ends, exactly as bare usage always intended. + int count = keywordDefined("count") ? keywords["count"].asInteger() : -1; + + __mysql *session = global.getSession(resolveKeyword("sessionid")); + + while((count != 0) && session->hasRow()) { processContainer(container); container.reset(); - global.getSession(keywords["sessionid"])->nextRow(); + session->nextRow(); --count; } - + } - + } diff --git a/__write.cpp b/__write.cpp index ff7504b..fc7ea06 100644 --- a/__write.cpp +++ b/__write.cpp @@ -24,19 +24,49 @@ namespace jet { throw coreutils::Exception("write tag must have a value, expr or a container."); if(!keywordDefined("mode")) throw coreutils::Exception("write tag must have a mode keyword."); - if(keywords[resolveKeyword("mode")] == "append") + // resolveKeyword("mode") already returns the resolved VALUE (e.g. + // "overwrite") - re-indexing keywords[...] with that value treats + // it as if it were a MAP KEY instead (auto-vivifying an empty + // string, since no keyword is literally named "overwrite"/ + // "append"), so this comparison was false for every real input + // and <write> threw "mode keyword must be..." unconditionally. + // Same double-indirection bug already fixed elsewhere in this + // codebase (__mysql.cpp's own constructor, __sql.cpp's sessionid + // handling) - confirmed via a real repro building the jet-2.0 + // htmleditor port's file-based storage. + coreutils::MString modeValue = resolveKeyword("mode"); + if(modeValue == "append") mode = O_APPEND; - else if(keywords[resolveKeyword("mode")] == "overwrite") + else if(modeValue == "overwrite") mode = O_TRUNC; else throw coreutils::Exception("mode keyword must be 'overwrite' or 'append'."); - int fd = open(keywords[resolveKeyword("file")].c_str(), mode, 0644); // TODO: Need to add O_CREAT and AUTH flags. + // mode alone (O_APPEND or O_TRUNC) has no access-mode bits at + // all - open()'s access mode defaults to O_RDONLY (0) unless + // O_WRONLY/O_RDWR is explicitly OR'd in, so the write() call below + // was failing on every real invocation (EBADF - can't write a + // read-only fd), and without O_CREAT this could never create a + // NEW file either, only (theoretically) write an existing one. + // Confirmed via a real repro: <write file="..." mode="overwrite"> + // silently produced a 0-byte or missing file every time. + // Same double-indirection bug as "mode" above. + int fd = open(resolveKeyword("file").c_str(), mode | O_WRONLY | O_CREAT, 0644); // TODO: Need to add AUTH flags. if(hasContainer && !evaluate) len = write(fd, container.getData(), container.getLength()); else if(hasContainer && evaluate) len = write(fd, out.getData(), out.getLength()); - else if(!hasContainer && keywordDefined("value")) - len = write(fd, variables[resolveKeyword("value")].getData(), keywords[resolveKeyword("value")].getLength()); + else if(!hasContainer && keywordDefined("value")) { + // Same double-indirection bug as "mode"/"file" above: + // variables[resolveKeyword("value")] used the resolved VALUE + // itself as a lookup key into global.variables (a DIFFERENT map + // entirely), not the literal value to write - looking up a + // variable literally named e.g. "hello world" auto-vivifies an + // empty entry, so this wrote 0 bytes for every real invocation. + // Confirmed via a real repro: <write value="hello world" + // mode="overwrite"> produced a genuinely empty file. + coreutils::MString writeValue = resolveKeyword("value"); + len = write(fd, writeValue.getData(), writeValue.getLength()); + } else if(!hasContainer && keywordDefined("expr")) len = write(fd, keywords["expr"].getData(), keywords["expr"].getLength()); close(fd); diff --git a/jetc/JetAST.h b/jetc/JetAST.h index 7c13ecc..8a737e0 100755 --- a/jetc/JetAST.h +++ b/jetc/JetAST.h @@ -8,7 +8,7 @@ namespace jetc { enum class NodeKind { - TEXT, VAR, SET, IF, FOR, WHILE, FALLBACK + TEXT, VAR, SET, IF, FOR, WHILE, FALLBACK, TAGDEF, CONTAINER_SLOT }; enum class CondMode { VALUES, EXPR }; @@ -55,6 +55,24 @@ namespace jetc { std::string forScope; std::vector<NodePtr> body; // also used by WHILE + // TAGDEF - a top-level <tag name="X">...</tag> custom-tag definition + // (only recognized at the file's true top level - see Parser::topLevel; + // one nested inside another tag's own definition body stays FALLBACK, + // unchanged). tagDefName is already lowercased, matching __tag.cpp's + // own registration key. tagDefBody is the compiled definition body + // (may itself contain CONTAINER_SLOT nodes for <container> markers, + // and FALLBACK leaves for anything still unsupported). rawSpan + // (shared with FALLBACK, below) still holds the exact original source + // text - codegen still emits the same raw interpretSpan-based + // registration into global.tags alongside the new compiled fast path, + // so anything that reaches this tag some other way keeps working + // exactly as it does today. + std::string tagDefName; + std::vector<NodePtr> tagDefBody; + + // CONTAINER_SLOT - a <container> marker (see Tag::renderContainer) - + // a pure leaf, no extra fields. + // FALLBACK - exact original source text of one unsupported tag std::string rawSpan; }; diff --git a/jetc/JetCodegen.cpp b/jetc/JetCodegen.cpp index 3dc16cf..ba9fafa 100644 --- a/jetc/JetCodegen.cpp +++ b/jetc/JetCodegen.cpp @@ -49,124 +49,181 @@ namespace jetc { return expr.c_str(); } - void Codegen::emitBody(const std::vector<NodePtr> &nodes, int indent) { - for (auto &n : nodes) emitNode(n, indent); + void Codegen::emitBody(const std::vector<NodePtr> &nodes, int indent, std::ostringstream &target) { + for (auto &n : nodes) emitNode(n, indent, target); } - void Codegen::emitNode(const NodePtr &n, int indent) { + void Codegen::emitNode(const NodePtr &n, int indent, std::ostringstream &target) { std::string I = ind(indent); switch (n->kind) { case NodeKind::TEXT: { if (n->text.empty()) break; - out << I << "out << " << cstr(n->text) << ";\n"; + target << I << "out << " << cstr(n->text) << ";\n"; break; } case NodeKind::VAR: { - out << I << "{ coreutils::ZString v(" << cstr(n->varText) << "); out << scope.getVariable(v, true); }\n"; + target << I << "{ coreutils::ZString v(" << cstr(n->varText) << "); out << scope.getVariable(v, true); }\n"; break; } case NodeKind::SET: { std::string k = id("set_"); - out << I << "{\n"; - out << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->setName) << ";\n"; - out << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->setScope) << ";\n"; - out << I << " coreutils::MString val;\n"; + target << I << "{\n"; + target << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->setName) << ";\n"; + target << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->setScope) << ";\n"; + target << I << " coreutils::MString val;\n"; if (n->setHasExpr) { - out << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->setExpr) << ";\n"; - out << I << " val = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).string;\n"; + target << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->setExpr) << ";\n"; + target << I << " val = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).string;\n"; } else { - out << I << " scope.keywords[\"" << k << "value\"] = " << cstr(n->setHasValue ? n->setValue : std::string()) << ";\n"; - out << I << " val = scope.resolveKeyword(\"" << k << "value\");\n"; + target << I << " scope.keywords[\"" << k << "value\"] = " << cstr(n->setHasValue ? n->setValue : std::string()) << ";\n"; + target << I << " val = scope.resolveKeyword(\"" << k << "value\");\n"; } - out << I << " scope.assignVariable(scope.resolveKeyword(\"" << k << "name\"), val, scope.resolveKeyword(\"" << k << "scope\"));\n"; - out << I << "}\n"; + target << I << " scope.assignVariable(scope.resolveKeyword(\"" << k << "name\"), val, scope.resolveKeyword(\"" << k << "scope\"));\n"; + target << I << "}\n"; break; } case NodeKind::IF: { std::string k = id("if_"); - out << I << "{\n"; - out << I << " bool cond;\n"; + target << I << "{\n"; + target << I << " bool cond;\n"; if (n->cond.mode == CondMode::EXPR) { - out << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n"; - out << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n"; + target << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n"; + target << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n"; } else { - out << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n"; - out << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n"; - out << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n"; - out << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n"; + target << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n"; + target << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n"; + target << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n"; + target << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n"; } - out << I << " if (cond) {\n"; - emitBody(n->thenBody, indent + 2); + target << I << " if (cond) {\n"; + emitBody(n->thenBody, indent + 2, target); if (n->hasElse) { - out << I << " } else {\n"; - emitBody(n->elseBody, indent + 2); + target << I << " } else {\n"; + emitBody(n->elseBody, indent + 2, target); } - out << I << " }\n"; - out << I << "}\n"; + target << I << " }\n"; + target << I << "}\n"; break; } case NodeKind::FOR: { std::string k = id("for_"); - out << I << "{\n"; - out << I << " scope.keywords[\"" << k << "start\"] = " << cstr(n->forHasStart ? n->forStart : std::string("0")) << ";\n"; - out << I << " scope.keywords[\"" << k << "end\"] = " << cstr(n->forEnd) << ";\n"; - out << I << " scope.keywords[\"" << k << "step\"] = " << cstr(n->forStep) << ";\n"; - out << I << " double ix = scope.resolveKeyword(\"" << k << "start\").asDouble();\n"; + target << I << "{\n"; + target << I << " scope.keywords[\"" << k << "start\"] = " << cstr(n->forHasStart ? n->forStart : std::string("0")) << ";\n"; + target << I << " scope.keywords[\"" << k << "end\"] = " << cstr(n->forEnd) << ";\n"; + target << I << " scope.keywords[\"" << k << "step\"] = " << cstr(n->forStep) << ";\n"; + target << I << " double ix = scope.resolveKeyword(\"" << k << "start\").asDouble();\n"; if (n->forHasName) { - out << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->forName) << ";\n"; - out << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->forScope) << ";\n"; + target << I << " scope.keywords[\"" << k << "name\"] = " << cstr(n->forName) << ";\n"; + target << I << " scope.keywords[\"" << k << "scope\"] = " << cstr(n->forScope) << ";\n"; } - out << I << " for (; ix <= scope.resolveKeyword(\"" << k << "end\").asDouble(); ix += scope.resolveKeyword(\"" << k << "step\").asDouble()) {\n"; + target << I << " for (; ix <= scope.resolveKeyword(\"" << k << "end\").asDouble(); ix += scope.resolveKeyword(\"" << k << "step\").asDouble()) {\n"; if (n->forHasName) { - out << I << " coreutils::MString nameSel = scope.resolveKeyword(\"" << k << "name\");\n"; - out << I << " coreutils::MString scopeSel = scope.resolveKeyword(\"" << k << "scope\");\n"; - out << I << " if ((scopeSel == \"\") || (scopeSel == \"global\")) global.variables[nameSel] = ix;\n"; - out << I << " else if (scopeSel == \"local\") scope.local->variables[nameSel] = ix;\n"; - out << I << " else if (scopeSel == \"parent\") scope.parent->local->variables[nameSel] = ix;\n"; - out << I << " else throw coreutils::Exception(\"scope value is not valid.\");\n"; + target << I << " coreutils::MString nameSel = scope.resolveKeyword(\"" << k << "name\");\n"; + target << I << " coreutils::MString scopeSel = scope.resolveKeyword(\"" << k << "scope\");\n"; + target << I << " if ((scopeSel == \"\") || (scopeSel == \"global\")) global.variables[nameSel] = ix;\n"; + target << I << " else if (scopeSel == \"local\") scope.local->variables[nameSel] = ix;\n"; + target << I << " else if (scopeSel == \"parent\") scope.parent->local->variables[nameSel] = ix;\n"; + target << I << " else throw coreutils::Exception(\"scope value is not valid.\");\n"; } - emitBody(n->body, indent + 2); - out << I << " }\n"; - out << I << "}\n"; + emitBody(n->body, indent + 2, target); + target << I << " }\n"; + target << I << "}\n"; break; } case NodeKind::WHILE: { std::string k = id("while_"); - out << I << "{\n"; - out << I << " bool cond;\n"; + target << I << "{\n"; + target << I << " bool cond;\n"; if (n->cond.mode == CondMode::EXPR) { - out << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n"; - out << I << " coreutils::MString exprSaved = scope.keywords[\"" << k << "expr\"];\n"; - out << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n"; - out << I << " while (cond) {\n"; - emitBody(n->body, indent + 2); - out << I << " scope.keywords[\"" << k << "expr\"].reset();\n"; - out << I << " scope.keywords[\"" << k << "expr\"] = exprSaved;\n"; - out << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n"; - out << I << " }\n"; + target << I << " scope.keywords[\"" << k << "expr\"] = " << cstr(n->cond.expr) << ";\n"; + target << I << " coreutils::MString exprSaved = scope.keywords[\"" << k << "expr\"];\n"; + target << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n"; + target << I << " while (cond) {\n"; + emitBody(n->body, indent + 2, target); + target << I << " scope.keywords[\"" << k << "expr\"].reset();\n"; + target << I << " scope.keywords[\"" << k << "expr\"] = exprSaved;\n"; + target << I << " cond = jet::Operand(scope.keywords[\"" << k << "expr\"], scope).boolean;\n"; + target << I << " }\n"; } else { - out << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n"; - out << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n"; - out << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n"; - out << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n"; - out << I << " while (cond) {\n"; - emitBody(n->body, indent + 2); - out << I << " int rc2 = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n"; - out << I << " cond = (" << staticTypeOp(n->cond.type, "rc2") << ");\n"; - out << I << " }\n"; + target << I << " scope.keywords[\"" << k << "value1\"] = " << cstr(n->cond.value1) << ";\n"; + target << I << " scope.keywords[\"" << k << "value2\"] = " << cstr(n->cond.value2) << ";\n"; + target << I << " int rc = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n"; + target << I << " cond = (" << staticTypeOp(n->cond.type, "rc") << ");\n"; + target << I << " while (cond) {\n"; + emitBody(n->body, indent + 2, target); + target << I << " int rc2 = scope.resolveKeyword(\"" << k << "value1\").compare(scope.resolveKeyword(\"" << k << "value2\"));\n"; + target << I << " cond = (" << staticTypeOp(n->cond.type, "rc2") << ");\n"; + target << I << " }\n"; } - out << I << "}\n"; + target << I << "}\n"; break; } case NodeKind::FALLBACK: { - out << I << "{ coreutils::ZString span(" << cstr(n->rawSpan) << "); scope.interpretSpan(span, out); }\n"; + target << I << "{ coreutils::ZString span(" << cstr(n->rawSpan) << "); scope.interpretSpan(span, out); }\n"; + break; + } + + case NodeKind::CONTAINER_SLOT: { + // Reuses the interpreter's own <container> scope-walking logic + // verbatim (see Tag::renderContainer) rather than re-deriving + // it - this is exactly what the runtime's own <container> + // dispatch does when it's reached via interpretSpan, just + // without the text-scanning step to get there. + target << I << "scope.renderContainer(out);\n"; + break; + } + + case NodeKind::TAGDEF: { + // Codegen can still fail for a node that parsed successfully - + // e.g. an <if>/<while> whose type= isn't one of eq/ne/lt/le/gt/ge + // (staticTypeOp's own check - real legacy content occasionally + // has one of these sitting in a branch that's never actually + // reached at runtime either, so there's nothing to "correctly" + // compile). Parsing this definition's body already succeeded + // (see parseTagDef's own try/catch for that stage) or this node + // wouldn't exist as TAGDEF at all - so mirror that same + // graceful-degradation approach here for the emit stage: try + // building the compiled function into a throwaway stream first, + // and only splice it into tagDefFuncs (and register the + // compiledTags fast path) if that fully succeeds. A failure + // here just means this one definition stays interpreted, + // unchanged - zero risk to the rest of the file. + std::string funcName = "jetc_tagdef_" + id(""); + std::ostringstream trial; + bool compiledOk = true; + try { + trial << "static void " << funcName << "(jet::Tag &scope, coreutils::MString &out) {\n"; + emitBody(n->tagDefBody, 1, trial); + trial << "}\n\n"; + } catch (const std::exception &) { + compiledOk = false; + } + + // Emitted in place, at this TAGDEF's original position in + // program flow - matches exactly what FALLBACK would have + // emitted for this same <tag name="X">...</tag> span, so + // global.tags["x"] still gets populated as before (needed as a + // correctness fallback: any invocation this compiler doesn't + // itself recognize - e.g. reached through content this pass + // never compiled - still resolves it exactly as it always has). + target << I << "{ coreutils::ZString span(" << cstr(n->rawSpan) << "); scope.interpretSpan(span, out); }\n"; + if (compiledOk) { + // The registration into compiledTags right after is the only + // new behavior beyond the raw registration above - a pure + // additive fast path __dotag checks first (see __dotag.cpp); + // a miss there falls through to the now-unchanged interpreted + // path, which is exactly what happens when compiledOk is + // false here too. + tagDefFuncs << trial.str(); + target << I << "global.compiledTags[coreutils::MString(" << cstr(n->tagDefName) << ")] = &" << funcName << ";\n"; + } break; } } @@ -177,6 +234,9 @@ namespace jetc { const std::vector<ApiRoute> &apiRoutes, const std::string &apiSymbol) { out.str(""); out.clear(); + tagDefFuncs.str(""); + tagDefFuncs.clear(); + out << "// " << sourceFileComment << "\n"; out << "// Generated by jetc - DO NOT EDIT BY HAND.\n"; out << "#include \"Global.h\"\n"; @@ -187,51 +247,61 @@ namespace jetc { out << "#include \"Exception.h\"\n"; out << "#include <iostream>\n\n"; + // The entry function's own text is built into a separate stream so + // tagDefFuncs - populated as a SIDE EFFECT of the emitBody() call + // below, since TAGDEF nodes append to it as they're encountered - + // can be spliced in ahead of it. A compiled tag-definition function + // never needs to be forward-declared for anything defined before it + // in program order (nothing upstream calls it directly - see + // __dotag.cpp's runtime lookup instead), so this ordering is purely + // for readability, not a correctness requirement. + std::ostringstream body; + if (sharedMode) { if (!apiRoutes.empty()) { // A plain C struct, not any CoreUtils/JetCore type - this is the // one piece of the plugin ABI a host loader needs to know about // independent of the rest of this same-ABI C++ boundary. - out << "struct JetApiRoute { const char *method; const char *path; };\n"; - out << "extern \"C\" const JetApiRoute *" << apiSymbol << "(int *count) {\n"; - out << " static const JetApiRoute routes[] = {\n"; + body << "struct JetApiRoute { const char *method; const char *path; };\n"; + body << "extern \"C\" const JetApiRoute *" << apiSymbol << "(int *count) {\n"; + body << " static const JetApiRoute routes[] = {\n"; for (auto &r : apiRoutes) - out << " { " << cstr(r.method) << ", " << cstr(r.path) << " },\n"; - out << " };\n"; - out << " *count = " << apiRoutes.size() << ";\n"; - out << " return routes;\n"; - out << "}\n\n"; + body << " { " << cstr(r.method) << ", " << cstr(r.path) << " },\n"; + body << " };\n"; + body << " *count = " << apiRoutes.size() << ";\n"; + body << " return routes;\n"; + body << "}\n\n"; } - out << "extern \"C\" void " << entrySymbol << "(jet::Global &global, coreutils::MString &out) {\n"; - out << " coreutils::ZString jetc_empty(\"\");\n"; - out << " coreutils::MString jetc_scopeOut;\n"; - out << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n"; - emitBody(top, 1); - out << "}\n"; - return out.str(); + body << "extern \"C\" void " << entrySymbol << "(jet::Global &global, coreutils::MString &out) {\n"; + body << " coreutils::ZString jetc_empty(\"\");\n"; + body << " coreutils::MString jetc_scopeOut;\n"; + body << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n"; + emitBody(top, 1, body); + body << "}\n"; + return out.str() + tagDefFuncs.str() + body.str(); } - out << "static void jetc_render(jet::Global &global, coreutils::MString &out) {\n"; - out << " coreutils::ZString jetc_empty(\"\");\n"; - out << " coreutils::MString jetc_scopeOut;\n"; - out << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n"; - emitBody(top, 1); - out << "}\n\n"; - out << "int main(int argc, char **argv, char **envp) {\n"; - out << " jet::Global global(envp);\n"; - out << " coreutils::MString out;\n"; - out << " try {\n"; - out << " jetc_render(global, out);\n"; - out << " global.outputHeaders();\n"; - out << " std::cout << out;\n"; - out << " } catch(coreutils::Exception e) {\n"; - out << " std::cout << out;\n"; - out << " std::cout << \"\\n----- Error: \" << e.text << std::endl;\n"; - out << " return 1;\n"; - out << " }\n"; - out << " return 0;\n"; - out << "}\n"; - return out.str(); + body << "static void jetc_render(jet::Global &global, coreutils::MString &out) {\n"; + body << " coreutils::ZString jetc_empty(\"\");\n"; + body << " coreutils::MString jetc_scopeOut;\n"; + body << " jet::Tag scope(jetc_empty, jetc_scopeOut, global, NULL, NULL);\n"; + emitBody(top, 1, body); + body << "}\n\n"; + body << "int main(int argc, char **argv, char **envp) {\n"; + body << " jet::Global global(envp);\n"; + body << " coreutils::MString out;\n"; + body << " try {\n"; + body << " jetc_render(global, out);\n"; + body << " global.outputHeaders();\n"; + body << " std::cout << out;\n"; + body << " } catch(coreutils::Exception e) {\n"; + body << " std::cout << out;\n"; + body << " std::cout << \"\\n----- Error: \" << e.text << std::endl;\n"; + body << " return 1;\n"; + body << " }\n"; + body << " return 0;\n"; + body << "}\n"; + return out.str() + tagDefFuncs.str() + body.str(); } } diff --git a/jetc/JetCodegen.h b/jetc/JetCodegen.h index 31a60d2..e1d3149 100644 --- a/jetc/JetCodegen.h +++ b/jetc/JetCodegen.h @@ -62,11 +62,28 @@ namespace jetc { private: std::ostringstream out; + + // Free functions generated for compiled <tag name="X"> definitions + // (see the TAGDEF case in emitNode) accumulate here, in the order + // encountered, and are spliced into the final output ahead of the + // entry function (see generate()) - C++ doesn't care about + // definition order within one translation unit as long as nothing + // outside of it needs a forward declaration, but keeping them + // grouped together ahead of the entry point makes the generated + // file easier to read/debug than interleaving them positionally. + std::ostringstream tagDefFuncs; + size_t nextId = 0; std::string id(const std::string &prefix) { return prefix + std::to_string(nextId++) + "_"; } - void emitBody(const std::vector<NodePtr> &nodes, int indent); - void emitNode(const NodePtr &n, int indent); + // target is the stream this node's statements are written to - `out` + // for everything at the top level or nested inside ordinary control + // flow (IF/FOR/WHILE bodies stay in the same stream as their + // enclosing scope), but a TAGDEF's own body is rendered into a + // fresh stream to become a separate generated function (see + // emitNode's TAGDEF case) rather than being inlined in place. + void emitBody(const std::vector<NodePtr> &nodes, int indent, std::ostringstream &target); + void emitNode(const NodePtr &n, int indent, std::ostringstream &target); static std::string ind(int indent); static std::string cstr(const std::string &raw); // C++ string-literal escape static const char *staticTypeOp(const std::string &type, const char *rcVar); diff --git a/jetc/JetParser.cpp b/jetc/JetParser.cpp index 643af20..537259e 100755 --- a/jetc/JetParser.cpp +++ b/jetc/JetParser.cpp @@ -1,6 +1,11 @@ #include "JetParser.h" #include <cctype> #include <stdexcept> +#include <set> +#include <cstdio> +#include <fstream> +#include <sstream> +#include <cstdio> namespace jetc { @@ -29,37 +34,148 @@ namespace jetc { return s.substr(start, p - start); } + namespace { + // jet-1.4 (the language this parser also has to accept, alongside + // real 2.0 source) was fully case-insensitive on tag names - the + // production source mixes case freely, even within one tag's own + // body (e.g. <SET ...> and <set ...> side by side). Tag names are + // matched case-insensitively throughout this parser for that + // reason; nothing else (attribute values, text content) is + // affected by this helper. + bool ciEquals(const char *a, size_t alen, const std::string &b) { + if (alen != b.size()) return false; + for (size_t i = 0; i < alen; i++) + if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i])) + return false; + return true; + } + } + bool Parser::matchTagOpen(const std::string &name, size_t at) const { if (s.compare(at, 1, "<") != 0) return false; size_t p = at + 1; size_t nstart = p; while (p < s.size() && isNameChar(s[p])) p++; - if (s.compare(nstart, p - nstart, name) != 0) return false; + if (!ciEquals(s.data() + nstart, p - nstart, name)) return false; if (p >= s.size()) return false; char c = s[p]; return c == ' ' || c == '>' || c == '/'; } bool Parser::matchTagClose(const std::string &name, size_t at) const { - std::string closer = "</" + name + ">"; - return s.compare(at, closer.size(), closer) == 0; + if (s.compare(at, 2, "</") != 0) return false; + size_t p = at + 2; + size_t nstart = p; + while (p < s.size() && isNameChar(s[p])) p++; + if (!ciEquals(s.data() + nstart, p - nstart, name)) return false; + return p < s.size() && s[p] == '>'; } - bool Parser::skipOpenTag(size_t &p) const { + namespace { + // jet-1.4's real parser knew which tags never take a body (its own + // "void element" list) - kept here, ahead of skipOpenTag/ + // skipEmbeddedTag/parseFallback, since all of them need it. + bool isVoidTag(const std::string &name) { + static const std::set<std::string> voidTags = { + // jet's own built-ins that never take a body. exclude is + // NOT here - __exclude.cpp's own constructor requires a + // real body (throws "exclude must have a container" + // otherwise), it was previously miscategorized as void. + "set", "include", "cookie", "header", "read", "write", + "call", "system", "dump", + // <container> (see parseContainerSlot/Tag::renderContainer) - + // real content always writes it as a bare marker with no + // separate closing tag, even when it carries its own + // attributes (e.g. SESSIONCTL.tag's own + // <CONTAINER EVAL="NO">) - without this it was previously only + // reachable via the generic FALLBACK path (scanBody would + // incorrectly scan forward for a "</container>" that never + // exists, consuming unrelated following content). + "container", + // standard HTML5 void elements - real 1.4-era content (e.g. + // SESSIONCTL.tag) writes these the classic HTML way too, + // bare '>' and never a closing tag. + "br", "img", "input", "hr", "meta", "link", "area", "base", + "col", "embed", "source", "track", "wbr", + // <else> is a marker inside an <if>/<while>/<until> body + // (scanBody's own lookForElse handles it at the top level) - + // it never has its own close tag, so when it shows up + // embedded (inside a quoted attribute or another tag's + // attribute region, same as <if> does in <linkbutton>) it + // needs the same void treatment. + "else" + }; + std::string lower = name; + for (char &c : lower) c = (char)std::tolower((unsigned char)c); + return voidTags.count(lower) != 0; + } + } + + // Real 1.4 content nests real jet tags in two places a plain + // quote/angle-bracket scanner can't handle on its own: + // 1. Inside a quoted attribute value - SESSIONCTL.tag's <linkbutton> + // has HREF="javascript: <IF VALUE1="..." VALUE2="" TYPE=NE> ... + // </IF> ... " - the outer string and the embedded tag's own + // attributes reuse the same quote character with no escaping. + // 2. Inside another tag's OWN opening-tag attribute region, before + // its closing '>' - linkbutton's <A ...> has a conditional + // <if value1="#[CLASS]" value2="" type="ne">CLASS="#[CLASS]"</if> + // sitting between its other attributes and its own '>' - the + // literal '>' that ends the embedded <if ...>'s OWN opening tag + // would otherwise be mistaken for <A>'s closing '>'. + // skipEmbeddedTag() skips one such nested tag as a whole unit - + // its own attributes (recursing through skipOpenTag, so an embedded + // tag's embedded tag works too) and, if it isn't a void tag and wasn't + // self-closed, its real body up through its own matching close tag + // (via the same matchTagOpen/matchTagClose depth-tracking scanBody + // uses at the top level). skipOpenTag's own attribute scan and + // skipQuotedValue both call this for anything that looks like a + // nested tag, instead of treating it as ordinary content. + void Parser::skipEmbeddedTag(size_t &p) { + size_t nameStart = p + 1; + size_t nameEnd = nameStart; + while (nameEnd < s.size() && isNameChar(s[nameEnd])) nameEnd++; + std::string tagName = s.substr(nameStart, nameEnd - nameStart); + bool selfClosing = skipOpenTag(p); + if (!selfClosing && !isVoidTag(tagName)) { + auto span = scanBody(p, tagName, false); + p = span.first + 3 + tagName.size(); + } + } + + void Parser::skipQuotedValue(size_t &p) { + p++; // opening '"' + while (p < s.size()) { + if (s[p] == '"') { p++; return; } + if (s[p] == '\\' && p + 1 < s.size()) { p += 2; continue; } + if (s[p] == '<' && p + 1 < s.size() && isNameChar(s[p + 1])) { + skipEmbeddedTag(p); + continue; + } + if (s[p] == '<' && s.compare(p, 2, "</") == 0) { + p += 2; + while (p < s.size() && isNameChar(s[p])) p++; + if (p < s.size() && s[p] == '>') p++; + continue; + } + p++; + } + } + + bool Parser::skipOpenTag(size_t &p) { p++; // '<' while (p < s.size() && isNameChar(s[p])) p++; while (p < s.size()) { if (s[p] == '"') { - p++; - while (p < s.size() && s[p] != '"') { - if (s[p] == '\\' && p + 1 < s.size()) p++; - p++; - } - if (p < s.size()) p++; + skipQuotedValue(p); continue; } if (s.compare(p, 2, "/>") == 0) { p += 2; return true; } if (s[p] == '>') { p += 1; return false; } + if (s[p] == '<' && p + 1 < s.size() && isNameChar(s[p + 1])) { + skipEmbeddedTag(p); + continue; + } p++; } throw std::runtime_error("jetc: unterminated tag near end of input"); @@ -74,14 +190,40 @@ namespace jetc { if (starts(">")) { pos += 1; hasContainer = true; break; } std::string aname = readName(); if (aname.empty()) throw std::runtime_error("jetc: malformed attribute near offset " + std::to_string(pos)); - if (starts("=\"")) { - pos += 2; + if (starts("=")) { + pos += 1; std::string val; - while (!eof() && s[pos] != '"') { - if (s[pos] == '\\' && pos + 1 < s.size()) { val += s[pos]; val += s[pos + 1]; pos += 2; continue; } - val += s[pos++]; + if (!eof() && s[pos] == '"') { + pos++; + while (!eof() && s[pos] != '"') { + if (s[pos] == '\\' && pos + 1 < s.size()) { val += s[pos]; val += s[pos + 1]; pos += 2; continue; } + val += s[pos++]; + } + if (!eof()) pos++; // closing quote + } else if (!eof() && s[pos] == '\'') { + // Single-quoted value - real 1.4 content writes both + // (SESSIONCTL.tag's own name='tmp_#[name]' alongside + // ordinary name="..." elsewhere in the same file). + pos++; + while (!eof() && s[pos] != '\'') { + if (s[pos] == '\\' && pos + 1 < s.size()) { val += s[pos]; val += s[pos + 1]; pos += 2; continue; } + val += s[pos++]; + } + if (!eof()) pos++; // closing quote + } else { + // Unquoted bareword value - real 1.4 content writes this + // too (type=eq, format=tobrowser, TYPE=NE throughout + // SESSIONCTL.tag) - the runtime interpreter's own + // attribute scanning already tolerates it; jetc's own + // parser previously didn't, since nothing had ever + // recursively parsed a tag definition's own body deeply + // enough to reach one before (see parseTagDef). Runs to + // the next whitespace or the tag's own closing '>'/'/>' . + while (!eof() && !std::isspace((unsigned char)s[pos]) && s[pos] != '>' && + !(s[pos] == '/' && pos + 1 < s.size() && s[pos + 1] == '>')) { + val += s[pos++]; + } } - if (!eof()) pos++; // closing quote attrs[aname] = val; } } @@ -126,7 +268,40 @@ namespace jetc { } std::vector<NodePtr> Parser::parseSub(size_t start, size_t end) { - Parser sub(s.substr(start, end - start)); + // Always topLevel=false - an <if>/<for>/<while> body is never the + // file's true top level, regardless of what topLevel this Parser + // itself was constructed with (see parseTagDef/isVoidTag's doc + // comments - a <tag name="X"> found here stays FALLBACK, unchanged). + Parser sub(s.substr(start, end - start), baseDir, includeDepth, false); + return sub.parseNodes(); + } + + std::vector<NodePtr> Parser::parseInclude() { + pos += 8; // "<include" + bool hasContainer; + // <include> is always void, the same way <set> is (see parseSet) - + // real content writes it bare (<include file="X">, no explicit + // '/>'), which parseAttrs reports as hasContainer=true since it + // can't tell that apart from a real container tag on syntax alone. + auto attrs = parseAttrs(hasContainer); + (void)hasContainer; + auto it = attrs.find("file"); + if (it == attrs.end()) + throw std::runtime_error("jetc: <include> must have a file attribute."); + if (includeDepth > 32) + throw std::runtime_error("jetc: <include> depth exceeded (possible include cycle) resolving " + it->second); + std::string fullPath = baseDir.empty() ? it->second : baseDir + "/" + it->second; + std::ifstream in(fullPath); + if (!in) + throw std::runtime_error("jetc: <include> cannot open " + fullPath); + std::ostringstream ss; + ss << in.rdbuf(); + // Inherits the includer's own topLevel - an <include> spliced in at + // the file's true top level reads as if its content were written + // inline there (see this method's own doc comment), so a <tag + // name="X"> defined in the included file is just as eligible for + // compilation as one written directly in the includer. + Parser sub(ss.str(), baseDir, includeDepth + 1, topLevel); return sub.parseNodes(); } @@ -146,21 +321,19 @@ namespace jetc { } NodePtr Parser::parseSet() { - size_t origStart = pos; pos += 4; // "<set" bool hasContainer; auto attrs = parseAttrs(hasContainer); - if (hasContainer) { - // Container-form <set> (value comes from rendering nested content) - // isn't modeled by this prototype - fall back to full interpretation - // of the whole tag instead of failing to compile. - auto span = scanBody(pos, "set", false); - pos = span.first + 3 + 3; // "</set>" - auto n = std::make_shared<Node>(); - n->kind = NodeKind::FALLBACK; - n->rawSpan = s.substr(origStart, pos - origStart); - return n; - } + // jet-1.4's <set name=... value=... expr=...> is always a plain + // attribute-based assignment - it never had a container/body form. + // Real 1.4 content (config.cfg, SESSIONCTL.tag, throughout) writes + // it with a bare closing '>' and no explicit </set>, which is + // syntactically indistinguishable from a (speculative, and per the + // comment this replaced, never actually implemented) 2.0 + // container-form <set>...</set>. Since the container form was never + // real to begin with, <set> is always treated as self-closing here, + // matching 1.4's actual, exclusive behavior rather than guessing. + (void)hasContainer; auto n = std::make_shared<Node>(); n->kind = NodeKind::SET; n->setName = attrs.count("name") ? attrs["name"] : ""; @@ -246,7 +419,7 @@ namespace jetc { std::string tagName = peekTagName(pos); size_t p = pos; bool selfClosing = skipOpenTag(p); - if (!selfClosing) { + if (!selfClosing && !isVoidTag(tagName)) { auto span = scanBody(p, tagName, false); p = span.first + 3 + tagName.size(); } @@ -257,6 +430,94 @@ namespace jetc { return n; } + NodePtr Parser::parseTagDef() { + size_t origStart = pos; + pos += 4; // "<tag" + bool hasContainer; + auto attrs = parseAttrs(hasContainer); + std::string name = attrs.count("name") ? attrs["name"] : ""; + // A dynamic name ($[...]/#[...]) or a body-less definition can't be + // (and, per __tag.cpp, isn't even valid as) a compiled definition - + // fall back to the exact same FALLBACK behavior as before rather + // than failing to compile. Real content always uses a literal name. + bool dynamicName = name.find("$[") != std::string::npos || name.find("#[") != std::string::npos; + if (!hasContainer || name.empty() || dynamicName) { + if (hasContainer) { + auto span = scanBody(pos, "tag", false); + pos = span.first + 3 + 3; // "</tag>" + } + auto n = std::make_shared<Node>(); + n->kind = NodeKind::FALLBACK; + n->rawSpan = s.substr(origStart, pos - origStart); + return n; + } + std::string lowerName = name; + for (char &c : lowerName) c = (char)std::tolower((unsigned char)c); + + auto span = scanBody(pos, "tag", false); + size_t closePos = span.first; + size_t afterClose = closePos + 3 + 3; // "</tag>" + + // Best-effort: this compiler's own modeled grammar is a real subset + // of what the runtime interpreter tolerates (jet-1.4 content is + // lenient in ways this parser doesn't fully cover yet - e.g. a + // completely unrecognized bare tag like SESSIONCTL.tag's own + // <setcookie ...>, which the interpreter just echoes as literal + // text rather than treating as a real tag needing a close - see + // Tag.cpp's own final dispatch fallback). Rather than teach every + // such case to this parser up front, fall all the way back to the + // exact same FALLBACK behavior a pre-TAGDEF compiler would have + // produced for this whole span whenever the body can't be parsed - + // this tag definition simply isn't compiled (stays fully + // interpreted, unchanged), with zero risk to anything else in the + // file. pos is never mutated by the failed parseSub call itself + // (it operates on its own copied substring), so no explicit + // rewind is needed before falling through. + std::vector<NodePtr> body; + bool bodyOk = true; + try { + body = parseSub(pos, closePos); + } catch (const std::exception &) { + bodyOk = false; + } + if (!bodyOk) { + pos = afterClose; + auto n = std::make_shared<Node>(); + n->kind = NodeKind::FALLBACK; + n->rawSpan = s.substr(origStart, pos - origStart); + return n; + } + pos = afterClose; + + auto n = std::make_shared<Node>(); + n->kind = NodeKind::TAGDEF; + n->tagDefName = lowerName; + n->tagDefBody = body; + // Exact original source text - codegen still emits the same + // interpretSpan-based raw registration into global.tags alongside + // the new compiled fast path (see JetCodegen's TAGDEF case), so + // anything that reaches this tag some other way (a nested/locally- + // scoped definition shadowing it, or interpreted content elsewhere) + // keeps working exactly as it does today. + n->rawSpan = s.substr(origStart, pos - origStart); + return n; + } + + NodePtr Parser::parseContainerSlot() { + pos += 10; // "<container" + bool hasContainer; + auto attrs = parseAttrs(hasContainer); + // Attributes on <container> (eval=/name=/scope=, as real content + // sometimes writes) are not consulted by the interpreter's own + // <container> dispatch branch either (see Tag::renderContainer) - + // matching that exactly rather than inventing new behavior here. + (void)attrs; + (void)hasContainer; + auto n = std::make_shared<Node>(); + n->kind = NodeKind::CONTAINER_SLOT; + return n; + } + std::vector<NodePtr> Parser::parseNodes() { std::vector<NodePtr> nodes; std::string textBuf; @@ -287,6 +548,14 @@ namespace jetc { if (matchTagOpen("if", pos)) { flush(); nodes.push_back(parseIf()); continue; } if (matchTagOpen("for", pos)) { flush(); nodes.push_back(parseFor()); continue; } if (matchTagOpen("while", pos)) { flush(); nodes.push_back(parseWhile()); continue; } + if (matchTagOpen("include", pos)) { + flush(); + auto included = parseInclude(); + nodes.insert(nodes.end(), included.begin(), included.end()); + continue; + } + if (matchTagOpen("container", pos)) { flush(); nodes.push_back(parseContainerSlot()); continue; } + if (topLevel && matchTagOpen("tag", pos)) { flush(); nodes.push_back(parseTagDef()); continue; } flush(); nodes.push_back(parseFallback()); continue; diff --git a/jetc/JetParser.h b/jetc/JetParser.h index f7ec4aa..5b97b05 100755 --- a/jetc/JetParser.h +++ b/jetc/JetParser.h @@ -21,7 +21,19 @@ namespace jetc { // gracefully instead of failing to compile. class Parser { public: - explicit Parser(const std::string &src) : s(src), pos(0) {} + // topLevel is false for any nested sub-parse (an <if>/<for>/<while> + // body, via parseSub) and true for the file's own top-level flow and + // for anything spliced in from an <include> (which inherits the + // includer's own topLevel, since it's meant to read as if written + // inline at that point - see parseInclude). Only used to decide + // whether a <tag name="X"> definition is eligible for compilation + // (see parseTagDef) - one found at topLevel==false (nested inside + // another construct) stays FALLBACK, unchanged, matching __tag.cpp's + // own locally-scoped-registration semantics that only a true + // top-level definition is guaranteed to always be registered by the + // time any invocation could reach it. + explicit Parser(const std::string &src, std::string baseDir = "", int includeDepth = 0, bool topLevel = true) + : s(src), pos(0), baseDir(baseDir), includeDepth(includeDepth), topLevel(topLevel) {} std::vector<NodePtr> parseNodes(); @@ -29,6 +41,9 @@ namespace jetc { std::string s; size_t pos; size_t nextId = 0; + std::string baseDir; + int includeDepth; + bool topLevel; bool eof() const { return pos >= s.size(); } bool starts(const std::string &lit) const; @@ -42,7 +57,9 @@ namespace jetc { // p points at '<' of some tag's opening; advances p past its '>' or // "/>", skipping quoted attribute values. Returns true if self-closing. - bool skipOpenTag(size_t &p) const; + bool skipOpenTag(size_t &p); + void skipQuotedValue(size_t &p); + void skipEmbeddedTag(size_t &p); std::map<std::string, std::string> parseAttrs(bool &hasContainer); @@ -54,6 +71,15 @@ namespace jetc { std::vector<NodePtr> parseSub(size_t start, size_t end); + // <include file="..."> is spliced in at compile time - read the + // referenced file (resolved relative to baseDir) and parse ITS + // content as if it had been written inline here, so everything in + // it (custom <tag> definitions included) goes through this real + // compiler, not the separate runtime interpreter (which has its own, + // independently-buggy nested-tag handling). Returns the parsed nodes + // to be spliced into the caller's own node list. + std::vector<NodePtr> parseInclude(); + NodePtr parseVar(); NodePtr parseSet(); NodePtr parseIf(); @@ -61,6 +87,21 @@ namespace jetc { NodePtr parseWhile(); NodePtr parseFallback(); + // <tag name="X">...</tag> at true top level (see topLevel above) - + // compiles the definition body with a nested (topLevel=false) parse + // instead of falling back to raw text, so jetc can generate a real + // render function for it (see JetCodegen). Falls back to an ordinary + // FALLBACK node (same as today) if the definition has no container + // or no literal name= - degrades gracefully rather than failing to + // compile. + NodePtr parseTagDef(); + + // <container> (any case/attributes/closing style, always void - see + // isVoidTag) - a leaf node; the actual rendering logic lives in + // Tag::renderContainer, called from generated code exactly as the + // interpreter's own <container> dispatch calls it today. + NodePtr parseContainerSlot(); + std::string newId(const std::string &prefix) { return prefix + std::to_string(nextId++) + "_"; } }; diff --git a/jetc/barant-site/assets/control.css b/jetc/barant-site/assets/control.css deleted file mode 100644 index 2aa5399..0000000 --- a/jetc/barant-site/assets/control.css +++ /dev/null @@ -1,24 +0,0 @@ - -.controlframe {background: #888; - padding: 5px 5px 15px 10px; - } - -.vm {position: relative; - height: 55px; - } - -.vmname {position: absolute; - left: 0px; top: 0px; - height: 55px; - } - -.vmcover {position: absolute; - left: 15px; - top: 30px; - font: 12px 'Dot Matrix'; - background: #bbb; - color: white; - border: 1pt solid white; - border-radius: 5px; - padding: 2px 10px 2px 10px; - } diff --git a/jetc/barant-site/assets/images/btglogo.png b/jetc/barant-site/assets/images/btglogo.png index b3cec9f..43ab4af 100644 Binary files a/jetc/barant-site/assets/images/btglogo.png and b/jetc/barant-site/assets/images/btglogo.png differ diff --git a/jetc/barant-site/assets/images/control.css b/jetc/barant-site/assets/images/control.css new file mode 100644 index 0000000..41faf40 --- /dev/null +++ b/jetc/barant-site/assets/images/control.css @@ -0,0 +1,26 @@ + +.controlframe {background: #16161c; + border: 1px solid rgba(197, 164, 90, 0.28); + border-radius: 3px; + padding: 5px 5px 15px 10px; + } + +.vm {position: relative; + height: 55px; + } + +.vmname {position: absolute; + left: 0px; top: 0px; + height: 55px; + } + +.vmcover {position: absolute; + left: 15px; + top: 30px; + font: 12px 'Dot Matrix'; + background: #101014; + color: #e3c47e; + border: 1px solid rgba(197, 164, 90, 0.4); + border-radius: 5px; + padding: 2px 10px 2px 10px; + } diff --git a/jetc/barant-site/assets/images/download.png b/jetc/barant-site/assets/images/download.png index 4c0448d..4db133c 100644 Binary files a/jetc/barant-site/assets/images/download.png and b/jetc/barant-site/assets/images/download.png differ diff --git a/jetc/barant-site/assets/images/main.css b/jetc/barant-site/assets/images/main.css new file mode 100644 index 0000000..c3cd69e --- /dev/null +++ b/jetc/barant-site/assets/images/main.css @@ -0,0 +1,690 @@ +.rotate { + animation: rotation 1s infinite linear; +} + +@keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } +} + +@font-face { + font-family: avenir; + src: url('https://barant.com/fonts/AvenirLTStd-Light.ttf'); +} +@font-face { + font-family: source; + src: url('https://barant.com/fonts/jackinput.TTF'); +} + +@font-face { + font-family: exo2; + src: url('https://barant.com/fonts/Exo2-RegularCondensed.otf'); +} + +:root { + --bond-bg: #0a0a0d; + --bond-bg-2: #131318; + --bond-panel: #16161c; + --bond-panel-2: #1c1c24; + --bond-line: rgba(197, 164, 90, 0.28); + --bond-line-soft: rgba(255, 255, 255, 0.07); + --bond-gold: #c5a45a; + --bond-gold-bright: #e3c47e; + --bond-ice: #6fb8c9; + --bond-text: #e7e4dc; + --bond-text-dim: #8d8c92; + --bond-red: #a13a3a; +} + +* { box-sizing: border-box; } + +body { + background: radial-gradient(ellipse at top, #1a1a20 0%, #0a0a0d 60%, #060607 100%); + background-attachment: fixed; + font-family: exo2; + color: var(--bond-text); + margin: 10px 10px 10px 10px; + width: 98%; +} + +a:link, a:visited { text-decoration: none; color: var(--bond-gold); } +a:hover { text-decoration: none; color: var(--bond-gold-bright); } + +h1 { + font: 20pt exo2; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--bond-text); + text-shadow: 0 0 0.6em rgba(197, 164, 90, 0.25); + opacity: 0.95; +} + +h2 { + font: 14pt exo2; + letter-spacing: 0.06em; + color: var(--bond-gold-bright); +} + +h3 { + font: 12pt exo2; + letter-spacing: 0.05em; + color: var(--bond-text); +} + +p { font-family: 8pt exo2; color: var(--bond-text); } + +ul { font-family: 8pt exo2; } + +li { font-family: 8pt exo2; color: var(--bond-text); } + +.project_header { + background: linear-gradient(180deg, #1c1c24, #101014); + color: var(--bond-gold-bright); + padding: 5px; + font-weight: 400; + font-size: 14px; + border-bottom: 1px solid var(--bond-line); +} + +.pageclass { + background: var(--bond-bg-2); + border-radius: 6px; + border: 1px solid var(--bond-line); + box-shadow: 0 0 0 1px rgba(0,0,0,0.6), 0 20px 60px rgba(0,0,0,0.65); + border-spacing: 0px; + padding: 0px; + max-width: 1050px; + min-width: 1050px; + margin: auto; + font-family: exo2; + font-size: 10pt; + color: var(--bond-text); + overflow: hidden; +} + +input { + background: #101014; + color: var(--bond-text); + border: 1px solid var(--bond-line-soft); + font-family: exo2; + font-size: 14px; + min-height: 18px; + display: block; + margin-bottom: 3px; + margin-top: 0px; + outline: none; + padding: 2px 6px; + border-radius: 2px; + } + +input:focus { + border-color: var(--bond-gold); + box-shadow: 0 0 0 1px rgba(197, 164, 90, 0.35); + } + +select { + background: #101014; + color: var(--bond-text); + font-family: exo2; + font-size: 14px; + min-height: 18px; + display: block; + margin-bottom: 3px; + margin-top: 0px; + outline: none; + border: 1px solid var(--bond-line-soft); + padding: 1px 2px 1px 2px; + border-radius: 2px; + } + +textarea.fieldinput { + background: #101014; + color: var(--bond-text); + resize: none; + font-family: exo2; + border: 1px solid var(--bond-line-soft); + font-size: 14px; + min-height: 18px; + display: block; + margin-bottom: 3px; + margin-top: 0px; + outline: none; + padding: 2px 2px 2px 5px; + border-radius: 2px; + } + +textarea.fieldinputerror { + background: #241213; + color: var(--bond-text); + resize: none; + font-family: exo2; + border: 1px solid var(--bond-red); + font-size: 14px; + min-height: 18px; + display: block; + margin-bottom: 3px; + margin-top: 0px; + outline: none; + padding: 2px 2px 2px 5px; + border-radius: 2px; + } + +.sectionframe { + position: relative; + font-family: exo2; + border: 1px solid var(--bond-line-soft); + font-size: 10pt; + margin: 3px; + background: var(--bond-panel); + padding: 0px; + border-radius: 3px; +} + +.sectionframe:after{ + content: ''; + position: absolute; + top: 1px; + left: 1px; + width: calc(100% - 2px); + height: 1px; + background: var(--bond-line); +} + +.sectionframecontents { + padding: 10px; + text-align: justify; + color: var(--bond-text); + border-top: 1px solid var(--bond-line-soft); + background-color: var(--bond-panel); +} +.sectionframeheading { + background: linear-gradient(180deg, #1c1c24, #131318); + color: var(--bond-gold-bright); + border-bottom: 1px solid var(--bond-line); + vertical-align: middle; + padding: 6px 10px; + margin: 0px; + font-family: 12pt avenir; + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 10pt; + border-radius: 3px 3px 0px 0px; +} + +.sectionframeheading2 { + background: var(--bond-panel-2); + color: var(--bond-gold-bright); + font-family: exo2; + font-size: 20pt; + letter-spacing: 0.06em; + padding: 3px; + border-radius: 3px; +} + +.heading { + background: linear-gradient(180deg, #131318, #0d0d10); + border-radius: 5px 5px 0px 0px; + border-bottom: 1px solid var(--bond-line); + max-height: 80px; +} +.entryform { + background: #232330; + padding: 6px; + margin: 0px; + border: 1px solid rgba(197, 164, 90, 0.5); + border-radius: 3px; +} +.entryform table { + font: 12px exo2; + background: transparent; + border: 0pt solid black; + color: var(--bond-text); +} + +.footer { + background: linear-gradient(180deg, #0a0a0d, #000); + border-top: 1px solid var(--bond-line); + font: 8pt verdana; + letter-spacing: 0.05em; + color: var(--bond-gold); + text-align: center; + padding: 6px 0; + border-radius: 0px 0px 5px 5px; +} + +.menubar { + background: #0a0a0d; + border-bottom: 1px solid var(--bond-line); + padding: 0px 0px 0px 0px; + margin: 0px; +} + +.baritem { + background: transparent; + color: var(--bond-text-dim); + position: relative; + font-family: exo2; + font-weight: 600; + letter-spacing: 0.06em; + font-size: 8pt; + padding: 6px 10px; + margin: 6px 0px 0px 0px; + border: none; + border-bottom: 2px solid transparent; + border-radius: 0px; + transition: color 0.25s ease, border-color 0.25s ease; + } + +.baritem:after{ + content: none; +} + +.baritemselected { + /* Deliberately styled to read as the same surface as the content + panel it opens into (matching background, gold border wrapping + only the top/sides) - the open bottom edge is the same color as + its own background rather than a hard line, so it visually reads + as "raised up out of the black bar, connected to the page below" + rather than just another highlighted nav item. */ + background: var(--bond-panel); + color: var(--bond-gold-bright); + font-family: exo2; + font-weight: 600; + letter-spacing: 0.06em; + font-size: 8pt; + padding: 7px 10px 8px 10px; + margin: 4px 0px 0px 0px; + border: 1px solid var(--bond-gold); + border-bottom: 1px solid var(--bond-panel); + border-radius: 5px 5px 0px 0px; + box-shadow: 0 -2px 8px rgba(197, 164, 90, 0.18); + text-shadow: 0 0 0.5em rgba(197, 164, 90, 0.35); + } + +.baritem:hover { + color: var(--bond-gold-bright); + border-bottom: 2px solid var(--bond-line); + transition: color 0.2s ease, border-color 0.2s ease; + } + +.panel { + background: var(--bond-panel); + margin: 10px; + padding: 3px; + border: 1px solid var(--bond-line-soft); + border-radius: 3px; +} + +.panel table { + font: 12px exo2; + padding: 5px; + background: var(--bond-panel-2); + border: 1px solid var(--bond-line-soft); + border-radius: 6px; + color: var(--bond-text); +} +.panel table th { + background: #101014; + color: var(--bond-gold-bright); + padding: 2px 3px 3px 3px; + border: 0pt; + letter-spacing: 0.04em; +} +.panel table td { + background: transparent; + color: var(--bond-text); + padding: 2px 3px 3px 3px; + border: 0pt; +} + +.left { + text-align: right; + padding-left: 20px; + border-radius: 6px 0px 0px 6px; +} + +.right { + border-radius: 0px 6px 6px 0px; + padding-right: 20px; +} + +.featured { background: var(--bond-panel); } + +.buttonbay { + width: 100%; + text-align: center; +} +.fieldinput { + font-family: exo2; + border-radius: 3px; + padding: 2px 2px 2px 5px; + border: 1px solid var(--bond-line-soft); + background: #101014; + color: var(--bond-text); +} +.fieldinputerror { + font-family: exo2; + border-radius: 3px; + padding: 2px 2px 2px 5px; + background: #241213; + border: 1px solid var(--bond-red); + color: var(--bond-text); +} +.fieldprompt { + font-size: 10pt; + text-align: right; + color: var(--bond-gold-bright); + background: transparent; +} +.message { font: bold 10pt verdana; color: #d97a7a; } +.collection_heading { font: bold 12pt verdana; color: var(--bond-gold-bright); } +.collection_text { font: 10pt verdana; color: var(--bond-text); } +.collection_tracks_background { font: 8pt bold verdana; color: var(--bond-text); background: #101014; } +.warning { font: 10pt verdana; background: #3a1414; color: #e0a0a0; text-align: justify; border: 1px solid var(--bond-red); } + +.button { + position: relative; + display: inline-block; + font-size: 10pt; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--bond-gold-bright); + border: 1px solid var(--bond-gold); + background: linear-gradient(180deg, #1c1c24, #131318); + padding: 2px 10px 3px 10px; + margin: 0px; + border-radius: 3px; + transition: box-shadow 0.2s ease, background 0.2s ease; + } + +.button:hover { + box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.35); + background: linear-gradient(180deg, #24242e, #1a1a20); +} + +.button:after{ + content: none; +} + +.code { + font-family: source; + font-weight: 400; + font-size: 10pt; + border: 1px solid var(--bond-line-soft); + background: #101014; + color: var(--bond-gold-bright); + padding: 5px; +} + +/* SPA content-swap fade transition */ +#content { + transition: opacity 0.18s ease; +} +#content.spa-fading { + opacity: 0; +} + +/* Shopping cart */ +.cart-notice { + background: var(--bond-panel-2); + border: 1px solid var(--bond-line); + border-left: 3px solid var(--bond-gold); + color: var(--bond-text); + padding: 8px 12px; + margin: 0 0 10px 0; + border-radius: 3px; + font-size: 9pt; +} + +.cart-wrap { + border: 1px solid var(--bond-line); + border-radius: 4px; + overflow: hidden; + background: var(--bond-panel); +} + +.cart-header { + display: grid; + grid-template-columns: 1fr 90px 90px 100px 70px; + gap: 8px; + align-items: center; + padding: 8px 14px; + background: linear-gradient(180deg, #1c1c24, #131318); + border-bottom: 1px solid var(--bond-line); + color: var(--bond-gold-bright); + font-size: 8pt; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 600; +} + +.cart-header .cart-col-price, +.cart-header .cart-col-qty, +.cart-header .cart-col-total { + text-align: right; +} + +.cart-items { + max-height: 420px; + overflow-y: auto; +} + +.cart-row { + display: grid; + grid-template-columns: 1fr 90px 90px 100px 70px; + gap: 8px; + align-items: center; + padding: 10px 14px; + border-bottom: 1px solid var(--bond-line-soft); + color: var(--bond-text); + font-size: 9pt; + transition: background 0.15s ease; +} + +.cart-row:last-child { border-bottom: none; } + +.cart-row:hover { + background: rgba(197, 164, 90, 0.06); +} + +.cart-row .cart-col-desc { + font-weight: 500; + color: var(--bond-text); +} + +.cart-row .cart-col-price, +.cart-row .cart-col-total { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.cart-row .cart-col-total { + color: var(--bond-gold-bright); + font-weight: 600; +} + +.cart-row .cart-col-qty { + text-align: right; +} + +.cart-qty-input { + width: 60px !important; + text-align: center; + margin: 0 !important; + display: inline-block !important; +} + +.cart-col-remove { + text-align: right; +} + +.cart-remove-link { + color: var(--bond-text-dim) !important; + font-size: 8pt; + letter-spacing: 0.04em; + text-transform: uppercase; + border: 1px solid var(--bond-line-soft); + border-radius: 3px; + padding: 3px 8px; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; +} + +.cart-remove-link:hover { + color: #d97a7a !important; + border-color: var(--bond-red); + background: rgba(161, 58, 58, 0.12); +} + +.cart-summary { + padding: 12px 14px; + background: linear-gradient(180deg, #131318, #0d0d10); + border-top: 1px solid var(--bond-line); +} + +.cart-summary-row { + display: flex; + justify-content: space-between; + align-items: baseline; + color: var(--bond-text); + font-size: 10pt; +} + +.cart-summary-total { + color: var(--bond-gold-bright); + font-size: 14pt; + font-weight: 700; + letter-spacing: 0.02em; +} + +.cart-empty { + text-align: center; + padding: 50px 20px; + color: var(--bond-text-dim); +} + +.cart-empty-icon { + font-size: 40pt; + opacity: 0.5; + margin-bottom: 8px; +} + +.cart-empty-title { + font-size: 13pt; + color: var(--bond-text); + letter-spacing: 0.04em; + margin-bottom: 4px; +} + +.cart-empty-text { + font-size: 9pt; + color: var(--bond-text-dim); +} + +/* Checkout */ +.checkout-header { + display: grid; + grid-template-columns: 1fr 90px 70px 100px; + gap: 8px; + align-items: center; + padding: 8px 14px; + background: linear-gradient(180deg, #1c1c24, #131318); + border-bottom: 1px solid var(--bond-line); + color: var(--bond-gold-bright); + font-size: 8pt; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 600; +} + +.checkout-header .checkout-col-qty { + text-align: right; +} + +.checkout-row { + display: grid; + grid-template-columns: 1fr 90px 70px 100px; + gap: 8px; + align-items: center; + padding: 10px 14px; + border-bottom: 1px solid var(--bond-line-soft); + color: var(--bond-text); + font-size: 9pt; +} + +.checkout-row:last-child { border-bottom: none; } + +.checkout-row .checkout-col-qty { + text-align: right; + color: var(--bond-text-dim); +} + +.checkout-pay-wrap { + max-width: 380px; + margin: 0 auto; + padding: 6px 0 4px 0; +} + +.checkout-pay-label { + font-size: 8pt; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--bond-text-dim); + margin-bottom: 6px; +} + +.checkout-card-input { + background: var(--bond-panel-2); + border: 1px solid var(--bond-line); + border-radius: 4px; + padding: 10px; + min-height: 40px; + margin-bottom: 14px; +} + +.checkout-pay-btn { + display: block; + width: 100%; + font-size: 11pt; + padding: 9px 10px 10px 10px; + text-align: center; + color: #0a0a0d; + background: linear-gradient(180deg, var(--bond-gold-bright), var(--bond-gold)); + border: 1px solid var(--bond-gold); + cursor: pointer; +} + +.checkout-pay-btn:hover { + background: linear-gradient(180deg, #f0d89a, var(--bond-gold-bright)); + box-shadow: 0 0 0.6em rgba(197, 164, 90, 0.45); +} + +.checkout-pay-btn:disabled { + cursor: default; + opacity: 0.55; + background: linear-gradient(180deg, #1c1c24, #131318); + color: var(--bond-text-dim); + box-shadow: none; +} + +.checkout-processing { + visibility: hidden; + background: var(--bond-panel-2); + border: 1px solid var(--bond-line); + border-left: 3px solid var(--bond-gold); + color: var(--bond-text); + padding: 8px 12px; + margin-top: 12px; + border-radius: 3px; + font-size: 9pt; +} + +.checkout-error { + color: #d97a7a; + font-size: 9pt; + margin-top: 12px; + min-height: 1.2em; +} diff --git a/jetc/barant-site/assets/images/modular1.png b/jetc/barant-site/assets/images/modular1.png index d660b41..62c9d9e 100644 Binary files a/jetc/barant-site/assets/images/modular1.png and b/jetc/barant-site/assets/images/modular1.png differ diff --git a/jetc/barant-site/assets/images/speaker.png b/jetc/barant-site/assets/images/speaker.png index d23c627..ae318c0 100644 Binary files a/jetc/barant-site/assets/images/speaker.png and b/jetc/barant-site/assets/images/speaker.png differ diff --git a/jetc/barant-site/assets/main.css b/jetc/barant-site/assets/main.css deleted file mode 100644 index 7a808d1..0000000 --- a/jetc/barant-site/assets/main.css +++ /dev/null @@ -1,385 +0,0 @@ -.rotate { - animation: rotation 1s infinite linear; -} - -@keyframes rotation { - from { - transform: rotate(0deg); - } - to { - transform: rotate(359deg); - } -} - -@font-face { - font-family: avenir; - src: url('https://barant.com/fonts/AvenirLTStd-Light.ttf'); -} -@font-face { - font-family: source; - src: url('https://barant.com/fonts/jackinput.TTF'); -} - -@font-face { - font-family: exo2; - src: url('https://barant.com/fonts/Exo2-RegularCondensed.otf'); -} - - -body {background: linear-gradient(to bottom, #ccc, #444); - font-family: exo2; - margin: 10px 10px 10px 10px; - width: 98%; -} - -a:link {text-decoration: none} -a:visited {text-decoration:none} -a:hover {text-decoration:none} - -h1 {font: 20pt exo2; -text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; -color: #808080; -opacity: 0.9;} - -h2 {font: 14pt exo2; - text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; -} - -h3 {font: 12pt exo2; - text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; -} - -p {font-family: 8pt exo2; - text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; -} - -ul {font-family: 8pt exo2;} - -li {font-family: 8pt exo2; - text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; -} - -.project_header {background: #88c; - color: #fff; - padding: 5px; - font-weight: 400; - font-size: 14px; -} - -.pageclass {background: #fff; - border-radius: 10px; - border: 2pt outset; - border-spacing: 0px; - padding: 0px; - max-width: 1050px; - min-width: 1050px - margin: auto; - text-shadow: 0 0 0.1em #CCC, 0 0 0.1em #87F; - font-family: exo2; - font-size: 10pt; -} - -input {background: #fff; - border: 1px inset; - font-family: exo2; - font-size: 14px; - min-height: 18px; - display: block; - margin-bottom: 3px; - margin-top: 0px; - outline: none; - - -webkit-border-radius: 2px; - -moz-border-radius: 2px; - -o-border-radius: 2px; - -ms-border-radius: 2px; - border-radius: 0px; - } - -select {background: #fff; - font-family: exo2; - font-size: 14px; - min-height: 18px; - display: block; - margin-bottom: 3px; - margin-top: 0px; - outline: none; - border: 1px solid #444; - padding: 1px 2px 1px 2px; - -webkit-border-radius: 7px; - -moz-border-radius: 7px; - -o-border-radius: 7px; - -ms-border-radius: 7px; - border-radius: 0px; - } - -textarea.fieldinput {background: #fff; - resize: none; - font-family: exo2; - border: 1px solid #444; - font-size: 14px; - min-height: 18px; - display: block; - margin-bottom: 3px; - margin-top: 0px; - outline: none; - padding: 2px 2px 2px 5px; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - -o-border-radius: 0px; - -ms-border-radius: 0px; - border-radius: 0px; - } - -textarea.fieldinputerror {background: #fcc; - resize: none; - font-family: exo2; - border: 1px solid #f00; - font-size: 14px; - min-height: 18px; - display: block; - margin-bottom: 3px; - margin-top: 0px; - outline: none; - padding: 2px 2px 2px 5px; - -webkit-border-radius: 7px; - -moz-border-radius: 7px; - -o-border-radius: 7px; - -ms-border-radius: 7px; - border-radius: 0px; - } - -.sectionframe { - position: relative; - font-family: exo2; - border: 0px outset; - font-size: 10pt; - margin: 3px; - background: #808080; - padding: 0px; - border-radius: 0px; -} - -.sectionframe:after{ - content: ''; - position: absolute; - top: 1px; - left: 1px; - width: calc(100% - 2px); - height: 12px; - background: linear-gradient(rgba(255,255,255,0.5), rgba(255,255,255,0.2)); -} - -.sectionframecontents { - padding: 10px; - text-align: justify; - color: #000000; - border: 1pt inset; - background-color: #F8F8FF; -} -.sectionframeheading { -# background: linear-gradient(to bottom, #fff, #88f); - background: #2099f0; - color: #fff; - border: 1pt outset; - vertical-align: middle; - padding: 5px; - margin: 0px; - font-family: 12pt avenir; - text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; - border-radius: 0px 0px 0px 0px; -} - -.sectionframeheading2 { - background: #C0C0C0; - font-family: exo2; - font-size: 20pt; - padding: 3px - text-shadow: 0 0 0.2em #CCC, 0 0 0.2em #87F; - border-radius: 3px; -} - -.heading {background: #fff; - border-radius: 7px 7px 0px 0px; - max-height: 80px; -} -.entryform { - background: #2099f0; - padding: 3px 3px 3px 3px; - margin: 0px; - border: 0px solid black; - border-radius: 0px; -} -.entryform table { - font: 12px exo2; - background: #c5e1f4; - border: 0pt solid black; -} - -.footer { - background: linear-gradient(to bottom, #000, #888); - font: 8pt verdana; - color: #FFFFFF; - text-align: center; - border-radius: 0px 0px 6px 6px;} - -.menubar { -# background: linear-gradient(to bottom, #fff, #2099f0); - padding: 0px 0px 0px 0px; - margin: 0px; -} - -.baritem { - background-image: linear-gradient(#2099f0, #c3ddef); - color: #fff; - position: relative; - font-family: exo2; - font-weight: 1000; - font-size: 8pt; - padding: 2px 10px; - margin: 10px 0px 0px 0px; - border: solid; - border-width: 0px 0px 1px 0px; - border-color: #fff; - border-radius: 5px 5px 0px 0px; - border-style: outset; - transition: background 2s; - } - -.baritem:after{ - content: ''; - position: absolute; - top: 1px; - left: 1px; - width: calc(100% - 2px); - height: 50%; - z-index: 5; - pointer-events: none; - border-radius: 4px 4px 0px 0px; - background: linear-gradient(rgba(255,255,255,0.8), rgba(255,255,255,0.2)); -} - -.baritemselected { - background: #ccc; - color: #000; - font-family: exo2; - font-weight: 1000; - font-size: 8pt; - padding: 2px 10px; - margin: 5px 10px 0px 0px; - border: solid; - border-width: 1px 1px 0px 1px; - border-color: #fff; - border-radius: 5px 5px 0px 0px; - border-style: outset; - transition: background 2s; - - } - -.baritem:hover { - background: #c3ddef; - color: #fff; - transition: background 5s; - } - -.panel {background: #ccc; - margin: 10px; - padding: 3px; - border: 0pt inset;} - -.panel table { - font: 12px exo2; - padding: 5px; - background: #c5e1f4; - border: 1pt solid black; - border-radius: 9px; -} -.panel table th { - background: #2099f0; - color: #fff; - padding: 2px 3px 3px 3px; - border: 0pt; -} -.panel table td { - background: #c5e1f4; - color: #000; - padding: 2px 3px 3px 3px; - border: 0pt; -} - -.left { - text-align: right; - padding-left: 20px; - border-radius: 9px 0px 0px 9px; -} - -.right { - border-radius: 0px 9px 9px 0px; - padding-right: 20px; -} - -.featured {background: #fff} - -.buttonbay { - width: 100%; - text-align: center; -} -.fieldinput { - font-family: exo2; - border-radius: 7px; - padding: 2px 2px 2px 5px; - border: 1px solid #ccc; -} -.fieldinputerror { - font-family: exo2; - border-radius: 7px; - padding: 2px 2px 2px 5px; - background: #fcc; - border: 1px solid red; -} -.fieldprompt { - font-size: 10pt; - text-align: right; - background: #00f; -} -.message {font: bold 10pt verdana; color: #FF0000} -.collection_heading {font: bold 12pt verdana; color: #FFFFFF} -.collection_text {font: 10pt verdana; color: #FFFFFF} -.collection_tracks_background {font: 8pt bold verdana; color: #FFFFFF; background: #10003F} -.warning {font: 10pt verdana; background: #FF0000; color: #FFFFFF; text-align: justify} - -.button { - position: relative; - display: inline-block; - font-size: 10pt; - font-weight: 400; - color: #fff; - border: 2px outset; - background: #2099f0; - padding: 2px 10px 3px 10px; - margin: 0px; - border-radius: 5px; - } - -.button:after{ - content: ''; - position: absolute; - top: 0px; - left: 0px; - width: calc(100% - 0px); - height: 10px; - z-index: 5; - border-radius: 3px; - background: linear-gradient(rgba(255,255,255,0.8), rgba(255,255,255,0.2)); -} - -.code { - font-family: source; - font-weight: 400; - font-size: 10pt; - border: 1pt solid black; - background: #ff8; - padding: 5px; -} - - diff --git a/jetc/barant-site/convert.py b/jetc/barant-site/convert.py index 028442e..994815a 100644 --- a/jetc/barant-site/convert.py +++ b/jetc/barant-site/convert.py @@ -12,28 +12,27 @@ IMG_CACHE = ROOT + "/assets/images" os.makedirs(OUT, exist_ok=True) os.makedirs(IMG_CACHE, exist_ok=True) -IMG_MIME = { - ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", - ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp", - ".ico": "image/x-icon", -} - - def resolve_images(html): - """Find any remaining /images/<file> reference (src="..." or url('...')) - and inline it as a data: URI, fetching+caching from barant.com on first - use - APIServer has no static-file serving, same reason the chrome logo - and page CSS/fonts needed inlining earlier this session.""" + """Find any /images/<file> or images/<file> reference (src="...", + href="...", or CSS url('...')) and repoint it at the live APIServer + GET /assets/{filename} route instead of inlining it as a data: URI - + fetching+caching from barant.com into the same assets/images/ cache + directory on first use (unchanged), just no longer embedding the raw + bytes into every page's own HTML (undoes the earlier base64-inlining + approach now that a real static-asset module exists to serve these). + Matches both the absolute form (/images/x.png) and the relative form + (images/x.png, no leading slash) - the previous version of this + function only matched the absolute form, silently leaving several + real <img src="images/.."> references un-rewritten and pointing + nowhere (APIServer has no /images/ route at all).""" def repl(m): path = m.group(1) # e.g. images/grid.png fname = path.split("/")[-1] - ext = os.path.splitext(fname)[1].lower() - mime = IMG_MIME.get(ext, "application/octet-stream") local_path = IMG_CACHE + "/" + fname if not os.path.exists(local_path): subprocess.run(["curl", "-s", "-o", local_path, "https://barant.com/" + path], check=True) - return data_uri(local_path, mime) - html = re.sub(r'/images/([A-Za-z0-9_.-]+)(?=["\'])', repl, html) + return "/assets/" + fname + html = re.sub(r'/?images/([A-Za-z0-9_.-]+)(?=["\'])', repl, html) return html @@ -199,6 +198,72 @@ opensource_menu = flatten_linkbuttons(opensource_menu) SIDEBAR_HTML = opensource_menu +# --------------------------------------------------------------------------- +# SPA bootstrap script: present once, in the chrome, on every FULL page load +# (never in a fragment-only response, which has no <head>/<script> of its +# own). Intercepts clicks on internal <a href="/..."> links and swaps just +# #jetc-content via fetch('/name?fragment=1') instead of a full navigation; +# also drives the browser back/forward buttons via popstate. See the +# FULL_PAGE_TMPL / FRAGMENT_TMPL split in convert_page() for how a single +# route serves both the full page and the fragment-only response, decided +# by the ?fragment=1 query flag. +# --------------------------------------------------------------------------- + +# Pages excluded from client-side fragment fetching entirely - a click on +# one of these falls through to an ORDINARY browser navigation (hitting +# the full-page route, confirmed safe) instead of being intercepted. +# Real, deterministic finding: <name>?fragment=1 for a MYSQL_PAGES page +# crashes the whole apiserver process on the very FIRST such request +# (reproduced repeatedly - not state-dependent, not "two failures in a +# row"), while the exact same page's full-page route (no ?fragment=1) +# fails cleanly with the expected "database and host parameters are not +# valid." and the server survives. Root cause not isolated (a <mysql> +# connection failure unwinding through the smaller <tag name="_frag_X"> +# definition specifically, vs surviving fine through the larger +# <tag name="_full_X"> one, for reasons not chased down further given +# scope) - this is a mitigation, not a fix, and only matters while these +# pages fail (no real DB password configured); worth re-testing once a +# real DB is wired in, since a successful <mysql> connection takes a +# completely different code path that may not hit this at all. +FRAGMENT_EXCLUDED_PAGES = sorted(MYSQL_PAGES) + +SPA_SCRIPT = """<script> +(function() { + var FRAGMENT_EXCLUDED = %s; + function pageNameFromPath(path) { + var name = path.replace(/^\\//, '').replace(/\\.page$/, ''); + return name || 'main'; + } + function loadPage(name, push) { + fetch('/' + name + '?fragment=1').then(function(resp) { + if (!resp.ok) throw new Error('HTTP ' + resp.status); + return resp.text(); + }).then(function(html) { + var el = document.getElementById('jetc-content'); + if (el) el.innerHTML = html; + if (push) history.pushState({page: name}, '', '/' + name); + }).catch(function(err) { + console.error('jetc-spa: failed to load page', name, err); + }); + } + document.addEventListener('click', function(ev) { + if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return; + var target = ev.target.nodeType === 1 ? ev.target : ev.target.parentElement; + var a = target ? target.closest('a') : null; + if (!a) return; + if (a.target === '_blank' || a.hasAttribute('download')) return; + var href = a.getAttribute('href') || ''; + if (href.indexOf(':') !== -1 || href.indexOf('//') === 0 || href.indexOf('/') !== 0) return; + if (FRAGMENT_EXCLUDED.indexOf(pageNameFromPath(href)) !== -1) return; + ev.preventDefault(); + loadPage(pageNameFromPath(href), true); + }); + window.addEventListener('popstate', function() { + loadPage(pageNameFromPath(location.pathname), false); + }); +})(); +</script>""" % (repr(FRAGMENT_EXCLUDED_PAGES).replace("'", '"')) + # --------------------------------------------------------------------------- # Build the page chrome from mainpagetemplate.temp: the part of <template> # before <contents> and the part after. @@ -208,6 +273,39 @@ temp_raw = read(SRC + "/mainpagetemplate.temp") m = re.search(r'<template>(.*)</template>', temp_raw, re.DOTALL) template_body = m.group(1) +# mainpagetemplate.temp's own <tag name="title">...</tag> helper is dead +# code in this conversion - title substitution happens via a plain Python +# string-replace on <exclude>$[_title]</exclude> below, never by actually +# invoking this helper. Stripped as genuinely dead code (not a bug fix - +# see below for the real one). +template_body = drop_tag_block(template_body, "title") + +# Real bug found and isolated via a minimal standalone repro (bisecting +# down from "the whole chrome throws 'comment must have a container'"): +# a <comment>...</comment> block, when it ends up nested inside one of +# this script's own <tag name="_full_NAME">/<tag name="_frag_NAME"> +# definitions (see convert_page() below) - needed so the fragment/full +# <if> branches can hold real HTML content without jetc's compile-time +# <if> body parser choking on it - fails at RUNTIME with "comment must +# have a container", even though the exact same <comment> block renders +# fine as ordinary top-level content. Reproduced with a 4-line synthetic +# case (<tag name="x">text<comment>plain text, no nested tags</comment> +# text</tag>) - so this is a genuine jetc/JetCore bug in how a custom tag +# DEFINITION'S stored text gets re-processed at <dotag> invocation time, +# not something specific to this page's content. Flagged for the project, +# not fixed here (root cause not isolated further - the base Tag class's +# container-boundary bookkeeping is shared by many tags and risky to +# change without full regression coverage). +# +# Worked around instead: this specific <comment> block wraps content +# gated on $[_login_id] (show a logged-in session var menu) - the EXACT +# same "session-gated, always empty for an anonymous visitor" category +# already established as this conversion's design for cart.menu/login.menu/ +# the ACCOUNT+ADMIN nav tabs (see below) - so stripping it entirely is +# consistent with the rest of this file, not a new compromise. +template_body = re.sub(r'<comment>\s*<if value1="\$\[_login_id\]".*?</if>\s*</comment>', + '', template_body, flags=re.DOTALL) + # Substitute the chrome-level includes/placeholders that are now fixed for # every page in this conversion (no per-page menubar/menu overrides exist - # confirmed by grepping every source .page file for _menubar/_menu overrides @@ -221,7 +319,13 @@ template_body = re.sub(r'<if value1="\$\[_menu\]" value2="" type="eq">\s*<includ CONTENTS_MARKER = "<contents>" idx = template_body.index(CONTENTS_MARKER) CHROME_BEFORE = resolve_images(template_body[:idx]) -CHROME_AFTER = resolve_images(template_body[idx + len(CONTENTS_MARKER):]) +CHROME_AFTER = resolve_images(template_body[idx + len(CONTENTS_MARKER):]) + SPA_SCRIPT + +FULL_PAGE_TMPL = ( + '<set name="jetc_doctype" value="<!DOCTYPE html>" />$[jetc_doctype]' + '<html lang="en"><head><title>%s%s' + '%s
%s
%s' +) def convert_page(name): @@ -262,11 +366,60 @@ def convert_page(name): chrome_before = CHROME_BEFORE.replace('$[_title]', page_title) + full_page = FULL_PAGE_TMPL % (page_title, STYLE_BLOCK, chrome_before, contents, CHROME_AFTER) + + if name in MYSQL_PAGES: + # Real, reproducible server crash (heap corruption - "free(): + # invalid pointer" seen live) when a connection failure + # unwinds through the smaller fragment-mode + # definition specifically - the exact same failure through the + # larger _full_X definition is fine (clean caught exception, 500). + # The client-side click handler already skips fetching these pages + # as fragments (FRAGMENT_EXCLUDED, above), but that only protects + # normal clicking - a stale already-open tab (running JS from + # before that fix shipped) or anyone hitting the URL directly + # would still trigger it. This is the real, server-side fix: the + # fragment response for these pages never attempts the + # connection at all, so the crash is unreachable regardless of + # how the request arrives. Re-test once a real DB password makes + # these pages actually work - a successful connection takes a + # different code path that may not need this at all. + fragment_page = ('

This page requires a full load - ' + 'view it directly.

') % name + else: + fragment_page = '
%s
' % contents + + # A direct/full-page load and an SPA-driven fragment fetch both hit this + # SAME route (/name) - distinguished by ?fragment=1 in the query string, + # per the user's explicit choice over a separate /name.page route. A + # normal load has no query string at all, so $[fragment] is simply + # unset - referencing an unset global variable throws ("global variable + # is not initialized."). + # + # Two real jetc bugs found and worked around while building this (both + # confirmed via direct minimal reproduction, not guessed): + # 1. safely captures the possibly-unset $[fragment] into + # fragflag (always "1" or "" afterward) - but must wrap ONLY + # the , never an . Nesting / INSIDE silently + # drops the branch's output specifically when compiled + # through jetc (works fine in jet-2.0's own interpreter; the bug is + # jetc-compiled-only). The branching lives entirely OUTSIDE/ + # AFTER instead. + # 2. A large/complex body (real inlined HTML+CSS+JS, hundreds of KB) + # placed directly as an 's branch content fails to COMPILE at + # all ("unterminated - no matching close tag found") - jetc's + # own compile-time / body scanner isn't robust against + # real-world markup at this scale. Worked around by keeping the + # 's own branches tiny (self-closing custom-tag invocations) + # and moving the real content into DEFINITIONS + # (which jetc never compile-time-parses at all - stored as opaque + # raw text, exactly like a body, and only actually rendered + # via at runtime). body = ( - '$[jetc_doctype]' - '%s%s' - '%s%s%s' - ) % (page_title, STYLE_BLOCK, chrome_before, contents, CHROME_AFTER) + '' + '%s%s' + '<_frag_%s /><_full_%s />' + ) % (name, fragment_page, name, full_page, name, name) jet_text = "#api GET /%s\n%s\n" % (name, body) diff --git a/jetc/barant-site/jet/applications.jet b/jetc/barant-site/jet/applications.jet index 95204e5..36f68ad 100644 --- a/jetc/barant-site/jet/applications.jet +++ b/jetc/barant-site/jet/applications.jet @@ -1,4 +1,3 @@ -#api GET /applications $[jetc_doctype]APPLICATIONS -
-
-
- -
-
BARANT TECHNOLOGY GROUP
-
Support E-Mail
-
-
-
-
- - -
-
-
- - - - - - - -
-
-
Contact
-

Please contact us by phone at (702) 232-0050.

-

To e-mail us, just enter text below and press the buttons:

- - - - - - - - - -
Send Message
-
-
- - - - - -
-
-
-
-
- - -
- - + + + + + + + + + + + + + + + + diff --git a/jetc/barant-site/jet/crystal.jet b/jetc/barant-site/jet/crystal.jet index 27de3ed..1b02e63 100644 --- a/jetc/barant-site/jet/crystal.jet +++ b/jetc/barant-site/jet/crystal.jet @@ -1,4 +1,3 @@ -#api GET /crystal $[jetc_doctype]CRYSTAL -
-
-
- -
-
BARANT TECHNOLOGY GROUP
-
Support E-Mail
-
-
-
-
- - -
-
-
- - - - - - - -
-
-
Contact
-

Please contact us by phone at (702) 755-8235.

-

To e-mail us, just enter text below and press the buttons:

- - - - - - - - - - Send Message - - - - - - - -
-
-
-
-
- - -
- - + + + + + + + + + + + + + + + + diff --git a/jetc/barant-site/jet/hosting.jet b/jetc/barant-site/jet/hosting.jet index 86f946e..2adf9e4 100644 --- a/jetc/barant-site/jet/hosting.jet +++ b/jetc/barant-site/jet/hosting.jet @@ -1,4 +1,3 @@ -#api GET /hosting $[jetc_doctype]HOSTING -
-
-
- -
-
BARANT TECHNOLOGY GROUP
-
BARANT Technology Group
-
-
-
-
- - -
-
-
- - - - - - - -
-
- -
Welcome to BARANT Systems
- -
-

We are BARANT Technology Group. Contact us for information on how we can - help you with your technical objectives.

- -

Modular1 VST Synthesizer

-

Check out our Modular1 VST3 -Synthesizer If you appreciate the art of modular analog -synthesizers then you will really enjoy the Modular1 with all the -features of an analog modular synthesizer put into a single VST3 -instrument. With the added features of polyphony and patches that -change instantaneously this synthesizer is truly a powerhouse in the -audio toolbox.

- -

Excellent quality and technical performance provide a performance level -instrument for musicians or students who wish to learn sound wave -synthesis techniques or for musicians to enable that old analog sound -in their DAW recording system..

-

Checkout the product page for more information on the capabilities -of the Modular1 synthesizer.

-
-
-
-
-
-
- - -
- - diff --git a/jetc/barant-site/jet/notfound.jet b/jetc/barant-site/jet/notfound.jet index 638ef31..0c4ad92 100644 --- a/jetc/barant-site/jet/notfound.jet +++ b/jetc/barant-site/jet/notfound.jet @@ -1,4 +1,3 @@ -#api GET /notfound $[jetc_doctype]Page Not Found