Multipart.parse takes &String, and a Carp String is a bare char* —
String.length is strlen, and String.to-bytes truncates the same way. So a
body carrying a NUL byte ends at that NUL before any parsing happens, and
everything after it, including the closing delimiter, is invisible.
Every binary upload contains NUL bytes. PNG, PDF, zip, JPEG, any of them.
Measured on a 62-byte body — --B CRLF Content-Disposition… CRLF CRLF XX <NUL> Y CRLF --B-- CRLF:
real-bytes=62 String.length=51 Multipart.parse -> Result.Success []
It does not error. It returns zero parts, so a handler sees an upload that
succeeded and carried nothing.
This is long-standing and not a regression — it predates #39, which measured it
while replacing the boundary scan and deliberately did not change it. The scan
itself is now NUL-agnostic; the truncation happens above it, when the bytes
become a String at all.
web routes every multipart/form-data request through this
(web.carp:645, Form.decode-multipart), so it is reachable from any
form-with-a-file-input in a web app.
What a fix costs
The body has to stop being a String:
(defn parse [body boundary]) ; body: &String
(defn parse [body boundary]) ; body: &(Array Byte)
That moves FormPart's body field too, and web's call site with it. It is a
public signature change in both packages, which is why #39 stopped at measuring
it rather than taking it — the shape of the replacement is your call. A
parse-bytes alongside the existing parse would avoid the break, at the cost
of leaving a function in the API that silently loses data.
Happy to implement whichever way you want it.
Filed by the carpentry-org heartbeat agent (Claude), out of the measurements in #39.
Multipart.parsetakes&String, and a CarpStringis a barechar*—String.lengthisstrlen, andString.to-bytestruncates the same way. So abody carrying a NUL byte ends at that NUL before any parsing happens, and
everything after it, including the closing delimiter, is invisible.
Every binary upload contains NUL bytes. PNG, PDF, zip, JPEG, any of them.
Measured on a 62-byte body —
--B CRLF Content-Disposition… CRLF CRLF XX <NUL> Y CRLF --B-- CRLF:It does not error. It returns zero parts, so a handler sees an upload that
succeeded and carried nothing.
This is long-standing and not a regression — it predates #39, which measured it
while replacing the boundary scan and deliberately did not change it. The scan
itself is now NUL-agnostic; the truncation happens above it, when the bytes
become a
Stringat all.webroutes everymultipart/form-datarequest through this(
web.carp:645,Form.decode-multipart), so it is reachable from anyform-with-a-file-input in a
webapp.What a fix costs
The body has to stop being a
String:That moves
FormPart'sbodyfield too, andweb's call site with it. It is apublic signature change in both packages, which is why #39 stopped at measuring
it rather than taking it — the shape of the replacement is your call. A
parse-bytesalongside the existingparsewould avoid the break, at the costof leaving a function in the API that silently loses data.
Happy to implement whichever way you want it.
Filed by the carpentry-org heartbeat agent (Claude), out of the measurements in #39.