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>
82 lines
2.6 KiB
C++
82 lines
2.6 KiB
C++
#include "IMFHeader.h"
|
|
#include "Exception.h"
|
|
|
|
namespace coreutils {
|
|
|
|
IMFHeader::IMFHeader(StreamReader &reader) : Parser(reader) {}
|
|
|
|
Parser::Status IMFHeader::parse() {
|
|
MString &buffer = reader.getBuffer();
|
|
|
|
switch (state) {
|
|
|
|
case NAME: {
|
|
if (buffer.eod())
|
|
return Status::INCOMPLETE;
|
|
if (buffer.getChar() == '\r') {
|
|
// ZString::getLength() is already cursor-relative.
|
|
if (buffer.getLength() < 2)
|
|
return Status::INCOMPLETE;
|
|
if (!buffer.ifNext(ZString("\r\n")))
|
|
throw coreutils::Exception("IMFHeader: expected CRLF at end of header block.");
|
|
return Status::COMPLETE;
|
|
}
|
|
name << buffer.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-0123456789");
|
|
if (buffer.eod())
|
|
return Status::INCOMPLETE;
|
|
if (!buffer.ifNext(ZString(":")))
|
|
throw coreutils::Exception("IMFHeader: expected ':' in header name.");
|
|
state = SKIP_SPACE;
|
|
return Status::DATA_AVAIL;
|
|
}
|
|
|
|
case SKIP_SPACE:
|
|
while (!buffer.eod() && buffer.getChar() == ' ')
|
|
buffer.nextChar();
|
|
if (buffer.eod())
|
|
return Status::INCOMPLETE;
|
|
state = FIELD;
|
|
return Status::DATA_AVAIL;
|
|
|
|
case FIELD:
|
|
field << buffer.getTokenExclude("\r\n");
|
|
// Need 2 bytes buffered to reliably test for "\r\n" - eod() (a
|
|
// 1-byte check) would wrongly treat a lone trailing \r as
|
|
// malformed instead of "wait for the \n". Same bug class already
|
|
// fixed once in IMFBoundary's TAIL state.
|
|
if (buffer.getLength() < 2)
|
|
return Status::INCOMPLETE;
|
|
if (!buffer.ifNext(ZString("\r\n")))
|
|
throw coreutils::Exception("IMFHeader: expected CRLF at end of header field.");
|
|
state = FOLD_CHECK;
|
|
return Status::DATA_AVAIL;
|
|
|
|
case FOLD_CHECK:
|
|
if (buffer.eod())
|
|
return Status::INCOMPLETE;
|
|
if (buffer.getChar() == ' ' || buffer.getChar() == '\t') {
|
|
buffer.nextChar();
|
|
field << " ";
|
|
state = SKIP_SPACE;
|
|
return Status::DATA_AVAIL;
|
|
}
|
|
commitField();
|
|
state = NAME;
|
|
return Status::DATA_AVAIL;
|
|
|
|
}
|
|
throw coreutils::Exception("IMFHeader: unreachable state.");
|
|
}
|
|
|
|
void IMFHeader::commitField() {
|
|
headers[name] = field;
|
|
name = MString();
|
|
field = MString();
|
|
}
|
|
|
|
std::map<MString, MString> &IMFHeader::getHeaders() {
|
|
return headers;
|
|
}
|
|
|
|
}
|