protocol-zmtp 0.8.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5372fe6734b5faeded3069e3b510a7ff648458508c82e3a6c2b9d353ebd57179
4
- data.tar.gz: b6e5cb324afac1deece4df0a0c8cc3aadbfa2ca3a31d29b38ece87dcea777c82
3
+ metadata.gz: edfecc19ca6c0befbc5835da0fbfc1fc364007207f10e6f1de64c733d48f2336
4
+ data.tar.gz: d6616b661ee77e73191ed6f33e0a3c5b73b04e6b3fe8e8e5cac30067ae75bb42
5
5
  SHA512:
6
- metadata.gz: 21ec084b6bf898ed867c9c80700a3d9fad53146798d906a1cadeaed25f28745fa56d47f350e35e6af55e9ba5f7c8c7919342d2bba8e21d6068f585fb54105cc8
7
- data.tar.gz: 90812f6950f8f558a9f33a7f57c2142c63b7c21bab6d4a5d2e788bdb5c9b26154a41b50dde097852e7bfab46e901d706c34d81b6acca0bfa5fd920bfcc655fe7
6
+ metadata.gz: 4520a2435a1490fcfb7aebd070f58d10e97a05da685a3c36a47417db2e82b8d7ef48d0c5d19ee6c7db99c7b5da552cd4fec2f061aee3fd9e55908d7d30cf301d
7
+ data.tar.gz: d8c6f26fe3197a25f32e5c5a6668d53eb8cecf109dbe084b00c63b2fcb396cbe05dfd37c9b01ecbc39a960f35ce56ca70fa02c49761e9e0065246ffffeb36437
@@ -130,9 +130,9 @@ module Protocol
130
130
  # @param ttl [Numeric] time-to-live in seconds (sent as deciseconds)
131
131
  # @param context [String] optional context bytes (up to 16 bytes)
132
132
  # @return [Command]
133
- def self.ping(ttl: 0, context: "".b)
133
+ def self.ping(ttl: 0, context: EMPTY_BINARY)
134
134
  ttl_ds = (ttl * 10).to_i
135
- new("PING", [ttl_ds].pack("n") + context.b)
135
+ new("PING", [ttl_ds].pack("n") + (context.encoding == Encoding::BINARY ? context : context.b))
136
136
  end
137
137
 
138
138
 
@@ -140,8 +140,8 @@ module Protocol
140
140
  #
141
141
  # @param context [String] context bytes echoed from the PING
142
142
  # @return [Command]
143
- def self.pong(context: "".b)
144
- new("PONG", context.b)
143
+ def self.pong(context: EMPTY_BINARY)
144
+ new("PONG", context.encoding == Encoding::BINARY ? context : context.b)
145
145
  end
146
146
 
147
147
 
@@ -150,7 +150,7 @@ module Protocol
150
150
  # @return [Array(Numeric, String)] [ttl_seconds, context_bytes]
151
151
  def ping_ttl_and_context
152
152
  ttl_ds = @data.unpack1("n")
153
- context = @data.bytesize > 2 ? @data.byteslice(2..) : "".b
153
+ context = @data.bytesize > 2 ? @data.byteslice(2..) : EMPTY_BINARY
154
154
  [ttl_ds / 10.0, context]
155
155
  end
156
156
 
@@ -28,42 +28,6 @@ module Protocol
28
28
  FLAG_BYTES = Array.new(256) { |i| i.chr.b.freeze }.freeze
29
29
 
30
30
 
31
- # @return [String] frame body (binary)
32
- attr_reader :body
33
-
34
- # @param body [String] frame body
35
- # @param more [Boolean] more frames follow
36
- # @param command [Boolean] this is a command frame
37
- def initialize(body, more: false, command: false)
38
- @body = body.b
39
- @more = more
40
- @command = command
41
- end
42
-
43
-
44
- # @return [Boolean] true if more frames follow in this message
45
- def more? = @more
46
-
47
- # @return [Boolean] true if this is a command frame
48
- def command? = @command
49
-
50
- # Encodes to wire bytes.
51
- #
52
- # @return [String] binary wire representation (flags + size + body)
53
- def to_wire
54
- size = @body.bytesize
55
- flags = 0
56
- flags |= FLAGS_MORE if @more
57
- flags |= FLAGS_COMMAND if @command
58
-
59
- if size > SHORT_MAX
60
- FLAG_BYTES[flags | FLAGS_LONG] + [size].pack("Q>") + @body
61
- else
62
- FLAG_BYTES[flags] + FLAG_BYTES[size] + @body
63
- end
64
- end
65
-
66
-
67
31
  # Encodes a multi-part message into a single wire-format string.
