From 3d96de5d9c24adc6c605b1bc9aab156fec97cdad Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Mon, 24 Aug 2026 23:04:46 +0200 Subject: [PATCH 1/2] Scan multipart boundaries with Boyer-Moore-Horspool Multipart.at? recomputed (String.length s) on every call, and String.length is strlen over the whole body; index-from called at? once per byte offset, so each candidate position cost a full pass over a multi-megabyte upload. The scan was quadratic: 8 KB took 11 ms, 256 KB took 10 s. at? now takes the body length as a parameter, and index-from is Boyer-Moore-Horspool over a 256-entry bad-character table built once per parse from the CRLF--boundary needle, so a typical ~40-byte boundary skips most positions instead of testing each one. 256 KB now parses in 3 ms, 4 MB in 50 ms. parse's preamble search moved onto the same scan, replacing its String.index-of-string call, which stopped at the first NUL byte. Verified byte-identical to the old scan on a 24-case corpus covering empty parts, boundary substrings in bodies, preambles, missing final --, bare-LF headers, a boundary at the very end, and generated multi-part bodies. --- http.carp | 52 +++++++++++++++++++++++++++++++++----------------- test/http.carp | 28 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/http.carp b/http.carp index 286769e..1b2aeaf 100644 --- a/http.carp +++ b/http.carp @@ -1781,11 +1781,10 @@ request for you. (defmodule Multipart (hidden at?) (private at?) - ; does `s` contain `sub` starting at byte offset `pos`? - (defn at? [s pos sub] - (let [subl (String.length sub) - sl (String.length s)] - (if (or (< pos 0) (> (+ pos subl) sl)) + ; does `s`, whose byte length is `slen`, hold `sub` at byte offset `pos`? + (defn at? [s slen pos sub] + (let [subl (String.length sub)] + (if (or (< pos 0) (> (+ pos subl) slen)) false (let-do [ok true j 0] @@ -1795,16 +1794,29 @@ request for you. (set! ok false))) ok)))) + (hidden skip-table) + (private skip-table) + ; Boyer-Moore-Horspool bad-character shifts for `needle`, indexed by byte. + (defn skip-table [needle nlen] + (let-do [t (Array.replicate 256 &nlen) + last (Int.dec nlen)] + (for [i 0 last] + (Array.aset! &t (Char.to-int (String.char-at needle i)) (- last i))) + t)) + (hidden index-from) (private index-from) - ; first byte offset >= `from` where `needle` occurs in `s`, or -1. - (defn index-from [s needle from] - (let-do [slen (String.length s) - nlen (String.length needle) - res -1 - i from] + ; first byte offset >= `from` where `needle` occurs in `s`, or -1. `skip` is + ; the needle’s `skip-table`. + (defn index-from [s slen needle nlen skip from] + (let-do [res -1 + i from + last (Int.dec nlen)] (while (and (< res 0) (<= (+ i nlen) slen)) - (if (at? s i needle) (set! res i) (set! i (Int.inc i)))) + (let [c (String.char-at s (+ i last))] + (if (and (= c (String.char-at needle last)) (at? s slen i needle)) + (set! res i) + (set! i (+ i @(Array.unsafe-nth skip (Char.to-int c))))))) res)) (hidden header-value) @@ -1857,13 +1869,14 @@ into its `FormPart`s. Fails when the opening boundary delimiter is absent.") cdlen (String.length &crlf-dash) dashlen (String.length &dash) len (String.length body) + skip (Multipart.skip-table &crlf-dash cdlen) ; the opening delimiter is `--boundary`, either at the very start or ; after a preamble that ends in CRLF; every later delimiter is ; `CRLF--boundary`, so searching for that avoids matching a bare ; `--boundary` that happens to occur inside a part body. - open (if (Multipart.at? body 0 &dash) + open (if (Multipart.at? body len 0 &dash) dashlen - (let [p (String.index-of-string body &crlf-dash)] + (let [p (Multipart.index-from body len &crlf-dash cdlen &skip 0)] (if (< p 0) -1 (+ p cdlen))))] (if (< open 0) (Result.Error @"multipart: boundary delimiter not found") @@ -1872,11 +1885,16 @@ into its `FormPart`s. Fails when the opening boundary delimiter is absent.") done false] (while (not done) (cond - (Multipart.at? body pos "--") (set! done true) + (Multipart.at? body len pos "--") (set! done true) (>= pos len) (set! done true) (do - (when (Multipart.at? body pos "\r\n") (set! pos (+ pos 2))) - (let [next (Multipart.index-from body &crlf-dash pos)] + (when (Multipart.at? body len pos "\r\n") (set! pos (+ pos 2))) + (let [next (Multipart.index-from body + len + &crlf-dash + cdlen + &skip + pos)] (if (< next 0) (set! done true) (do diff --git a/test/http.carp b/test/http.carp index ff11f48..7eef4e2 100644 --- a/test/http.carp +++ b/test/http.carp @@ -1236,6 +1236,34 @@ (Result.Error _) true (Result.Success _) false) "a missing boundary delimiter is an error") + (assert-equal test + &(continuation 64) + &(mp-body + &(mp-parts + &(fmt + "--b\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\n%s\r\n--b--\r\n" + &(continuation 64)) + "b") + 0) + "a part body that is not valid UTF-8 round-trips") + (assert-equal test + "ok" + &(mp-body + &(mp-parts + &(fmt + "%s\r\n--b\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nok\r\n--b--\r\n" + &(continuation 64)) + "b") + 0) + "a preamble that is not valid UTF-8 is ignored") + (assert-equal test + "888--LongBoundary8" + &(mp-body + &(mp-parts + "--LongBoundary8\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\n888--LongBoundary8\r\n--LongBoundary8--\r\n" + "LongBoundary8") + 0) + "a body repeating the delimiter’s last byte is scanned correctly") ; ---- Request.multipart-data ---- (assert-equal test From 57bb789cef67112a7ff933d4742a3ebee84e7b25 Mon Sep 17 00:00:00 2001 From: "carpentry-heartbeat[bot]" Date: Tue, 25 Aug 2026 04:25:24 +0200 Subject: [PATCH 2/2] Pin the boundary scan's end bound with a delimiter-at-end case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `index-from`'s `(<= (+ i nlen) slen)` and `at?`'s `(> (+ pos subl) slen)` both decide whether a delimiter that ends exactly at the end of the body is found, and neither was pinned: mutating either one to its off-by-one neighbour left the suite at 479 passed / 0 failed, while the review's randomised differential put them at 1830 and 1949 differing bodies. The falsifying shape — a body whose final delimiter is the last thing in it, which is what a truncated upload looks like — was not in the suite. One assert-equal covers both bounds: under either mutation the body decodes to zero parts and the run aborts in Array.unsafe-nth. 480 passed / 0 failed. --- test/http.carp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/http.carp b/test/http.carp index 7eef4e2..a274628 100644 --- a/test/http.carp +++ b/test/http.carp @@ -1264,6 +1264,14 @@ "LongBoundary8") 0) "a body repeating the delimiter’s last byte is scanned correctly") + (assert-equal test + "hello" + &(mp-body + &(mp-parts + "--b\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nhello\r\n--b" + "b") + 0) + "a delimiter that ends at the end of the body is still found") ; ---- Request.multipart-data ---- (assert-equal test