protocol-zmtp 0.8.1 → 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: 81270e34092271f0e0aaceed0ce882885751def59b75716872d074952e4d6b41
4
- data.tar.gz: ccbf823376d70d8654384b99c1bb30b270dc9493cd432c524a3caab175eaa364
3
+ metadata.gz: edfecc19ca6c0befbc5835da0fbfc1fc364007207f10e6f1de64c733d48f2336
4
+ data.tar.gz: d6616b661ee77e73191ed6f33e0a3c5b73b04e6b3fe8e8e5cac30067ae75bb42
5
5
  SHA512:
6
- metadata.gz: f148b0ac01768320f35f2edf7ac67f3d51ad3e5daa4a9c9dafccb57168e8e9f616f6f27425c00b82edcd7e0af898188ae37ea3a78f404a786bdd9a94c01cb675
7
- data.tar.gz: bdab4d4b763394ea1f11aec4503a74d68f499671c93851adc96c2e34bcd7134de837b71941119a1aa029a0966c4ed4cfea8def3f5140244e50f9c2a9b3b0355d
6
+ metadata.gz: 4520a2435a1490fcfb7aebd070f58d10e97a05da685a3c36a47417db2e82b8d7ef48d0c5d19ee6c7db99c7b5da552cd4fec2f061aee3fd9e55908d7d30cf301d
7
+ data.tar.gz: d8c6f26fe3197a25f32e5c5a6668d53eb8cecf109dbe084b00c63b2fcb396cbe05dfd37c9b01ecbc39a960f35ce56ca70fa02c49761e9e0065246ffffeb36437
@@ -28,51 +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
-
35
- # @param body [String] frame body
36
- # @param more [Boolean] more frames follow
37
- # @param command [Boolean] this is a command frame
38
- def initialize(body, more: false, command: false)
39
- @body = body.encoding == Encoding::BINARY ? body : body.b
40
- @more = more
41
- @command = command
42
- end
43
-
44
-
45
- # @return [Boolean] true if more frames follow in this message
46
- def more?
47
- @more
48
- end
49
-
50
-
51
- # @return [Boolean] true if this is a command frame
52
- def command?
53
- @command
54
- end
55
-
56
-
57
- # Encodes to wire bytes.
58
- #
59
- # @return [String] binary wire representation (flags + size + body)
60
- def to_wire
61
- size = @body.bytesize
62
- flags = 0
63
- flags |= FLAGS_MORE if @more
64
- flags |= FLAGS_COMMAND if @command
65
-
66
- if size > SHORT_MAX
67
- buf = String.new(capacity: 9 + size, encoding: Encoding::BINARY)
68
- buf << FLAG_BYTES[flags | FLAGS_LONG] << [size].pack("Q>") << @body
69
- else
70
- buf = String.new(capacity: 2 + size, encoding: Encoding::BINARY)
71
- buf << FLAG_BYTES[flags] << FLAG_BYTES[size] << @body
72
- end
73
- end
74
-
75
-
76
31
  # Encodes a multi-part message into a single wire-format string.
77
32
  # The result can be written to multiple connections without
78
33
  # re-encoding each time (useful for fan-out patterns like PUB).
@@ -82,19 +37,20 @@ module Protocol
82
37
  #
83
38
  def self.encode_message(parts)
84
39
  if parts.size == 1
85
- s = parts[0].bytesize
86
- wire_size = s > SHORT_MAX ? 9 + s : 2 + s
40
+ s = parts.first.bytesize
41
+ wire = s > SHORT_MAX ? 9 + s : 2 + s
87
42
  else
88
- wire_size = 0
89
- j = 0
43
+ wire = 0
44
+ j = 0
45
+
90
46
  while j < parts.size
91
- s = parts[j].bytesize
92
- wire_size += s > SHORT_MAX ? 9 + s : 2 + s
93
- j += 1
47
+ s = parts[j].bytesize
48
+ wire += s > SHORT_MAX ? 9 + s : 2 + s
49
+ j += 1
94
50
  end
95
51
  end
96
52
 
97
- buf = String.new(capacity: wire_size, encoding: Encoding::BINARY)
53
+ buf = String.new(capacity: wire, encoding: Encoding::BINARY)
98
54
  last = parts.size - 1
99
55
  i = 0
100
56
 
@@ -105,10 +61,15 @@ module Protocol
105
61
  flags = i < last ? FLAGS_MORE : 0
106
62
 
107
63
  if size > SHORT_MAX
108
- 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
109
67
  else
110
- buf << FLAG_BYTES[flags] << FLAG_BYTES[size] << body
68
+ buf << FLAG_BYTES[flags]
69
+ buf << FLAG_BYTES[size]
70
+ buf << body
111
71
  end
72
+
112
73
  i += 1
113
74
  end
114
75
 
@@ -118,49 +79,107 @@ module Protocol
118
79
 
119
80
  # Reads one frame from an IO-like object.
120
81
  #
121
- # @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]
122
90
  # @return [Frame]
123
91
  # @raise [Error] on invalid frame
124
92
  # @raise [EOFError] if the connection is closed
125
93
  def self.read_from(io, max_message_size: nil)
126
- # Every valid frame has at least 2 header bytes (flags + 1 size
127
- # byte for short frames, or flags + first size byte for long).
128
- # Fetching both up-front gives short frames a 2-call read path
129
- # (header + body) instead of 3.
130
- head = io.read_exactly(2)
131
- 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
132
100
 
101
+ flags = buf.getbyte(0)
133
102
  more = (flags & FLAGS_MORE) != 0
134
103
  long = (flags & FLAGS_LONG) != 0
135
104
  command = (flags & FLAGS_COMMAND) != 0
136
- 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
137
122
 
138
123
  if max_message_size && size > max_message_size
139
124
  raise Error, "frame size #{size} exceeds max_message_size #{max_message_size}"
140
125
  end
141
126
 
142
- 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
143
131
 
144
- 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)
145
134
  end
146
135
 
147
136
 
148
- # Reads the remaining 7 bytes of a long frame's 8-byte big-endian
149
- # size field and combines them with +msb+ (already consumed as the
150
- # second byte of the 2-byte speculative header read).
151
- #
152
- # @param io [#read_exactly]
153
- # @param msb [Integer] first (most-significant) byte of the size
154
- # @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.
155
164
  #
156
- def self.read_long_size(io, msb)
157
- rest = io.read_exactly(7)
158
-
159
- (msb << 56) |
160
- (rest.getbyte(0) << 48) | (rest.getbyte(1) << 40) |
161
- (rest.getbyte(2) << 32) | (rest.getbyte(3) << 24) |
162
- (rest.getbyte(4) << 16) | (rest.getbyte(5) << 8) |
163
- 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
164
183
  end
165
184
 
166
185
  end
@@ -42,7 +42,7 @@ module Protocol
42
42
  attr_reader :peer_minor
43
43
 
44
44
 
45
- # @return [Object] transport IO (#read_exactly, #write, #flush, #close)
45
+ # @return [Object] transport IO (#peek, #read_exactly, #write, #flush, #close)
46
46
  attr_reader :io
47
47
 
48
48
 
@@ -50,7 +50,7 @@ module Protocol
50
50
  attr_reader :last_received_at
51
51
 
52
52
 
53
- # @param io [#read_exactly, #write, #flush, #close] transport IO
53
+ # @param io [#peek, #read_exactly, #write, #flush, #close] transport IO
54
54
  # @param socket_type [String] our socket type name (e.g. "REQ")
55
55
  # @param identity [String] our identity
56
56
  # @param as_server [Boolean] whether we are the server side
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Protocol
4
4
  module ZMTP
5
- VERSION = "0.8.1"
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.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger