aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCaleb Maclennan <caleb@alerque.com>2026-08-31 10:57:58 +0300
committerGitHub <noreply@github.com>2026-08-31 10:57:58 +0300
commit8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266 (patch)
treec11ae97c461d7f9c3cfe84773b9975de48d5ba9c
parent535178a3f0e2cff59f4d59ee3a655bee263a5c90 (diff)
parent4863f32b358a8d533f629084a86fb4932ebbd2f3 (diff)
downloadluasocket-8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266.tar.gz
luasocket-8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266.tar.bz2
luasocket-8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266.zip
Merge pull request #463 from lunarmodules/feat/receive-limit
feat(receive): add maxsize argument to bound memory usage
-rw-r--r--CHANGELOG.md4
-rw-r--r--docs/tcp.html32
-rw-r--r--src/buffer.c110
-rw-r--r--test/testclnt.lua195
-rw-r--r--test/utestclnt.lua45
5 files changed, 356 insertions, 30 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 d223abf..715d28f 100644
--- a/docs/tcp.html
+++ b/docs/tcp.html
@@ -361,7 +361,7 @@ method returns <b><tt>nil</tt></b> followed by an error message.
361<!-- receive ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> 361<!-- receive ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
362 362
363<p class="name" id="receive"> 363<p class="name" id="receive">
364client:<b>receive(</b>[pattern [, prefix]]<b>)</b> 364client:<b>receive(</b>[pattern [, prefix [, maxsize]]]<b>)</b>
365</p> 365</p>
366 366
367<p class="description"> 367<p class="description">
@@ -390,23 +390,35 @@ of bytes from the socket.</li>
390of any received data before return. 390of any received data before return.
391</p> 391</p>
392 392
393<p class="parameters">
394<tt>Maxsize</tt> is an optional positive integer bounding the number of
395payload bytes the call may accumulate, <em>including</em> <tt>prefix</tt>.
396Omitted or <tt><b>nil</b></tt> means unlimited.
397</p>
398
393<p class="return"> 399<p class="return">
394If successful, the method returns the received pattern. In case of error, 400If successful, the method returns the received pattern. In case of error,
395the method returns <tt><b>nil</b></tt> followed by an error 401the method returns <tt><b>nil</b></tt> followed by an error
396message, followed by a (possibly empty) string containing 402message, followed by a (possibly empty) string containing
397the partial that was received. The error message can be 403the partial that was received. The error message can be
398the string '<tt>closed</tt>' in case the connection was 404the string '<tt>closed</tt>' in case the connection was
399closed before the transmission was completed or the string 405closed before the transmission was completed.
400'<tt>timeout</tt>' in case there was a timeout during the operation. 406'<tt>timeout</tt>' indicates there was a timeout during the operation. And
407when <tt>maxsize</tt> was given, the string '<tt>oversized</tt>' in case
408the pattern did not complete within <tt>maxsize</tt> bytes (in which case
409the third return value holds exactly <tt>maxsize</tt> bytes).
401</p> 410</p>
402 411
403<p class="note"> 412<p class="note">
404<b>Important note</b>: This function was changed <em>severely</em>. It used 413Retrying with <tt>prefix</tt> set to the previous partial result and an
405to support multiple patterns (but I have never seen this feature used) and 414<em>unchanged</em> <tt>maxsize</tt> raises the length-check error above by
406now it doesn't anymore. Partial results used to be returned in the same 415design. A <tt>timeout</tt> partial is always strictly shorter
407way as successful results. This last feature violated the idea that all 416than <tt>maxsize</tt>, so it is always safe to feed straight back as
408functions should return <tt><b>nil</b></tt> on error. Thus it was changed 417<tt>prefix</tt> with the same <tt>maxsize</tt>. Finally, note that
409too. 418<tt>maxsize</tt> bounds the payload <em>returned</em>, not necessarily the
419bytes taken off the wire: for the <tt>*l</tt> pattern the discarded CR
420characters and the line terminator mean more bytes may have been consumed
421than the returned length suggests.
410</p> 422</p>
411 423
412<!-- send +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> 424<!-- send +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
diff --git a/src/buffer.c b/src/buffer.c
index 7148be3..3d48a09 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,35 @@ 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 && n < (lua_Number) ((size_t) -1), 2,
129 "invalid receive pattern");
130 wanted = (size_t) n;
131 } else {
132 const char *p = luaL_optstring(L, 2, "*l");
133 luaL_argcheck(L, p[0] == '*' && (p[1] == 'l' || p[1] == 'a'),
134 2, "invalid receive pattern");
135 }
136 if (!lua_isnoneornil(L, 4)) {
137 double m = luaL_checknumber(L, 4);
138 luaL_argcheck(L, m >= 1 && m < (lua_Number) ((size_t) -1), 4,
139 "maxsize must be a positive number");
140 maxsize = (size_t) m;
141 luaL_argcheck(L, size < maxsize, 4,
142 "prefix length >= maxsize (drain with prefix=\"\" or raise maxsize)");
143 if (numeric)
144 luaL_argcheck(L, wanted <= maxsize, 4,
145 "maxsize smaller than requested byte count");
146 budget = maxsize - size;
147 }
148
110 timeout_markstart(buf->tm); 149 timeout_markstart(buf->tm);
111 /* make sure we don't confuse buffer stuff with arguments */ 150 /* make sure we don't confuse buffer stuff with arguments */
112 lua_settop(L, 3); 151 lua_settop(L, 3);
@@ -116,24 +155,28 @@ int buffer_meth_receive(lua_State *L, p_buffer buf) {
116 luaL_buffinit(L, &b); 155 luaL_buffinit(L, &b);
117 luaL_addlstring(&b, part, size); 156 luaL_addlstring(&b, part, size);
118 /* receive new patterns */ 157 /* receive new patterns */
119 if (!lua_isnumber(L, 2)) { 158 if (!numeric) {
120 const char *p= luaL_optstring(L, 2, "*l"); 159 const char *p= luaL_optstring(L, 2, "*l");
121 if (p[0] == '*' && p[1] == 'l') err = recvline(buf, &b); 160 if (p[0] == '*' && p[1] == 'l') err = recvline(buf, &b, budget);
122 else if (p[0] == '*' && p[1] == 'a') err = recvall(buf, &b); 161 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 162 /* get a fixed number of bytes (minus what was already partially
125 * received) */ 163 * received) */
126 } else { 164 } 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) 165 if (size == 0 || wanted > size)
131 err = recvraw(buf, wanted-size, &b); 166 err = recvraw(buf, wanted-size, &b);
132 } 167 }
133 /* check if there was an error */ 168 /* check if there was an error */
134 if (err != IO_DONE) { 169 /* luaL_pushresult(&b) must come first (its accumulator lives on the
135 /* we can't push anyting in the stack before pushing the 170 * 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 */ 171 * both error branches push buffer/error/buffer-copy/nil, then
172 * lua_replace the nil into slot 1. */
173 if (err == BUF_OVERSIZED) {
174 luaL_pushresult(&b);
175 lua_pushliteral(L, "oversized");
176 lua_pushvalue(L, -2);
177 lua_pushnil(L);
178 lua_replace(L, -4);
179 } else if (err != IO_DONE) {
137 luaL_pushresult(&b); 180 luaL_pushresult(&b);
138 lua_pushstring(L, buf->io->error(buf->io->ctx, err)); 181 lua_pushstring(L, buf->io->error(buf->io->ctx, err));
139 lua_pushvalue(L, -2); 182 lua_pushvalue(L, -2);
@@ -201,36 +244,61 @@ static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b) {
201 244
202/*-------------------------------------------------------------------------*\ 245/*-------------------------------------------------------------------------*\
203* Reads everything until the connection is closed (buffered) 246* Reads everything until the connection is closed (buffered)
247* budget == 0 means unlimited; otherwise the number of payload bytes still
248* allowed. Completion (connection closed) beats the cap: filling the cap
249* exactly and then seeing EOF means the whole stream was received.
204\*-------------------------------------------------------------------------*/ 250\*-------------------------------------------------------------------------*/
205static int recvall(p_buffer buf, luaL_Buffer *b) { 251static int recvall(p_buffer buf, luaL_Buffer *b, size_t budget) {
206 int err = IO_DONE; 252 int err = IO_DONE;
207 size_t total = 0; 253 size_t total = 0;
208 while (err == IO_DONE) { 254 while (err == IO_DONE) {
209 const char *data; size_t count; 255 const char *data; size_t count;
210 err = buffer_get(buf, &data, &count); 256 err = buffer_get(buf, &data, &count);
257 if (budget && count > budget - total) { /* strictly more than fits */
258 count = budget - total;
259 luaL_addlstring(b, data, count);
260 buffer_skip(buf, count);
261 return BUF_OVERSIZED;
262 }
211 total += count; 263 total += count;
212 luaL_addlstring(b, data, count); 264 luaL_addlstring(b, data, count);
213 buffer_skip(buf, count); 265 buffer_skip(buf, count);
214 } 266 }
215 if (err == IO_CLOSED) { 267 if (err == IO_CLOSED) { /* completion beats the cap */
216 if (total > 0) return IO_DONE; 268 if (total > 0) return IO_DONE;
217 else return IO_CLOSED; 269 else return IO_CLOSED;
218 } else return err; 270 }
271 if (budget && total == budget) return BUF_OVERSIZED;
272 return err;
219} 273}
220 274
221/*-------------------------------------------------------------------------*\ 275/*-------------------------------------------------------------------------*\
222* Reads a line terminated by a CR LF pair or just by a LF. The CR and LF 276* 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 277* are not returned by the function and are discarded from the buffer
278* budget == 0 means unlimited; otherwise the number of payload bytes still
279* allowed. The cap test sits before consuming a byte, so a line of exactly
280* budget payload bytes succeeds while budget+1 reports oversized. A timeout
281* or close with the payload exactly at the cap and no terminator yet also
282* resolves to oversized, never to timeout/closed.
224\*-------------------------------------------------------------------------*/ 283\*-------------------------------------------------------------------------*/
225static int recvline(p_buffer buf, luaL_Buffer *b) { 284static int recvline(p_buffer buf, luaL_Buffer *b, size_t budget) {
226 int err = IO_DONE; 285 int err = IO_DONE;
286 size_t total = 0;
227 while (err == IO_DONE) { 287 while (err == IO_DONE) {
228 size_t count, pos; const char *data; 288 size_t count, pos; const char *data;
229 err = buffer_get(buf, &data, &count); 289 err = buffer_get(buf, &data, &count);
230 pos = 0; 290 pos = 0;
231 while (pos < count && data[pos] != '\n') { 291 while (pos < count && data[pos] != '\n') {
232 /* we ignore all \r's */ 292 /* we ignore all \r's -- they are consumed but never counted */
233 if (data[pos] != '\r') luaL_addchar(b, data[pos]); 293 if (data[pos] != '\r') {
294 if (budget && total == budget) {
295 /* leave the offending byte in the buffer for the next call */
296 buffer_skip(buf, pos);
297 return BUF_OVERSIZED;
298 }
299 luaL_addchar(b, data[pos]);
300 total++;
301 }
234 pos++; 302 pos++;
235 } 303 }
236 if (pos < count) { /* found '\n' */ 304 if (pos < count) { /* found '\n' */
@@ -239,7 +307,9 @@ static int recvline(p_buffer buf, luaL_Buffer *b) {
239 } else /* reached the end of the buffer */ 307 } else /* reached the end of the buffer */
240 buffer_skip(buf, pos); 308 buffer_skip(buf, pos);
241 } 309 }
242 return err; 310 if (err == IO_DONE) return IO_DONE; /* '\n' found: success, regardless of total */
311 if (budget && total == budget) return BUF_OVERSIZED; /* stalled/closed exactly at the cap: I1 */
312 return err; /* real timeout/closed, below the cap */
243} 313}
244 314
245/*-------------------------------------------------------------------------*\ 315/*-------------------------------------------------------------------------*\
diff --git a/test/testclnt.lua b/test/testclnt.lua
index 170e187..bbe48bb 100644
--- a/test/testclnt.lua
+++ b/test/testclnt.lua
@@ -619,6 +619,198 @@ 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 ok = pcall(data.receive, data, "*l", nil, math.huge)
644 assert(not ok, "A7 failed: maxsize=math.huge should raise (size_t overflow)")
645 ok = pcall(data.receive, data, math.huge)
646 assert(not ok, "A8 failed: wanted=math.huge should raise (size_t overflow)")
647 data:settimeout(0.1)
648 ok = pcall(data.receive, data, 50, nil, 100)
649 assert(ok, "A9 failed: wanted <= maxsize should not raise")
650 data:settimeout(-1)
651 remote [[ data:send('intact\n') ]]
652 local line, err = data:receive("*l", nil, 100)
653 assert(line == "intact",
654 "A10 failed: socket touched by a failed argcheck (err=" ..
655 tostring(err) .. ")")
656 pass("ok")
657
658 -- Group B: *l boundary
659 reconnect()
660 printf("*l boundary: ")
661 remote [[ data:send('hello\n') ]]
662 local d, e, p = data:receive("*l", nil, 100)
663 assert(d == "hello" and e == nil, "B1 failed")
664
665 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 100))
666 d, e = data:receive("*l", nil, 100)
667 assert(d == string.rep("a", 100) and e == nil,
668 "B2 failed: exact-budget line should succeed")
669
670 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101))
671 d, e, p = data:receive("*l", nil, 100)
672 assert(d == nil and e == "oversized" and p == string.rep("a", 100),
673 "B3 failed: one-over-budget should be oversized")
674 d, e = data:receive("*l", nil, 100)
675 assert(d == "a" and e == nil,
676 "B3 failed: leftover byte and terminator should still be there")
677
678 remote(string.format([[data:send(string.rep('\r',%d) .. string.rep('a',%d) .. '\n')]], 50, 100))
679 d, e = data:receive("*l", nil, 100)
680 assert(d == string.rep("a", 100) and e == nil, "B4 failed: CRs must not count")
681
682 remote(string.format([[data:send(string.rep('a',%d) .. '\r\n')]], 100))
683 d, e = data:receive("*l", nil, 100)
684 assert(d == string.rep("a", 100) and e == nil, "B5 failed: CRLF at boundary")
685 pass("ok")
686
687 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 101))
688 d, e, p = data:receive("*l", nil, 100)
689 assert(d == nil and e == "oversized" and p == string.rep("a", 100),
690 "B6 failed: one-over-budget should be oversized")
691 d, e = data:receive("*l", p, 150) -- increase limit, try again with prefix
692 assert(d == string.rep("a", 101) and e == nil,
693 "B6 failed: leftover byte and terminator should still be there")
694
695 -- Group C: *l and I1 (timeout/close at the cap)
696 reconnect()
697 printf("I1 (timeout/closed at the cap): ")
698 remote(string.format([[data:send(string.rep('a',%d))]], 50))
699 data:settimeout(0.5)
700 d, e, p = data:receive("*l", nil, 100)
701 assert(d == nil and e == "timeout", "C1 failed: expected timeout below cap")
702 assert(#p < 100, "C1 failed: timeout partial must be strictly < maxsize")
703 ok = pcall(data.receive, data, "*l", p, 100)
704 assert(ok, "C1 failed: retry with prefix=partial, same maxsize must not raise")
705
706 reconnect()
707 remote(string.format([[data:send(string.rep('a',%d))]], 100))
708 data:settimeout(0.5)
709 d, e, p = data:receive("*l", nil, 100)
710 assert(e == "oversized" and #p == 100,
711 "C2 failed: exactly-at-cap timeout must be oversized, got " .. tostring(e))
712
713 reconnect()
714 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100))
715 d, e, p = data:receive("*l", nil, 100)
716 assert(e == "oversized" and #p == 100,
717 "C3 failed: exactly-at-cap close must be oversized, got " .. tostring(e))
718 pass("ok")
719
720 -- Group D: drain idiom
721 -- 5030 (not a multiple of the 100 cap) so the boundary of the 50th
722 -- oversized chunk doesn't land exactly on the '\n': when it does, the
723 -- terminator check wins over the cap check (by design -- see B2/I1) and
724 -- the would-be 50th oversized chunk instead succeeds outright.
725 reconnect()
726 printf("drain idiom: ")
727 remote(string.format([[data:send(string.rep('x',%d) .. '\n' .. 'next\n')]], 5030))
728 local iterations = 0
729 repeat
730 d, e, p = data:receive("*l", "", 100)
731 if e == "oversized" then
732 assert(#p == 100, "D1 failed: oversized partial length " .. #p)
733 iterations = iterations + 1
734 end
735 until e ~= "oversized"
736 assert(iterations == 50,
737 "D1 failed: expected 50 oversized iterations, got " .. iterations)
738 assert(d == string.rep("x", 30) and e == nil, "D1 failed: final call should succeed")
739 local nextline = data:receive("*l")
740 assert(nextline == "next", "D1 failed: stream misaligned, got " .. tostring(nextline))
741 pass("ok")
742
743 -- Group E: *a
744 reconnect()
745 printf("*a boundary: ")
746 remote [[ data:send('abc') data:close() data = nil ]]
747 d, e = data:receive("*a", nil, 100)
748 assert(d == "abc" and e == nil, "E1 failed")
749
750 reconnect()
751 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 100))
752 d, e = data:receive("*a", nil, 100)
753 assert(d == string.rep("a", 100) and e == nil,
754 "E2 failed: completion should beat the cap")
755
756 reconnect()
757 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 250))
758 d, e, p = data:receive("*a", "", 100)
759 assert(e == "oversized" and #p == 100, "E3a failed: first chunk")
760 d, e, p = data:receive("*a", "", 100)
761 assert(e == "oversized" and #p == 100, "E3a failed: second chunk")
762 d, e = data:receive("*a", "", 100)
763 assert(d == string.rep("a", 50) and e == nil, "E3a failed: final chunk")
764
765 reconnect()
766 remote(string.format([[data:send(string.rep('a',%d)) data:close() data = nil]], 250))
767 d, e, p = data:receive("*a", "", 100)
768 assert(e == "oversized" and #p == 100, "E3b failed: first chunk")
769 d, e, p = data:receive("*a", p, 200) -- increase maxsize, try again with prefix
770 assert(e == "oversized" and #p == 200, "E3b failed: second chunk")
771 d, e = data:receive("*a", p, 300) -- increase maxsize, try again with prefix
772 assert(d == string.rep("a", 250) and e == nil, "E3b failed: final chunk")
773
774 reconnect()
775 remote(string.format([[data:send(string.rep('a',%d))]], 100))
776 data:settimeout(0.3)
777 d, e, p = data:receive("*a", nil, 100)
778 assert(e == "oversized" and #p == 100,
779 "E4 failed: expected oversized not timeout, got " .. tostring(e))
780
781 reconnect()
782 remote(string.format([[data:send(string.rep('a',%d))]], 50))
783 data:settimeout(0.3)
784 d, e, p = data:receive("*a", nil, 100)
785 assert(e == "timeout" and #p < 100, "E5 failed")
786 pass("ok")
787
788 -- Group F: numeric pattern (recvraw untouched)
789 reconnect()
790 printf("numeric pattern (recvraw unchanged): ")
791 remote(string.format([[data:send(string.rep('a',%d))]], 50))
792 d, e = data:receive(50, nil, 100)
793 assert(d == string.rep("a", 50) and e == nil, "F1 failed")
794
795 reconnect()
796 remote(string.format([[data:send(string.rep('a',%d))]], 25))
797 d, e = data:receive(50, string.rep("p", 25), 100)
798 assert(e == nil and #d == 50, "F2 failed")
799 pass("ok")
800
801 -- Group G: no-maxsize regression snapshot
802 reconnect()
803 printf("no-maxsize regression: ")
804 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 5000))
805 d, e = data:receive("*l")
806 assert(d == string.rep("a", 5000) and e == nil, "G2 failed: plain *l")
807 remote(string.format([[data:send(string.rep('a',%d) .. '\n')]], 5000))
808 d, e = data:receive("*l", "")
809 assert(d == string.rep("a", 5000) and e == nil, "G2 failed: *l with prefix")
810 pass("ok")
811end
812
813------------------------------------------------------------------------
622test("method registration") 814test("method registration")
623 815
624local tcp_methods = { 816local tcp_methods = {
@@ -796,6 +988,9 @@ test_blockingtimeoutreceive(800091, 2, 3)
796test_blockingtimeoutreceive(800091, 3, 2) 988test_blockingtimeoutreceive(800091, 3, 2)
797test_blockingtimeoutreceive(800091, 3, 1) 989test_blockingtimeoutreceive(800091, 3, 1)
798 990
991test("receive maxsize")
992test_maxsize()
993
799test("shutting server down") 994test("shutting server down")
800reconnect() 995reconnect()
801remote("os.exit()") 996remote("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))