amq-protocol 2.3.4 → 2.5.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +23 -0
  3. data/CLAUDE.md +1 -0
  4. data/ChangeLog.md +30 -3
  5. data/GEMINI.md +1 -0
  6. data/Gemfile +5 -0
  7. data/README.md +2 -7
  8. data/benchmarks/frame_encoding.rb +75 -0
  9. data/benchmarks/method_encoding.rb +198 -0
  10. data/benchmarks/pack_unpack.rb +158 -0
  11. data/benchmarks/run_all.rb +64 -0
  12. data/benchmarks/table_encoding.rb +110 -0
  13. data/lib/amq/bit_set.rb +1 -0
  14. data/lib/amq/endianness.rb +2 -0
  15. data/lib/amq/int_allocator.rb +1 -0
  16. data/lib/amq/pack.rb +33 -42
  17. data/lib/amq/protocol/client.rb +30 -37
  18. data/lib/amq/protocol/constants.rb +2 -0
  19. data/lib/amq/protocol/exceptions.rb +3 -1
  20. data/lib/amq/protocol/float_32bit.rb +2 -0
  21. data/lib/amq/protocol/frame.rb +9 -3
  22. data/lib/amq/protocol/table.rb +20 -17
  23. data/lib/amq/protocol/table_value_decoder.rb +48 -52
  24. data/lib/amq/protocol/table_value_encoder.rb +1 -0
  25. data/lib/amq/protocol/type_constants.rb +1 -0
  26. data/lib/amq/protocol/version.rb +1 -1
  27. data/lib/amq/settings.rb +1 -0
  28. data/spec/amq/bit_set_spec.rb +22 -0
  29. data/spec/amq/endianness_spec.rb +23 -0
  30. data/spec/amq/int_allocator_spec.rb +26 -3
  31. data/spec/amq/pack_spec.rb +14 -24
  32. data/spec/amq/protocol/exceptions_spec.rb +70 -0
  33. data/spec/amq/protocol/float_32bit_spec.rb +27 -0
  34. data/spec/amq/protocol/frame_spec.rb +64 -0
  35. data/spec/amq/protocol/table_spec.rb +32 -0
  36. data/spec/amq/protocol/value_decoder_spec.rb +97 -0
  37. data/spec/amq/protocol/value_encoder_spec.rb +21 -0
  38. data/spec/amq/settings_spec.rb +37 -1
  39. data/spec/amq/uri_parsing_spec.rb +7 -0
  40. metadata +14 -3
@@ -1,6 +1,6 @@
1
1
  # encoding: binary
2
+ # frozen_string_literal: true
2
3
 
3
- require "amq/endianness"
4
4
  require "amq/protocol/type_constants"
5
5
  require "amq/protocol/float_32bit"
6
6
 
@@ -15,15 +15,23 @@ module AMQ
15
15
 
16
16
  include TypeConstants
17
17
 
18
+ # Pack format strings use explicit endianness (available as of Ruby 1.9.3)
19
+ PACK_UINT32_BE = 'N'.freeze
20
+ PACK_INT64_BE = 'q>'.freeze
21
+ PACK_INT16_BE = 's>'.freeze
22
+ PACK_UINT64_BE = 'Q>'.freeze
23
+ PACK_FLOAT32 = 'f'.freeze # single precision float (native endian, matches encoder)
24
+ PACK_FLOAT64 = 'G'.freeze # big-endian double precision float
25
+ PACK_UCHAR_UINT32 = 'CN'.freeze
18
26
 
19
27
  #
20
28
  # API
21
29
  #
22
30
 
23
31
  def self.decode_array(data, initial_offset)
24
- array_length = data.slice(initial_offset, 4).unpack(PACK_UINT32).first
32
+ array_length = data.byteslice(initial_offset, 4).unpack1(PACK_UINT32_BE)
25
33
 
26
- ary = Array.new
34
+ ary = []
27
35
  offset = initial_offset + 4
28
36
 
29
37
  while offset <= (initial_offset + array_length)
@@ -54,25 +62,25 @@ module AMQ
54
62
  when TYPE_BOOLEAN
