thrift 0.22.0 → 0.24.0

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.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +235 -17
  3. data/benchmark/benchmark.rb +23 -8
  4. data/benchmark/client.rb +50 -6
  5. data/benchmark/server.rb +46 -7
  6. data/benchmark/thin_server.rb +2 -0
  7. data/ext/binary_protocol_accelerated.c +154 -42
  8. data/ext/bytes.c +14 -0
  9. data/ext/compact_protocol.c +138 -45
  10. data/ext/constants.h +12 -0
  11. data/ext/extconf.rb +17 -8
  12. data/ext/memory_buffer.c +44 -7
  13. data/ext/protocol.c +29 -0
  14. data/ext/protocol.h +35 -0
  15. data/ext/struct.c +48 -17
  16. data/ext/thrift_native.c +28 -3
  17. data/lib/thrift/bytes.rb +70 -100
  18. data/lib/thrift/client.rb +54 -13
  19. data/lib/thrift/exceptions.rb +6 -5
  20. data/lib/thrift/multiplexed_processor.rb +20 -10
  21. data/lib/thrift/processor.rb +7 -6
  22. data/lib/thrift/protocol/base_protocol.rb +52 -21
  23. data/lib/thrift/protocol/binary_protocol.rb +65 -20
  24. data/lib/thrift/protocol/binary_protocol_accelerated.rb +6 -5
  25. data/lib/thrift/protocol/compact_protocol.rb +76 -41
  26. data/lib/thrift/protocol/header_protocol.rb +321 -0
  27. data/lib/thrift/protocol/json_protocol.rb +44 -27
  28. data/lib/thrift/protocol/multiplexed_protocol.rb +6 -5
  29. data/lib/thrift/protocol/protocol_decorator.rb +13 -4
  30. data/lib/thrift/serializer/deserializer.rb +6 -5
  31. data/lib/thrift/serializer/serializer.rb +5 -5
  32. data/lib/thrift/server/base_server.rb +5 -4
  33. data/lib/thrift/server/mongrel_http_server.rb +7 -6
  34. data/lib/thrift/server/nonblocking_server.rb +35 -9
  35. data/lib/thrift/server/simple_server.rb +13 -5
  36. data/lib/thrift/server/thin_http_server.rb +4 -3
  37. data/lib/thrift/server/thread_pool_server.rb +7 -6
  38. data/lib/thrift/server/threaded_server.rb +13 -5
  39. data/lib/thrift/struct.rb +12 -11
  40. data/lib/thrift/struct_union.rb +14 -9
  41. data/lib/thrift/thrift_native.rb +3 -2
  42. data/lib/thrift/transport/base_server_transport.rb +8 -5
  43. data/lib/thrift/transport/base_transport.rb +18 -14
  44. data/lib/thrift/transport/buffered_transport.rb +7 -6
  45. data/lib/thrift/transport/framed_transport.rb +8 -7
  46. data/lib/thrift/transport/header_transport.rb +562 -0
  47. data/lib/thrift/transport/http_client_transport.rb +2 -1
  48. data/lib/thrift/transport/io_stream_transport.rb +4 -3
  49. data/lib/thrift/transport/memory_buffer_transport.rb +13 -6
  50. data/lib/thrift/transport/server_socket.rb +14 -8
  51. data/lib/thrift/transport/socket.rb +126 -60
  52. data/lib/thrift/transport/ssl_server_socket.rb +4 -3
  53. data/lib/thrift/transport/ssl_socket.rb +45 -13
  54. data/lib/thrift/transport/unix_server_socket.rb +9 -5
  55. data/lib/thrift/transport/unix_socket.rb +7 -6
  56. data/lib/thrift/types.rb +10 -6
  57. data/lib/thrift/union.rb +15 -8
  58. data/lib/thrift/uuid.rb +50 -0
  59. data/lib/thrift.rb +4 -1
  60. data/spec/ThriftSpec.thrift +21 -1
  61. data/spec/base_protocol_spec.rb +21 -2
  62. data/spec/base_transport_spec.rb +48 -8
  63. data/spec/binary_protocol_accelerated_spec.rb +1 -0
  64. data/spec/binary_protocol_spec.rb +1 -2
  65. data/spec/binary_protocol_spec_shared.rb +206 -155
  66. data/spec/bytes_spec.rb +71 -114
  67. data/spec/client_spec.rb +86 -19
  68. data/spec/compact_protocol_spec.rb +153 -16
  69. data/spec/constants_demo_spec.rb +102 -0
  70. data/spec/exception_spec.rb +1 -1
  71. data/spec/flat_spec.rb +1 -0
  72. data/spec/header_protocol_spec.rb +476 -0
  73. data/spec/header_transport_spec.rb +431 -0
  74. data/spec/http_client_spec.rb +5 -6
  75. data/spec/json_protocol_spec.rb +69 -47
  76. data/spec/multiplexed_processor_spec.rb +75 -0
  77. data/spec/namespaced_spec.rb +1 -1
  78. data/spec/nonblocking_server_spec.rb +174 -8
  79. data/spec/processor_spec.rb +1 -1
  80. data/spec/recursion_depth_spec.rb +223 -0
  81. data/spec/serializer_spec.rb +1 -1
  82. data/spec/server_socket_spec.rb +38 -1
  83. data/spec/server_spec.rb +60 -9
  84. data/spec/socket_spec.rb +119 -13
  85. data/spec/socket_spec_shared.rb +73 -9
  86. data/spec/spec_helper.rb +2 -1
  87. data/spec/ssl_server_socket_spec.rb +52 -1
  88. data/spec/ssl_socket_spec.rb +181 -11
  89. data/spec/struct_nested_containers_spec.rb +2 -2
  90. data/spec/struct_spec.rb +114 -9
  91. data/spec/support/header_protocol_helper.rb +55 -0
  92. data/spec/thin_http_server_spec.rb +4 -18
  93. data/spec/types_spec.rb +26 -26
  94. data/spec/union_spec.rb +70 -11
  95. data/spec/unix_socket_spec.rb +17 -2
  96. data/spec/uuid_validation_spec.rb +239 -0
  97. data/test/fuzz/Makefile +779 -0
  98. data/test/fuzz/Makefile.am +173 -0
  99. data/test/fuzz/Makefile.in +775 -0
  100. data/test/fuzz/README.md +149 -0
  101. data/test/fuzz/fuzz_common.rb +96 -0
  102. data/{lib/thrift/core_ext.rb → test/fuzz/fuzz_parse_binary_protocol.rb} +4 -4
  103. data/{lib/thrift/core_ext/fixnum.rb → test/fuzz/fuzz_parse_binary_protocol_accelerated.rb} +7 -13
  104. data/test/fuzz/fuzz_parse_binary_protocol_accelerated_harness.rb +23 -0
  105. data/test/fuzz/fuzz_parse_binary_protocol_harness.rb +23 -0
  106. data/test/fuzz/fuzz_parse_compact_protocol.rb +23 -0
  107. data/test/fuzz/fuzz_parse_compact_protocol_harness.rb +23 -0
  108. data/test/fuzz/fuzz_parse_json_protocol.rb +23 -0
  109. data/test/fuzz/fuzz_parse_json_protocol_harness.rb +23 -0
  110. data/test/fuzz/fuzz_roundtrip_binary_protocol.rb +23 -0
  111. data/test/fuzz/fuzz_roundtrip_binary_protocol_accelerated.rb +23 -0
  112. data/test/fuzz/fuzz_roundtrip_binary_protocol_accelerated_harness.rb +23 -0
  113. data/test/fuzz/fuzz_roundtrip_binary_protocol_harness.rb +23 -0
  114. data/test/fuzz/fuzz_roundtrip_compact_protocol.rb +23 -0
  115. data/test/fuzz/fuzz_roundtrip_compact_protocol_harness.rb +23 -0
  116. data/test/fuzz/fuzz_roundtrip_json_protocol.rb +23 -0
  117. data/test/fuzz/fuzz_roundtrip_json_protocol_harness.rb +23 -0
  118. data/test/fuzz/fuzz_tracer.rb +29 -0
  119. metadata +105 -70
