From 6ac33601a02b7eb95efa2273d1815eff814f8095 Mon Sep 17 00:00:00 2001 From: Brad Arant Date: Sun, 23 Aug 2026 23:11:20 -0700 Subject: [PATCH] Fix $[...] variable parsing to consume the entire bracket contents renderVariableName() built the variable name by whitelisting an identifier charset (letters/digits/._-) via getTokenInclude(), then threw "invalid variable name." on the first character outside that set - including a plain space. Real-world CGI form field names aren't restricted to identifier characters (an HTML is completely valid), so $[:first name] failed outright instead of looking up the field as sent. Rewrote to exclude only the genuinely structural characters (']', ';', and the start of a nested $[.../#[...substitution) via getTokenExclude, so everything else between the brackets - spaces included - is consumed as part of the name before any lookup happens, rather than stopping early and erroring. This also let the separate ':' handling branch be dropped entirely: since ':' is no longer excluded, it now flows through the same sweep naturally (needed for both the CGI sigil name itself and the "$[:name:2]" explicit-index suffix). Verified live: $[:first name] against a real multipart CGI body with a field literally named "first name" now resolves correctly (previously threw before ever reaching the lookup). A stray, non-nested '$' or '#' in a name is kept as literal text rather than erroring. Re-verified existing sigils are unaffected - plain $[x], $[!x] (forced-global), and tests/posttest.sh's ^session/:.name3 lookups all still resolve exactly as before. Co-Authored-By: Claude Sonnet 5 --- Tag.cpp | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Tag.cpp b/Tag.cpp index 14a4485..45d0be0 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -472,20 +472,35 @@ namespace jet { } void Tag::renderVariableName(coreutils::ZString &variable, coreutils::MString &name, coreutils::MString &modifier) { + // Consumes everything between '[' (already eaten by the caller) and the + // matching ']' as the variable name, stopping early only for the + // genuinely structural bits: the ';' modifier separator and a nested + // $[...]/#[...] substitution. Real-world CGI field names are not + // restricted to identifier characters (an HTML form field can be named + // "first name", "cost$", etc.) - excluding only the structural + // characters (rather than whitelisting an identifier charset and + // throwing on anything else) means the full bracket contents are + // always consumed before a lookup happens, matching how a caller + // actually writing $[:field name] expects it to behave. while(!variable.ifNext("]")) { - name << variable.getTokenInclude("?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-"); + name << variable.getTokenExclude("];$#"); if(variable.ifNext(";")) { renderVariableName(variable, modifier, modifier); return; - } else if(variable.ifNext(":")) { - name << ":"; } else if(variable.startsWith("$[") || variable.startsWith("#[")) { name << getVariable(variable); - } else if(variable.ifNext("]")) - return; - else if(!variable.ifNextInclude("?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-")) - throw coreutils::Exception("invalid variable name."); - } + } else if(variable.ifNext("]")) { + return; + } else if(variable.eod()) { + throw coreutils::Exception("unterminated variable reference - missing ']'."); + } else { + // A literal '$' or '#' that isn't the start of a nested + // substitution (getTokenExclude stopped on it defensively, since + // it can't look ahead past a single character) - keep it as + // ordinary name text and continue. + name << variable.nextChar(); + } + } return; }