55
63
  v, offset = decode_boolean(data, offset)
56
64
  v
57
- when TYPE_BYTE then
65
+ when TYPE_BYTE
58
66
  v, offset = decode_byte(data, offset)
59
67
  v
60
- when TYPE_SIGNED_16BIT then
68
+ when TYPE_SIGNED_16BIT
61
69
  v, offset = decode_short(data, offset)
62
70
  v
63
- when TYPE_SIGNED_64BIT then
71
+ when TYPE_SIGNED_64BIT
64
72
  v, offset = decode_long(data, offset)
65
73
  v
66
- when TYPE_32BIT_FLOAT then
74
+ when TYPE_32BIT_FLOAT
67
75
  v, offset = decode_32bit_float(data, offset)
68
76
  v
69
- when TYPE_64BIT_FLOAT then
77
+ when TYPE_64BIT_FLOAT
70
78
  v, offset = decode_64bit_float(data, offset)
71
79
  v
72
80
  when TYPE_VOID
73
81
  nil
74
82
  when TYPE_ARRAY
75
- v, offset = TableValueDecoder.decode_array(data, offset)
83
+ v, offset = decode_array(data, offset)
76
84
  v
77
85
  else
78
86
  raise ArgumentError.new("unsupported type in a table value: #{type.inspect}, do not know how to decode!")
@@ -83,113 +91,101 @@ module AMQ
83
91
 
84
92
 
85
93
  [ary, initial_offset + array_length + 4]
86
- end # self.decode_array(data, initial_offset)
94
+ end
87
95
 
88
96
 
89
97
  def self.decode_string(data, offset)
90
- length = data.slice(offset, 4).unpack(PACK_UINT32).first
98
+ length = data.byteslice(offset, 4).unpack1(PACK_UINT32_BE)
91
99
  offset += 4
92
- v = data.slice(offset, length)
100
+ v = data.byteslice(offset, length)
93
101
  offset += length
94
102
 
95
103
  [v, offset]
96
- end # self.decode_string(data, offset)
104
+ end
97
105
 
98
106
 
99
107
  def self.decode_integer(data, offset)
100
- v = data.slice(offset, 4).unpack(PACK_UINT32).first
108
+ v = data.byteslice(offset, 4).unpack1(PACK_UINT32_BE)
101
109
  offset += 4
102
110
 
103
111
  [v, offset]
104
- end # self.decode_integer(data, offset)
105
-
112
+ end
106
113
 
107
- if AMQ::Endianness.big_endian?
108
- def self.decode_long(data, offset)
109
- v = data.slice(offset, 8).unpack(PACK_INT64)
110
114
 
111
- offset += 8
112
- [v, offset]
113
- end
114
- else
115
- def self.decode_long(data, offset)
116
- slice = data.slice(offset, 8).bytes.to_a.reverse.map(&:chr).join
117
- v = slice.unpack(PACK_INT64).first
118
-
119
- offset += 8
120
- [v, offset]
121
- end
115
+ def self.decode_long(data, offset)
116
+ v = data.byteslice(offset, 8).unpack1(PACK_INT64_BE)
117
+ offset += 8
118
+ [v, offset]
122
119
  end
123
120
 
124
121
 
125
122
  def self.decode_big_decimal(data, offset)
126
- decimals, raw = data.slice(offset, 5).unpack(PACK_UCHAR_UINT32)
123
+ decimals, raw = data.byteslice(offset, 5).unpack(PACK_UCHAR_UINT32)
127
124
  offset += 5
128
125
  v = BigDecimal(raw.to_s) * (BigDecimal(TEN) ** -decimals)
129
126
 
130
127
  [v, offset]
131
- end # self.decode_big_decimal(data, offset)
128
+ end
132
129
 
133
130
 
134
131
  def self.decode_time(data, offset)
135
- timestamp = data.slice(offset, 8).unpack(PACK_UINT64_BE).last
132
+ timestamp = data.byteslice(offset, 8).unpack1(PACK_UINT64_BE)
136
133
  v = Time.at(timestamp)
137
134
  offset += 8
138
135
 
139
136
  [v, offset]
140
- end # self.decode_time(data, offset)
137
+ end
141
138
 
142
139
 
143
140
  def self.decode_boolean(data, offset)
144
- integer = data.slice(offset, 2).unpack(PACK_CHAR).first # 0 or 1
141
+ byte = data.getbyte(offset)
145
142
  offset += 1
146
- [(integer == 1), offset]
147
- end # self.decode_boolean(data, offset)
143
+ [(byte == 1), offset]
144
+ end
148
145
 
149
146
 
150
147
  def self.decode_32bit_float(data, offset)
151
- v = data.slice(offset, 4).unpack(PACK_32BIT_FLOAT).first
148
+ v = data.byteslice(offset, 4).unpack1(PACK_FLOAT32)
152
149
  offset += 4
153
150
 
154
151
  [v, offset]
155
- end # self.decode_32bit_float(data, offset)
152
+ end
156
153
 
157
154
 
158
155
  def self.decode_64bit_float(data, offset)
159
- v = data.slice(offset, 8).unpack(PACK_64BIT_FLOAT).first
156
+ v = data.byteslice(offset, 8).unpack1(PACK_FLOAT64)
160
157
  offset += 8
161
158
 
162
159
  [v, offset]
163
- end # self.decode_64bit_float(data, offset)
160
+ end
164
161
 
165
162
 
166
163
  def self.decode_value_type(data, offset)
167
- [data.slice(offset, 1), offset + 1]
168
- end # self.decode_value_type(data, offset)
169
-
164
+ [data.byteslice(offset, 1), offset + 1]
165
+ end
170
166
 
171
167
 
172
168
  def self.decode_hash(data, offset)
173
- length = data.slice(offset, 4).unpack(PACK_UINT32).first
174
- v = Table.decode(data.slice(offset, length + 4))
169
+ length = data.byteslice(offset, 4).unpack1(PACK_UINT32_BE)
170
+ v = Table.decode(data.byteslice(offset, length + 4))
175
171
  offset += 4 + length
176
172
 
177
173
  [v, offset]
178
- end # self.decode_hash(data, offset)
174
+ end
179
175
 
180
176
 
181
177
  # Decodes/Converts a byte value from the data at the provided offset.
182
178
  #
183
- # @param [Array] data - A big-endian ordered array of bytes.
184
- # @param [Fixnum] offset - The offset which bytes the byte is consumed.
185
- # @return [Array] - The Fixnum value and new offset pair.
179
+ # @param [String] data - Binary data string
180
+ # @param [Integer] offset - The offset from which to read the byte
181
+ # @return [Array] - The Integer value and new offset pair
186
182
  def self.decode_byte(data, offset)
187
- [data.slice(offset, 1).unpack(PACK_CHAR).first, offset += 1]
183
+ [data.getbyte(offset), offset + 1]
188
184
  end
189
185
 
190
186
 
191
187
  def self.decode_short(data, offset)
192
- v = AMQ::Hacks.unpack_int16_big_endian(data.slice(offset, 2)).first
188
+ v = data.byteslice(offset, 2).unpack1(PACK_INT16_BE)
193
189
  offset += 2
194
190
  [v, offset]
195
191
  end
@@ -1,4 +1,5 @@
1
1
  # encoding: binary
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "amq/protocol/type_constants"
4
5
  require "date"
@@ -1,4 +1,5 @@
1
1
  # encoding: binary
2
+ # frozen_string_literal: true
2
3
 
3
4
  module AMQ
4
5
  module Protocol
@@ -1,5 +1,5 @@
1
1
  module AMQ
2
2
  module Protocol
3
- VERSION = "2.3.4"
3
+ VERSION = "2.5.0"
4
4
  end # Protocol
5
5
  end # AMQ
data/lib/amq/settings.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "amq/protocol/client"
4
5
  require "amq/uri"
@@ -223,5 +223,27 @@ RSpec.describe AMQ::BitSet do
223
223
  subject.unset(254)
224
224
  expect(subject.get(254)).to be_falsey
225
225
  end # it
226
+
227
+ it "returns -1 when all bits are set" do
228
+ bs = described_class.new(64)
229
+ 0.upto(63) { |i| bs.set(i) }
230
+ expect(bs.next_clear_bit).to eq(-1)
231
+ end
232
+ end # describe
233
+
234
+ describe "#to_s" do
235
+ it "returns a string representation of the bit set" do
236
+ bs = described_class.new(64)
237
+ result = bs.to_s
238
+ expect(result).to be_a(String)
239
+ expect(result).to include(":")
240
+ end
241
+
242
+ it "shows set bits" do
243
+ bs = described_class.new(64)
244
+ bs.set(0)
245
+ result = bs.to_s
246
+ expect(result).to end_with("1:")
247
+ end
226
248
  end # describe
227
249
  end
@@ -0,0 +1,23 @@
1
+ # encoding: binary
2
+
3
+ require "amq/endianness"
4
+
5
+ RSpec.describe AMQ::Endianness do
6
+ describe ".big_endian?" do
7
+ it "returns a boolean" do
8
+ expect([true, false]).to include(described_class.big_endian?)
9
+ end
10
+ end
11
+
12
+ describe ".little_endian?" do
13
+ it "returns the opposite of big_endian?" do
14
+ expect(described_class.little_endian?).to eq(!described_class.big_endian?)
15
+ end
16
+ end
17
+
18
+ describe "BIG_ENDIAN constant" do
19
+ it "is a boolean" do
20
+ expect([true, false]).to include(AMQ::Endianness::BIG_ENDIAN)
21
+ end
22
+ end
23
+ end
@@ -11,13 +11,17 @@ RSpec.describe AMQ::IntAllocator do
11
11
  end
12
12
 
13
13
 
14
- # ...
15
-
16
-
17
14
  #
18
15
  # Examples
19
16
  #
20
17
 
18
+ describe "#initialize" do
19
+ it "raises ArgumentError when hi <= lo" do
20
+ expect { described_class.new(5, 5) }.to raise_error(ArgumentError)
21
+ expect { described_class.new(10, 5) }.to raise_error(ArgumentError)
22
+ end
23
+ end
24
+
21
25
  describe "#number_of_bits" do
22
26
  it "returns number of bits available for allocation" do
23
27
  expect(subject.number_of_bits).to eq(4)
@@ -110,4 +114,23 @@ RSpec.describe AMQ::IntAllocator do
110
114
  end
111
115
  end
112
116
  end
117
+
118
+ describe "#release" do
119
+ it "is an alias for #free" do
120
+ subject.allocate
121
+ expect(subject.allocated?(1)).to be_truthy
122
+ subject.release(1)
123
+ expect(subject.allocated?(1)).to be_falsey
124
+ end
125
+ end
126
+
127
+ describe "#reset" do
128
+ it "releases all allocations" do
129
+ 4.times { subject.allocate }
130
+ expect(subject.allocate).to eq(-1)
131
+
132
+ subject.reset
133
+ expect(subject.allocate).to eq(1)
134
+ end
135
+ end
113
136
  end
@@ -4,10 +4,18 @@ RSpec.describe AMQ::Pack do
4
4
  context "16-bit big-endian packing / unpacking" do
5
5
  let(:examples_16bit) {
6
6
  {
7
- 0x068D => "\x06\x8D" # 1677
7
+ 0x068D => "\x06\x8D", # 1677
8
+ 0x0000 => "\x00\x00", # 0
9
+ 0x7FFF => "\x7F\xFF" # 32767 (max positive signed 16-bit)
8
10
  }
9
11
  }
10
12
 
13
+ it "packs signed integers into a big-endian string" do
14
+ examples_16bit.each do |key, value|
15
+ expect(described_class.pack_int16_big_endian(key)).to eq(value)
16
+ end
17
+ end
18
+
11
19
  it "unpacks signed integers from a string to a number" do
12
20
  examples_16bit.each do |key, value|
13
21
  expect(described_class.unpack_int16_big_endian(value)[0]).to eq(key)
@@ -35,34 +43,16 @@ RSpec.describe AMQ::Pack do
35
43
  end
36
44
  end
37
45
 
38
- it "should unpack string representation into integer" do
46
+ it "unpacks string representation into integer" do
39
47
  examples.each do |key, value|
40
48
  expect(described_class.unpack_uint64_big_endian(value)[0]).to eq(key)
41
49
  end
42
50
  end
51
+ end
43
52
 
44
- if RUBY_VERSION < '1.9'
45
- describe "with utf encoding" do
46
- before do
47
- $KCODE = 'u'
48
- end
49
-
50
- after do
51
- $KCODE = 'NONE'
52
- end
53
-
54
- it "packs integers into big-endian string" do
55
- examples.each do |key, value|
56
- expect(described_class.pack_uint64_big_endian(key)).to eq(value)
57
- end
58
- end
59
-
60
- it "should unpack string representation into integer" do
61
- examples.each do |key, value|
62
- expect(described_class.unpack_uint64_big_endian(value)[0]).to eq(key)
63
- end
64
- end
65
- end
53
+ describe "AMQ::Hacks alias" do
54
+ it "is an alias for AMQ::Pack (backwards compatibility)" do
55
+ expect(AMQ::Hacks).to eq(AMQ::Pack)
66
56
  end
67
57
  end
68
58
  end
@@ -0,0 +1,70 @@
1
+ # encoding: binary
2
+
3
+ RSpec.describe AMQ::Protocol::Error do
4
+ describe ".[]" do
5
+ it "looks up exception class by error code" do
6
+ # This only works if subclasses define VALUE constant
7
+ # Default case: no subclass with VALUE defined returns nil
8
+ expect { described_class[999999] }.to raise_error(/No such exception class/)
9
+ end
10
+ end
11
+
12
+ describe ".subclasses_with_values" do
13
+ it "returns subclasses that define VALUE constant" do
14
+ result = described_class.subclasses_with_values
15
+ expect(result).to be_an(Array)
16
+ end
17
+ end
18
+
19
+ describe "#initialize" do
20
+ it "uses default message when none provided" do
21
+ error = described_class.new
22
+ expect(error.message).to eq("AMQP error")
23
+ end
24
+
25
+ it "uses custom message when provided" do
26
+ error = described_class.new("Custom error")
27
+ expect(error.message).to eq("Custom error")
28
+ end
29
+ end
30
+ end
31
+
32
+ RSpec.describe AMQ::Protocol::FrameTypeError do
33
+ it "formats message with valid types" do
34
+ error = described_class.new([:method, :headers])
35
+ expect(error.message).to include("[:method, :headers]")
36
+ end
37
+ end
38
+
39
+ RSpec.describe AMQ::Protocol::EmptyResponseError do
40
+ it "has a default message" do
41
+ error = described_class.new
42
+ expect(error.message).to eq("Empty response received from the server.")
43
+ end
44
+
45
+ it "accepts custom message" do
46
+ error = described_class.new("Custom empty response")
47
+ expect(error.message).to eq("Custom empty response")
48
+ end
49
+ end
50
+
51
+ RSpec.describe AMQ::Protocol::BadResponseError do
52
+ it "formats message with argument, expected, and actual values" do
53
+ error = described_class.new("channel", 1, 2)
54
+ expect(error.message).to include("channel")
55
+ expect(error.message).to include("1")
56
+ expect(error.message).to include("2")
57
+ end
58
+ end
59
+
60
+ RSpec.describe AMQ::Protocol::SoftError do
61
+ it "is a subclass of Protocol::Error" do
62
+ expect(described_class.superclass).to eq(AMQ::Protocol::Error)
63
+ end
64
+ end
65
+
66
+ RSpec.describe AMQ::Protocol::HardError do
67
+ it "is a subclass of Protocol::Error" do
68
+ expect(described_class.superclass).to eq(AMQ::Protocol::Error)
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ # encoding: binary
2
+
3
+ RSpec.describe AMQ::Protocol::Float32Bit do
4
+ describe "#initialize" do
5
+ it "stores the value" do
6
+ f = described_class.new(3.14)
7
+ expect(f.value).to eq(3.14)
8
+ end
9
+ end
10
+
11
+ describe "#value" do
12
+ it "returns the stored value" do
13
+ f = described_class.new(2.718)
14
+ expect(f.value).to eq(2.718)
15
+ end
16
+
17
+ it "works with zero" do
18
+ f = described_class.new(0.0)
19
+ expect(f.value).to eq(0.0)
20
+ end
21
+
22
+ it "works with negative values" do
23
+ f = described_class.new(-1.5)
24
+ expect(f.value).to eq(-1.5)
25
+ end
26
+ end
27
+ end
@@ -86,6 +86,70 @@ module AMQ
86
86
  expect(subject.properties[:delivery_mode]).to eq(2)
87
87
  expect(subject.properties[:priority]).to eq(0)
88
88
  end
89
+
90
+ it "is not final" do
91
+ expect(subject.final?).to eq(false)
92
+ end
93
+ end
94
+
95
+ describe BodyFrame do
96
+ subject { BodyFrame.new("test payload", 1) }
97
+
98
+ it "returns payload as decode_payload" do
99
+ expect(subject.decode_payload).to eq("test payload")
100
+ end
101
+
102
+ it "is not final" do
103
+ expect(subject.final?).to eq(false)
104
+ end
105
+
106
+ it "has correct size" do
107
+ expect(subject.size).to eq(12)
108
+ end
109
+ end
110
+
111
+ describe HeartbeatFrame do
112
+ it "encodes with empty payload on channel 0" do
113
+ encoded = HeartbeatFrame.encode
114
+ expect(encoded.bytes.last).to eq(0xCE)
115
+ end
116
+
117
+ it "is final" do
118
+ frame = HeartbeatFrame.new("", 0)
119
+ expect(frame.final?).to eq(true)
120
+ end
121
+ end
122
+
123
+ describe MethodFrame do
124
+ it "is not final when method has content" do
125
+ # Basic.Publish has content
126
+ payload = "\x00\x3C\x00\x28\x00\x00\x00\x00\x00"
127
+ frame = MethodFrame.new(payload, 1)
128
+ # This will depend on the method class
129
+ expect(frame).to respond_to(:final?)
130
+ end
131
+ end
132
+
133
+ describe FrameSubclass do
134
+ subject { BodyFrame.new("test", 1) }
135
+
136
+ it "has channel accessor" do
137
+ expect(subject.channel).to eq(1)
138
+ subject.channel = 2
139
+ expect(subject.channel).to eq(2)
140
+ end
141
+
142
+ it "encodes to array" do
143
+ result = subject.encode_to_array
144
+ expect(result).to be_an(Array)
145
+ expect(result.size).to eq(3)
146
+ end
147
+
148
+ it "encodes to string" do
149
+ result = subject.encode
150
+ expect(result).to be_a(String)
151
+ expect(result.bytes.last).to eq(0xCE)
152
+ end
89
153
  end
90
154
  end
91
155
  end
@@ -254,6 +254,38 @@ module AMQ
254
254
  end
255
255
 
256
256
  end # describe
257
+
258
+ describe ".length" do
259
+ it "returns the table length from binary data" do
260
+ encoded = Table.encode({"test" => 1})
261
+ expect(Table.length(encoded)).to eq(14)
262
+ end
263
+
264
+ it "returns 0 for empty table" do
265
+ encoded = Table.encode({})
266
+ expect(Table.length(encoded)).to eq(0)
267
+ end
268
+ end
269
+
270
+ describe ".hash_size" do
271
+ it "calculates size for simple hash" do
272
+ size = Table.hash_size({"key" => "value"})
273
+ expect(size).to be > 0
274
+ end
275
+
276
+ it "returns 0 for empty hash" do
277
+ size = Table.hash_size({})
278
+ expect(size).to eq(0)
279
+ end
280
+ end
281
+
282
+ describe "Table::InvalidTableError" do
283
+ it "formats error message with key and value" do
284
+ error = Table::InvalidTableError.new("mykey", Object.new)
285
+ expect(error.message).to include("mykey")
286
+ expect(error.message).to include("Object")
287
+ end
288
+ end
257
289
  end
258
290
  end
259
291
  end