@@ -0,0 +1,562 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+ #
4
+ # Licensed to the Apache Software Foundation (ASF) under one
5
+ # or more contributor license agreements. See the NOTICE file
6
+ # distributed with this work for additional information
7
+ # regarding copyright ownership. The ASF licenses this file
8
+ # to you under the Apache License, Version 2.0 (the
9
+ # "License"); you may not use this file except in compliance
10
+ # with the License. You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing,
15
+ # software distributed under the License is distributed on an
16
+ # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17
+ # KIND, either express or implied. See the License for the
18
+ # specific language governing permissions and limitations
19
+ # under the License.
20
+ #
21
+
22
+ require 'stringio'
23
+ require 'zlib'
24
+
25
+ module Thrift
26
+ # Client type constants for Header protocol
27
+ module HeaderClientType
28
+ HEADERS = 0x00
29
+ FRAMED_BINARY = 0x01
30
+ UNFRAMED_BINARY = 0x02
31
+ FRAMED_COMPACT = 0x03
32
+ UNFRAMED_COMPACT = 0x04
33
+ end
34
+
35
+ # Subprotocol ID constants for Header transport
36
+ module HeaderSubprotocolID
37
+ BINARY = 0x00
38
+ COMPACT = 0x02
39
+ end
40
+
41
+ # Transform ID constants for Header transport
42
+ module HeaderTransformID
43
+ ZLIB = 0x01
44
+ end
45
+
46
+ # Info header type constants
47
+ module HeaderInfoType
48
+ KEY_VALUE = 0x01
49
+ end
50
+
51
+ # HeaderTransport implements the THeader framing protocol.
52
+ #
53
+ # THeader is a transport that adds headers and supports multiple protocols
54
+ # and transforms. It can auto-detect and communicate with legacy protocols
55
+ # (framed/unframed binary/compact) for backward compatibility.
56
+ #
57
+ # Wire format:
58
+ # +----------------------------------------------------------------+
59
+ # | LENGTH (4 bytes, big-endian, excludes itself) |
60
+ # +----------------------------------------------------------------+
61
+ # | HEADER MAGIC (2 bytes: 0x0FFF) | FLAGS (2 bytes) |
62
+ # +----------------------------------------------------------------+
63
+ # | SEQUENCE NUMBER (4 bytes) |
64
+ # +----------------------------------------------------------------+
65
+ # | HEADER SIZE/4 (2 bytes) | HEADER DATA (variable)... |
66
+ # +----------------------------------------------------------------+
67
+ # | PAYLOAD (variable) |
68
+ # +----------------------------------------------------------------+
69
+ #
70
+ class HeaderTransport < BaseTransport
71
+ # Header magic value (first 2 bytes of header)
72
+ HEADER_MAGIC = 0x0FFF
73
+
74
+ # Maximum frame size (~1GB)
75
+ MAX_FRAME_SIZE = 0x3FFFFFFF
76
+
77
+ # Default decompressed-size cap for ZLIB transform (~15.6 MB, matches other Thrift bindings)
78
+ DEFAULT_MAX_DECOMPRESSED_SIZE = 16_384_000
79
+
80
+ # Chunk size for streaming inflate loop
81
+ ZLIB_INFLATE_CHUNK_SIZE = 16_384
82
+
83
+ # Binary protocol version mask and version 1
84
+ BINARY_VERSION_MASK = 0xffff0000
85
+ BINARY_VERSION_1 = 0x80010000
86
+
87
+ # Compact protocol ID
88
+ COMPACT_PROTOCOL_ID = 0x82
89
+ COMPACT_VERSION_MASK = 0x1f
90
+ COMPACT_VERSION = 0x01
91
+
92
+ attr_reader :protocol_id, :sequence_id, :flags
93
+
94
+ # Creates a new HeaderTransport wrapping the given transport.
95
+ #
96
+ # @param transport [BaseTransport] The underlying transport to wrap
97
+ # @param allowed_client_types [Array<Integer>] Allowed client types for auto-detection.
98
+ # Defaults to all types for backward compatibility.
99
+ # @param default_protocol [Integer] Default protocol ID (BINARY or COMPACT)
100
+ def initialize(transport, allowed_client_types = nil, default_protocol = HeaderSubprotocolID::COMPACT)
101
+ @transport = transport
102
+ @client_type = HeaderClientType::HEADERS
103
+ @protocol_id = default_protocol
104
+ @allowed_client_types = allowed_client_types || [
105
+ HeaderClientType::HEADERS,
106
+ HeaderClientType::FRAMED_BINARY,
107
+ HeaderClientType::UNFRAMED_BINARY,
108
+ HeaderClientType::FRAMED_COMPACT,
109
+ HeaderClientType::UNFRAMED_COMPACT
110
+ ]
111
+
112
+ @read_buffer = StringIO.new(Bytes.empty_byte_buffer)
113
+ @write_buffer = StringIO.new(Bytes.empty_byte_buffer)
114
+
115
+ @read_headers = {}
116
+ @write_headers = {}
117
+ @write_transforms = []
118
+
119
+ @sequence_id = 0
120
+ @flags = 0
121
+ @max_frame_size = MAX_FRAME_SIZE
122
+ @max_decompressed_size = DEFAULT_MAX_DECOMPRESSED_SIZE
123
+ end
124
+
125
+ def sequence_id=(sequence_id)
126
+ if sequence_id < -(2**31) || sequence_id > (2**31) - 1
127
+ raise RangeError, "sequence_id must be a signed int32"
128
+ end
129
+
130
+ @sequence_id = sequence_id
131
+ end
132
+
133
+ def open?
134
+ @transport.open?
135
+ end
136
+
137
+ def open
138
+ @transport.open
139
+ end
140
+
141
+ def close
142
+ @transport.close
143
+ end
144
+
145
+ # Returns the headers read from the last frame
146
+ def get_headers
147
+ @read_headers
148
+ end
149
+
150
+ # Sets a header to be written with the next flush
151
+ #
152
+ # @param key [String] Header key (must be binary string)
153
+ # @param value [String] Header value (must be binary string)
154
+ def set_header(key, value)
155
+ key = Bytes.force_binary_encoding(key.to_s)
156
+ value = Bytes.force_binary_encoding(value.to_s)
157
+ @write_headers[key] = value
158
+ end
159
+
160
+ # Clears all write headers
161
+ def clear_headers
162
+ @write_headers.clear
163
+ end
164
+
165
+ # Adds a transform to apply when writing
166
+ #
167
+ # @param transform_id [Integer] Transform ID (e.g., HeaderTransformID::ZLIB)
168
+ def add_transform(transform_id)
169
+ unless transform_id == HeaderTransformID::ZLIB
170
+ raise TransportException.new(TransportException::UNKNOWN, "Unknown transform: #{transform_id}")
171
+ end
172
+ @write_transforms << transform_id unless @write_transforms.include?(transform_id)
173
+ end
174
+
175
+ # Sets the maximum allowed frame size
176
+ def set_max_frame_size(size)
177
+ if size <= 0 || size > MAX_FRAME_SIZE
178
+ raise ArgumentError, "max_frame_size must be > 0 and <= #{MAX_FRAME_SIZE}"
179
+ end
180
+ @max_frame_size = size
181
+ end
182
+
183
+ # Sets the maximum allowed decompressed size for ZLIB transforms
184
+ def set_max_decompressed_size(size)
185
+ if size <= 0 || size > MAX_FRAME_SIZE
186
+ raise ArgumentError, "max_decompressed_size must be > 0 and <= #{MAX_FRAME_SIZE}"
187
+ end
188
+ @max_decompressed_size = size
189
+ end
190
+
191
+ def read(sz)
192
+ # Try reading from existing buffer
193
+ data = @read_buffer.read(sz)
194
+ data = Bytes.empty_byte_buffer if data.nil?
195
+
196
+ bytes_left = sz - data.bytesize
197
+ return data if bytes_left == 0
198
+
199
+ # Handle unframed passthrough - read directly from underlying transport
200
+ if @client_type == HeaderClientType::UNFRAMED_BINARY ||
201
+ @client_type == HeaderClientType::UNFRAMED_COMPACT
202
+ return data + @transport.read(bytes_left)
203
+ end
204
+
205
+ # Need to read the next frame
206
+ read_frame(bytes_left)
207
+ additional = @read_buffer.read(bytes_left)
208
+ data + (additional || Bytes.empty_byte_buffer)
209
+ end
210
+
211
+ def write(buf)
212
+ @write_buffer.write(Bytes.force_binary_encoding(buf))
213
+ end
214
+
215
+ def flush
216
+ payload = @write_buffer.string
217
+ @write_buffer = StringIO.new(Bytes.empty_byte_buffer)
218
+
219
+ return if payload.empty?
220
+ if payload.bytesize > @max_frame_size
221
+ raise TransportException.new(TransportException::UNKNOWN, "Attempting to send frame that is too large")
222
+ end
223
+
224
+ case @client_type
225
+ when HeaderClientType::HEADERS
226
+ flush_header_format(payload)
227
+ when HeaderClientType::FRAMED_BINARY, HeaderClientType::FRAMED_COMPACT
228
+ flush_framed(payload)
229
+ when HeaderClientType::UNFRAMED_BINARY, HeaderClientType::UNFRAMED_COMPACT
230
+ @transport.write(payload)
231
+ @transport.flush
232
+ else
233
+ flush_header_format(payload)
234
+ end
235
+ end
236
+
237
+ def to_s
238
+ "header(#{@transport.to_s})"
239
+ end
240
+
241
+ # Reads the next frame to detect protocol/client type before decoding.
242
+ def reset_protocol
243
+ return unless @read_buffer.nil? || @read_buffer.eof?
244
+
245
+ read_frame(0)
246
+ end
247
+
248
+ private
249
+
250
+ # Sets the client type after validation
251
+ def set_client_type(client_type)
252
+ unless @allowed_client_types.include?(client_type)
253
+ raise TransportException.new(TransportException::UNKNOWN, "Client type #{client_type} not allowed by server")
254
+ end
255
+ @client_type = client_type
256
+ end
257
+
258
+ # Reads the next frame, detecting client type on first read
259
+ def read_frame(req_sz)
260
+ # Read first 4 bytes - could be frame length or protocol magic
261
+ first_word = @transport.read_all(4)
262
+ frame_size = first_word.unpack('N').first
263
+
264
+ # Check for unframed binary protocol
265
+ if (frame_size & BINARY_VERSION_MASK) == BINARY_VERSION_1
266
+ set_client_type(HeaderClientType::UNFRAMED_BINARY)
267
+ @protocol_id = HeaderSubprotocolID::BINARY
268
+ handle_unframed(first_word, req_sz)
269
+ return
270
+ end
271
+
272
+ # Check for unframed compact protocol
273
+ if Bytes.get_string_byte(first_word, 0) == COMPACT_PROTOCOL_ID &&
274
+ (Bytes.get_string_byte(first_word, 1) & COMPACT_VERSION_MASK) == COMPACT_VERSION
275
+ set_client_type(HeaderClientType::UNFRAMED_COMPACT)
276
+ @protocol_id = HeaderSubprotocolID::COMPACT
277
+ handle_unframed(first_word, req_sz)
278
+ return
279
+ end
280
+
281
+ # It's a framed protocol - validate frame size
282
+ if frame_size > @max_frame_size
283
+ raise TransportException.new(TransportException::UNKNOWN, "Frame size #{frame_size} exceeds maximum #{@max_frame_size}")
284
+ end
285
+
286
+ # Read the complete frame
287
+ frame_data = @transport.read_all(frame_size)
288
+ frame_buf = StringIO.new(frame_data)
289
+
290
+ # Check the second word for protocol type
291
+ second_word = frame_buf.read(4)
292
+ frame_buf.rewind
293
+
294
+ magic = second_word.unpack('n').first
295
+
296
+ if magic == HEADER_MAGIC
297
+ if frame_size < 10
298
+ raise TransportException.new(TransportException::UNKNOWN, "Header transport frame is too small")
299
+ end
300
+ set_client_type(HeaderClientType::HEADERS)
301
+ @read_buffer = parse_header_format(frame_buf)
302
+ elsif (second_word.unpack('N').first & BINARY_VERSION_MASK) == BINARY_VERSION_1
303
+ set_client_type(HeaderClientType::FRAMED_BINARY)
304
+ @protocol_id = HeaderSubprotocolID::BINARY
305
+ @read_buffer = frame_buf
306
+ elsif Bytes.get_string_byte(second_word, 0) == COMPACT_PROTOCOL_ID &&
307
+ (Bytes.get_string_byte(second_word, 1) & COMPACT_VERSION_MASK) == COMPACT_VERSION
308
+ set_client_type(HeaderClientType::FRAMED_COMPACT)
309
+ @protocol_id = HeaderSubprotocolID::COMPACT
310
+ @read_buffer = frame_buf
311
+ else
312
+ raise TransportException.new(TransportException::UNKNOWN, "Could not detect client transport type")
313
+ end
314
+ end
315
+
316
+ # Handles unframed protocol - puts first_word back in buffer
317
+ def handle_unframed(first_word, req_sz)
318
+ bytes_left = req_sz - 4
319
+ if bytes_left > 0
320
+ rest = @transport.read(bytes_left)
321
+ @read_buffer = StringIO.new(first_word + rest)
322
+ else
323
+ @read_buffer = StringIO.new(first_word)
324
+ end
325
+ end
326
+
327
+ # Parses a Header format frame
328
+ def parse_header_format(buf)
329
+ # Skip magic (already identified)
330
+ buf.read(2)
331
+
332
+ # Read flags and sequence ID
333
+ @flags = buf.read(2).unpack('n').first
334
+ @sequence_id = signed_int32(buf.read(4).unpack('N').first)
335
+
336
+ # Read header length (in 32-bit words)
337
+ header_words = buf.read(2).unpack('n').first
338
+ if header_words >= 16_384
339
+ raise TransportException.new(TransportException::UNKNOWN, "Header size is unreasonable")
340
+ end
341
+ header_length = header_words * 4
342
+ end_of_headers = buf.pos + header_length
343
+
344
+ if end_of_headers > buf.string.bytesize
345
+ raise TransportException.new(TransportException::UNKNOWN, "Header size exceeds frame size")
346
+ end
347
+
348
+ # Read protocol ID
349
+ @protocol_id = read_varint32(buf, end_of_headers)
350
+
351
+ # Read transforms
352
+ transforms = []
353
+ transform_count = read_varint32(buf, end_of_headers)
354
+ transform_count.times do
355
+ transform_id = read_varint32(buf, end_of_headers)
356
+ unless transform_id == HeaderTransformID::ZLIB
357
+ raise TransportException.new(TransportException::UNKNOWN, "Unknown transform: #{transform_id}")
358
+ end
359
+ transforms << transform_id
360
+ end
361
+ # Read info headers
362
+ @read_headers = {}
363
+ while buf.pos < end_of_headers
364
+ info_type = read_varint32(buf, end_of_headers)
365
+ if info_type == 0
366
+ # header padding
367
+ break
368
+ elsif info_type == HeaderInfoType::KEY_VALUE
369
+ count = read_varint32(buf, end_of_headers)
370
+ count.times do
371
+ key = read_varstring(buf, end_of_headers)
372
+ value = read_varstring(buf, end_of_headers)
373
+ @read_headers[key] = value
374
+ end
375
+ else
376
+ # Unknown info type, skip to end of headers
377
+ break
378
+ end
379
+ end
380
+
381
+ # Skip any remaining header padding
382
+ buf.pos = end_of_headers
383
+
384
+ # Read payload and apply transforms
385
+ payload = buf.read
386
+ transforms.each do |transform_id|
387
+ if transform_id == HeaderTransformID::ZLIB
388
+ payload = bounded_inflate(payload)
389
+ end
390
+ end
391
+
392
+ StringIO.new(payload)
393
+ end
394
+
395
+ # Inflates +compressed+ with a running byte-count check against @max_decompressed_size.
396
+ # Raises TransportException::SIZE_LIMIT if the decompressed output would exceed the limit.
397
+ def bounded_inflate(compressed)
398
+ inflater = Zlib::Inflate.new
399
+ buffer = Bytes.empty_byte_buffer
400
+ offset = 0
401
+ begin
402
+ while offset < compressed.bytesize
403
+ buffer << inflater.inflate(compressed.byteslice(offset, ZLIB_INFLATE_CHUNK_SIZE))
404
+ if buffer.bytesize > @max_decompressed_size
405
+ raise TransportException.new(
406
+ TransportException::SIZE_LIMIT,
407
+ "Decompressed size exceeds limit of #{@max_decompressed_size}"
408
+ )
409
+ end
410
+ offset += ZLIB_INFLATE_CHUNK_SIZE
411
+ end
412
+ buffer << inflater.finish
413
+ if buffer.bytesize > @max_decompressed_size
414
+ raise TransportException.new(
415
+ TransportException::SIZE_LIMIT,
416
+ "Decompressed size exceeds limit of #{@max_decompressed_size}"
417
+ )
418
+ end
419
+ buffer
420
+ ensure
421
+ inflater.close rescue nil
422
+ end
423
+ end
424
+
425
+ # Flushes data in Header format
426
+ def flush_header_format(payload)
427
+ # Apply transforms
428
+ @write_transforms.each do |transform_id|
429
+ if transform_id == HeaderTransformID::ZLIB
430
+ payload = Zlib::Deflate.deflate(payload)
431
+ end
432
+ end
433
+
434
+ # Build header data
435
+ header_buf = StringIO.new(Bytes.empty_byte_buffer)
436
+
437
+ # Protocol ID
438
+ write_varint32(header_buf, @protocol_id)
439
+
440
+ # Transforms
441
+ write_varint32(header_buf, @write_transforms.size)
442
+ @write_transforms.each { |t| write_varint32(header_buf, t) }
443
+
444
+ # Info headers (key-value pairs)
445
+ unless @write_headers.empty?
446
+ write_varint32(header_buf, HeaderInfoType::KEY_VALUE)
447
+ write_varint32(header_buf, @write_headers.size)
448
+ @write_headers.each do |key, value|
449
+ write_varstring(header_buf, key)
450
+ write_varstring(header_buf, value)
451
+ end
452
+ @write_headers = {}
453
+ end
454
+
455
+ # Pad header to 4-byte boundary
456
+ header_data = header_buf.string
457
+ padding = (4 - (header_data.bytesize % 4)) % 4
458
+ header_data += "\x00" * padding
459
+
460
+ # Calculate total frame size (excludes the 4-byte length field itself)
461
+ # Frame = magic(2) + flags(2) + seqid(4) + header_len(2) + header_data + payload
462
+ frame_size = 2 + 2 + 4 + 2 + header_data.bytesize + payload.bytesize
463
+
464
+ # Write complete frame
465
+ frame = Bytes.empty_byte_buffer
466
+ frame << [frame_size].pack('N') # Length
467
+ frame << [HEADER_MAGIC].pack('n') # Magic
468
+ frame << [@flags].pack('n') # Flags
469
+ frame << [unsigned_int32(@sequence_id)].pack('N') # Sequence ID
470
+ frame << [header_data.bytesize / 4].pack('n') # Header length (in 32-bit words)
471
+ frame << header_data # Header data
472
+ frame << payload # Payload
473
+
474
+ @transport.write(frame)
475
+ @transport.flush
476
+ end
477
+
478
+ # Flushes data in simple framed format (for legacy compatibility)
479
+ def flush_framed(payload)
480
+ frame = [payload.bytesize].pack('N') + payload
481
+ @transport.write(frame)
482
+ @transport.flush
483
+ end
484
+
485
+ # Reads a varint32 from the given IO
486
+ def read_varint32(io, boundary_pos = nil)
487
+ shift = 0
488
+ result = 0
489
+ loop do
490
+ if boundary_pos && io.pos >= boundary_pos
491
+ raise TransportException.new(TransportException::UNKNOWN, "Trying to read past header boundary")
492
+ end
493
+ byte = io.getbyte
494
+ raise TransportException.new(TransportException::END_OF_FILE, "Unexpected EOF reading varint") if byte.nil?
495
+ result |= (byte & 0x7f) << shift
496
+ break if (byte & 0x80) == 0
497
+ shift += 7
498
+ end
499
+ result
500
+ end
501
+
502
+ # Writes a varint32 to the given IO
503
+ def write_varint32(io, n)
504
+ loop do
505
+ if (n & ~0x7F) == 0
506
+ io.write([n].pack('C'))
507
+ break
508
+ else
509
+ io.write([(n & 0x7F) | 0x80].pack('C'))
510
+ n >>= 7
511
+ end
512
+ end
513
+ end
514
+
515
+ # Reads a varint-prefixed string from the given IO
516
+ def read_varstring(io, boundary_pos = nil)
517
+ size = read_varint32(io, boundary_pos)
518
+ if size < 0
519
+ raise TransportException.new(TransportException::UNKNOWN, "Negative string size: #{size}")
520
+ end
521
+ if boundary_pos && size > (boundary_pos - io.pos)
522
+ raise TransportException.new(TransportException::UNKNOWN, "Info header length exceeds header size")
523
+ end
524
+ data = io.read(size)
525
+ if data.nil? || data.bytesize < size
526
+ raise TransportException.new(TransportException::END_OF_FILE, "Unexpected EOF reading string")
527
+ end
528
+ data
529
+ end
530
+
531
+ # Writes a varint-prefixed string to the given IO
532
+ def write_varstring(io, value)
533
+ value = Bytes.force_binary_encoding(value)
534
+ write_varint32(io, value.bytesize)
535
+ io.write(value)
536
+ end
537
+
538
+ def signed_int32(value)
539
+ value > 0x7fffffff ? value - (2**32) : value
540
+ end
541
+
542
+ def unsigned_int32(value)
543
+ value < 0 ? value + (2**32) : value
544
+ end
545
+ end
546
+
547
+ # Factory for creating HeaderTransport instances
548
+ class HeaderTransportFactory < BaseTransportFactory
549
+ def initialize(allowed_client_types = nil, default_protocol = HeaderSubprotocolID::BINARY)
550
+ @allowed_client_types = allowed_client_types
551
+ @default_protocol = default_protocol
552
+ end
553
+
554
+ def get_transport(transport)
555
+ HeaderTransport.new(transport, @allowed_client_types, @default_protocol)
556
+ end
557
+
558
+ def to_s
559
+ "header"
560
+ end
561
+ end
562
+ end
@@ -1,4 +1,5 @@
1
1
  # encoding: ascii-8bit
2
+ # frozen_string_literal: true
2
3
  #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
@@ -55,7 +56,7 @@ module Thrift
55
56
  ensure
56
57
  @outbuf = Bytes.empty_byte_buffer
57
58
  end
58
-
59
+
59
60
  def to_s
60
61
  "@{self.url}"
61
62
  end
@@ -1,5 +1,6 @@
1
1
  # encoding: ascii-8bit
2
- #
2
+ # frozen_string_literal: true
3
+ #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
5
6
  # distributed with this work for additional information
@@ -7,9 +8,9 @@
7
8
  # to you under the Apache License, Version 2.0 (the
8
9
  # "License"); you may not use this file except in compliance
9
10
  # with the License. You may obtain a copy of the License at
10
- #
11
+ #
11
12
  # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
+ #
13
14
  # Unless required by applicable law or agreed to in writing,
14
15
  # software distributed under the License is distributed on an
15
16
  # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -1,5 +1,6 @@
1
1
  # encoding: ascii-8bit
2
- #
2
+ # frozen_string_literal: true
3
+ #
3
4
  # Licensed to the Apache Software Foundation (ASF) under one
4
5
  # or more contributor license agreements. See the NOTICE file
5
6
  # distributed with this work for additional information
@@ -7,9 +8,9 @@
7
8
  # to you under the Apache License, Version 2.0 (the
8
9
  # "License"); you may not use this file except in compliance
9
10
  # with the License. You may obtain a copy of the License at
10
- #
11
+ #
11
12
  # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
+ #
13
14
  # Unless required by applicable law or agreed to in writing,
14
15
  # software distributed under the License is distributed on an
15
16
  # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -70,6 +71,12 @@ module Thrift
70
71
  data
71
72
  end
72
73
 
74
+ def read_all(size)
75
+ raise TransportException.new(TransportException::NEGATIVE_SIZE, 'Negative size') unless size >= 0
76
+
77
+ read(size)
78
+ end
79
+
73
80
  def read_byte
74
81
  raise EOFError.new("Not enough bytes remain in buffer") if @index >= @buf.size
75
82
  val = Bytes.get_string_byte(@buf, @index)
@@ -112,16 +119,16 @@ module Thrift
112
119
  # if idx != 0
113
120
  # out << " "
114
121
  # end
115
-
122
+
116
123
  if idx == @index
117
124
  out << ">"
118
125
  end
119
-
126
+
120
127
  out << @buf[idx].ord.to_s(16)
121
128
  end
122
129
  out.join(" ")
123
130
  end
124
-
131
+
125
132
  def to_s
126
133
  "memory"
127
134
  end