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 | |
| 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.
| -rw-r--r-- | CHANGELOG.md | 4 | ||||
| -rw-r--r-- | docs/tcp.html | 58 | ||||
| -rw-r--r-- | src/buffer.c | 108 | ||||
| -rw-r--r-- | test/testclnt.lua | 174 | ||||
| -rw-r--r-- | test/utestclnt.lua | 45 |
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"> |
| 354 | client:<b>receive(</b>[pattern [, prefix]]<b>)</b> | 354 | client:<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> | |||
| 380 | of any received data before return. | 380 | of 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 | ||
| 385 | payload bytes the call may accumulate, <em>including</em> <tt>prefix</tt>. | ||
| 386 | Omitted or <tt><b>nil</b></tt> means unlimited. | ||
| 387 | </p> | ||
| 388 | |||
| 383 | <p class="return"> | 389 | <p class="return"> |
| 384 | If successful, the method returns the received pattern. In case of error, | 390 | If successful, the method returns the received pattern. In case of error, |
| 385 | the method returns <tt><b>nil</b></tt> followed by an error | 391 | the method returns <tt><b>nil</b></tt> followed by an error |
| 386 | message, followed by a (possibly empty) string containing | 392 | message, followed by a (possibly empty) string containing |
| 387 | the partial that was received. The error message can be | 393 | the partial that was received. The error message can be |
| 388 | the string '<tt>closed</tt>' in case the connection was | 394 | the string '<tt>closed</tt>' in case the connection was |
| 389 | closed before the transmission was completed or the string | 395 | closed 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, |
| 397 | when <tt>maxsize</tt> was given, the string '<tt>oversized</tt>' in case | ||
| 398 | the pattern did not complete within <tt>maxsize</tt> bytes -- in which case | ||
| 399 | the 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 | |||
| 399 | too. | 408 | too. |
| 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 | ||
| 413 | than 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 | ||
| 415 | than <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 | ||
| 417 | they are detected before any byte is read from the socket. To drain and | ||
| 418 | discard an oversized line while keeping memory bounded and the stream | ||
| 419 | aligned: | ||
| 420 | </p> | ||
| 421 | |||
| 422 | <pre class="example"> | ||
| 423 | local data, err, part | ||
| 424 | repeat | ||
| 425 | data, err, part = client:receive("*l", "", 4096) | ||
| 426 | until err ~= "oversized" | ||
| 427 | </pre> | ||
| 428 | |||
| 429 | <p class="note"> | ||
| 430 | To instead retry and eventually get the whole thing, carry the partial | ||
| 431 | forward as <tt>prefix</tt> and grow <tt>maxsize</tt>: | ||
| 432 | </p> | ||
| 433 | |||
| 434 | <pre class="example"> | ||
| 435 | local data, err, part = client:receive("*l", nil, 4096) | ||
| 436 | if err == "oversized" then | ||
| 437 | data, err, part = client:receive("*l", part, 65536) -- larger cap, or this raises | ||
| 438 | end | ||
| 439 | </pre> | ||
| 440 | |||
| 441 | <p class="note"> | ||
| 442 | Retrying 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 | ||
| 444 | design -- otherwise it would be a zero-progress spin: no I/O, no timeout, | ||
| 445 | no error, just CPU. A <tt>timeout</tt> partial is always strictly shorter | ||
| 446 | than <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 | ||
| 449 | bytes taken off the wire: for the <tt>*l</tt> pattern the discarded CR | ||
| 450 | characters and the line terminator mean more bytes may have been consumed | ||
| 451 | than 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 | \*=========================================================================*/ |
| 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,33 @@ 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, 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 | \*-------------------------------------------------------------------------*/ |
| 205 | static int recvall(p_buffer buf, luaL_Buffer *b) { | 249 | static 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 | \*-------------------------------------------------------------------------*/ |
| 225 | static int recvline(p_buffer buf, luaL_Buffer *b) { | 282 | static 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([[ | |||
| 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 | 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") | ||
| 790 | end | ||
| 791 | |||
| 792 | ------------------------------------------------------------------------ | ||
| 622 | test("method registration") | 793 | test("method registration") |
| 623 | 794 | ||
| 624 | local tcp_methods = { | 795 | local tcp_methods = { |
| @@ -796,6 +967,9 @@ test_blockingtimeoutreceive(800091, 2, 3) | |||
| 796 | test_blockingtimeoutreceive(800091, 3, 2) | 967 | test_blockingtimeoutreceive(800091, 3, 2) |
| 797 | test_blockingtimeoutreceive(800091, 3, 1) | 968 | test_blockingtimeoutreceive(800091, 3, 1) |
| 798 | 969 | ||
| 970 | test("receive maxsize") | ||
| 971 | test_maxsize() | ||
| 972 | |||
| 799 | test("shutting server down") | 973 | test("shutting server down") |
| 800 | reconnect() | 974 | reconnect() |
| 801 | remote("os.exit()") | 975 | 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)) |
