From 51133e0c76db30710cb2abfefcb820887c1c9acf Mon Sep 17 00:00:00 2001 From: Brad Arant Date: Sun, 23 Aug 2026 15:07:30 -0700 Subject: [PATCH] Wire CGI form data through the new Parser-based CGIFormData Global now owns a long-lived StreamReader (cgiReader) that CGIFormData (a Parser) holds a reference to - declared before cgiData so it's constructed first and destroyed last. __jet.cpp's POST handling constructs CGIFormData against it and feeds the request body off stdin in 4KB chunks, driving doParse() after each feed rather than blocking to read the whole body up front - the same feed()/doParse() pattern a socket-fed StreamReader would use, keeping memory bounded for large multipart uploads. Also fixes two real, pre-existing bugs surfaced while getting an actual end-to-end CGI request working through jet-2.0 (not just compiling): - Tag::Tag's opening-tag parser was gated on "parent && in.ifNext(\"<\")", but jet-2.0.cpp's main() constructs the top-level __jet with parent=NULL - meaning the outer tag's own attributes (cgi="yes" and everything else) were never actually parsed at all, just echoed as literal passthrough content. parent=NULL is used at exactly one call site in the whole codebase, so dropping the "parent &&" requirement only changes behavior for that one top-level case - every nested tag construction already passes a real parent. - __jet.cpp's boundary-extraction code calls contentType.find("boundary=") to locate the multipart boundary parameter, but find() repositions contentType's own cursor to the match as a side effect. That same cursor-shifted contentType then got passed into CGIFormData's constructor, whose startsWith("multipart/form-data")/ startsWith("application/x-www-form-urlencoded") checks are cursor-relative and so silently matched neither, falling through to an unrecognized content type that parsed nothing (reported COMPLETE immediately, no error, just empty results). Fixed with contentType.reset() before the CGIFormData construction. Verified with a real end-to-end run: jet-2.0 invoked with simulated CGI environment variables (REQUEST_METHOD=POST, CONTENT_TYPE with a boundary=, CONTENT_LENGTH) and a genuine multipart/form-data body (including a file-upload part) piped over stdin, using $[:name] in a .jet script - correctly returns each field's value and correctly leaves the skipped upload field empty. Co-Authored-By: Claude Sonnet 5 --- Global.h | 11 +++++++++++ Tag.cpp | 10 +++++++++- __jet.cpp | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Global.h b/Global.h index f325c14..794ea8d 100644 --- a/Global.h +++ b/Global.h @@ -4,6 +4,7 @@ # include "MString.h" # include "CGIFormattedData.h" # include "CGIFormData.h" +# include "StreamReader.h" # include namespace jet { @@ -28,6 +29,16 @@ namespace jet { char *errorCursor = NULL; coreutils::CGIFormattedData cookies; + + /// + /// cgiReader backs cgiData below - it must outlive it, since + /// CGIFormData (a Parser) only holds a reference to it, not its own + /// copy of the request body. Declared before cgiData so it's + /// constructed first and destroyed last (member destruction order + /// mirrors declaration order). + /// + + coreutils::StreamReader cgiReader; coreutils::CGIFormData *cgiData = NULL; std::map variables; std::map sessions; diff --git a/Tag.cpp b/Tag.cpp index 962b1ef..8e0ccdb 100644 --- a/Tag.cpp +++ b/Tag.cpp @@ -34,7 +34,15 @@ namespace jet { : ZString(in), parentOut(parentOut), global(global), parent(parent), local(local) { this->splitTagName = splitTagName; global.errorCursor = in.getCursor(); - if(parent && in.ifNext("<")) { + // Previously gated on "parent &&" too, which meant the sole + // parent==NULL construction site (jet-2.0.cpp's top-level __jet, see + // main()) never parsed its own opening tag/attributes at + // all - the whole wrapper (including cgi="yes" and every other + // attribute) was silently treated as literal passthrough content + // instead. parent is otherwise always non-NULL (every nested tag + // construction already passes a real parent), so dropping this + // requirement only changes behavior for that one top-level case. + if(in.ifNext("<")) { name = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_!"); if(in.startsWith(" ") || in.startsWith("/") || in.startsWith(">")) { bool finished = false; diff --git a/__jet.cpp b/__jet.cpp index ea2b07e..6d09696 100644 --- a/__jet.cpp +++ b/__jet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace jet { @@ -40,8 +41,39 @@ namespace jet { if(boundaryPos != -1) { boundary = contentType.substring(boundaryPos + 9); } + // find() leaves contentType's own cursor at the match position - + // reset it before handing contentType to CGIFormData, whose + // startsWith("multipart/form-data")/startsWith("application/...") + // checks are cursor-relative and would otherwise silently miss, + // falling through to an unrecognized content type that never + // parses anything. + contentType.reset(); - global.cgiData = new coreutils::CGIFormData(0, contentType, contentLength.asInteger(), boundary); + int bodyLength = contentLength.asInteger(); + global.cgiData = new coreutils::CGIFormData(global.cgiReader, contentType, bodyLength, boundary); + + // Feed the request body off stdin in chunks and drive the parser + // resumably (the same feed()/doParse() pattern as a socket-fed + // StreamReader) rather than blocking to read it all before parsing + // starts - keeps memory bounded for large multipart uploads and + // exercises the real Parser::Status::INCOMPLETE resumption path. + char cgiReadBuf[4096]; + int cgiBytesRead = 0; + coreutils::Parser::Status cgiStatus = coreutils::Parser::Status::INCOMPLETE; + while(cgiBytesRead < bodyLength && cgiStatus != coreutils::Parser::Status::COMPLETE) { + int want = bodyLength - cgiBytesRead; + if(want > (int)sizeof(cgiReadBuf)) + want = sizeof(cgiReadBuf); + int got = ::read(0, cgiReadBuf, want); + if(got <= 0) + throw coreutils::Exception("CGI request body ended before Content-Length was reached."); + cgiBytesRead += got; + coreutils::ZString chunk(cgiReadBuf, got); + global.cgiReader.feed(chunk); + cgiStatus = global.cgiData->doParse(); + } + if(cgiStatus != coreutils::Parser::Status::COMPLETE) + throw coreutils::Exception("CGI request body did not parse to completion."); } } processContainer(container, NULL, true);