aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2026-07-30 19:22:17 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2026-07-30 22:06:04 +0200
commit73b0780321acb14bab6707006f5136ea9998951d (patch)
tree30c5d0cff89913185df7f5fb7ebe1636827cfd58
parente13de2013749961edaa126697f3290d3dca91823 (diff)
downloadluasocket-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.
-rw-r--r--CHANGELOG.md4
-rw-r--r--docs/tcp.html58
-rw-r--r--src/buffer.c108
-rw-r--r--test/testclnt.lua174
-rw-r--r--test/utestclnt.lua45
5 files changed, 366 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a25186..03947c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
1# Changelog 1# Changelog
2 2
3## Unreleased
4
5* Add `maxsize` argument to `receive` to bound the memory a single call may accumulate, returning `"oversized"` instead of growing without limit – @Tieske
6
3## [v3.1.0](https://github.com/lunarmodules/luasocket/releases/v3.1.0) — 2022-07-27 7## [v3.1.0](https://github.com/lunarmodules/luasocket/releases/v3.1.0) — 2022-07-27
4 8
5* Add support for TCP Defer Accept – @Zash 9* Add support for TCP Defer Accept – @Zash
diff --git a/docs/tcp.html b/docs/tcp.html
index a26228d..4c6c6bc 100644
--- a/docs/tcp.html
+++ b/docs/tcp.html
@@ -351,7 +351,7 @@ method returns <b><tt>nil</tt></b> followed by an error message.
351<!-- receive ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> 351<!-- receive ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
352 352
353<p class="name" id="receive"> 353<p class="name" id="receive">
354client:<b>receive(</b>[pattern [, prefix]]<b>)</b> 354client:<b>receive(</b>[pattern [, prefix [, maxsize]]]<b>)</b>
355</p> 355</p>
356 356
357<p class="description"> 357<p class="description">
@@ -380,14 +380,23 @@ of bytes from the socket.</li>
380of any received data before return. 380of any received data before return.
381</p> 381</p>
382 382
383<p class="parameters">
384<tt>Maxsize</tt> is an optional positive integer bounding the number of
385payload bytes the call may accumulate, <em>including</em> <tt>prefix</tt>.
386Omitted or <tt><b>nil</b></tt> means unlimited.
387</p>
388
383<p class="return"> 389<p class="return">
384If successful, the method returns the received pattern. In case of error, 390If successful, the method returns the received pattern. In case of error,
385the method returns <tt><b>nil</b></tt> followed by an error 391the method returns <tt><b>nil</b></tt> followed by an error
386message, followed by a (possibly empty) string containing 392message, followed by a (possibly empty) string containing
387the partial that was received. The error message can be 393the partial that was received. The error message can be
388the string '<tt>closed</tt>' in case the connection was 394the string '<tt>closed</tt>' in case the connection was
389closed before the transmission was completed or the string 395closed before the transmission was completed, the string
390'<tt>timeout</tt>' in case there was a timeout during the operation. 396'<tt>timeout</tt>' in case there was a timeout during the operation, or,
397when <tt>maxsize</tt> was given, the string '<tt>oversized</tt>' in case
398the pattern did not complete within <tt>maxsize</tt> bytes -- in which case
399the third return value holds exactly <tt>maxsize</tt> bytes.
391</p> 400</p>
392 401
393<p class="note"> 402<p class="note">
@@ -399,6 +408,49 @@ functions should return <tt><b>nil</b></tt> on error. Thus it was changed
399too. 408too.
400</p> 409</p>
401 410
411<p class="note">
412<b>Note on <tt>maxsize</tt></b>: passing a <tt>maxsize</tt> that is smaller
413than 1, a <tt>prefix</tt> whose length is greater than or equal to
414<tt>maxsize</tt>, or, for a numeric <tt>pattern</tt>, a byte count greater
415than <tt>maxsize</tt>, all raise a Lua error rather than returning
416<tt><b>nil</b></tt> plus a message -- these are caller logic errors, and
417they are detected before any byte is read from the socket. To drain and
418discard an oversized line while keeping memory bounded and the stream
419aligned:
420</p>
421
422<pre class="example">
423local data, err, part
424repeat
425 data, err, part = client:receive("*l", "", 4096)
426until err ~= "oversized"
427</pre>
428
429<p class="note">
430To instead retry and eventually get the whole thing, carry the partial
431forward as <tt>prefix</tt> and grow <tt>maxsize</tt>:
432</p>
433
434<pre class="example">
435local data, err, part = client:receive("*l", nil, 4096)
436if err == "oversized" then
437 data, err, part = client:receive("*l", part, 65536) -- larger cap, or this raises
438end
439</pre>
440
441<p class="note">
442Retrying with <tt>prefix</tt> set to the previous partial result and an
443<em>unchanged</em> <tt>maxsize</tt> raises the length-check error above by
444design -- otherwise it would be a zero-progress spin: no I/O, no timeout,
445no error, just CPU. A <tt>timeout</tt> partial is always strictly shorter
446than <tt>maxsize</tt>, so it is always safe to feed straight back as
447<tt>prefix</tt> with the same <tt>maxsize</tt>. Finally, note that
448<tt>maxsize</tt> bounds the payload <em>returned</em>, not necessarily the
449bytes taken off the wire: for the <tt>*l</tt> pattern the discarded CR
450characters and the line terminator mean more bytes may have been consumed
451than the returned length suggests.
452</p>
453
402<!-- send +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> 454<!-- send +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
403 455
404<p class="name" id="send"> 456<p class="name" id="send">
diff --git a/src/buffer.c b/src/buffer.c
index 7148be3..05d6fb3 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -9,12 +9,24 @@
9* Internal function prototypes 9* Internal function prototypes
10\*=========================================================================*/ 10\*=========================================================================*/
11static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b); 11static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b);
12static int recvline(p_buffer buf, luaL_Buffer *b); 12static int recvline(p_buffer buf, luaL_Buffer *b, size_t budget);
13static int recvall(p_buffer buf, luaL_Buffer *b); 13static int recvall(p_buffer buf, luaL_Buffer *b, size_t budget);
14static int buffer_get(p_buffer buf, const char **data, size_t *count); 14static int buffer_get(p_buffer buf, const char **data, size_t *count);
15static void buffer_skip(p_buffer buf, size_t count); 15static void buffer_skip(p_buffer buf, size_t count);
16static int sendraw(p_buffer buf, const char *data, size_t count, size_t *sent); 16static int sendraw(p_buffer buf, const char *data, size_t count, size_t *sent);
17 17
18/* Internal completion code for buffer_meth_receive. err is not confined to
19 * the IO_* enum: socket_recv/socket_send (usocket.c/wsocket.c) propagate raw
20 * platform errors (POSIX errno, Windows WSA codes) straight through, and
21 * those are always positive, so a positive sentinel here could collide with
22 * a genuine transport error (e.g. errno 1 == EPERM) and get misreported as
23 * "oversized". Chosen negative and outside {IO_DONE, IO_TIMEOUT, IO_CLOSED,
24 * IO_UNKNOWN} (0, -1, -2, -3) so it can never collide with anything err
25 * legitimately takes.
26 * MUST be handled before buf->io->error() is called -- it is not a transport
27 * error. */
28#define BUF_OVERSIZED (-1000)
29
18/* min and max macros */ 30/* min and max macros */
19#ifndef MIN 31#ifndef MIN
20#define MIN(x, y) ((x) < (y) ? x : y) 32#define MIN(x, y) ((x) < (y) ? x : y)
@@ -105,8 +117,33 @@ int buffer_meth_send(lua_State *L, p_buffer buf) {
105int buffer_meth_receive(lua_State *L, p_buffer buf) { 117int buffer_meth_receive(lua_State *L, p_buffer buf) {
106 int err = IO_DONE, top; 118 int err = IO_DONE, top;
107 luaL_Buffer b; 119 luaL_Buffer b;
108 size_t size; 120 size_t size, wanted = 0, maxsize = 0;
121 size_t budget = 0; /* 0 == unlimited */
122 int numeric = lua_isnumber(L, 2);
109 const char *part = luaL_optlstring(L, 3, "", &size); 123 const char *part = luaL_optlstring(L, 3, "", &size);
124
125 /* ---- validation: must precede timeout_markstart() and any I/O ---- */
126 if (numeric) {
127 double n = lua_tonumber(L, 2);
128 luaL_argcheck(L, n >= 0, 2, "invalid receive pattern");
129 wanted = (size_t) n;
130 } else {
131 const char *p = luaL_optstring(L, 2, "*l");
132 luaL_argcheck(L, p[0] == '*' && (p[1] == 'l' || p[1] == 'a'),
133 2, "invalid receive pattern");
134 }
135 if (!lua_isnoneornil(L, 4)) {
136 double m = luaL_checknumber(L, 4);
137 luaL_argcheck(L, m >= 1, 4, "maxsize must be a positive number");
138 maxsize = (size_t) m;
139 luaL_argcheck(L, size < maxsize, 4,
140 "prefix length >= maxsize (drain with prefix=\"\" or raise maxsize)");
141 if (numeric)
142 luaL_argcheck(L, wanted <= maxsize, 4,
143 "maxsize smaller than requested byte count");
144 budget = maxsize - size;
145 }
146
110 timeout_markstart(buf->tm); 147 timeout_markstart(buf->tm);
111 /* make sure we don't confuse buffer stuff with arguments */ 148 /* make sure we don't confuse buffer stuff with arguments */
112 lua_settop(L, 3); 149 lua_settop(L, 3);
@@ -116,24 +153,28 @@ int buffer_meth_receive(lua_State *L, p_buffer buf) {
116 luaL_buffinit(L, &b); 153 luaL_buffinit(L, &b);
117 luaL_addlstring(&b, part, size); 154 luaL_addlstring(&b, part, size);
118 /* receive new patterns */ 155 /* receive new patterns */
119 if (!lua_isnumber(L, 2)) { 156 if (!numeric) {
120 const char *p= luaL_optstring(L, 2, "*l"); 157 const char *p= luaL_optstring(L, 2, "*l");
121 if (p[0] == '*' && p[1] == 'l') err = recvline(buf, &b); 158 if (p[0] == '*' && p[1] == 'l') err = recvline(buf, &b, budget);
122 else if (p[0] == '*' && p[1] == 'a') err = recvall(buf, &b); 159 else err = recvall(buf, &b, budget);
123 else luaL_argcheck(L, 0, 2, "invalid receive pattern");
124 /* get a fixed number of bytes (minus what was already partially 160 /* get a fixed number of bytes (minus what was already partially
125 * received) */ 161 * received) */
126 } else { 162 } else {
127 double n = lua_tonumber(L, 2);
128 size_t wanted = (size_t) n;
129 luaL_argcheck(L, n >= 0, 2, "invalid receive pattern");
130 if (size == 0 || wanted > size) 163 if (size == 0 || wanted > size)
131 err = recvraw(buf, wanted-size, &b); 164 err = recvraw(buf, wanted-size, &b);
132 } 165 }
133 /* check if there was an error */ 166 /* check if there was an error */
134 if (err != IO_DONE) { 167 /* luaL_pushresult(&b) must come first (its accumulator lives on the
135 /* we can't push anyting in the stack before pushing the 168 * stack), but the partial it produces belongs in slot 3, not 1 -- so
136 * contents of the buffer. this is the reason for the complication */ 169 * both error branches push buffer/error/buffer-copy/nil, then
170 * lua_replace the nil into slot 1. */
171 if (err == BUF_OVERSIZED) {
172 luaL_pushresult(&b);
173 lua_pushliteral(L, "oversized");
174 lua_pushvalue(L, -2);
175 lua_pushnil(L);
176 lua_replace(L, -4);
177 } else if (err != IO_DONE) {
137 luaL_pushresult(&b); 178 luaL_pushresult(&b);
138 lua_pushstring(L, buf->io->error(buf->io->ctx, err)); 179 lua_pushstring(L, buf->io->error(buf->io->ctx, err));
139 lua_pushvalue(L, -2); 180 lua_pushvalue(L, -2);
@@ -201,36 +242,61 @@ static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b) {
201 242
202/*-------------------------------------------------------------------------*\ 243/*-------------------------------------------------------------------------*\
203* Reads everything until the connection is closed (buffered) 244* Reads everything until the connection is closed (buffered)
245* budget == 0 means unlimited; otherwise the number of payload bytes still
246* allowed. Completion (connection closed) beats the cap: filling the cap
247* exactly and then seeing EOF means the whole stream was received.
204\*-------------------------------------------------------------------------*/ 248\*-------------------------------------------------------------------------*/
205static int recvall(p_buffer buf, luaL_Buffer *b) { 249static int recvall(p_buffer buf, luaL_Buffer *b, size_t budget) {
206 int err = IO_DONE; 250 int err = IO_DONE;
207 size_t total = 0; 251 size_t total = 0;
208 while (err == IO_DONE) { 252 while (err == IO_DONE) {
209 const char *data; size_t count; 253 const char *data; size_t count;
210 err = buffer_get(buf, &data, &count); 254 err = buffer_get(buf, &data, &count);
255 if (budget && count > budget - total) { /* strictly more than fits */
256 count = budget - total;
257 luaL_addlstring(b, data, count);
258 buffer_skip(buf, count);
259 return BUF_OVERSIZED;
260 }
211 total += count; 261 total += count;
212 luaL_addlstring(b, data, count); 262 luaL_addlstring(b, data, count);
213 buffer_skip(buf, count); 263 buffer_skip(buf, count);
214 } 264 }
215 if (err == IO_CLOSED) { 265 if (err == IO_CLOSED) { /* completion beats the cap */
216 if (total > 0) return IO_DONE; 266 if (total > 0) return IO_DONE;
217 else return IO_CLOSED; 267 else return IO_CLOSED;
218 } else return err; 268 }
269 if (budget && total == budget) return BUF_OVERSIZED;
270 return err;
219} 271}
220 272
221/*-------------------------------------------------------------------------*\ 273/*-------------------------------------------------------------------------*\
222* Reads a line terminated by a CR LF pair or just by a LF. The CR and LF 274* Reads a line terminated by a CR LF pair or just by a LF. The CR and LF
223* are not returned by the function and are discarded from the buffer 275* are not returned by the function and are discarded from the buffer
276* budget == 0 means unlimited; otherwise the number of payload bytes still
277* allowed. The cap test sits before consuming a byte, so a line of exactly
278* budget payload bytes succeeds while budget+1 reports oversized. A timeout
279* or close with the payload exactly at the cap and no terminator yet also
280* resolves to oversized, never to timeout/closed.
224\*-------------------------------------------------------------------------*/ 281\*-------------------------------------------------------------------------*/
225static int recvline(p_buffer buf, luaL_Buffer *b) { 282static int recvline(p_buffer buf, luaL_Buffer *b, size_t budget) {
226 int err = IO_DONE; 283 int err = IO_DONE;
284 size_t total = 0;
227 while (err == IO_DONE) { 285 while (err == IO_DONE) {
228 size_t count, pos; const char *data; 286 size_t count, pos; const char *data;
229 err = buffer_get(buf, &data, &count); 287 err = buffer_get(buf, &data, &count);
230 pos = 0; 288 pos = 0;
231 while (pos < count && data[pos] != '\n') { 289 while (pos < count && data[pos] != '\n') {
232 /* we ignore all \r's */ 290 /* we ignore all \r's -- they are consumed but never counted */
233 if (data[pos] != '\r') luaL_addchar(b, data[pos]); 291 if (data[pos] != '\r') {
292 if (budget && total == budget) {
293 /* leave the offending byte in the buffer for the next call */
294 buffer_skip(buf, pos);
295 return BUF_OVERSIZED;
296 }
297 luaL_addchar(b, data[pos]);
298 total++;
299 }
234 pos++; 300 pos++;
235 } 301 }
236 if (pos < count) { /* found '\n' */ 302 if (pos < count) { /* found '\n' */
@@ -239,7 +305,9 @@ static int recvline(p_buffer buf, luaL_Buffer *b) {
239 } else /* reached the end of the buffer */ 305 } else /* reached the end of the buffer */
240 buffer_skip(buf, pos); 306 buffer_skip(buf, pos);
241 } 307 }
242 return err; 308 if (err == IO_DONE) return IO_DONE; /* '\n' found: success, regardless of total */
309 if (budget && total == budget) return BUF_OVERSIZED; /* stalled/closed exactly at the cap: I1 */
310 return err; /* real timeout/closed, below the cap */
243} 311}
244 312
245/*-------------------------------------------------------------------------*\ 313/*-------------------------------------------------------------------------*\
diff --git a/test/testclnt.lua b/test/testclnt.lua
index 170e187..3e897c9 100644
--- a/test/testclnt.lua
+++ b/test/testclnt.lua
@@ -619,6 +619,177 @@ remote([[
619end 619end
620 620
621------------------------------------------------------------------------ 621------------------------------------------------------------------------
622function test_maxsize()
623 -- Group A: argument errors, raised before any I/O
624 reconnect()
625 printf("argument errors: ")
626 -- bounds pre-implementation calls (where arg 4 is silently dropped and
627 -- these become real, otherwise-unbounded blocking reads on an idle
628 -- socket) so a meaningful failure doesn't hang the suite
629 data:settimeout(0.2)
630 local ok
631 ok = pcall(data.receive, data, "*l", nil, 0)
632 assert(not ok, "A1 failed: maxsize=0 should raise")
633 ok = pcall(data.receive, data, "*l", nil, -1)
634 assert(not ok, "A2 failed: maxsize=-1 should raise")
635 ok = pcall(data.receive, data, "*l", nil, "abc")
636 assert(not ok, "A3 failed: non-number maxsize should raise")
637 ok = pcall(data.receive, data, "*l", string.rep("x", 10), 10)
638 assert(not ok, "A4 failed: #prefix == maxsize should raise")
639 ok = pcall(data.receive, data, "*l", string.rep("x", 11), 10)
640 assert(not ok, "A5 failed: #prefix > maxsize should raise")
641 ok = pcall(data.receive, data, 100, nil, 50)
642 assert(not ok, "A6 failed: wanted > maxsize should raise")
643 data:settimeout(0.1)
644 ok = pcall(data.receive, data, 50, nil, 100)
645 assert(ok, "A7 failed: wanted <= maxsize should not raise")
646 data:settimeout(-1)
647 remote [[ data:send('intact\n') ]]
648 local line, err = data:receive("*l", nil, 100)
649 assert(line == "intact",
650 "A8 failed: socket touched by a failed argcheck (err=" ..
651 tostring(err) .. ")")
652 pass("ok")
653
654 -- Group B: *l boundary
655 reconnect()
656 printf("*l boundary: ")
657 remote [[ data:send('hello\n') ]]
658 local d, e, p = data:receive("*l", nil, 100)
659 assert(d == "hello" and e == nil, "B1 failed")
660
661 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 100))
662 d, e = data:receive("*l", nil, 100)
663 assert(d == string.rep("a", 100) and e == nil,
664 "B2 failed: exact-budget line should succeed")
665
666 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101))
667 d, e, p = data:receive("*l", nil, 100)
668 assert(d == nil and e == "oversized" and p == string.rep("a", 100),
669 "B3 failed: one-over-budget should be oversized")
670 d, e = data:receive("*l", nil, 100)
671 assert(d == "a" and e == nil,
672 "B3 failed: leftover byte and terminator should still be there")
673
674 remote(string.format([[data:send(string.rep('\r',%d) .. string.rep('a',%d) .. '\n')]], 50, 100))
675 d, e = data:receive("*l", nil, 100)
676 assert(d == string.rep("a", 100) and e == nil, "B4 failed: CRs must not count")
677
678 remote(string.format([[data:send(string.rep('a',%d) .. '\r\n')]], 100))
679 d, e = data:receive("*l", nil, 100)
680 assert(d == string.rep("a", 100) and e == nil, "B5 failed: CRLF at boundary")
681 pass("ok")
682
683 -- Group C: *l and I1 (timeout/close at the cap)
684 reconnect()
685 printf("I1 (timeout/closed at the cap): ")
686 remote(string.format([[data:send(string.rep('a',%d))]], 50))
687 data:settimeout(0.5)
688 d, e, p = data:receive("*l", nil, 100)
689 assert(d == nil and e == "timeout", "C1 failed: expected timeout below cap")
690 assert(#p < 100, "C1 failed: timeout partial must be strictly < maxsize")
691 ok = pcall(data.receive, data, "*l", p, 100)
692 assert(ok, "C1 failed: retry with prefix=partial, same maxsize must not raise")
693
694 reconnect()
695 remote(string.format([[data:send(string.rep('a',%d))]], 100))
696 data:settimeout(0.5)
697 d, e, p = data:receive("*l", nil, 100)
698 assert(e == "oversized" and #p == 100,
699 "C2 failed: exactly-at-cap timeout must be oversized, got " .. tostring(e))
700
701 reconnect()
702 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100))
703 d, e, p = data:receive("*l", nil, 100)
704 assert(e == "oversized" and #p == 100,
705 "C3 failed: exactly-at-cap close must be oversized, got " .. tostring(e))
706 pass("ok")
707
708 -- Group D: drain idiom
709 -- 5030 (not a multiple of the 100 cap) so the boundary of the 50th
710 -- oversized chunk doesn't land exactly on the '\n': when it does, the
711 -- terminator check wins over the cap check (by design -- see B2/I1) and
712 -- the would-be 50th oversized chunk instead succeeds outright.
713 reconnect()
714 printf("drain idiom: ")
715 remote(string.format([[data:send(string.rep('x',%d) .. '\n' .. 'next\n')]], 5030))
716 local iterations = 0
717 repeat
718 d, e, p = data:receive("*l", "", 100)
719 if e == "oversized" then
720 assert(#p == 100, "D1 failed: oversized partial length " .. #p)
721 iterations = iterations + 1
722 end
723 until e ~= "oversized"
724 assert(iterations == 50,
725 "D1 failed: expected 50 oversized iterations, got " .. iterations)
726 assert(d == string.rep("x", 30) and e == nil, "D1 failed: final call should succeed")
727 local nextline = data:receive("*l")
728 assert(nextline == "next", "D1 failed: stream misaligned, got " .. tostring(nextline))
729 pass("ok")
730
731 -- Group E: *a
732 reconnect()
733 printf("*a boundary: ")
734 remote [[ data:send('abc') data:close() data = nil ]]
735 d, e = data:receive("*a", nil, 100)
736 assert(d == "abc" and e == nil, "E1 failed")
737
738 reconnect()
739 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100))
740 d, e = data:receive("*a", nil, 100)
741 assert(d == string.rep("a", 100) and e == nil,
742 "E2 failed: completion should beat the cap")
743
744 reconnect()
745 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 250))
746 d, e, p = data:receive("*a", "", 100)
747 assert(e == "oversized" and #p == 100, "E3 failed: first chunk")
748 d, e, p = data:receive("*a", "", 100)
749 assert(e == "oversized" and #p == 100, "E3 failed: second chunk")
750 d, e = data:receive("*a", "", 100)
751 assert(d == string.rep("a", 50) and e == nil, "E3 failed: final chunk")
752
753 reconnect()
754 remote(string.format([[data:send(string.rep('a',%d))]], 100))
755 data:settimeout(0.3)
756 d, e, p = data:receive("*a", nil, 100)
757 assert(e == "oversized" and #p == 100,
758 "E4 failed: expected oversized not timeout, got " .. tostring(e))
759
760 reconnect()
761 remote(string.format([[data:send(string.rep('a',%d))]], 50))
762 data:settimeout(0.3)
763 d, e, p = data:receive("*a", nil, 100)
764 assert(e == "timeout" and #p < 100, "E5 failed")
765 pass("ok")
766
767 -- Group F: numeric pattern (recvraw untouched)
768 reconnect()
769 printf("numeric pattern (recvraw unchanged): ")
770 remote(string.format([[data:send(string.rep('a',%d))]], 50))
771 d, e = data:receive(50, nil, 100)
772 assert(d == string.rep("a", 50) and e == nil, "F1 failed")
773
774 reconnect()
775 remote(string.format([[data:send(string.rep('a',%d))]], 25))
776 d, e = data:receive(50, string.rep("p", 25), 100)
777 assert(e == nil and #d == 50, "F2 failed")
778 pass("ok")
779
780 -- Group G: no-maxsize regression snapshot
781 reconnect()
782 printf("no-maxsize regression: ")
783 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 5000))
784 d, e = data:receive("*l")
785 assert(d == string.rep("a", 5000) and e == nil, "G2 failed: plain *l")
786 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 5000))
787 d, e = data:receive("*l", "")
788 assert(d == string.rep("a", 5000) and e == nil, "G2 failed: *l with prefix")
789 pass("ok")
790end
791
792------------------------------------------------------------------------
622test("method registration") 793test("method registration")
623 794
624local tcp_methods = { 795local tcp_methods = {
@@ -796,6 +967,9 @@ test_blockingtimeoutreceive(800091, 2, 3)
796test_blockingtimeoutreceive(800091, 3, 2) 967test_blockingtimeoutreceive(800091, 3, 2)
797test_blockingtimeoutreceive(800091, 3, 1) 968test_blockingtimeoutreceive(800091, 3, 1)
798 969
970test("receive maxsize")
971test_maxsize()
972
799test("shutting server down") 973test("shutting server down")
800reconnect() 974reconnect()
801remote("os.exit()") 975remote("os.exit()")
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([[
511end 511end
512 512
513------------------------------------------------------------------------ 513------------------------------------------------------------------------
514function 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")
553end
554
555------------------------------------------------------------------------
514 556
515test("method registration") 557test("method registration")
516test_methods(socket.unix(), { 558test_methods(socket.unix(), {
@@ -641,4 +683,7 @@ test_blockingtimeoutreceive(800091, 2, 3)
641test_blockingtimeoutreceive(800091, 3, 2) 683test_blockingtimeoutreceive(800091, 3, 2)
642test_blockingtimeoutreceive(800091, 3, 1) 684test_blockingtimeoutreceive(800091, 3, 1)
643 685
686test("receive maxsize")
687test_maxsize()
688
644test(string.format("done in %.2fs", socket.gettime() - start)) 689test(string.format("done in %.2fs", socket.gettime() - start))