@@ -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