diff options
| author | Caleb Maclennan <caleb@alerque.com> | 2026-08-31 10:57:58 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-31 10:57:58 +0300 |
| commit | 8f18ce95bb38c7f5c4bef5b3684cf1b0df1fc266 (patch) | |
| tree | c11ae97c461d7f9c3cfe84773b9975de48d5ba9c | |
| parent | 535178a3f0e2cff59f4d59ee3a655bee263a5c90 (diff) | |
| parent | 4863f32b358a8d533f629084a86fb4932ebbd2f3 (diff) | |
| download | luasocket-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.md | 4 | ||||
| -rw-r--r-- | docs/tcp.html | 32 | ||||
| -rw-r--r-- | src/buffer.c | 110 | ||||
| -rw-r--r-- | test/testclnt.lua | 195 | ||||
| -rw-r--r-- | test/utestclnt.lua | 45 |
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"> |
| 364 | client:<b>receive(</b>[pattern [, prefix]]<b>)</b> | 364 | client:<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> | |||
| 390 | of any received data before return. | 390 | of 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 | ||
| 395 | payload bytes the call may accumulate, <em>including</em> <tt>prefix</tt>. | ||
| 396 | Omitted or <tt><b>nil</b></tt> means unlimited. | ||
| 397 | </p> | ||
| 398 | |||
| 393 | <p class="return"> | 399 | <p class="return"> |
| 394 | If successful, the method returns the received pattern. In case of error, | 400 | If successful, the method returns the received pattern. In case of error, |
| 395 | the method returns <tt><b>nil</b></tt> followed by an error | 401 | the method returns <tt><b>nil</b></tt> followed by an error |
| 396 | message, followed by a (possibly empty) string containing | 402 | message, followed by a (possibly empty) string containing |
| 397 | the partial that was received. The error message can be | 403 | the partial that was received. The error message can be |
| 398 | the string '<tt>closed</tt>' in case the connection was | 404 | the string '<tt>closed</tt>' in case the connection was |
| 399 | closed before the transmission was completed or the string | 405 | closed 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 |
| 407 | when <tt>maxsize</tt> was given, the string '<tt>oversized</tt>' in case | ||
| 408 | the pattern did not complete within <tt>maxsize</tt> bytes (in which case | ||
| 409 | the 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 | 413 | Retrying with <tt>prefix</tt> set to the previous partial result and an |
| 405 | to 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 |
| 406 | now it doesn't anymore. Partial results used to be returned in the same | 415 | design. A <tt>timeout</tt> partial is always strictly shorter |
| 407 | way as successful results. This last feature violated the idea that all | 416 | than <tt>maxsize</tt>, so it is always safe to feed straight back as |
| 408 | functions 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 |
| 409 | too. | 418 | <tt>maxsize</tt> bounds the payload <em>returned</em>, not necessarily the |
| 419 | bytes taken off the wire: for the <tt>*l</tt> pattern the discarded CR | ||
| 420 | characters and the line terminator mean more bytes may have been consumed | ||
| 421 | than 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 | \*=========================================================================*/ |
| 11 | static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b); | 11 | static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b); |
| 12 | static int recvline(p_buffer buf, luaL_Buffer *b); | 12 | static int recvline(p_buffer buf, luaL_Buffer *b, size_t budget); |
| 13 | static int recvall(p_buffer buf, luaL_Buffer *b); | 13 | static int recvall(p_buffer buf, luaL_Buffer *b, size_t budget); |
| 14 | static int buffer_get(p_buffer buf, const char **data, size_t *count); | 14 | static int buffer_get(p_buffer buf, const char **data, size_t *count); |
| 15 | static void buffer_skip(p_buffer buf, size_t count); | 15 | static void buffer_skip(p_buffer buf, size_t count); |
| 16 | static int sendraw(p_buffer buf, const char *data, size_t count, size_t *sent); | 16 | static 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) { | |||
| 105 | int buffer_meth_receive(lua_State *L, p_buffer buf) { | 117 | int 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 | \*-------------------------------------------------------------------------*/ |
| 205 | static int recvall(p_buffer buf, luaL_Buffer *b) { | 251 | static 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 | \*-------------------------------------------------------------------------*/ |
| 225 | static int recvline(p_buffer buf, luaL_Buffer *b) { | 284 | static 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([[ | |||
| 619 | end | 619 | end |
| 620 | 620 | ||
| 621 | ------------------------------------------------------------------------ | 621 | ------------------------------------------------------------------------ |
| 622 | function 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") | ||
| 811 | end | ||
| 812 | |||
| 813 | ------------------------------------------------------------------------ | ||
| 622 | test("method registration") | 814 | test("method registration") |
| 623 | 815 | ||
| 624 | local tcp_methods = { | 816 | local tcp_methods = { |
| @@ -796,6 +988,9 @@ test_blockingtimeoutreceive(800091, 2, 3) | |||
| 796 | test_blockingtimeoutreceive(800091, 3, 2) | 988 | test_blockingtimeoutreceive(800091, 3, 2) |
| 797 | test_blockingtimeoutreceive(800091, 3, 1) | 989 | test_blockingtimeoutreceive(800091, 3, 1) |
| 798 | 990 | ||
| 991 | test("receive maxsize") | ||
| 992 | test_maxsize() | ||
| 993 | |||
| 799 | test("shutting server down") | 994 | test("shutting server down") |
| 800 | reconnect() | 995 | reconnect() |
| 801 | remote("os.exit()") | 996 | remote("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([[ | |||
| 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)) |
