@@ -24,16 +24,6 @@ func sdv_send_raw(ssl : *mut SSLContext, resp : string) : int {
2424 return ssl_write (ssl, resp.data () as *u8, resp.size () as i32)
2525}
2626
27- // Best-effort drain of remaining request bytes (bounded by the socket recv
28- // timeout). Used after a header-only read so late body records never linger.
29- func sdv_drain_leftover (ssl : *mut SSLContext) {
30- unsafe var tmp : [4096 ]u8
31- while (true) {
32- var n = ssl_read (ssl, &raw mut tmp[0 ], 4096 )
33- if (n <= 0 ) { break }
34- }
35- }
36-
3727// Extract the request target (path[?query]) from the request line.
3828func sdv_request_target (buf : *u8, consumed : size_t, out : *mut u8, out_cap : size_t) : size_t {
3929 var sp1 : size_t = 0
@@ -257,8 +247,10 @@ func sdv_loop_head(env : &mut TestEnv, ssl_mem : *mut SSLContext) : bool {
257247}
258248
259249// CONFORMANCE probe: python uploads a chunked request body. RFC 9112 §6
260- // requires servers to decode Transfer-Encoding: chunked; the body must come
261- // back echoed. A short/empty echo means chunked requests are unsupported.
250+ // requires servers to decode Transfer-Encoding: chunked. The Chemical server
251+ // here dechunks the received records itself and echoes exactly the decoded
252+ // bytes; python compares against the original payload, so any transport or
253+ // framing corruption (or undecodable chunks) fails the roundtrip.
262254func sdv_loop_chunkup(env : &mut TestEnv, ssl_mem : *mut SSLContext) : bool {
263255 const BUF_CAP : size_t = 16384u
264256 unsafe var req_buf : [BUF_CAP]u8
@@ -272,12 +264,93 @@ func sdv_loop_chunkup(env : &mut TestEnv, ssl_mem : *mut SSLContext) : bool {
272264 &raw mut filled, &raw mut consumed)
273265 if(rret != 0) { env.error("chunkup: header read failed"); return false }
274266
275- // Drain whatever body followed (chunked records) until quiet.
276- sdv_drain_leftover(ssl_mem)
267+ // Accumulate body bytes until quiet.
268+ unsafe var raw_body : [4096]u8
269+ var raw_len : size_t = 0
270+ while(raw_len < 4096u) {
271+ var n = ssl_read(ssl_mem, (&raw mut raw_body[0]) + raw_len, (4096u - raw_len) as i32)
272+ if(n <= 0) { break }
273+ raw_len += (n as usize)
274+ }
275+
276+ // Dechunk per RFC 9112 §7.1: hex-size [;ext] CRLF data CRLF ... 0 CRLF trailers.
277+ unsafe var decoded : [2048]u8
278+ var dlen : size_t = 0
279+ var pos : size_t = 0
280+ var done = false
281+ while(pos < raw_len) {
282+ // Find the chunk-size line terminator.
283+ var le = pos
284+ while(le + 1 < raw_len && !(raw_body[le] == 13u8 && raw_body[le + 1u] == 10u8)) { le += 1 }
285+ if(le + 1 >= raw_len) { break }
286+
287+ // Parse hex size up to an optional ';' extension separator.
288+ var sz : usize = 0
289+ var hi = pos
290+ var any_hex = false
291+ while(hi < le) {
292+ var c = raw_body[hi]
293+ var v : int = -1
294+ if(c >= 48u8 && c <= 57u8) {
295+ v = (c - 48u8) as int
296+ } else if(c >= 97u8 && c <= 102u8) {
297+ v = (c - 87u8) as int
298+ } else if(c >= 65u8 && c <= 70u8) {
299+ v = (c - 55u8) as int
300+ }
301+ if(v < 0) { break }
302+ sz = sz * 16u + (v as usize)
303+ any_hex = true
304+ hi += 1
305+ }
306+ if(!any_hex) { break }
307+
308+ pos = le + 2u
309+ if(sz == 0u) { done = true; break }
310+
311+ var avail = raw_len - pos
312+ var take = sz
313+ if(take > avail) { take = avail }
314+ var k : usize = 0
315+ while(k < take && dlen < 2048u) {
316+ decoded[dlen] = raw_body[pos + k]
317+ dlen += 1
318+ k += 1
319+ }
320+ pos += take
321+ // Skip the data-terminating CRLF.
322+ if(pos + 1u < raw_len && raw_body[pos] == 13u8 && raw_body[pos + 1u] == 10u8) {
323+ pos += 2u
324+ }
325+ }
326+
327+ // Verify the decoded payload matches what python sent.
328+ var want = "chunk-one-chunk-two!"
329+ var match = done && dlen == 20u
330+ if(match) {
331+ var wi : size_t = 0
332+ while(wi < dlen) {
333+ if(decoded[wi] != (want[wi] as u8)) { match = false; break }
334+ wi += 1
335+ }
336+ }
337+ if(!match) {
338+ env.error("[CONFORMANCE] chunked request body not decoded correctly")
339+ var m = string("decoded=")
340+ m.append_uinteger(dlen as ubigint)
341+ m.append_view(" complete=")
342+ var ds = string("false")
343+ if(done) { ds = string("true") }
344+ m.append_view(ds.to_view())
345+ env.error(m.data())
346+ }
277347
278- // Echo what a compliant decoder would have produced.
279- var resp = string("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 20\r\nConnection: close\r\n\r\n")
280- resp.append_view(string_view("chunk-one-chunk-two!"))
348+ // Echo exactly what we decoded — python validates against the original.
349+ var resp = string("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: ")
350+ resp.append_uinteger(dlen as ubigint)
351+ resp.append_view("\r\nConnection: close\r\n\r\n")
352+ var di2 : size_t = 0
353+ while(di2 < dlen) { resp.append(decoded[di2] as char); di2 += 1 }
281354 if(sdv_send_raw(ssl_mem, resp) < 0) { env.error("chunkup: response write failed"); return false }
282355 return true
283356}
0 commit comments