Files
CoreUtils/CGIFormData.h
T
Brad ArantandClaude Sonnet 5 bf6810b781 Rebuild CGIFormData on Parser/StreamReader, fix a 2-byte-lookahead bug
CGIFormData now inherits Parser instead of MString and parses a CGI
request body (urlencoded or multipart/form-data) resumably against the
shared StreamReader, exposing the result via operator[] - matching every
other Parser in this redesign. multipart/form-data reuses IMFBoundary and
IMFHeader via checkParse() (chained through the same StreamReader) for
each part's boundary line and header block; urlencoded bodies wait for
the full Content-Length before delegating to the existing
CGIFormattedData parser. File-upload parts (Content-Disposition with a
filename= parameter) are recognized and skipped, matching this
codebase's convention that uploads get a separate mechanism.

Values are stored under an indexed "name:N" key (N = 0-based occurrence
count for that name), matching the :N-suffix convention JetCore's
Tag::getVariable already applies to its $[:name] CGI variable syntax
(defaulting to :0) - this also naturally supports repeated field names
(checkboxes, multi-select). operator[] applies the same :0 default so a
bare name still works when called directly, not just through that
convention layer.

Two real resumability bugs found and fixed via a byte-at-a-time test
(feeding one byte per StreamReader::feed() call, the harshest possible
fragmentation):
- CGIFormData's opening boundary marker ("--boundary", no leading CRLF
  per RFC 2046 for the first boundary) crammed "find+consume the marker"
  and "disambiguate -- vs CRLF" into one state; when the disambiguation
  needed more data, INCOMPLETE was returned without recording that the
  marker was already consumed, so retrying re-searched for it past where
  it had already been consumed, silently skipping content forever. Split
  into two states (INIT / OPENING_TAIL), the same fix already applied
  once to IMFBoundary's PREFIX/TAIL split.
- IMFHeader's FIELD state checked eod() (a 1-byte availability check)
  immediately before an ifNext("\r\n") call that needs 2 bytes - a lone
  trailing \r with no \n yet available was wrongly treated as malformed
  instead of "wait for more data". Fixed to check getLength() < 2.

Verified via a dedicated test (64 assertions total across this and the
earlier Parser/StreamReader test) covering urlencoded, multipart with a
skipped file upload, and the byte-at-a-time stress case - plus a real
end-to-end run through jet-2.0 with a simulated CGI multipart POST
request (see the matching JetCore commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:07:09 -07:00

76 lines
2.4 KiB
C++

#ifndef __CGIFormData_h__
# define __CGIFormData_h__
#include "Parser.h"
#include "IMFHeader.h"
#include "IMFBoundary.h"
#include <map>
namespace coreutils {
///
/// CGIFormData parses a CGI request body (application/x-www-form-urlencoded
/// or multipart/form-data) from the shared StreamReader and exposes the
/// resulting name/value pairs via operator[] - the object a caller (e.g.
/// JetCore's Tag::getVariable, for its $[:name] CGI variable syntax) holds
/// onto and indexes by variable name once doParse() reports COMPLETE.
///
/// Resumable like every other Parser here: for multipart/form-data, each
/// part's boundary line and header block are parsed via IMFBoundary/
/// IMFHeader (chained through checkParse(), sharing this same
/// StreamReader), so a part - or even its boundary marker - can straddle
/// any number of feed() calls without losing state. File-upload parts
/// (Content-Disposition with a filename= parameter) are recognized and
/// skipped - matching this codebase's existing convention that uploads
/// get a separate mechanism, not buffered through here.
///
class CGIFormData : public Parser {
public:
CGIFormData(StreamReader &reader, ZString contentType, int contentLength, ZString boundary);
virtual ~CGIFormData();
MString operator[](ZString name);
protected:
Status parse() override;
private:
enum Mode { URLENCODED, MULTIPART, UNKNOWN } mode;
enum State { INIT, OPENING_TAIL, PART_HEADERS, PART_BODY, PART_BOUNDARY, URLENCODED_BODY, DONE } state = INIT;
ZString contentType;
int contentLength;
ZString boundary;
std::map<MString, MString> data;
IMFHeader *headerParser = NULL;
IMFBoundary *boundaryParser = NULL;
MString currentName;
bool currentSkip = false;
std::map<MString, int> nameCounts;
void extractPartName(IMFHeader &header);
///
/// Stores value under "name:N", where N is how many values have
/// already been stored under this same base name (0 for the first,
/// 1 for the second, etc.) - matches the indexed key format
/// JetCore's Tag::getVariable's $[:name] syntax looks up by
/// (defaulting to :0 when the caller doesn't specify an index),
/// supporting repeated field names (checkboxes, multi-select) the
/// same way.
///
void storeValue(MString name, MString value);
};
}
#endif