From 73b0780321acb14bab6707006f5136ea9998951d Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Thu, 30 Jul 2026 19:22:17 +0200 Subject: feat(receive): add maxsize argument to bound memory usage client:receive("*l") and receive("*a") are unbounded: a peer that never sends a newline, or never closes, makes LuaSocket buffer until the process runs out of memory (e.g. src/http.lua reading response headers in a loop). Add an optional maxsize argument that caps the payload a single call may accumulate, including prefix. - Hoist all argument validation ahead of timeout_markstart() so bad calls (maxsize < 1, #prefix >= maxsize, numeric pattern > maxsize) raise before any I/O and leave the socket untouched. - recvline/recvall take a budget and return a new internal BUF_OVERSIZED code, surfaced to Lua as the "oversized" error alongside "timeout"/"closed", with the partial held in the 3rd return value. - recvraw is left untouched: argument checks make the cap unreachable for numeric patterns. - Preserve three invariants: a timeout partial is always shorter than maxsize (safe to retry as prefix), completion beats the cap for *a, and no bytes are lost or skipped on overflow. - tcp.c, unixstream.c and serial.c all share this code path unchanged. Adds test coverage (argument errors, *l/*a boundaries, timeout/close at the cap, the drain idiom, numeric patterns, unix-stream mirror) and documents the new argument, error, and recovery idioms in docs/tcp.html. --- test/utestclnt.lua | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) (limited to 'test/utestclnt.lua') diff --git a/test/utestclnt.lua b/test/utestclnt.lua index 7f10643..395260c 100644 --- a/test/utestclnt.lua +++ b/test/utestclnt.lua @@ -510,6 +510,48 @@ remote(string.format([[ print("ok") end +------------------------------------------------------------------------ +function test_maxsize() + -- A4: #prefix == maxsize raises (mirrors testclnt.lua group A) + reconnect() + pass("argument errors") + -- bounds the pre-implementation call (arg 4 silently dropped, so this + -- becomes a real blocking read on an idle socket) so a meaningful + -- failure doesn't hang the suite + data:settimeout(0.2) + local ok = pcall(data.receive, data, "*l", string.rep("x", 10), 10) + assert(not ok, "A4 failed: #prefix == maxsize should raise") + data:settimeout(-1) + + -- B2/B3: *l boundary (mirrors testclnt.lua group B) + pass("*l boundary") + remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 100)) + local d, e, p = data:receive("*l", nil, 100) + assert(d == string.rep("a", 100) and e == nil, "B2 failed") + + remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101)) + d, e, p = data:receive("*l", nil, 100) + assert(d == nil and e == "oversized" and p == string.rep("a", 100), "B3 failed") + d, e = data:receive("*l", nil, 100) + assert(d == "a" and e == nil, "B3 failed: leftover byte") + + -- C2: timeout exactly at the cap is oversized, not timeout (I1) + reconnect() + remote(string.format([[data:send(string.rep('a',%d))]], 100)) + data:settimeout(0.5) + d, e, p = data:receive("*l", nil, 100) + assert(e == "oversized" and #p == 100, + "C2 failed: expected oversized, got " .. tostring(e)) + + -- E2: completion beats the cap (I2) + reconnect() + remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100)) + d, e = data:receive("*a", nil, 100) + assert(d == string.rep("a", 100) and e == nil, + "E2 failed: completion should beat the cap") + pass("ok") +end + ------------------------------------------------------------------------ test("method registration") @@ -641,4 +683,7 @@ test_blockingtimeoutreceive(800091, 2, 3) test_blockingtimeoutreceive(800091, 3, 2) test_blockingtimeoutreceive(800091, 3, 1) +test("receive maxsize") +test_maxsize() + test(string.format("done in %.2fs", socket.gettime() - start)) -- cgit v1.2.3-55-g6feb