Skip to content

Commit dd2775d

Browse files
Move Frame#write -> Framer#write_frame and elide Frame@length.
1 parent ae76bd5 commit dd2775d

7 files changed

Lines changed: 72 additions & 87 deletions

File tree

lib/protocol/websocket/frame.rb

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class Frame
2121

2222
OPCODE = 0
2323

24-
# @parameter length [Integer] The length of the payload, or nil if the header has not been read yet.
2524
# @parameter mask [Boolean | String] An optional 4-byte string which is used to mask the payload.
2625
def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPCODE, mask: false)
2726
if mask == true
@@ -32,7 +31,6 @@ def initialize(finished = true, payload = nil, flags: 0, opcode: self.class::OPC
3231
@flags = flags
3332
@opcode = opcode
3433
@mask = mask
35-
@length = payload&.bytesize
3634
@payload = payload
3735
end
3836

@@ -51,9 +49,9 @@ def <=> other
5149
end
5250

5351
# Convert this frame to an array of its fields for comparison or inspection.
54-
# @returns [Array] An array of `[finished, flags, opcode, mask, length, payload]`.
52+
# @returns [Array] An array of `[finished, flags, opcode, mask, payload]`.
5553
def to_ary
56-
[@finished, @flags, @opcode, @mask, @length, @payload]
54+
[@finished, @flags, @opcode, @mask, @payload]
5755
end
5856

5957
# Check whether this is a control frame (opcode has bit 3 set).
@@ -104,9 +102,14 @@ def continued?
104102
attr_accessor :flags
105103
attr_accessor :opcode
106104
attr_accessor :mask
107-
attr_accessor :length
108105
attr_accessor :payload
109106

107+
# The byte length of the payload.
108+
# @returns [Integer | nil]
109+
def length
110+
@payload&.bytesize
111+
end
112+
110113
if IO.const_defined?(:Buffer) && IO::Buffer.respond_to?(:for) && IO::Buffer.method_defined?(:xor!)
111114
private def mask_xor(data, mask)
112115
buffer = data.dup
@@ -135,18 +138,14 @@ def continued?
135138
# @parameter data [String] The payload data to pack.
136139
# @returns [Frame] Returns `self`.
137140
def pack(data = "")
138-
length = data.bytesize
139-
140-
if length.bit_length > 63
141-
raise ProtocolError, "Frame length #{@length} bigger than allowed maximum!"
141+
if data.bytesize.bit_length > 63
142+
raise ProtocolError, "Frame length #{data.bytesize} bigger than allowed maximum!"
142143
end
143144

144145
if @mask
145-
@payload = mask_xor(data, mask)
146-
@length = length
146+
@payload = mask_xor(data, @mask)
147147
else
148148
@payload = data
149-
@length = length
150149
end
151150

152151
return self
@@ -167,45 +166,6 @@ def unpack
167166
def apply(connection)
168167
connection.receive_frame(self)
169168
end
170-
171-
# Write this frame to the given stream.
172-
# @parameter stream [IO] The stream to write the serialized frame to.
173-
# @raises [ProtocolError] If the frame has invalid length or mask.
174-
def write(stream)
175-
buffer = String.new(encoding: Encoding::BINARY)
176-
177-
if @payload&.bytesize != @length
178-
raise ProtocolError, "Invalid payload length: #{@length} != #{@payload.bytesize} for #{self}!"
179-
end
180-
181-
if @mask and @mask.bytesize != 4
182-
raise ProtocolError, "Invalid mask length!"
183-
end
184-
185-
if length <= 125
186-
short_length = length
187-
elsif length.bit_length <= 16
188-
short_length = 126
189-
else
190-
short_length = 127
191-
end
192-
193-
buffer << [
194-
(@finished ? 0b1000_0000 : 0) | (@flags << 4) | @opcode,
195-
(@mask ? 0b1000_0000 : 0) | short_length,
196-
].pack("CC")
197-
198-
if short_length == 126
199-
buffer << [@length].pack("n")
200-
elsif short_length == 127
201-
buffer << [@length].pack("Q>")
202-
end
203-
204-
buffer << @mask if @mask
205-
206-
stream.write(buffer)
207-
stream.write(@payload)
208-
end
209169
end
210170
end
211171
end

lib/protocol/websocket/framer.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,38 @@ def read_frame(maximum_frame_size = MAXIMUM_ALLOWED_FRAME_SIZE)
117117
end
118118

119119
# Write a frame to the underlying stream.
120+
# @parameter frame [Frame] The frame to serialize and write.
121+
# @raises [ProtocolError] If the frame has an invalid mask.
120122
def write_frame(frame)
121-
frame.write(@stream)
123+
if frame.mask and frame.mask.bytesize != 4
124+
raise ProtocolError, "Invalid mask length!"
125+
end
126+
127+
length = frame.length
128+
129+
if length <= 125
130+
short_length = length
131+
elsif length.bit_length <= 16
132+
short_length = 126
133+
else
134+
short_length = 127
135+
end
136+
137+
buffer = [
138+
(frame.finished ? 0b1000_0000 : 0) | (frame.flags << 4) | frame.opcode,
139+
(frame.mask ? 0b1000_0000 : 0) | short_length,
140+
].pack("CC")
141+
142+
if short_length == 126
143+
buffer << [length].pack("n")
144+
elsif short_length == 127
145+
buffer << [length].pack("Q>")
146+
end
147+
148+
buffer << frame.mask if frame.mask
149+
150+
@stream.write(buffer)
151+
@stream.write(frame.payload)
122152
end
123153
end
124154
end

test/protocol/websocket/binary_frame.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
require "protocol/websocket/a_websocket_frame"
77
require "protocol/websocket/binary_frame"
8+
require "protocol/websocket/framer"
89

910
describe Protocol::WebSocket::BinaryFrame do
1011
let(:frame) {subject.new}
@@ -28,9 +29,7 @@
2829

2930
it "encodes binary representation" do
3031
buffer = StringIO.new
31-
32-
frame.write(buffer)
33-
32+
Protocol::WebSocket::Framer.new(buffer).write_frame(frame)
3433
expect(buffer.string).to be == "\x82\x8Babcd)\a\x0F\b\x0EB4\v\x13\x0E\a"
3534
end
3635
end
@@ -42,9 +41,7 @@
4241

4342
it "encodes binary representation" do
4443
buffer = StringIO.new
45-
46-
frame.write(buffer)
47-
44+
Protocol::WebSocket::Framer.new(buffer).write_frame(frame)
4845
expect(buffer.string).to be == "\x82\vHello World"
4946
end
5047
end

test/protocol/websocket/close_frame.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343

4444
it "rejects a close frame with an invalid format" do
4545
frame.payload = "1"
46-
frame.length = 1
4746

4847
expect do
4948
frame.unpack

test/protocol/websocket/frame.rb

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,4 @@
2929
end
3030
end
3131

32-
with ".write" do
33-
let(:stream) {StringIO.new}
34-
35-
it "fails with invalid payload length" do
36-
frame.length = 5
37-
frame.payload = "1234"
38-
39-
expect do
40-
frame.write(stream)
41-
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid payload length/)
42-
end
43-
44-
it "fails with invalid mask size" do
45-
frame.length = 5
46-
frame.payload = "12345"
47-
frame.mask = "bad"
48-
49-
expect do
50-
frame.write(stream)
51-
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid mask length/)
52-
end
53-
end
5432
end

test/protocol/websocket/framer.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,30 @@
99
let(:stream) {StringIO.new}
1010
let(:framer) {subject.new(stream)}
1111

12+
with "#write_frame" do
13+
it "fails with invalid mask size" do
14+
frame = Protocol::WebSocket::Frame.new(true, "12345")
15+
frame.mask = "bad"
16+
17+
expect do
18+
framer.write_frame(frame)
19+
end.to raise_exception(Protocol::WebSocket::ProtocolError, message: be =~ /Invalid mask length/)
20+
end
21+
22+
it "writes a text frame and reads it back" do
23+
output = StringIO.new
24+
writer = Protocol::WebSocket::Framer.new(output)
25+
26+
frame = Protocol::WebSocket::TextFrame.new(true, "Hello")
27+
writer.write_frame(frame)
28+
29+
reader = Protocol::WebSocket::Framer.new(StringIO.new(output.string))
30+
received = reader.read_frame
31+
expect(received).to be_a(Protocol::WebSocket::TextFrame)
32+
expect(received.payload).to be == "Hello"
33+
end
34+
end
35+
1236
with "#read_frame" do
1337
it "fails if it can't read the frame header" do
1438
expect do

test/protocol/websocket/text_frame.rb

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
require "protocol/websocket/a_websocket_frame"
77
require "protocol/websocket/text_frame"
8+
require "protocol/websocket/framer"
89

910
describe Protocol::WebSocket::TextFrame do
1011
let(:frame) {subject.new}
@@ -24,9 +25,7 @@
2425

2526
it "encodes binary representation" do
2627
buffer = StringIO.new
27-
28-
frame.write(buffer)
29-
28+
Protocol::WebSocket::Framer.new(buffer).write_frame(frame)
3029
expect(buffer.string).to be == "\x81\x8Babcd)\a\x0F\b\x0EB4\v\x13\x0E\a"
3130
end
3231
end
@@ -38,9 +37,7 @@
3837

3938
it "encodes binary representation" do
4039
buffer = StringIO.new
41-
42-
frame.write(buffer)
43-
40+
Protocol::WebSocket::Framer.new(buffer).write_frame(frame)
4441
expect(buffer.string).to be == "\x81\vHello World"
4542
end
4643
end

0 commit comments

Comments
 (0)