Skip to content

Commit 8cedc69

Browse files
committed
WIP: Encode iodata
1 parent 7491498 commit 8cedc69

File tree

2 files changed

+52
-24
lines changed

2 files changed

+52
-24
lines changed

lib/mint/web_socket.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ defmodule Mint.WebSocket do
152152
@typedoc """
153153
A WebSocket frame.
154154
155-
* `{:binary, binary}` - a frame containing binary data. Binary frames
155+
* `{:binary, iodata}` - a frame containing binary data. Binary frames
156156
can be used to send arbitrary binary data such as a PDF.
157157
* `{:text, text}` - a frame containing string data. Text frames must be
158158
valid utf8. Elixir has wonderful support for utf8: `String.valid?/1`
@@ -188,7 +188,7 @@ defmodule Mint.WebSocket do
188188
"""
189189
@type frame ::
190190
{:text, String.t()}
191-
| {:binary, binary()}
191+
| {:binary, iodata()}
192192
| {:ping, binary()}
193193
| {:pong, binary()}
194194
| {:close, code :: non_neg_integer() | nil, reason :: binary() | nil}
@@ -532,7 +532,7 @@ defmodule Mint.WebSocket do
532532
end
533533

534534
@doc """
535-
Encodes a frame into a binary.
535+
Encodes a frame into iodata
536536
537537
The resulting binary may be sent with `stream_request_body/3`.
538538
@@ -545,7 +545,7 @@ defmodule Mint.WebSocket do
545545
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, websocket_ref, data)
546546
547547
"""
548-
@spec encode(t(), shorthand_frame() | frame()) :: {:ok, t(), binary()} | {:error, t(), any()}
548+
@spec encode(t(), shorthand_frame() | frame()) :: {:ok, t(), iodata()} | {:error, t(), any()}
549549
defdelegate encode(websocket, frame), to: Frame
550550

551551
@doc """

lib/mint/web_socket/frame.ex

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,16 @@ defmodule Mint.WebSocket.Frame do
6060

6161
defguard is_fin(frame) when elem(frame, 4) == true
6262

63+
# we can't valiate iodata with a guard but we can sanity check that
64+
# it's either a binary or a list
65+
defguardp is_iodata(data) when is_binary(data) or is_list(data)
66+
6367
# guards frames dealt with in the user-space (not records)
6468
defguardp is_friendly_frame(frame)
6569
when frame in [:ping, :pong, :close] or
66-
(is_tuple(frame) and elem(frame, 0) in [:text, :binary, :ping, :pong] and
70+
(is_tuple(frame) and elem(frame, 0) in [:text, :ping, :pong] and
6771
is_binary(elem(frame, 1))) or
72+
(is_tuple(frame) and elem(frame, 0) == :binary and is_iodata(elem(frame, 1))) or
6873
(is_tuple(frame) and elem(frame, 0) == :close and is_integer(elem(frame, 1)) and
6974
is_binary(elem(frame, 2)))
7075

@@ -94,7 +99,7 @@ defmodule Mint.WebSocket.Frame do
9499
def new_mask, do: :crypto.strong_rand_bytes(4)
95100

96101
@spec encode(Mint.WebSocket.t(), Mint.WebSocket.shorthand_frame() | Mint.WebSocket.frame()) ::
97-
{:ok, Mint.WebSocket.t(), bitstring()}
102+
{:ok, Mint.WebSocket.t(), iodata()}
98103
| {:error, Mint.WebSocket.t(), WebSocketError.t()}
99104
def encode(websocket, frame) when is_friendly_frame(frame) do
100105
{frame, extensions} =
@@ -103,29 +108,35 @@ defmodule Mint.WebSocket.Frame do
103108
|> Extension.encode(websocket.extensions)
104109

105110
websocket = put_in(websocket.extensions, extensions)
106-
frame = encode_to_binary(frame)
111+
frame = encode_to_iodata(frame)
107112

108113
{:ok, websocket, frame}
109114
catch
110115
:throw, {:mint, reason} -> {:error, websocket, reason}
111116
end
112117

113-
@spec encode_to_binary(frame_record()) :: bitstring()
114-
defp encode_to_binary(frame) do
118+
@spec encode_to_iodata(frame_record()) :: iodata()
119+
defp encode_to_iodata(frame) do
115120
payload = payload(frame)
116121
mask = mask(frame)
117122
masked? = if mask == nil, do: 0, else: 1
118123
encoded_payload_length = encode_payload_length(elem(frame, 0), byte_size(payload))
119124

120-
<<
121-
encode_fin(frame)::bitstring,
122-
reserved(frame)::bitstring,
123-
encode_opcode(frame)::bitstring,
124-
masked?::size(1),
125-
encoded_payload_length::bitstring,
126-
mask || <<>>::binary,
127-
apply_mask(payload, mask)::bitstring
128-
>>
125+
[
126+
# Note this is always a binary despite the small sized bitstrings
127+
# used to construct the frame. The payload length is either 7, 23 or 71
128+
# bits so this binary always has bits divisible by 8.
129+
# (This is important because bitstrings are not valid iodata.)
130+
<<
131+
encode_fin(frame)::bitstring,
132+
reserved(frame)::bitstring,
133+
encode_opcode(frame)::bitstring,
134+
masked?::size(1),
135+
encoded_payload_length::bitstring
136+
>>,
137+
mask || <<>>,
138+
apply_mask(payload, mask)
139+
]
129140
end
130141

131142
defp payload(close(code: nil, reason: nil)) do
@@ -176,9 +187,19 @@ defmodule Mint.WebSocket.Frame do
176187
# bytes (where the mask bytes repeat).
177188
# This is an "involution" function: applying the mask will mask
178189
# the data and applying the mask again will unmask it.
179-
def apply_mask(payload, mask, acc \\ <<>>)
190+
@spec apply_mask(iodata(), binary() | nil) :: iodata()
191+
def apply_mask(payload, nil), do: payload
192+
193+
def apply_mask(payload, mask) when is_binary(payload) do
194+
apply_mask_binary(payload, mask, [])
195+
end
180196

181-
def apply_mask(payload, nil, _acc), do: payload
197+
def apply_mask(payload, mask) when is_list(payload) do
198+
apply_mask_iodata(payload, mask, [])
199+
end
200+
201+
@spec apply_mask_binary(binary(), binary(), iodata()) :: iodata()
202+
def apply_mask_binary(payload, mask)
182203

183204
# n=4 is the happy path
184205
# n=3..1 catches cases where the remaining byte_size/1 of the payload is shorter
@@ -187,20 +208,27 @@ defmodule Mint.WebSocket.Frame do
187208
# Elixir 1.17+ and instead of `4..1//-1` to maintain compatibility with older
188209
# Elixir versions that do not support the range-step syntax.
189210
for n <- [4, 3, 2, 1] do
190-
def apply_mask(
211+
def apply_mask_binary(
191212
<<part_key::integer-size(8)-unit(unquote(n)), payload_rest::binary>>,
192213
<<mask_key::integer-size(8)-unit(unquote(n)), _::binary>> = mask,
193214
acc
194215
) do
195-
apply_mask(
216+
apply_mask_binary(
196217
payload_rest,
197218
mask,
198-
<<acc::binary, :erlang.bxor(mask_key, part_key)::integer-size(8)-unit(unquote(n))>>
219+
[<<:erlang.bxor(mask_key, part_key)::integer-size(8)-unit(unquote(n))>> | acc]
199220
)
200221
end
201222
end
202223

203-
def apply_mask(<<>>, _mask, acc), do: acc
224+
def apply_mask_binary(<<>>, _mask, acc), do: :lists.reverse(acc)
225+
226+
@spec apply_mask_iodata(iodata(), binary(), iodata()) :: iodata()
227+
def apply_mask_iodata(_iodata, _mask, _acc) do
228+
# TODO: encode the payload by applying the mask like with binaries
229+
# above, but don't switch the payload to a binary to do it.
230+
:todo
231+
end
204232

205233
@spec decode(Mint.WebSocket.t(), binary()) ::
206234
{:ok, Mint.WebSocket.t(), [Mint.WebSocket.frame() | {:error, term()}]}

0 commit comments

Comments
 (0)