68
32
  # The result can be written to multiple connections without
69
33
  # re-encoding each time (useful for fan-out patterns like PUB).
@@ -72,71 +36,152 @@ module Protocol
72
36
  # @return [String] frozen binary wire representation
73
37
  #
74
38
  def self.encode_message(parts)
75
- buf = String.new(encoding: Encoding::BINARY)
39
+ if parts.size == 1
40
+ s = parts.first.bytesize
41
+ wire = s > SHORT_MAX ? 9 + s : 2 + s
42
+ else
43
+ wire = 0
44
+ j = 0
45
+
46
+ while j < parts.size
47
+ s = parts[j].bytesize
48
+ wire += s > SHORT_MAX ? 9 + s : 2 + s
49
+ j += 1
50
+ end
51
+ end
52
+
53
+ buf = String.new(capacity: wire, encoding: Encoding::BINARY)
76
54
  last = parts.size - 1
77
55
  i = 0
56
+
78
57
  while i < parts.size
79
58
  body = parts[i]
80
59
  body = body.b unless body.encoding == Encoding::BINARY
81
60
  size = body.bytesize
82
61
  flags = i < last ? FLAGS_MORE : 0
62
+
83
63
  if size > SHORT_MAX
84
- buf << FLAG_BYTES[flags | FLAGS_LONG] << [size].pack("Q>") << body
64
+ buf << FLAG_BYTES[flags | FLAGS_LONG]
65
+ buf << [size].pack("Q>")
66
+ buf << body
85
67
  else
86
- buf << FLAG_BYTES[flags] << FLAG_BYTES[size] << body
68
+ buf << FLAG_BYTES[flags]
69
+ buf << FLAG_BYTES[size]
70
+ buf << body
87
71
  end
72
+
88
73
  i += 1
89
74
  end
75
+
90
76
  buf.freeze
91
77
  end
92
78
 
93
79
 
94
80
  # Reads one frame from an IO-like object.
95
81
  #
96
- # @param io [#read_exactly] must support read_exactly(n)
82
+ # Uses #peek to buffer just enough header bytes (2 for short frames,
83
+ # 9 for long), then drains header + body in a single #read_exactly.
84
+ # This is 2 calls for both short and long frames, vs the naive 3 for
85
+ # long. A speculative read_exactly(9) would be unsafe: a <7-byte
86
+ # short frame at idle would hang waiting for bytes that never arrive,
87
+ # or consume bytes from the next frame on a mixed stream.
88
+ #
89
+ # @param io [#peek, #read_exactly]
97
90
  # @return [Frame]
98
91
  # @raise [Error] on invalid frame
99
92
  # @raise [EOFError] if the connection is closed
100
93
  def self.read_from(io, max_message_size: nil)
101
- # Every valid frame has at least 2 header bytes (flags + 1 size
102
- # byte for short frames, or flags + first size byte for long).
103
- # Fetching both up-front gives short frames a 2-call read path
104
- # (header + body) instead of 3.
105
- head = io.read_exactly(2)
106
- flags = head.getbyte(0)
94
+ buf = io.peek do |b|
95
+ next false if b.bytesize < 2
96
+ (b.getbyte(0) & FLAGS_LONG) == 0 || b.bytesize >= 9
97
+ end
98
+
99
+ raise EOFError, "Stream finished before reading frame header" if buf.bytesize < 2
107
100
 
101
+ flags = buf.getbyte(0)
108
102
  more = (flags & FLAGS_MORE) != 0
109
103
  long = (flags & FLAGS_LONG) != 0
110
104
  command = (flags & FLAGS_COMMAND) != 0
111
- size = long ? read_long_size(io, head.getbyte(1)) : head.getbyte(1)
105
+
106
+ if long
107
+ raise EOFError, "Stream finished before reading long frame size" if buf.bytesize < 9
108
+
109
+ size = (buf.getbyte(1) << 56) |
110
+ (buf.getbyte(2) << 48) |
111
+ (buf.getbyte(3) << 40) |
112
+ (buf.getbyte(4) << 32) |
113
+ (buf.getbyte(5) << 24) |
114
+ (buf.getbyte(6) << 16) |
115
+ (buf.getbyte(7) << 8) |
116
+ buf.getbyte(8)
117
+ header_size = 9
118
+ else
119
+ size = buf.getbyte(1)
120
+ header_size = 2
121
+ end
112
122
 
113
123
  if max_message_size && size > max_message_size
114
124
  raise Error, "frame size #{size} exceeds max_message_size #{max_message_size}"
115
125
  end
116
126
 
117
- body = size > 0 ? io.read_exactly(size) : EMPTY_BINARY
127
+ if size.zero?
128
+ io.read_exactly(header_size)
129
+ return new(EMPTY_BINARY, more: more, command: command)
130
+ end
118
131
 
119
- new(body, more: more, command: command)
132
+ wire = io.read_exactly(header_size + size)
133
+ new(wire.byteslice(header_size, size), more: more, command: command)
120
134
  end
121
135
 
122
136
 
123
- # Reads the remaining 7 bytes of a long frame's 8-byte big-endian
124
- # size field and combines them with +msb+ (already consumed as the
125
- # second byte of the 2-byte speculative header read).
126
- #
127
- # @param io [#read_exactly]
128
- # @param msb [Integer] first (most-significant) byte of the size
129
- # @return [Integer] full 64-bit frame size
137
+ # @return [String] frame body (binary)
138
+ attr_reader :body
139
+
140
+
141
+ # @param body [String] frame body
142
+ # @param more [Boolean] more frames follow
143
+ # @param command [Boolean] this is a command frame
144
+ def initialize(body, more: false, command: false)
145
+ @body = body.encoding == Encoding::BINARY ? body : body.b
146
+ @more = more
147
+ @command = command
148
+ end
149
+
150
+
151
+ # @return [Boolean] true if more frames follow in this message
152
+ def more?
153
+ @more
154
+ end
155
+
156
+
157
+ # @return [Boolean] true if this is a command frame
158
+ def command?
159
+ @command
160
+ end
161
+
162
+
163
+ # Encodes to wire bytes.
130
164
  #
131
- def self.read_long_size(io, msb)
132
- rest = io.read_exactly(7)
133
-
134
- (msb << 56) |
135
- (rest.getbyte(0) << 48) | (rest.getbyte(1) << 40) |
136
- (rest.getbyte(2) << 32) | (rest.getbyte(3) << 24) |
137
- (rest.getbyte(4) << 16) | (rest.getbyte(5) << 8) |
138
- rest.getbyte(6)
165
+ # @return [String] binary wire representation (flags + size + body)
166
+ def to_wire
167
+ size = @body.bytesize
168
+ flags = 0
169
+ flags |= FLAGS_MORE if @more
170
+ flags |= FLAGS_COMMAND if @command
171
+
172
+ if size > SHORT_MAX
173
+ buf = String.new(capacity: 9 + size, encoding: Encoding::BINARY)
174
+ buf << FLAG_BYTES[flags | FLAGS_LONG]
175
+ buf << [size].pack("Q>")
176
+ buf << @body
177
+ else
178
+ buf = String.new(capacity: 2 + size, encoding: Encoding::BINARY)
179
+ buf << FLAG_BYTES[flags]
180
+ buf << FLAG_BYTES[size]
181
+ buf << @body
182
+ end
139
183
  end
184
+
140
185
  end
141
186
  end
142
187
  end
@@ -34,7 +34,7 @@ module Protocol
34
34
  # @return [String] binary frame body
35
35
  def body(prefix, cancel: false)
36
36
  flag = cancel ? FLAG_CANCEL : FLAG_SUBSCRIBE
37
- (flag + prefix.b).b
37
+ flag + (prefix.encoding == Encoding::BINARY ? prefix : prefix.b)
38
38
  end
39
39
 
40
40
 
@@ -15,33 +15,42 @@ module Protocol
15
15
  # @return [String] peer's socket type (from READY handshake)
16
16
  attr_reader :peer_socket_type
17
17
 
18
+
18
19
  # @return [String] peer's identity (from READY handshake)
19
20
  attr_reader :peer_identity
20
21
 
22
+
21
23
  # @return [Integer] peer's QoS level (from READY handshake, 0 if absent)
22
24
  attr_reader :peer_qos
23
25
 
26
+
24
27
  # @return [String] peer's supported hash algorithms in preference order
25
28
  attr_reader :peer_qos_hash
26
29
 
30
+
27
31
  # @return [Hash{String => String}, nil] full peer READY property hash
28
32
  # (set after a successful handshake; nil before)
29
33
  attr_reader :peer_properties
30
34
 
35
+
31
36
  # @return [Integer, nil] peer ZMTP major version (from greeting)
32
37
  attr_reader :peer_major
33
38
 
39
+
34
40
  # @return [Integer, nil] peer ZMTP minor version (from greeting);
35
41
  # 0 for ZMTP 3.0 peers, 1 for ZMTP 3.1+
36
42
  attr_reader :peer_minor
37
43
 
38
- # @return [Object] transport IO (#read_exactly, #write, #flush, #close)
44
+
45
+ # @return [Object] transport IO (#peek, #read_exactly, #write, #flush, #close)
39
46
  attr_reader :io
40
47
 
48
+
41
49
  # @return [Float, nil] monotonic timestamp of last received frame
42
50
  attr_reader :last_received_at
43
51
 
44
- # @param io [#read_exactly, #write, #flush, #close] transport IO
52
+
53
+ # @param io [#peek, #read_exactly, #write, #flush, #close] transport IO
45
54
  # @param socket_type [String] our socket type name (e.g. "REQ")
46
55
  # @param identity [String] our identity
47
56
  # @param as_server [Boolean] whether we are the server side
@@ -79,14 +88,12 @@ module Protocol
79
88
  # @return [void]
80
89
  # @raise [Error] on handshake failure
81
90
  def handshake!
82
- result = @mechanism.handshake!(
83
- @io,
91
+ result = @mechanism.handshake! @io,
84
92
  as_server: @as_server,
85
93
  socket_type: @socket_type,
86
94
  identity: @identity,
87
95
  qos: @qos,
88
- qos_hash: @qos_hash,
89
- )
96
+ qos_hash: @qos_hash
90
97
 
91
98
  @peer_socket_type = result[:peer_socket_type]
92
99
  @peer_identity = result[:peer_identity]
@@ -198,15 +205,19 @@ module Protocol
198
205
  # @raise [EOFError] if connection is closed
199
206
  def receive_message
200
207
  frames = []
208
+
201
209
  loop do
202
210
  frame = read_frame
211
+
203
212
  if frame.command?
204
213
  yield frame if block_given?
205
214
  next
206
215
  end
216
+
207
217
  frames << frame.body.freeze
208
218
  break unless frame.more?
209
219
  end
220
+
210
221
  frames.freeze
211
222
  end
212
223
 
@@ -244,6 +255,7 @@ module Protocol
244
255
  close
245
256
  raise
246
257
  end
258
+
247
259
  touch_heartbeat
248
260
 
249
261
  frame = @mechanism.decrypt(frame) if @mechanism.encrypted?
@@ -259,6 +271,7 @@ module Protocol
259
271
  next
260
272
  end
261
273
  end
274
+
262
275
  return frame
263
276
  end
264
277
  end
@@ -326,9 +339,11 @@ module Protocol
326
339
  last = parts.size - 1
327
340
 
328
341
  i = 0
342
+
329
343
  while i < parts.size
330
344
  part = parts[i]
331
345
  more = i < last
346
+
332
347
  if encrypted
333
348
  body = part.encoding == Encoding::BINARY ? part : part.b
334
349
  @io.write(@mechanism.encrypt(body, more: more))
@@ -336,18 +351,23 @@ module Protocol
336
351
  body = part.encoding == Encoding::BINARY ? part : part.b
337
352
  size = body.bytesize
338
353
  flags = more ? Codec::Frame::FLAGS_MORE : 0
354
+
339
355
  buf.clear
356
+
340
357
  if size > Codec::Frame::SHORT_MAX
341
358
  [flags | Codec::Frame::FLAGS_LONG, size].pack("CQ>", buffer: buf)
342
359
  else
343
360
  [flags, size].pack("CC", buffer: buf)
344
361
  end
362
+
345
363
  @io.write(buf)
346
364
  @io.write(body)
347
365
  end
366
+
348
367
  i += 1
349
368
  end
350
369
  end
370
+
351
371
  end
352
372
  end
353
373
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- VERSION = "0.8.0"
5
+ VERSION = "0.9.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-zmtp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger