dalli 5.1.3 → 5.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf1d386c0e4bbde88c4dcafcd1babe51ffa12eaa173e105019bdcce2f7a2acfa
4
- data.tar.gz: 07463ea0880be455439262d77dc8255562a0ed7035e5ec29ee07888612d844a2
3
+ metadata.gz: 70f17667b9c623f9233be342d920de385bab797b44a4bc9f1e85c233f766404f
4
+ data.tar.gz: 527b9ff3dd4699ad5d307107668b31d3a739675825ad4d0b2cfb35ac09581a81
5
5
  SHA512:
6
- metadata.gz: 98f088e6d7c01b13b912a18f4c870587d02db32ebb3b6ec471359f97bf15324261468b7d62186f1ffc03bfe66ff175884c18f551fd457cf6a80dc2aef1774608
7
- data.tar.gz: b3919430e8b9ed967e52495d626013b54a4c8a8b62c8a3042224569d6594b455188ff5d0ceb2342810994e5dc419dea780bf969c9cfa918395387127758a1b96
6
+ metadata.gz: 9e54f454a1bca3b79f229a219b458533d2778c1521043c1ee7c2d203ae0d25e1449fc375b59754aaadac79de6ea484170ba52c10008da6abd40451821300a60c
7
+ data.tar.gz: 1dea61992e48d8786dbb3ef929e30c53d3544bafed895334cf4316ad0710791c4ebb0887fb4fa63b79358c0029be11b3495a2875a24956a296246fe9d5e4eaa0
data/CHANGELOG.md CHANGED
@@ -4,6 +4,32 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 5.1.4
8
+ ==========
9
+
10
+ Security:
11
+
12
+ - Keep the caller's key when retrying a request (GHSA-m252-9cgf-vx2w)
13
+ - With a `namespace`, a request retried after a transient network error (a timeout, or a connection closed by memcached or a proxy) applied the namespace a second time, so a retried read could return a different key's value and a retried write could overwrite a different key
14
+ - Affects single-key operations since 5.0.3, single-server `get_multi` since 5.1.0, and `get_with_metadata` and `fetch_with_lock` since 4.1.0; fixed in 5.2.2, 5.1.4, 5.0.10 and 4.3.7. Clients without a namespace aren't affected
15
+ - Complete the fix for per-request `raw: true` on reads (GHSA-wr87-m4jw-29x5)
16
+ - A read made with `raw: true` doesn't ask for flags, but a reply carrying them anyway (from a proxy or a hostile server) still had its value deserialized. Raw reads now ignore flags in the reply
17
+ - Affects 5.1.3 and earlier; fixed in 5.2.2, 5.1.4, 5.0.10, 4.3.7 and 3.2.13
18
+
19
+ Bug fixes:
20
+
21
+ - `get_with_metadata` and `fetch_with_lock` retried the final error raised when a server is marked down, so with `down_retry_delay: 0` they retried an unresponsive server forever. They now retry only retryable errors, like other operations
22
+ - Fix a regression in 5.1.3: a `get_multi` that didn't finish within `socket_timeout` counted toward `socket_max_failures`, so two slow `get_multi` calls in a row marked a healthy server down. It now just closes the connection
23
+ - Fix a regression in 5.1.3: over TLS, every request buffered in a `quiet` block was sent as its own TLS record and system call (2.5 times slower for a block of 2000 deletes). Each flush is one write again
24
+ - Handle malformed replies from a broken or hostile server or proxy: a negative value size, a hit with no key, or a hit with no `s` flag could leave the connection out of step or raise a non-Dalli error
25
+ - With `decompressed_max_bytes` in effect, data after the end of a compressed value's stream was returned as part of the value. It's now ignored, as without the limit
26
+ - With a `digest_class` whose digests aren't short hex strings, shortening a long key could loop forever. It now raises `ArgumentError`
27
+
28
+ Development:
29
+
30
+ - Run the Tests, RuboCop and Profiles workflows on pushes to `main` and the `*-stable` branches only, so a pull request's branch isn't tested twice (backport of #1198)
31
+ - Fix flaky failover tests: move their ports out of Linux's ephemeral port range, and wait for memcached to accept connections after starting it (backport of #1184)
32
+
7
33
  5.1.3
8
34
  ==========
9
35
 
data/lib/dalli/client.rb CHANGED
@@ -136,14 +136,18 @@ module Dalli
136
136
  #
137
137
  def get_with_metadata(key, options = {})
138
138
  validate_routing_tokens!(options)
139
- key = key.to_s
140
- key = @key_manager.validate_key(key)
139
+ # A retry re-runs this method with its original arguments, so `key` must
140
+ # stay as the caller gave it; reassigning it would add the namespace again.
141
+ validated_key = @key_manager.validate_key(key.to_s)
141
142
 
142
- server = ring.server_for_key(key)
143
- Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
144
- server.request(:meta_get, key, options)
143
+ server = ring.server_for_key(validated_key)
144
+ Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', validated_key, server)) do
145
+ server.request(:meta_get, validated_key, options)
145
146
  end
146
- rescue NetworkError => e
147
+ # Only a retryable error is retried. The NetworkError raised when a server
148
+ # is marked down is final, as in #perform; retrying it looped forever once
149
+ # down_retry_delay let the same server be tried again.
150
+ rescue RetryableNetworkError => e
147
151
  Dalli.logger.debug { e.inspect }
148
152
  Dalli.logger.debug { 'retrying get_with_metadata with new server' }
149
153
  retry
@@ -302,14 +306,17 @@ module Dalli
302
306
  validate_integer!(:lock_ttl, lock_ttl)
303
307
  validate_integer!(:recache_threshold, recache_threshold)
304
308
  validate_routing_tokens!(req_options)
305
- key = key.to_s
306
- key = @key_manager.validate_key(key)
309
+ # Left unchanged for the retry below; see get_with_metadata
310
+ validated_key = @key_manager.validate_key(key.to_s)
307
311
 
308
- server = ring.server_for_key(key)
309
- Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
310
- fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
312
+ server = ring.server_for_key(validated_key)
313
+ Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', validated_key, server)) do
314
+ fetch_with_lock_request(validated_key, ttl, lock_ttl, recache_threshold, req_options, &block)
311
315
  end
312
- rescue NetworkError => e
316
+ # Only a retryable error is retried. The NetworkError raised when a server
317
+ # is marked down is final, as in #perform; retrying it looped forever once
318
+ # down_retry_delay let the same server be tried again.
319
+ rescue RetryableNetworkError => e
313
320
  Dalli.logger.debug { e.inspect }
314
321
  Dalli.logger.debug { 'retrying fetch_with_lock with new server' }
315
322
  retry
@@ -727,10 +734,11 @@ module Dalli
727
734
  # to route to at all is still silent, matching Ring#keys_grouped_by_server
728
735
  # dropping a key it can't route on both the single- and multi-server paths.
729
736
  def single_server_get_multi(keys, req_options = nil)
730
- keys.map! { |k| @key_manager.validate_key(k.to_s) }
737
+ # A new array, so the retry below starts again from the caller's keys
738
+ validated_keys = keys.map { |k| @key_manager.validate_key(k.to_s) }
731
739
  return {} unless (server = single_server)
732
740
 
733
- result = server.request(:read_multi_req, keys, req_options)
741
+ result = server.request(:read_multi_req, validated_keys, req_options)
734
742
  result.transform_keys! { |k| @key_manager.key_without_namespace(k) }
735
743
  result
736
744
  rescue Dalli::RetryableNetworkError => e
@@ -897,18 +905,20 @@ module Dalli
897
905
  # rubocop:enable Naming/MethodParameterName
898
906
  return yield if block_given?
899
907
 
900
- key = key.to_s
901
- key = @key_manager.validate_key(key)
908
+ # `retry` below re-runs this method with its original arguments, so `key`
909
+ # must stay as the caller gave it: reassigning it would apply the
910
+ # namespace a second time and send the retry to a different key.
911
+ validated_key = @key_manager.validate_key(key.to_s)
902
912
 
903
- server = ring.server_for_key(key)
913
+ server = ring.server_for_key(validated_key)
904
914
 
905
915
  if Instrumentation.enabled?
906
916
  op_name = op.name
907
- Instrumentation.trace(op_name, trace_attrs(op_name, key, server)) do
908
- server.request(op, key, ...)
917
+ Instrumentation.trace(op_name, trace_attrs(op_name, validated_key, server)) do
918
+ server.request(op, validated_key, ...)
909
919
  end
910
920
  else
911
- server.request(op, key, ...)
921
+ server.request(op, validated_key, ...)
912
922
  end
913
923
  rescue RetryableNetworkError => e
914
924
  Dalli.logger.debug { e.inspect }
@@ -39,7 +39,12 @@ module Dalli
39
39
  out = String.new(encoding: Encoding::BINARY)
40
40
  0.step(data.bytesize - 1, INFLATE_SLICE_BYTES) do |pos|
41
41
  inflate_slice(inflater, data.byteslice(pos, INFLATE_SLICE_BYTES), out, max_bytes)
42
+ # Anything after the end of the stream isn't part of the value, as
43
+ # with Zlib::Inflate.inflate; finish would return it as output
44
+ break if inflater.finished?
42
45
  end
46
+ return out if inflater.finished?
47
+
43
48
  append_within_limit(out, inflater.finish, max_bytes)
44
49
  ensure
45
50
  inflater.close
@@ -122,15 +122,18 @@ module Dalli
122
122
  ##
123
123
  def truncated_key(key)
124
124
  digest = digest_class.hexdigest(key)
125
- prefix = key[0, prefix_length(digest)]
125
+ prefix = key[0, [prefix_length(digest), 0].max]
126
126
  truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
127
127
  # A prefix with whitespace or non-ASCII characters sends the whole key
128
128
  # base64-encoded, which can still be too long on the wire; shorten it.
129
- while wire_length(truncated) > MAX_KEY_LENGTH
129
+ while wire_length(truncated) > MAX_KEY_LENGTH && !prefix.empty?
130
130
  prefix = prefix[0...-1]
131
131
  truncated = "#{prefix}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
132
132
  end
133
- truncated
133
+ return truncated if wire_length(truncated) <= MAX_KEY_LENGTH
134
+
135
+ # Only possible with a digest_class whose digests aren't short hex strings
136
+ raise ArgumentError, "#{digest_class} digests are too long to fit in a truncated key"
134
137
  end
135
138
 
136
139
  # memcached's key limit applies to the key as sent: in bytes, and after
@@ -197,10 +197,13 @@ module Dalli
197
197
  @connection_manager.abort_request!
198
198
  return true unless connected?
199
199
 
200
- # Closes the connection, which ensures that our connection
201
- # is in a clean state for future requests
202
- @connection_manager.error_on_request!('External timeout')
203
- rescue NetworkError
200
+ # Closes the connection, which ensures that our connection is in a
201
+ # clean state for future requests. The get_multi ran out of time
202
+ # overall rather than hitting a socket error, so it doesn't count
203
+ # toward socket_max_failures: two slow get_multis in a row would
204
+ # otherwise mark a healthy server down.
205
+ Dalli.logger.warn { "#{name} didn't finish a get_multi within socket_timeout; closing the connection" }
206
+ @connection_manager.close
204
207
  true
205
208
  end
206
209
 
@@ -328,6 +331,7 @@ module Dalli
328
331
  # Use clear (not reset) to keep pipeline_complete? = true, which is
329
332
  # the expected state before pipeline_response_setup is called.
330
333
  response_buffer.clear
334
+ response_buffer.raw = raw_request?(options)
331
335
 
332
336
  # The terminating noop is sent by pipeline_response_setup
333
337
  write(quiet_get_requests(keys, options, return_cas: return_cas))
@@ -339,6 +343,7 @@ module Dalli
339
343
  def pipelined_get_interleaved(keys, chunk_size, results, options = nil)
340
344
  # Initialize the response buffer for draining during send phase
341
345
  response_buffer.ensure_ready
346
+ response_buffer.raw = raw_request?(options)
342
347
 
343
348
  keys.each_slice(chunk_size) do |chunk|
344
349
  # Build and write this chunk of requests
@@ -334,13 +334,32 @@ module Dalli
334
334
 
335
335
  private
336
336
 
337
+ # One write per flush. A TLS socket sends each argument of
338
+ # write(*parts) as its own record and system call, so the parts are
339
+ # joined first.
337
340
  def flush_write_buffer
338
341
  return if @write_buffer.empty?
339
342
 
340
- @sock.write(*@write_buffer)
343
+ @sock.write(@write_buffer.size == 1 ? @write_buffer.first : joined_write_buffer)
341
344
  discard_write_buffer
342
345
  end
343
346
 
347
+ # Joined as bytes: requests can mix UTF-8 and binary strings, which
348
+ # String#<< refuses to combine when both hold non-ASCII bytes
349
+ if String.method_defined?(:append_as_bytes)
350
+ def joined_write_buffer
351
+ out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
352
+ @write_buffer.each { |part| out.append_as_bytes(part) }
353
+ out
354
+ end
355
+ else
356
+ def joined_write_buffer
357
+ out = String.new(capacity: @write_buffer_bytes, encoding: Encoding::BINARY)
358
+ @write_buffer.each { |part| out << part.b }
359
+ out
360
+ end
361
+ end
362
+
344
363
  def discard_write_buffer
345
364
  @write_buffer.clear
346
365
  @write_buffer_bytes = 0
@@ -38,7 +38,7 @@ module Dalli
38
38
  RequestFormatter.plain_meta_get(key, skip_flags)
39
39
  end
40
40
  flushed_write(req)
41
- response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
41
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
42
42
  end
43
43
 
44
44
  def quiet_get_request(key, options = nil)
@@ -59,7 +59,7 @@ module Dalli
59
59
  skip_flags = raw_mode? || (options && options[:raw])
60
60
  req = RequestFormatter.meta_get(key: key, ttl: ttl, skip_flags: skip_flags, **routing_token_kwargs(options))
61
61
  flushed_write(req)
62
- response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
62
+ response_processor.meta_get_with_value(cache_nils: cache_nils?(options), raw: raw_request?(options))
63
63
  end
64
64
 
65
65
  def touch(key, ttl)
@@ -75,7 +75,7 @@ module Dalli
75
75
  req = RequestFormatter.meta_get(key: key, value: true, return_cas: true, skip_flags: raw_request?(options),
76
76
  **routing_token_kwargs(options))
77
77
  flushed_write(req)
78
- response_processor.meta_get_with_value_and_cas
78
+ response_processor.meta_get_with_value_and_cas(raw: raw_request?(options))
79
79
  end
80
80
 
81
81
  # Comprehensive meta get with support for all metadata flags.
@@ -116,7 +116,7 @@ module Dalli
116
116
  response_processor.meta_get_with_metadata(
117
117
  cache_nils: cache_nils?(options), return_hit_status: options[:return_hit_status],
118
118
  return_last_access: options[:return_last_access],
119
- return_ttl_remaining: options[:return_ttl_remaining]
119
+ return_ttl_remaining: options[:return_ttl_remaining], raw: raw_request?(options)
120
120
  )
121
121
  end
122
122
 
@@ -300,7 +300,9 @@ module Dalli
300
300
  next unless line.start_with?('VA ')
301
301
 
302
302
  tokens = line.chomp!(TERMINATOR).split
303
- value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
303
+ size = tokens[1].to_i
304
+ response_processor.check_value_size!(size)
305
+ value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
304
306
  stale = response_processor.stale_from_tokens(tokens)
305
307
  cas = response_processor.cas_from_tokens(tokens)
306
308
  bitflags = is_raw ? 0 : response_processor.bitflags_from_tokens(tokens)
@@ -327,7 +329,9 @@ module Dalli
327
329
 
328
330
  def parse_multi_get_value(line, key_index, is_raw)
329
331
  tokens = line.chomp!(TERMINATOR).split
330
- value = @connection_manager.read(tokens[1].to_i + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
332
+ size = tokens[1].to_i
333
+ response_processor.check_value_size!(size)
334
+ value = @connection_manager.read(size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
331
335
  raw_key = tokens[key_index]
332
336
  return unless raw_key
333
337
 
@@ -15,6 +15,9 @@ module Dalli
15
15
  # when advancing through parsed responses.
16
16
  ##
17
17
  class ResponseBuffer
18
+ # Whether the current pipeline was requested with raw: true
19
+ attr_writer :raw
20
+
18
21
  def initialize(io_source, response_processor)
19
22
  @io_source = io_source
20
23
  @response_processor = response_processor
@@ -39,7 +42,7 @@ module Dalli
39
42
  # Attempts to process a single response from the buffer,
40
43
  # advancing the offset past the consumed bytes.
41
44
  def process_single_getk_response
42
- response = @response_processor.getk_response_from_buffer(@buffer, @offset)
45
+ response = @response_processor.getk_response_from_buffer(@buffer, @offset, raw: @raw || false)
43
46
  @offset += response.pop
44
47
  response
45
48
  end
@@ -48,12 +48,15 @@ module Dalli
48
48
  @value_marshaller = value_marshaller
49
49
  end
50
50
 
51
- def meta_get_with_value(cache_nils: false)
51
+ # raw: true (a request made with raw: true) returns the value as stored,
52
+ # ignoring any flags in the reply. The request didn't ask for flags, so
53
+ # a reply carrying them anyway must not get the value deserialized.
54
+ def meta_get_with_value(cache_nils: false, raw: false)
52
55
  line = read_line
53
56
  # A hit ("VA <size> f<flags>") is parsed in place rather than split
54
57
  # into tokens, which saves several allocations on the hottest path.
55
58
  if line&.start_with?(VA_PREFIX)
56
- return @value_marshaller.retrieve(read_data(size_from_va_line(line)), bitflags_from_va_line(line))
59
+ return retrieve(read_data(size_from_va_line(line)), raw ? 0 : bitflags_from_va_line(line), raw)
57
60
  end
58
61
 
59
62
  tokens = line&.split || []
@@ -61,7 +64,7 @@ module Dalli
61
64
  when EN
62
65
  cache_nils ? ::Dalli::NOT_FOUND : nil
63
66
  when VA # only reached for an unusually formatted hit line
64
- @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
67
+ retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
65
68
  when HD
66
69
  true
67
70
  else
@@ -69,14 +72,14 @@ module Dalli
69
72
  end
70
73
  end
71
74
 
72
- def meta_get_with_value_and_cas
75
+ def meta_get_with_value_and_cas(raw: false)
73
76
  tokens = error_on_unexpected!(T_VA_EN_HD)
74
77
  return [nil, 0] if tokens.first == EN
75
78
 
76
79
  cas = cas_from_tokens(tokens)
77
80
  return [nil, cas] unless tokens.first == VA
78
81
 
79
- [@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
82
+ [retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw), cas]
80
83
  end
81
84
 
82
85
  def meta_get_without_value
@@ -96,13 +99,13 @@ module Dalli
96
99
  # Used by meta_get for comprehensive metadata retrieval.
97
100
  # Supports thundering herd protection (N/R flags) and metadata flags (h/l/u).
98
101
  def meta_get_with_metadata(cache_nils: false, return_hit_status: false, return_last_access: false,
99
- return_ttl_remaining: false)
102
+ return_ttl_remaining: false, raw: false)
100
103
  tokens = error_on_unexpected!(T_VA_EN_HD)
101
104
  result = build_metadata_result(tokens)
102
105
  result[:hit_before] = hit_status_from_tokens(tokens) if return_hit_status
103
106
  result[:last_access] = last_access_from_tokens(tokens) if return_last_access
104
107
  result[:ttl_remaining] = ttl_remaining_from_tokens(tokens) if return_ttl_remaining
105
- result[:value] = parse_value_from_tokens(tokens, cache_nils)
108
+ result[:value] = parse_value_from_tokens(tokens, cache_nils, raw)
106
109
  result
107
110
  end
108
111
 
@@ -119,11 +122,11 @@ module Dalli
119
122
  }
120
123
  end
121
124
 
122
- def parse_value_from_tokens(tokens, cache_nils)
125
+ def parse_value_from_tokens(tokens, cache_nils, raw = false) # rubocop:disable Style/OptionalBooleanParameter
123
126
  return cache_nils ? ::Dalli::NOT_FOUND : nil if tokens.first == EN
124
127
  return unless tokens.first == VA
125
128
 
126
- @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
129
+ retrieve(read_data(tokens[1].to_i), raw ? 0 : bitflags_from_tokens(tokens), raw)
127
130
  end
128
131
 
129
132
  def meta_set_with_cas
@@ -214,7 +217,9 @@ module Dalli
214
217
  # The remaining three values in the array are the ResponseHeader,
215
218
  # key, and value.
216
219
  ##
217
- def getk_response_from_buffer(buf, offset = 0)
220
+ # raw: true returns values as stored, ignoring flags in the replies;
221
+ # see meta_get_with_value.
222
+ def getk_response_from_buffer(buf, offset = 0, raw: false)
218
223
  # Find the header terminator starting from offset
219
224
  term_idx = buf.byteindex(TERMINATOR, offset)
220
225
  return [0] unless term_idx
@@ -236,7 +241,9 @@ module Dalli
236
241
  when BYTE_B then base64 ||= token == 'b'
237
242
  end
238
243
  end
239
- body_len = flag_int(size, 's')
244
+ # memcached also gives the size positionally ("VA <size> ..."); use it
245
+ # when there's no s flag rather than reading the value as replies
246
+ body_len = size ? flag_int(size, 's') : positional_size(tokens)
240
247
 
241
248
  # A complete response with no body. Only the terminating noop's MN
242
249
  # ends the pipeline. Any other bodyless reply (CLIENT_ERROR,
@@ -256,12 +263,21 @@ module Dalli
256
263
  # The full response is in our buffer, so parse it and return
257
264
  # the values
258
265
  body = buf.byteslice(offset + header_len, body_len)
259
- value = @value_marshaller.retrieve(body, flag_int(bitflags, 'f'))
260
- key = key ? key.delete_prefix!('k') : 0
266
+ value = retrieve(body, raw ? 0 : flag_int(bitflags, 'f'), raw)
267
+ # A hit with no key can't be matched to a request: skip it (false
268
+ # status) rather than yield it, or take it for the end of the pipeline
269
+ return [false, resp_size] unless key
270
+
271
+ key = key.delete_prefix!('k')
261
272
  key = KeyRegularizer.decode(key) if base64
262
273
  [tokens.first == VA, flag_int(cas, 'c'), key, value, resp_size]
263
274
  end
264
275
 
276
+ # Values from a raw request are returned as stored
277
+ def retrieve(value, bitflags, raw)
278
+ raw ? value : @value_marshaller.retrieve(value, bitflags)
279
+ end
280
+
265
281
  # Integer value of a flag token such as "f123", or 0 when absent.
266
282
  # Strips the prefix in place, like value_from_tokens.
267
283
  def flag_int(token, flag)
@@ -352,6 +368,11 @@ module Dalli
352
368
 
353
369
  # A pipelined reply claiming an impossible size would otherwise have the
354
370
  # buffer wait for (and accumulate) that many bytes
371
+ # The size given right after "VA", or 0 when it isn't a number
372
+ def positional_size(tokens)
373
+ tokens.first == VA && tokens[1]&.match?(/\A\d+\z/) ? tokens[1].to_i : 0
374
+ end
375
+
355
376
  def check_value_size!(size)
356
377
  return if size.between?(0, MAX_VALUE_BYTES)
357
378
 
@@ -368,6 +389,9 @@ module Dalli
368
389
  end
369
390
 
370
391
  def read_data(data_size)
392
+ # Checked before the terminator is added: -1 or -2 would otherwise
393
+ # read 1 or 0 bytes and leave the connection out of step
394
+ check_value_size!(data_size)
371
395
  @io_source.read(data_size + TERMINATOR.bytesize)&.chomp!(TERMINATOR)
372
396
  end
373
397
  end
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.1.3'
4
+ VERSION = '5.1.4'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6.27'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.3
4
+ version: 5.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein