diff options
| author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-07-30 19:22:17 +0200 |
|---|---|---|
| committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2026-07-30 22:06:04 +0200 |
| commit | 73b0780321acb14bab6707006f5136ea9998951d (patch) | |
| tree | 30c5d0cff89913185df7f5fb7ebe1636827cfd58 /test/utestclnt.lua | |
| parent | e13de2013749961edaa126697f3290d3dca91823 (diff) | |
| download | luasocket-73b0780321acb14bab6707006f5136ea9998951d.tar.gz luasocket-73b0780321acb14bab6707006f5136ea9998951d.tar.bz2 luasocket-73b0780321acb14bab6707006f5136ea9998951d.zip | |
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.
Diffstat (limited to 'test/utestclnt.lua')
| -rw-r--r-- | test/utestclnt.lua | 45 |
1 files changed, 45 insertions, 0 deletions
diff --git a/test/utestclnt.lua b/test/utestclnt.lua index 7f10643..395260c 100644 --- a/test/utestclnt.lua +++ b/test/utestclnt.lua | |||
| @@ -511,6 +511,48 @@ remote(string.format([[ | |||
| 511 | end | 511 | end |
| 512 | 512 | ||
| 513 | ------------------------------------------------------------------------ | 513 | ------------------------------------------------------------------------ |
| 514 | function test_maxsize() | ||
| 515 | -- A4: #prefix == maxsize raises (mirrors testclnt.lua group A) | ||
| 516 | reconnect() | ||
| 517 | pass("argument errors") | ||
| 518 | -- bounds the pre-implementation call (arg 4 silently dropped, so this | ||
| 519 | -- becomes a real blocking read on an idle socket) so a meaningful | ||
| 520 | -- failure doesn't hang the suite | ||
| 521 | data:settimeout(0.2) | ||
| 522 | local ok = pcall(data.receive, data, "*l", string.rep("x", 10), 10) | ||
| 523 | assert(not ok, "A4 failed: #prefix == maxsize should raise") | ||
| 524 | data:settimeout(-1) | ||
| 525 | |||
| 526 | -- B2/B3: *l boundary (mirrors testclnt.lua group B) | ||
| 527 | pass("*l boundary") | ||
| 528 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 100)) | ||
| 529 | local d, e, p = data:receive("*l", nil, 100) | ||
| 530 | assert(d == string.rep("a", 100) and e == nil, "B2 failed") | ||
| 531 | |||
| 532 | remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101)) | ||
| 533 | d, e, p = data:receive("*l", nil, 100) | ||
| 534 | assert(d == nil and e == "oversized" and p == string.rep("a", 100), "B3 failed") | ||
| 535 | d, e = data:receive("*l", nil, 100) | ||
| 536 | assert(d == "a" and e == nil, "B3 failed: leftover byte") | ||
| 537 | |||
| 538 | -- C2: timeout exactly at the cap is oversized, not timeout (I1) | ||
| 539 | reconnect() | ||
| 540 | remote(string.format([[data:send(string.rep('a',%d))]], 100)) | ||
| 541 | data:settimeout(0.5) | ||
| 542 | d, e, p = data:receive("*l", nil, 100) | ||
| 543 | assert(e == "oversized" and #p == 100, | ||
| 544 | "C2 failed: expected oversized, got " .. tostring(e)) | ||
| 545 | |||
| 546 | -- E2: completion beats the cap (I2) | ||
| 547 | reconnect() | ||
| 548 | remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100)) | ||
| 549 | d, e = data:receive("*a", nil, 100) | ||
| 550 | assert(d == string.rep("a", 100) and e == nil, | ||
| 551 | "E2 failed: completion should beat the cap") | ||
| 552 | pass("ok") | ||
| 553 | end | ||
| 554 | |||
| 555 | ------------------------------------------------------------------------ | ||
| 514 | 556 | ||
| 515 | test("method registration") | 557 | test("method registration") |
| 516 | test_methods(socket.unix(), { | 558 | test_methods(socket.unix(), { |
| @@ -641,4 +683,7 @@ test_blockingtimeoutreceive(800091, 2, 3) | |||
| 641 | test_blockingtimeoutreceive(800091, 3, 2) | 683 | test_blockingtimeoutreceive(800091, 3, 2) |
| 642 | test_blockingtimeoutreceive(800091, 3, 1) | 684 | test_blockingtimeoutreceive(800091, 3, 1) |
| 643 | 685 | ||
| 686 | test("receive maxsize") | ||
| 687 | test_maxsize() | ||
| 688 | |||
| 644 | test(string.format("done in %.2fs", socket.gettime() - start)) | 689 | test(string.format("done in %.2fs", socket.gettime() - start)) |
