ruby-object-framework 1.0.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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/API.md +78 -0
  3. data/README.md +250 -0
  4. data/THIRD_PARTY_NOTICES.md +45 -0
  5. data/examples/guides/01_plain_use_case.rb +13 -0
  6. data/examples/guides/02_composed_application.rb +25 -0
  7. data/examples/guides/03_order_domain.rb +14 -0
  8. data/examples/guides/04_scoped_resources.rb +38 -0
  9. data/examples/guides/05_mapping.rb +39 -0
  10. data/examples/guides/06_associations.rb +49 -0
  11. data/examples/guides/07_http.rb +17 -0
  12. data/examples/guides/08_errors.rb +20 -0
  13. data/examples/guides/09_sqlite.rb +38 -0
  14. data/examples/guides/10_grpc.rb +26 -0
  15. data/examples/guides/11_ractor.rb +15 -0
  16. data/examples/guides/12_box.rb +11 -0
  17. data/examples/guides/13_testing.rb +24 -0
  18. data/examples/guides/manifest.yml +62 -0
  19. data/examples/orders/application.rb +51 -0
  20. data/examples/orders/contracts.rb +19 -0
  21. data/examples/orders/domain.rb +35 -0
  22. data/examples/orders/export_proto.rb +5 -0
  23. data/examples/orders/grpc_client.py +24 -0
  24. data/examples/orders/grpc_server.rb +23 -0
  25. data/examples/orders/server.rb +18 -0
  26. data/lib/object_framework/application.rb +136 -0
  27. data/lib/object_framework/capabilities.rb +59 -0
  28. data/lib/object_framework/errors.rb +85 -0
  29. data/lib/object_framework/execution.rb +184 -0
  30. data/lib/object_framework/grpc/messages.rb +30 -0
  31. data/lib/object_framework/grpc/server.rb +264 -0
  32. data/lib/object_framework/grpc/service.rb +55 -0
  33. data/lib/object_framework/http/io.rb +127 -0
  34. data/lib/object_framework/http/presentation.rb +55 -0
  35. data/lib/object_framework/http/protocol.rb +209 -0
  36. data/lib/object_framework/http/router.rb +80 -0
  37. data/lib/object_framework/http/server.rb +279 -0
  38. data/lib/object_framework/http2/connection.rb +399 -0
  39. data/lib/object_framework/http2/frame.rb +21 -0
  40. data/lib/object_framework/http2/hpack.rb +206 -0
  41. data/lib/object_framework/http2/hpack_tables.rb +333 -0
  42. data/lib/object_framework/http2/inbound.rb +75 -0
  43. data/lib/object_framework/memory/storage.rb +97 -0
  44. data/lib/object_framework/persistence/mapping.rb +150 -0
  45. data/lib/object_framework/persistence/query.rb +97 -0
  46. data/lib/object_framework/persistence/session.rb +173 -0
  47. data/lib/object_framework/persistence/transaction.rb +54 -0
  48. data/lib/object_framework/protobuf.rb +268 -0
  49. data/lib/object_framework/sqlite/compiler.rb +77 -0
  50. data/lib/object_framework/sqlite/schema.rb +95 -0
  51. data/lib/object_framework/sqlite/storage.rb +220 -0
  52. data/lib/object_framework/sqlite.rb +6 -0
  53. data/lib/object_framework/version.rb +5 -0
  54. data/lib/object_framework.rb +31 -0
  55. metadata +105 -0
@@ -0,0 +1,399 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module HTTP2
5
+ Stream = Struct.new(:id, :context, :input, :send_window, :receive_window,
6
+ :remote_closed, :local_closed, :reset, :received, :content_length,
7
+ keyword_init: true)
8
+
9
+ class Connection
10
+ PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".b.freeze
11
+ MAX_WINDOW = 2**31 - 1
12
+ attr_reader :io
13
+
14
+ def initialize(io:, on_stream:, reporter:, header_limit: 32 * 1024,
15
+ stream_limit: 128, idle_timeout: 30, write_timeout: 30,
16
+ tls: false)
17
+ @io, @on_stream, @reporter = io, on_stream, reporter
18
+ @header_limit, @stream_limit, @idle_timeout, @write_timeout, @tls = header_limit, stream_limit, idle_timeout, write_timeout, tls
19
+ @reader = HTTP::Reader.new(io:)
20
+ @decoder, @encoder = HPACK::Decoder.new(header_limit:), HPACK::Encoder.new
21
+ @mutex, @write_mutex, @changed = Mutex.new, Mutex.new, ConditionVariable.new
22
+ @streams, @last_stream, @closed, @draining = {}, 0, false, false
23
+ @peer_initial_window, @send_window, @receive_window = 65_535, 65_535, 65_535
24
+ @peer_frame_size, @continuation, @first_frame = 16_384, nil, true
25
+ @context = ExecutionContext.new
26
+ end
27
+
28
+ def run
29
+ handshake if @tls
30
+ raise ProtocolError, "Invalid HTTP/2 connection preface" unless @reader.read_exact(PREFACE.bytesize, deadline: deadline) == PREFACE
31
+ send_frame(4, 0, 0, [3, @stream_limit, 6, @header_limit].pack("nNnN"))
32
+ loop do
33
+ frame = Frame.read(@reader, deadline:)
34
+ if @first_frame
35
+ raise ProtocolError unless frame.type == 4 && frame.stream_id.zero? && (frame.flags & 1).zero?
36
+ @first_frame = false
37
+ end
38
+ raise ProtocolError if @continuation && (frame.type != 9 || frame.stream_id != @continuation[:id])
39
+ begin
40
+ process(frame)
41
+ rescue StreamError => error
42
+ reject_stream(error)
43
+ end
44
+ end
45
+ rescue EOFError, IOError, SystemCallError
46
+ nil
47
+ rescue StandardError => error
48
+ @reporter.report(error, context: @context)
49
+ begin
50
+ code = error.is_a?(ProtocolError) ? error.code : 2
51
+ send_frame(7, 0, 0, [@last_stream, code].pack("NN"))
52
+ rescue StandardError
53
+ nil
54
+ end
55
+ ensure
56
+ close
57
+ end
58
+
59
+ def close
60
+ streams = @mutex.synchronize do
61
+ @closed = true
62
+ @changed.broadcast
63
+ @streams.values.dup
64
+ end
65
+ streams.each { |stream| stream.input.cancel }
66
+ io.close unless io.closed?
67
+ rescue IOError, SystemCallError
68
+ nil
69
+ end
70
+
71
+ def send_headers(stream, headers, end_stream: false)
72
+ stream.context.cancellation.check! unless end_stream
73
+ block = @encoder.encode(headers)
74
+ raise CapacityExceeded, "Outgoing headers exceed configured limit" if headers.sum { |name, value| name.bytesize + value.bytesize + 32 } > @header_limit
75
+ @write_mutex.synchronize do
76
+ offset, first = 0, true
77
+ begin
78
+ chunk = block.byteslice(offset, @peer_frame_size) || "".b
79
+ offset += chunk.bytesize
80
+ flags = offset == block.bytesize ? 4 : 0
81
+ flags |= 1 if first && end_stream
82
+ write_frame(first ? 1 : 9, flags, stream.id, chunk)
83
+ first = false
84
+ end while offset < block.bytesize
85
+ end
86
+ finish_local(stream) if end_stream
87
+ end
88
+
89
+ def send_data(stream, bytes)
90
+ offset = 0
91
+ while offset < bytes.bytesize
92
+ count = @mutex.synchronize do
93
+ loop do
94
+ stream.context.cancellation.check!
95
+ raise Cancelled, "HTTP/2 stream is closed" if @closed || stream.reset || stream.local_closed
96
+ available = [@send_window, stream.send_window, @peer_frame_size, bytes.bytesize - offset].min
97
+ if available.positive?
98
+ @send_window -= available
99
+ stream.send_window -= available
100
+ break available
101
+ end
102
+ remaining = stream.context.cancellation.deadline ? stream.context.cancellation.deadline - HTTP::Timing.now : 0.1
103
+ @changed.wait(@mutex, [[remaining, 0].max, 0.1].min)
104
+ end
105
+ end
106
+ send_frame(0, 0, stream.id, bytes.byteslice(offset, count))
107
+ offset += count
108
+ end
109
+ end
110
+
111
+ def reset?(stream)
112
+ @mutex.synchronize { @closed || stream.reset }
113
+ end
114
+
115
+ private
116
+
117
+ def deadline; HTTP::Timing.now + @idle_timeout; end
118
+
119
+ def handshake
120
+ limit = deadline
121
+ loop do
122
+ result = io.accept_nonblock(exception: false)
123
+ break unless %i[wait_readable wait_writable].include?(result)
124
+ HTTP::Timing.wait(io, result == :wait_readable ? :read : :write, limit)
125
+ end
126
+ raise ProtocolError, "TLS must negotiate h2" unless io.alpn_protocol == "h2"
127
+ end
128
+
129
+ def process(frame)
130
+ case frame.type
131
+ when 0 then receive_data(frame)
132
+ when 1 then receive_headers(frame)
133
+ when 2
134
+ require_stream(frame)
135
+ require_size(frame, 5)
136
+ raise ProtocolError if frame.payload.unpack1("N") & MAX_WINDOW == frame.stream_id
137
+ when 3 then receive_reset(frame)
138
+ when 4 then receive_settings(frame)
139
+ when 5 then raise ProtocolError, "Clients cannot push streams"
140
+ when 6
141
+ require_connection(frame)
142
+ require_size(frame, 8)
143
+ send_frame(6, 1, 0, frame.payload) if (frame.flags & 1).zero?
144
+ when 7
145
+ require_connection(frame)
146
+ raise ProtocolError.new(code: 6) if frame.payload.bytesize < 8
147
+ @draining = true
148
+ when 8 then receive_window(frame)
149
+ when 9 then receive_continuation(frame)
150
+ end
151
+ end
152
+
153
+ def receive_settings(frame)
154
+ require_connection(frame)
155
+ if (frame.flags & 1).positive?
156
+ require_size(frame, 0)
157
+ return
158
+ end
159
+ raise ProtocolError.new(code: 6) unless (frame.payload.bytesize % 6).zero?
160
+ frame.payload.unpack("nN" * (frame.payload.bytesize / 6)).each_slice(2) do |id, value|
161
+ case id
162
+ when 2 then raise ProtocolError unless value <= 1
163
+ when 4
164
+ raise ProtocolError.new(code: 3) if value > MAX_WINDOW
165
+ @mutex.synchronize do
166
+ difference = value - @peer_initial_window
167
+ @streams.each_value do |stream|
168
+ raise ProtocolError.new(code: 3) if stream.send_window + difference > MAX_WINDOW
169
+ stream.send_window += difference
170
+ end
171
+ @peer_initial_window = value
172
+ @changed.broadcast
173
+ end
174
+ when 5
175
+ raise ProtocolError unless value.between?(16_384, 16_777_215)
176
+ @peer_frame_size = value
177
+ end
178
+ end
179
+ send_frame(4, 1, 0, "".b)
180
+ end
181
+
182
+ def receive_headers(frame)
183
+ require_stream(frame)
184
+ stream = @mutex.synchronize { @streams[frame.stream_id] }
185
+ fresh = !stream
186
+ if fresh
187
+ raise ProtocolError unless frame.stream_id.odd? && frame.stream_id > @last_stream && !@draining
188
+ @last_stream = frame.stream_id
189
+ else
190
+ raise StreamError.new(frame.stream_id, code: 5) if stream.remote_closed
191
+ raise StreamError.new(frame.stream_id) if (frame.flags & 1).zero?
192
+ end
193
+ payload, = unpad(frame)
194
+ if (frame.flags & 32).positive?
195
+ raise ProtocolError.new(code: 6) if payload.bytesize < 5
196
+ raise ProtocolError if payload.unpack1("N") & MAX_WINDOW == frame.stream_id
197
+ payload = payload.byteslice(5..)
198
+ end
199
+ @continuation = { id: frame.stream_id, bytes: payload, end_stream: (frame.flags & 1).positive?, fresh: }
200
+ complete_headers if (frame.flags & 4).positive?
201
+ end
202
+
203
+ def receive_continuation(frame)
204
+ raise ProtocolError unless @continuation && @continuation[:id] == frame.stream_id
205
+ @continuation[:bytes] += frame.payload
206
+ raise CompressionError if @continuation[:bytes].bytesize > @header_limit
207
+ complete_headers if (frame.flags & 4).positive?
208
+ end
209
+
210
+ def complete_headers
211
+ block, @continuation = @continuation, nil
212
+ headers = @decoder.decode(block[:bytes])
213
+ begin
214
+ validate_headers(headers, request: block[:fresh])
215
+ rescue ProtocolError => error
216
+ raise StreamError.new(block[:id], error.message, code: error.code)
217
+ end
218
+ if block[:fresh]
219
+ context = ExecutionContext.new
220
+ stream = Stream.new(id: block[:id], context:, send_window: @peer_initial_window,
221
+ receive_window: 65_535, remote_closed: block[:end_stream], local_closed: false,
222
+ reset: false, received: 0, content_length: nil)
223
+ stream.input = Inbound.new(cancellation: context.cancellation,
224
+ on_consume: ->(count) { release_capacity(stream, count) })
225
+ lengths = headers.select { |name, _| name == "content-length" }.map(&:last)
226
+ unless lengths.empty?
227
+ raise ProtocolError unless lengths.size == 1 && /\A[0-9]+\z/.match?(lengths.first)
228
+ stream.content_length = Integer(lengths.first, 10)
229
+ end
230
+ @mutex.synchronize do
231
+ raise StreamError.new(stream.id, "Peer exceeded concurrent stream limit", code: 7) if @streams.size >= @stream_limit
232
+ @streams[stream.id] = stream
233
+ end
234
+ finish_remote(stream) if block[:end_stream]
235
+ @on_stream.call(self, stream, headers)
236
+ else
237
+ stream = @mutex.synchronize { @streams.fetch(block[:id]) }
238
+ finish_remote(stream)
239
+ end
240
+ end
241
+
242
+ def validate_headers(headers, request:)
243
+ regular, pseudo = false, {}
244
+ headers.each do |name, value|
245
+ raise ProtocolError unless /\A:?[!#$%&'*+.^_`|~0-9a-z-]+\z/.match?(name)
246
+ raise ProtocolError if value.match?(/[\x00\r\n]/) || value.start_with?(" ", "\t") || value.end_with?(" ", "\t")
247
+ if name.start_with?(":")
248
+ raise ProtocolError if regular || !request || pseudo.key?(name) || !%w[:method :path :scheme :authority].include?(name)
249
+ pseudo[name] = value
250
+ else
251
+ regular = true
252
+ raise ProtocolError if %w[connection keep-alive proxy-connection transfer-encoding upgrade].include?(name)
253
+ raise ProtocolError if name == "te" && value != "trailers"
254
+ end
255
+ end
256
+ if request
257
+ raise ProtocolError unless pseudo[":method"] && %w[http https].include?(pseudo[":scheme"]) && pseudo[":path"]&.start_with?("/")
258
+ end
259
+ end
260
+
261
+ def receive_data(frame)
262
+ require_stream(frame)
263
+ payload, overhead = unpad(frame)
264
+ stream = @mutex.synchronize do
265
+ @receive_window -= frame.payload.bytesize
266
+ raise ProtocolError.new(code: 3) if @receive_window.negative?
267
+ @streams[frame.stream_id]
268
+ end
269
+ unless stream
270
+ raise ProtocolError if frame.stream_id > @last_stream
271
+ release_capacity(nil, frame.payload.bytesize)
272
+ send_frame(3, 0, frame.stream_id, [5].pack("N"))
273
+ return
274
+ end
275
+ if stream.remote_closed
276
+ release_capacity(nil, frame.payload.bytesize)
277
+ raise StreamError.new(stream.id, "DATA after end of stream", code: 5)
278
+ end
279
+ exceeded = false
280
+ @mutex.synchronize do
281
+ stream.receive_window -= frame.payload.bytesize
282
+ exceeded = stream.receive_window.negative?
283
+ stream.received += payload.bytesize
284
+ end
285
+ if exceeded
286
+ release_capacity(nil, frame.payload.bytesize)
287
+ raise StreamError.new(stream.id, code: 3)
288
+ end
289
+ release_capacity(stream, overhead) if overhead.positive?
290
+ stream.input.push(payload)
291
+ finish_remote(stream) if (frame.flags & 1).positive?
292
+ end
293
+
294
+ def receive_reset(frame)
295
+ require_stream(frame)
296
+ require_size(frame, 4)
297
+ stream = @mutex.synchronize do
298
+ raise ProtocolError if frame.stream_id > @last_stream
299
+ found = @streams.delete(frame.stream_id)
300
+ found.reset = true if found
301
+ @changed.broadcast
302
+ found
303
+ end
304
+ stream&.input&.cancel
305
+ stream&.input&.discard
306
+ end
307
+
308
+ def receive_window(frame)
309
+ require_size(frame, 4)
310
+ increment = frame.payload.unpack1("N") & MAX_WINDOW
311
+ if increment.zero?
312
+ raise ProtocolError if frame.stream_id.zero?
313
+ raise StreamError.new(frame.stream_id)
314
+ end
315
+ @mutex.synchronize do
316
+ if frame.stream_id.zero?
317
+ raise ProtocolError.new(code: 3) if @send_window + increment > MAX_WINDOW
318
+ @send_window += increment
319
+ elsif (stream = @streams[frame.stream_id])
320
+ raise StreamError.new(stream.id, code: 3) if stream.send_window + increment > MAX_WINDOW
321
+ stream.send_window += increment
322
+ elsif frame.stream_id > @last_stream
323
+ raise ProtocolError
324
+ end
325
+ @changed.broadcast
326
+ end
327
+ end
328
+
329
+ def finish_remote(stream)
330
+ if stream.content_length && stream.received != stream.content_length
331
+ raise StreamError.new(stream.id, "Content length does not match DATA")
332
+ end
333
+ @mutex.synchronize do
334
+ stream.remote_closed = true
335
+ @streams.delete(stream.id) if stream.local_closed
336
+ end
337
+ stream.input.finish
338
+ end
339
+
340
+ def finish_local(stream)
341
+ @mutex.synchronize do
342
+ stream.local_closed = true
343
+ @streams.delete(stream.id) if stream.remote_closed
344
+ @changed.broadcast
345
+ end
346
+ stream.input.discard
347
+ end
348
+
349
+ def release_capacity(stream, count)
350
+ return if count.zero?
351
+ active_stream = @mutex.synchronize do
352
+ return if @closed
353
+ @receive_window += count
354
+ if stream && @streams.key?(stream.id) && !stream.reset
355
+ stream.receive_window += count
356
+ true
357
+ end
358
+ end
359
+ send_frame(8, 0, 0, [count].pack("N"))
360
+ send_frame(8, 0, stream.id, [count].pack("N")) if active_stream
361
+ rescue IOError, SystemCallError
362
+ nil
363
+ end
364
+
365
+ def reject_stream(error)
366
+ stream = @mutex.synchronize do
367
+ found = @streams.delete(error.stream_id)
368
+ found.reset = true if found
369
+ @changed.broadcast
370
+ found
371
+ end
372
+ @reporter.report(error, context: stream&.context || ExecutionContext.new)
373
+ stream&.input&.cancel
374
+ stream&.input&.discard
375
+ send_frame(3, 0, error.stream_id, [error.code].pack("N"))
376
+ end
377
+
378
+ def unpad(frame)
379
+ return [frame.payload, 0] if (frame.flags & 8).zero?
380
+ padding = frame.payload.getbyte(0)
381
+ raise ProtocolError unless padding && padding < frame.payload.bytesize
382
+ [frame.payload.byteslice(1, frame.payload.bytesize - padding - 1), padding + 1]
383
+ end
384
+
385
+ def require_connection(frame); raise ProtocolError unless frame.stream_id.zero?; end
386
+ def require_stream(frame); raise ProtocolError if frame.stream_id.zero?; end
387
+ def require_size(frame, size); raise ProtocolError.new(code: 6) unless frame.payload.bytesize == size; end
388
+
389
+ def send_frame(type, flags, stream_id, payload)
390
+ @write_mutex.synchronize { write_frame(type, flags, stream_id, payload) }
391
+ end
392
+
393
+ def write_frame(type, flags, stream_id, payload)
394
+ frame = Frame.new(type:, flags:, stream_id:, payload:)
395
+ @reader.write(frame.encode, deadline: HTTP::Timing.now + @write_timeout)
396
+ end
397
+ end
398
+ end
399
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ObjectFramework
4
+ module HTTP2
5
+ Frame = Data.define(:type, :flags, :stream_id, :payload) do
6
+ def encode
7
+ raise ProtocolError, "Frame is too large" if payload.bytesize > 16_777_215
8
+ length = [payload.bytesize].pack("N").byteslice(1, 3)
9
+ length + [type, flags, stream_id & 0x7fff_ffff].pack("CCN") + payload.b
10
+ end
11
+
12
+ def self.read(reader, deadline:, max_size: 16_384)
13
+ header = reader.read_exact(9, deadline:)
14
+ length = ("\x00".b + header.byteslice(0, 3)).unpack1("N")
15
+ raise ProtocolError.new("Frame exceeds advertised size", code: 6) if length > max_size
16
+ type, flags, stream_id = header.byteslice(3, 6).unpack("CCN")
17
+ new(type:, flags:, stream_id: stream_id & 0x7fff_ffff, payload: reader.read_exact(length, deadline:))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hpack_tables"
4
+
5
+ module ObjectFramework
6
+ module HTTP2
7
+ class ProtocolError < StandardError
8
+ attr_reader :code
9
+ def initialize(message = "HTTP/2 protocol error", code: 1)
10
+ super(message)
11
+ @code = code
12
+ end
13
+ end
14
+ class CompressionError < ProtocolError
15
+ def initialize(message = "Invalid HPACK block"); super(message, code: 9); end
16
+ end
17
+ class StreamError < ProtocolError
18
+ attr_reader :stream_id
19
+ def initialize(stream_id, message = "Invalid HTTP/2 stream", code: 1)
20
+ super(message, code:)
21
+ @stream_id = stream_id
22
+ end
23
+ end
24
+
25
+ module HPACK
26
+ module Huffman
27
+ TREE = begin
28
+ root = {}
29
+ HUFFMAN_CODES.each_with_index do |(code, length), symbol|
30
+ node = root
31
+ (length - 1).downto(0) { |bit| node = (node[(code >> bit) & 1] ||= {}) }
32
+ node[:symbol] = symbol
33
+ end
34
+ freeze_tree = ->(node) { node.each_value { |value| freeze_tree.call(value) if value.is_a?(Hash) }; node.freeze }
35
+ freeze_tree.call(root)
36
+ end
37
+
38
+ def self.encode(bytes)
39
+ output, bits, length = +"".b, 0, 0
40
+ bytes.each_byte do |byte|
41
+ code, size = HUFFMAN_CODES.fetch(byte)
42
+ bits, length = (bits << size) | code, length + size
43
+ while length >= 8
44
+ length -= 8
45
+ output << ((bits >> length) & 255)
46
+ end
47
+ bits &= (1 << length) - 1
48
+ end
49
+ output << ((bits << (8 - length)) | ((1 << (8 - length)) - 1)) if length.positive?
50
+ output
51
+ end
52
+
53
+ def self.decode(bytes, limit: 32 * 1024)
54
+ output, node, tail, tail_size = +"".b, TREE, 0, 0
55
+ bytes.each_byte do |byte|
56
+ 7.downto(0) do |index|
57
+ bit = (byte >> index) & 1
58
+ node = node[bit] || raise(CompressionError)
59
+ tail, tail_size = (tail << 1) | bit, tail_size + 1
60
+ if (symbol = node[:symbol])
61
+ raise CompressionError if symbol == 256 || output.bytesize >= limit
62
+ output << symbol
63
+ node, tail, tail_size = TREE, 0, 0
64
+ end
65
+ end
66
+ end
67
+ raise CompressionError if tail_size > 7 || tail != (1 << tail_size) - 1
68
+ output
69
+ end
70
+ end
71
+
72
+ module IntegerCoding
73
+ def self.encode(number, prefix:, mask: 0)
74
+ maximum = (1 << prefix) - 1
75
+ return [mask | number].pack("C") if number < maximum
76
+ bytes, number = [mask | maximum].pack("C"), number - maximum
77
+ while number >= 128
78
+ bytes << ((number & 127) | 128)
79
+ number >>= 7
80
+ end
81
+ bytes << number
82
+ end
83
+ end
84
+
85
+ class Cursor
86
+ def initialize(bytes); @bytes, @offset = bytes.b, 0; end
87
+ def done?; @offset == @bytes.bytesize; end
88
+ def peek; @bytes.getbyte(@offset) || raise(CompressionError); end
89
+
90
+ def take(count)
91
+ raise CompressionError if count.negative? || @offset + count > @bytes.bytesize
92
+ result = @bytes.byteslice(@offset, count)
93
+ @offset += count
94
+ result
95
+ end
96
+
97
+ def integer(prefix)
98
+ maximum = (1 << prefix) - 1
99
+ value = take(1).getbyte(0) & maximum
100
+ return value if value < maximum
101
+ 9.times do |index|
102
+ byte = take(1).getbyte(0)
103
+ value += (byte & 127) << (index * 7)
104
+ raise CompressionError if value > 2**63 - 1
105
+ return value if byte < 128
106
+ end
107
+ raise CompressionError
108
+ end
109
+
110
+ def string(limit:)
111
+ huffman = (peek & 128).positive?
112
+ length = integer(7)
113
+ value = take(length)
114
+ value = Huffman.decode(value, limit:) if huffman
115
+ raise CompressionError if value.bytesize > limit
116
+ value
117
+ end
118
+ end
119
+
120
+ class Decoder
121
+ def initialize(table_limit: 4096, header_limit: 32 * 1024)
122
+ @maximum, @capacity, @header_limit = table_limit, table_limit, header_limit
123
+ @dynamic, @size = [], 0
124
+ end
125
+
126
+ def decode(bytes)
127
+ cursor, headers, total, saw_header = Cursor.new(bytes), [], 0, false
128
+ until cursor.done?
129
+ first = cursor.peek
130
+ if (first & 128).positive?
131
+ pair = lookup(cursor.integer(7))
132
+ elsif (first & 224) == 32
133
+ raise CompressionError if saw_header
134
+ capacity = cursor.integer(5)
135
+ raise CompressionError if capacity > @maximum
136
+ @capacity = capacity
137
+ evict
138
+ next
139
+ else
140
+ indexed = (first & 64).positive?
141
+ index = cursor.integer(indexed ? 6 : 4)
142
+ name = index.zero? ? cursor.string(limit: @header_limit - total) : lookup(index).first
143
+ value = cursor.string(limit: @header_limit - total)
144
+ pair = [name.freeze, value.freeze].freeze
145
+ add(pair) if indexed
146
+ end
147
+ total += entry_size(pair)
148
+ raise CompressionError if total > @header_limit
149
+ headers << pair
150
+ saw_header = true
151
+ end
152
+ headers.freeze
153
+ end
154
+
155
+ private
156
+
157
+ def lookup(index)
158
+ raise CompressionError if index.zero?
159
+ (index <= STATIC_TABLE.size ? STATIC_TABLE[index - 1] : @dynamic[index - STATIC_TABLE.size - 1]) || raise(CompressionError)
160
+ end
161
+
162
+ def entry_size(pair); pair[0].bytesize + pair[1].bytesize + 32; end
163
+
164
+ def add(pair)
165
+ size = entry_size(pair)
166
+ if size > @capacity
167
+ @dynamic.clear
168
+ @size = 0
169
+ else
170
+ @dynamic.unshift(pair)
171
+ @size += size
172
+ evict
173
+ end
174
+ end
175
+
176
+ def evict
177
+ @size -= entry_size(@dynamic.pop) while @size > @capacity
178
+ end
179
+ end
180
+
181
+ class Encoder
182
+ def encode(headers)
183
+ headers.map do |name, value|
184
+ name, value = name.to_s.b, value.to_s.b
185
+ exact = STATIC_TABLE.index([name, value])
186
+ if exact
187
+ IntegerCoding.encode(exact + 1, prefix: 7, mask: 128)
188
+ else
189
+ index = STATIC_TABLE.index { |pair| pair.first == name }
190
+ # Never index application metadata, including credentials and operation IDs.
191
+ prefix = IntegerCoding.encode(index ? index + 1 : 0, prefix: 4, mask: 16)
192
+ prefix += string(name) unless index
193
+ prefix + string(value)
194
+ end
195
+ end.join.b
196
+ end
197
+
198
+ private
199
+
200
+ def string(bytes)
201
+ IntegerCoding.encode(bytes.bytesize, prefix: 7) + bytes
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end