cover/mysql_protocol.COVER.html

1 %% MySQL/OTP – MySQL client library for Erlang/OTP
2 %% Copyright (C) 2014 Viktor Söderqvist
3 %% 2017 Piotr Nosek, Michal Slaski
4 %%
5 %% This file is part of MySQL/OTP.
6 %%
7 %% MySQL/OTP is free software: you can redistribute it and/or modify it under
8 %% the terms of the GNU Lesser General Public License as published by the Free
9 %% Software Foundation, either version 3 of the License, or (at your option)
10 %% any later version.
11 %%
12 %% This program is distributed in the hope that it will be useful, but WITHOUT
13 %% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
14 %% FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 %% more details.
16 %%
17 %% You should have received a copy of the GNU Lesser General Public License
18 %% along with this program. If not, see <https://www.gnu.org/licenses/>.
19
20 %% @doc This module implements parts of the MySQL client/server protocol.
21 %%
22 %% The protocol is described in the document "MySQL Internals" which can be
23 %% found under "MySQL Documentation: Expert Guides" on http://dev.mysql.com/.
24 %%
25 %% TCP communication is not handled in this module. Most of the public functions
26 %% take funs for data communitaction as parameters.
27 %% @private
28 -module(mysql_protocol).
29
30 -export([handshake/8, change_user/8, quit/2, ping/2,
31 query/6, fetch_query_response/5, prepare/3, unprepare/3,
32 execute/7, fetch_execute_response/5, reset_connnection/2,
33 valid_params/1, valid_path/1]).
34
35 -type query_filtermap() :: no_filtermap_fun | mysql:query_filtermap_fun().
36
37 -type auth_more_data() :: fast_auth_completed
38 | full_auth_requested
39 | {public_key, term()}.
40
41 %% How much data do we want per packet?
42 -define(MAX_BYTES_PER_PACKET, 16#1000000).
43
44 -include_lib("public_key/include/public_key.hrl").
45
46 -include("records.hrl").
47 -include("protocol.hrl").
48 -include("server_status.hrl").
49
50 %% Macros for pattern matching on packets.
51 -define(ok_pattern, <<?OK, _/binary>>).
52 -define(error_pattern, <<?ERROR, _/binary>>).
53 -define(eof_pattern, <<?EOF, _:4/binary>>).
54 -define(local_infile_pattern, <<?LOCAL_INFILE_REQUEST, _/binary>>).
55
56 %% Macros for auth methods.
57 -define(authmethod_none, <<>>).
58 -define(authmethod_mysql_native_password, <<"mysql_native_password">>).
59 -define(authmethod_sha256_password, <<"sha256_password">>).
60 -define(authmethod_caching_sha2_password, <<"caching_sha2_password">>).
61
62 %% @doc Performs a handshake using the supplied socket and socket module for
63 %% communication. Returns an ok or an error record. Raises errors when various
64 %% unimplemented features are requested.
65 -spec handshake(Host :: inet:socket_address() | inet:hostname(),
66 Username :: iodata(), Password :: iodata(),
67 Database :: iodata() | undefined,
68 SockModule :: module(), SSLOpts :: list() | undefined,
69 Socket :: term(),
70 SetFoundRows :: boolean()) ->
71 {ok, #handshake{}, SockModule :: module(), Socket :: term()} |
72 #error{}.
73
74 handshake(Host, Username, Password, Database, SockModule0, SSLOpts, Socket0,
75 SetFoundRows) ->
76 45 SeqNum0 = 0,
77 45 {ok, HandshakePacket, SeqNum1} = recv_packet(SockModule0, Socket0, SeqNum0),
78 45 case parse_handshake(HandshakePacket) of
79 #handshake{} = Handshake ->
80 43 {ok, SockModule, Socket, SeqNum2} =
81 maybe_do_ssl_upgrade(Host, SockModule0, Socket0, SeqNum1, Handshake,
82 SSLOpts, Database, SetFoundRows),
83 43 Response = build_handshake_response(Handshake, Username, Password,
84 Database, SetFoundRows),
85 43 {ok, SeqNum3} = send_packet(SockModule, Socket, Response, SeqNum2),
86 43 handshake_finish_or_switch_auth(Handshake, Password, SockModule, Socket,
87 SeqNum3);
88 #error{} = Error ->
89 1 Error
90 end.
91
92
93 handshake_finish_or_switch_auth(Handshake, Password, SockModule, Socket, SeqNum) ->
94 #handshake{auth_plugin_name = AuthPluginName,
95 auth_plugin_data = AuthPluginData,
96 server_version = ServerVersion,
97 43 status = Status} = Handshake,
98 43 AuthResult = auth_finish_or_switch(AuthPluginName, AuthPluginData, Password,
99 SockModule, Socket, ServerVersion, SeqNum),
100 43 case AuthResult of
101 #ok{status = OkStatus} ->
102 %% check status, ignoring bit 16#4000, SERVER_SESSION_STATE_CHANGED
103 %% and bit 16#0002, SERVER_STATUS_AUTOCOMMIT.
104 41 BitMask = bnot (?SERVER_SESSION_STATE_CHANGED bor ?SERVER_STATUS_AUTOCOMMIT),
105 41 StatusMasked = Status band BitMask,
106 41 StatusMasked = OkStatus band BitMask,
107 41 {ok, Handshake, SockModule, Socket};
108 Error ->
109 2 Error
110 end.
111
112 %% Finish the authentication, or switch to another auth method.
113 %%
114 %% An OK Packet signals authentication success.
115 %%
116 %% An Error Packet signals authentication failure.
117 %%
118 %% If the authentication process requires more data to be exchanged between
119 %% the server and client, this is done via More Data Packets. The formats and
120 %% meanings of the payloads in such packets depend on the auth method.
121 %%
122 %% An Auth Method Switch Packet signals a request for transition to another
123 %% auth method. The packet contains the name of the auth method to switch to,
124 %% and new auth plugin data.
125 auth_finish_or_switch(AuthPluginName, AuthPluginData, Password,
126 SockModule, Socket, ServerVersion, SeqNum0) ->
127 104 {ok, ConfirmPacket, SeqNum1} = recv_packet(SockModule, Socket, SeqNum0),
128 104 case parse_handshake_confirm(ConfirmPacket) of
129 #ok{} = Ok ->
130 %% Authentication success.
131 49 Ok;
132 #auth_method_switch{auth_plugin_name = SwitchAuthPluginName,
133 auth_plugin_data = SwitchAuthPluginData} ->
134 %% Server wants to transition to a different auth method.
135 %% Send hash of password, calculated according to the requested auth method.
136 %% (NOTE: Sending the password hash as a response to an auth method switch
137 %% is the answer for both mysql_native_password and caching_sha2_password
138 %% methods. It may be different for future other auth methods.)
139 52 Hash = hash_password(SwitchAuthPluginName, Password, SwitchAuthPluginData),
140 52 {ok, SeqNum2} = send_packet(SockModule, Socket, Hash, SeqNum1),
141 52 auth_finish_or_switch(SwitchAuthPluginName, SwitchAuthPluginData, Password,
142 SockModule, Socket, ServerVersion, SeqNum2);
143 fast_auth_completed ->
144 %% Server signals success by fast authentication (probably specific to
145 %% the caching_sha2_password method). This will be followed by an OK Packet.
146
:-(
auth_finish_or_switch(AuthPluginName, AuthPluginData, Password, SockModule,
147 Socket, ServerVersion, SeqNum1);
148 full_auth_requested when SockModule =:= ssl ->
149 %% Server wants full authentication (probably specific to the
150 %% caching_sha2_password method), and we are on a secure channel since
151 %% our connection is through SSL. We have to reply with the null-terminated
152 %% clear text password.
153
:-(
Password1 = case is_binary(Password) of
154
:-(
true -> Password;
155
:-(
false -> iolist_to_binary(Password)
156 end,
157
:-(
{ok, SeqNum2} = send_packet(SockModule, Socket, <<Password1/binary, 0>>, SeqNum1),
158
:-(
auth_finish_or_switch(AuthPluginName, AuthPluginData, Password, SockModule,
159 Socket, ServerVersion, SeqNum2);
160 full_auth_requested ->
161 %% Server wants full authentication (probably specific to the
162 %% caching_sha2_password method), and we are not on a secure channel.
163 %% Since we are not implementing the client-side caching of the server's
164 %% public key, we must ask for it by sending a single byte "2".
165
:-(
{ok, SeqNum2} = send_packet(SockModule, Socket, <<2:8>>, SeqNum1),
166
:-(
auth_finish_or_switch(AuthPluginName, AuthPluginData, Password, SockModule,
167 Socket, ServerVersion, SeqNum2);
168 {public_key, PubKey} ->
169 %% Serveri has sent its public key (certainly specific to the caching_sha2_password
170 %% method). We encrypt the password with the public key we received and send
171 %% it back to the server.
172
:-(
EncryptedPassword = encrypt_password(Password, AuthPluginData, PubKey,
173 ServerVersion),
174
:-(
{ok, SeqNum2} = send_packet(SockModule, Socket, EncryptedPassword, SeqNum1),
175
:-(
auth_finish_or_switch(AuthPluginName, AuthPluginData, Password, SockModule,
176 Socket, ServerVersion, SeqNum2);
177 Error ->
178 %% Authentication failure.
179 3 Error
180 end.
181
182 -spec quit(module(), term()) -> ok.
183 quit(SockModule, Socket) ->
184 32 {ok, SeqNum1} = send_packet(SockModule, Socket, <<?COM_QUIT>>, 0),
185 32 case recv_packet(SockModule, Socket, SeqNum1) of
186 32 {error, closed} -> ok; %% MySQL 5.5.40 and more
187
:-(
{ok, ?ok_pattern, _SeqNum2} -> ok %% Some older MySQL versions?
188 end.
189
190 -spec ping(module(), term()) -> #ok{}.
191 ping(SockModule, Socket) ->
192 4 {ok, SeqNum1} = send_packet(SockModule, Socket, <<?COM_PING>>, 0),
193 3 {ok, OkPacket, _SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
194 3 parse_ok_packet(OkPacket).
195
196 -spec query(Query :: iodata(), module(), term(), [binary()], query_filtermap(),
197 timeout()) ->
198 {ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
199 query(Query, SockModule, Socket, AllowedPaths, FilterMap, Timeout) ->
200 517 Req = <<?COM_QUERY, (iolist_to_binary(Query))/binary>>,
201 517 SeqNum0 = 0,
202 517 {ok, _SeqNum1} = send_packet(SockModule, Socket, Req, SeqNum0),
203 517 fetch_query_response(SockModule, Socket, AllowedPaths, FilterMap, Timeout).
204
205 %% @doc This is used by query/4. If query/4 returns {error, timeout}, this
206 %% function can be called to retry to fetch the results of the query.
207 fetch_query_response(SockModule, Socket, AllowedPaths, FilterMap, Timeout) ->
208 518 fetch_response(SockModule, Socket, Timeout, text, AllowedPaths, FilterMap, []).
209
210 %% @doc Prepares a statement.
211 -spec prepare(iodata(), module(), term()) -> #error{} | #prepared{}.
212 prepare(Query, SockModule, Socket) ->
213 117 Req = <<?COM_STMT_PREPARE, (iolist_to_binary(Query))/binary>>,
214 117 {ok, SeqNum1} = send_packet(SockModule, Socket, Req, 0),
215 117 {ok, Resp, SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
216 117 case Resp of
217 ?error_pattern ->
218 5 parse_error_packet(Resp);
219 <<?OK,
220 StmtId:32/little,
221 NumColumns:16/little,
222 NumParams:16/little,
223 0, %% reserved_1 -- [00] filler
224 WarningCount:16/little>> ->
225 %% This was the first packet.
226 %% Now: Parameter Definition Block. The parameter definitions don't
227 %% contain any useful data at all. They are always TYPE_VAR_STRING
228 %% with charset 'binary' so we have to select a type ourselves for
229 %% the parameters we have in execute/4.
230 112 {_ParamDefs, SeqNum3} =
231 fetch_column_definitions_if_any(NumParams, SockModule, Socket,
232 SeqNum2),
233 %% Column Definition Block. We get column definitions in execute
234 %% too, so we don't need them here. We *could* store them to be able
235 %% to provide the user with some info about a prepared statement.
236 112 {_ColDefs, _SeqNum4} =
237 fetch_column_definitions_if_any(NumColumns, SockModule, Socket,
238 SeqNum3),
239 112 #prepared{statement_id = StmtId,
240 orig_query = Query,
241 param_count = NumParams,
242 warning_count = WarningCount}
243 end.
244
245 %% @doc Deallocates a prepared statement.
246 -spec unprepare(#prepared{}, module(), term()) -> ok.
247 unprepare(#prepared{statement_id = Id}, SockModule, Socket) ->
248 91 {ok, _SeqNum} = send_packet(SockModule, Socket,
249 <<?COM_STMT_CLOSE, Id:32/little>>, 0),
250 91 ok.
251
252 %% @doc Executes a prepared statement.
253 -spec execute(#prepared{}, [term()], module(), term(), [binary()],
254 query_filtermap(), timeout()) ->
255 {ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
256 execute(#prepared{statement_id = Id, param_count = ParamCount}, ParamValues,
257 SockModule, Socket, AllowedPaths, FilterMap, Timeout)
258 when ParamCount == length(ParamValues) ->
259 %% Flags Constant Name
260 %% 0x00 CURSOR_TYPE_NO_CURSOR
261 %% 0x01 CURSOR_TYPE_READ_ONLY
262 %% 0x02 CURSOR_TYPE_FOR_UPDATE
263 %% 0x04 CURSOR_TYPE_SCROLLABLE
264 198 Flags = 0,
265 198 Req0 = <<?COM_STMT_EXECUTE, Id:32/little, Flags, 1:32/little>>,
266 198 Req = case ParamCount of
267 0 ->
268 115 Req0;
269 _ ->
270 %% We can't use the parameter types returned by the prepare call.
271 %% They are all reported as ?TYPE_VAR_STRING with character
272 %% set 'binary'.
273 83 NullBitMap = build_null_bitmap(ParamValues),
274 %% What does it mean to *not* bind new params? To use the same
275 %% params as last time? Right now we always bind params each time.
276 83 NewParamsBoundFlag = 1,
277 83 Req1 = <<Req0/binary, NullBitMap/binary, NewParamsBoundFlag>>,
278 %% For each value, first append type and signedness (16#80 signed or
279 %% 00 unsigned) for all values and then the binary encoded values.
280 83 EncodedParams = lists:map(fun encode_param/1, ParamValues),
281 83 {TypesAndSigns, EncValues} = lists:unzip(EncodedParams),
282 83 iolist_to_binary([Req1, TypesAndSigns, EncValues])
283 end,
284 198 {ok, _SeqNum1} = send_packet(SockModule, Socket, Req, 0),
285 198 fetch_execute_response(SockModule, Socket, AllowedPaths, FilterMap, Timeout).
286
287 %% @doc This is used by execute/5. If execute/5 returns {error, timeout}, this
288 %% function can be called to retry to fetch the results of the query.
289 fetch_execute_response(SockModule, Socket, AllowedPaths, FilterMap, Timeout) ->
290 200 fetch_response(SockModule, Socket, Timeout, binary, AllowedPaths, FilterMap, []).
291
292 %% @doc Changes the user of the connection.
293 -spec change_user(module(), term(), iodata(), iodata(), binary(), binary(),
294 undefined | iodata(), [integer()]) -> #ok{} | #error{}.
295 change_user(SockModule, Socket, Username, Password, AuthPluginName, AuthPluginData,
296 Database, ServerVersion) ->
297 9 DbBin = case Database of
298 8 undefined -> <<>>;
299 1 _ -> iolist_to_binary(Database)
300 end,
301 9 Hash = hash_password(AuthPluginName, Password, AuthPluginData),
302 9 Req0 = <<?COM_CHANGE_USER, (iolist_to_binary(Username))/binary, 0,
303 (lenenc_str_encode(Hash))/binary,
304 DbBin/binary, 0, (character_set(ServerVersion)):16/little>>,
305 9 Req1 = case AuthPluginName of
306 <<>> ->
307
:-(
Req0;
308 _ ->
309 9 <<Req0/binary, AuthPluginName/binary, 0>>
310 end,
311 9 {ok, SeqNum1} = send_packet(SockModule, Socket, Req1, 0),
312 9 auth_finish_or_switch(AuthPluginName, AuthPluginData, Password,
313 SockModule, Socket, ServerVersion, SeqNum1).
314
315 -spec reset_connnection(module(), term()) -> #ok{}|#error{}.
316 reset_connnection(SockModule, Socket) ->
317 1 {ok, SeqNum1} = send_packet(SockModule, Socket, <<?COM_RESET_CONNECTION>>, 0),
318 1 {ok, Packet, _SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
319 1 case Packet of
320 ?ok_pattern ->
321
:-(
parse_ok_packet(Packet);
322 ?error_pattern ->
323 1 parse_error_packet(Packet)
324 end.
325
326 %% --- internal ---
327
328 %% @doc Parses a handshake. This is the first thing that comes from the server
329 %% when connecting. If an unsupported version or variant of the protocol is used
330 %% an error is raised.
331 -spec parse_handshake(binary()) -> #handshake{} | #error{}.
332 parse_handshake(<<10, Rest/binary>>) ->
333 %% Protocol version 10.
334 43 {ServerVersion, Rest1} = nulterm_str(Rest),
335 <<ConnectionId:32/little,
336 AuthPluginDataPart1:8/binary-unit:8,
337 0, %% "filler" -- everything below is optional
338 CapabilitiesLower:16/little,
339 CharacterSet:8,
340 StatusFlags:16/little,
341 CapabilitiesUpper:16/little,
342 AuthPluginDataLength:8, %% if cabab & CLIENT_PLUGIN_AUTH, otherwise 0
343 _Reserved:10/binary-unit:8, %% 10 unused (reserved) bytes
344 43 Rest3/binary>> = Rest1,
345 43 Capabilities = CapabilitiesLower + 16#10000 * CapabilitiesUpper,
346 43 Len = case AuthPluginDataLength of
347
:-(
0 -> 13; %% Server has not CLIENT_PLUGIN_AUTH
348 43 K -> K - 8 %% Part 2 length = Total length minus the 8 bytes in part 1.
349 end,
350 43 <<AuthPluginDataPart2:Len/binary-unit:8, AuthPluginName/binary>> = Rest3,
351 43 AuthPluginData = <<AuthPluginDataPart1/binary, AuthPluginDataPart2/binary>>,
352 %% "Due to Bug#59453 the auth-plugin-name is missing the terminating
353 %% NUL-char in versions prior to 5.5.10 and 5.6.2."
354 %% Strip the final NUL byte if any.
355 %% This may also be <<>> in older versions.
356 43 L = byte_size(AuthPluginName) - 1,
357 43 AuthPluginName1 = case AuthPluginName of
358 43 <<AuthPluginNameTrimmed:L/binary, 0>> -> AuthPluginNameTrimmed;
359
:-(
_ -> AuthPluginName
360 end,
361 43 #handshake{server_version = server_version_to_list(ServerVersion),
362 connection_id = ConnectionId,
363 capabilities = Capabilities,
364 character_set = CharacterSet,
365 status = StatusFlags,
366 auth_plugin_data = AuthPluginData,
367 auth_plugin_name = AuthPluginName1};
368 parse_handshake(<<?ERROR, ErrNo:16/little, Msg/binary>>) ->
369 %% 'Too many connections' in MariaDB 10.1.21
370 %% (Error packet in pre-4.1 protocol)
371 1 #error{code = ErrNo, msg = Msg};
372 parse_handshake(<<Protocol:8, _/binary>>) when Protocol /= 10 ->
373 1 error(unknown_protocol).
374
375 %% @doc Converts a version on the form `<<"5.6.21">' to a list `[5, 6, 21]'.
376 -spec server_version_to_list(binary()) -> [integer()].
377 server_version_to_list(ServerVersion) ->
378 %% This must work with e.g. "5.5.40-0ubuntu0.12.04.1-log" and "5.5.33a".
379 43 {match, Parts} = re:run(ServerVersion, <<"^(\\d+)\\.(\\d+)\\.(\\d+)">>,
380 [{capture, all_but_first, binary}]),
381 43 lists:map(fun binary_to_integer/1, Parts).
382
383 -spec maybe_do_ssl_upgrade(Host :: inet:socket_address() | inet:hostname(),
384 SockModule0 :: module(),
385 Socket0 :: term(),
386 SeqNum1 :: non_neg_integer(),
387 Handshake :: #handshake{},
388 SSLOpts :: undefined | list(),
389 Database :: iodata() | undefined,
390 SetFoundRows :: boolean()) ->
391 {ok, SockModule :: module(), Socket :: term(),
392 SeqNum2 :: non_neg_integer()}.
393 maybe_do_ssl_upgrade(_Host, SockModule0, Socket0, SeqNum1, _Handshake,
394 undefined, _Database, _SetFoundRows) ->
395 42 {ok, SockModule0, Socket0, SeqNum1};
396 maybe_do_ssl_upgrade(Host, gen_tcp, Socket0, SeqNum1, Handshake, SSLOpts,
397 Database, SetFoundRows) ->
398 1 Response = build_handshake_response(Handshake, Database, SetFoundRows),
399 1 {ok, SeqNum2} = send_packet(gen_tcp, Socket0, Response, SeqNum1),
400 1 case ssl_connect(Host, Socket0, SSLOpts, 5000) of
401 {ok, SSLSocket} ->
402 1 {ok, ssl, SSLSocket, SeqNum2};
403 {error, Reason} ->
404
:-(
exit({failed_to_upgrade_socket, Reason})
405 end.
406
407 ssl_connect(Host, Port, ConfigSSLOpts, Timeout) ->
408 1 DefaultSSLOpts0 = [{versions, [tlsv1]}, {verify, verify_peer}],
409 1 DefaultSSLOpts1 = case is_list(Host) andalso inet:parse_address(Host) of
410
:-(
false -> DefaultSSLOpts0;
411
:-(
{ok, _} -> DefaultSSLOpts0;
412 1 {error, einval} -> [{server_name_indication, Host} | DefaultSSLOpts0]
413 end,
414 1 MandatorySSLOpts = [{active, false}],
415 1 MergedSSLOpts = merge_ssl_options(DefaultSSLOpts1, MandatorySSLOpts, ConfigSSLOpts),
416 1 ssl:connect(Port, MergedSSLOpts, Timeout).
417
418 -spec merge_ssl_options(list(), list(), list()) -> list().
419 merge_ssl_options(DefaultSSLOpts, MandatorySSLOpts, ConfigSSLOpts) ->
420 1 SSLOpts1 =
421 lists:foldl(fun({Key, _} = Opt, OptsAcc) ->
422 2 lists:keystore(Key, 1, OptsAcc, Opt)
423 end, DefaultSSLOpts, ConfigSSLOpts),
424 1 lists:foldl(fun({Key, _} = Opt, OptsAcc) ->
425 1 lists:keystore(Key, 1, OptsAcc, Opt)
426 end, SSLOpts1, MandatorySSLOpts).
427
428 %% @doc This function is used when upgrading to encrypted socket. In other,
429 %% cases, build_handshake_response/5 is used.
430 -spec build_handshake_response(#handshake{}, iodata() | undefined, boolean()) ->
431 binary().
432 build_handshake_response(Handshake, Database, SetFoundRows) ->
433 1 CapabilityFlags = basic_capabilities(Database /= undefined, SetFoundRows),
434 1 verify_server_capabilities(Handshake, CapabilityFlags),
435 1 ClientCapabilities = add_client_capabilities(CapabilityFlags),
436 1 ClientSSLCapabilities = ClientCapabilities bor ?CLIENT_SSL,
437 1 CharacterSet = character_set(Handshake#handshake.server_version),
438 1 <<ClientSSLCapabilities:32/little,
439 ?MAX_BYTES_PER_PACKET:32/little,
440 CharacterSet:8,
441 0:23/unit:8>>.
442
443 %% @doc The response sent by the client to the server after receiving the
444 %% initial handshake from the server
445 -spec build_handshake_response(#handshake{}, iodata(), iodata(),
446 iodata() | undefined, boolean()) ->
447 binary().
448 build_handshake_response(Handshake, Username, Password, Database,
449 SetFoundRows) ->
450 43 CapabilityFlags = basic_capabilities(Database /= undefined, SetFoundRows),
451 43 verify_server_capabilities(Handshake, CapabilityFlags),
452 %% Add some extra capability flags only for signalling to the server what
453 %% the client wants to do. The server doesn't say it handles them although
454 %% it does. (http://bugs.mysql.com/bug.php?id=42268)
455 43 ClientCapabilityFlags = add_client_capabilities(CapabilityFlags),
456 43 Hash = hash_password(Handshake#handshake.auth_plugin_name, Password,
457 Handshake#handshake.auth_plugin_data),
458 43 HashLength = size(Hash),
459 43 CharacterSet = character_set(Handshake#handshake.server_version),
460 43 UsernameUtf8 = unicode:characters_to_binary(Username),
461 43 DbBin = case Database of
462 42 undefined -> <<>>;
463 1 _ -> <<(iolist_to_binary(Database))/binary, 0>>
464 end,
465 43 <<ClientCapabilityFlags:32/little,
466 ?MAX_BYTES_PER_PACKET:32/little,
467 CharacterSet:8,
468 0:23/unit:8, %% reserverd
469 UsernameUtf8/binary,
470 0, %% NUL-terminator for the username
471 HashLength,
472 Hash/binary,
473 DbBin/binary>>.
474
475 -spec verify_server_capabilities(Handshake :: #handshake{},
476 CapabilityFlags :: integer()) ->
477 true | no_return().
478 verify_server_capabilities(Handshake, CapabilityFlags) ->
479 %% We require these capabilities. Make sure the server handles them.
480 44 Handshake#handshake.capabilities band CapabilityFlags == CapabilityFlags
481
:-(
orelse error(old_server_version).
482
483 -spec basic_capabilities(ConnectWithDB :: boolean(),
484 SetFoundRows :: boolean()) -> integer().
485 basic_capabilities(ConnectWithDB, SetFoundRows) ->
486 44 CapabilityFlags0 = ?CLIENT_PROTOCOL_41 bor
487 ?CLIENT_TRANSACTIONS bor
488 ?CLIENT_SECURE_CONNECTION,
489 44 CapabilityFlags1 = case ConnectWithDB of
490 1 true -> CapabilityFlags0 bor ?CLIENT_CONNECT_WITH_DB;
491 43 _ -> CapabilityFlags0
492 end,
493 44 case SetFoundRows of
494 1 true -> CapabilityFlags1 bor ?CLIENT_FOUND_ROWS;
495 43 _ -> CapabilityFlags1
496 end.
497
498 -spec add_client_capabilities(Caps :: integer()) -> integer().
499 add_client_capabilities(Caps) ->
500 Caps bor
501 ?CLIENT_MULTI_STATEMENTS bor
502 ?CLIENT_MULTI_RESULTS bor
503 ?CLIENT_PS_MULTI_RESULTS bor
504 ?CLIENT_PLUGIN_AUTH bor
505 44 ?CLIENT_LONG_PASSWORD bor
506 ?CLIENT_LOCAL_FILES.
507
508 -spec character_set([integer()]) -> integer().
509 character_set(ServerVersion) when ServerVersion >= [5, 5, 3] ->
510 %% https://dev.mysql.com/doc/relnotes/mysql/5.5/en/news-5-5-3.html
511 53 ?UTF8MB4;
512
513 character_set(_ServerVersion) ->
514
:-(
?UTF8MB3.
515
516 %% @doc Handles the second packet from the server, when we have replied to the
517 %% initial handshake. Returns an error if the server returns an error. Raises
518 %% an error if unimplemented features are required.
519 -spec parse_handshake_confirm(binary()) ->
520 #ok{} | #auth_method_switch{} | #error{} | auth_more_data().
521 parse_handshake_confirm(Packet = ?ok_pattern) ->
522 %% Connection complete.
523 49 parse_ok_packet(Packet);
524 parse_handshake_confirm(Packet = ?error_pattern) ->
525 %% Access denied, insufficient client capabilities, etc.
526 3 parse_error_packet(Packet);
527 parse_handshake_confirm(<<?EOF>>) ->
528 %% "Old Authentication Method Switch Request Packet consisting of a
529 %% single 0xfe byte. It is sent by server to request client to
530 %% switch to Old Password Authentication if CLIENT_PLUGIN_AUTH
531 %% capability is not supported (by either the client or the server)"
532
:-(
error(old_auth);
533 parse_handshake_confirm(<<?EOF, AuthMethodSwitch/binary>>) ->
534 %% "Authentication Method Switch Request Packet. If both server and
535 %% client support CLIENT_PLUGIN_AUTH capability, server can send
536 %% this packet to ask client to use another authentication method."
537 52 parse_auth_method_switch(AuthMethodSwitch);
538 parse_handshake_confirm(<<?MORE_DATA, MoreData/binary>>) ->
539 %% More Data Packet consisting of a 0x01 byte and a payload. This
540 %% kind of packet may be used in the authentication process to
541 %% provide more data to the client. It is usually followed by
542 %% either an OK Packet, an Error Packet, or another More Data
543 %% packet.
544
:-(
parse_auth_more_data(MoreData).
545
546 %% -- both text and binary protocol --
547
548 %% @doc Fetches one or more results and and parses the result set(s) using
549 %% either the text format (for plain queries) or the binary format (for
550 %% prepared statements).
551 -spec fetch_response(module(), term(), timeout(), text | binary, [binary()],
552 query_filtermap(), list()) ->
553 {ok, [#ok{} | #resultset{} | #error{}]} | {error, timeout}.
554 fetch_response(SockModule, Socket, Timeout, Proto, AllowedPaths, FilterMap, Acc) ->
555 736 case recv_packet(SockModule, Socket, Timeout, any) of
556 {ok, ?local_infile_pattern = Packet, SeqNum2} ->
557 4 Filename = parse_local_infile_packet(Packet),
558 4 Acc1 = case send_file(SockModule, Socket, Filename, AllowedPaths, SeqNum2) of
559 {ok, _SeqNum3} ->
560 2 Acc;
561 {{error, not_allowed}, _SeqNum3} ->
562 1 ErrorMsg = <<"The server requested a file not permitted by the client: ",
563 Filename/binary>>,
564 1 [#error{code = -1, msg = ErrorMsg}|Acc];
565 {{error, FileError}, _SeqNum3} ->
566 1 FileErrorMsg = list_to_binary(file:format_error(FileError)),
567 1 ErrorMsg = <<"The server requested a file which could not be opened "
568 "by the client: ", Filename/binary,
569 " (", FileErrorMsg/binary, ")">>,
570 1 [#error{code = -2, msg = ErrorMsg}|Acc]
571 end,
572 4 fetch_response(SockModule, Socket, Timeout, Proto, AllowedPaths,
573 FilterMap, Acc1);
574 {ok, Packet, SeqNum2} ->
575 729 Result = case Packet of
576 ?ok_pattern ->
577 435 parse_ok_packet(Packet);
578 ?error_pattern ->
579 8 parse_error_packet(Packet);
580 ResultPacket ->
581 %% The first packet in a resultset is only the column count.
582 286 {ColCount, <<>>} = lenenc_int(ResultPacket),
583 286 fetch_resultset(SockModule, Socket, ColCount, Proto,
584 FilterMap, SeqNum2)
585 end,
586 729 Acc1 = [Result | Acc],
587 729 case more_results_exists(Result) of
588 true ->
589 14 fetch_response(SockModule, Socket, Timeout, Proto,
590 AllowedPaths, FilterMap, Acc1);
591 false ->
592 715 {ok, lists:reverse(Acc1)}
593 end;
594 {error, timeout} ->
595 3 {error, timeout}
596 end.
597
598 %% @doc Fetches a result set.
599 -spec fetch_resultset(module(), term(), integer(), text | binary,
600 query_filtermap(), integer()) ->
601 #resultset{} | #error{}.
602 fetch_resultset(SockModule, Socket, FieldCount, Proto, FilterMap, SeqNum0) ->
603 286 {ok, ColDefs0, SeqNum1} = fetch_column_definitions(SockModule, Socket,
604 SeqNum0, FieldCount, []),
605 286 {ok, DelimPacket, SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
606 286 #eof{} = parse_eof_packet(DelimPacket),
607 286 ColDefs1 = lists:map(fun parse_column_definition/1, ColDefs0),
608 286 case fetch_resultset_rows(SockModule, Socket, FieldCount, ColDefs1, Proto,
609 FilterMap, SeqNum2, []) of
610 {ok, Rows, _SeqNum3, #eof{status = S, warning_count = W}} ->
611 285 #resultset{cols = ColDefs1, rows = Rows, status = S,
612 warning_count = W};
613 #error{} = E ->
614 1 E
615 end.
616
617 %% @doc Fetches the rows for a result set and decodes them using either the text
618 %% format (for plain queries) or binary format (for prepared statements).
619 -spec fetch_resultset_rows(module(), term(), integer(), [#col{}], text | binary,
620 query_filtermap(), integer(), [[term()]]) ->
621 {ok, [[term()]], integer(), #eof{}} | #error{}.
622 fetch_resultset_rows(SockModule, Socket, FieldCount, ColDefs, Proto,
623 FilterMap, SeqNum0, Acc) ->
624 581 {ok, Packet, SeqNum1} = recv_packet(SockModule, Socket, SeqNum0),
625 581 case Packet of
626 ?error_pattern ->
627 1 parse_error_packet(Packet);
628 ?eof_pattern ->
629 285 Eof = parse_eof_packet(Packet),
630 285 {ok, lists:reverse(Acc), SeqNum1, Eof};
631 RowPacket ->
632 295 Row0=decode_row(FieldCount, ColDefs, RowPacket, Proto),
633 295 Acc1 = case filtermap_resultset_row(FilterMap, ColDefs, Row0) of
634 false ->
635 6 Acc;
636 true ->
637 283 [Row0|Acc];
638 {true, Row1} ->
639 6 [Row1|Acc]
640 end,
641 295 fetch_resultset_rows(SockModule, Socket, FieldCount, ColDefs,
642 Proto, FilterMap, SeqNum1, Acc1)
643 end.
644
645 -spec filtermap_resultset_row(query_filtermap(), [#col{}], [term()]) ->
646 boolean() | {true, term()}.
647 filtermap_resultset_row(no_filtermap_fun, _, _) ->
648 277 true;
649 filtermap_resultset_row(Fun, _, Row) when is_function(Fun, 1) ->
650 9 Fun(Row);
651 filtermap_resultset_row(Fun, ColDefs, Row) when is_function(Fun, 2) ->
652 9 Fun([Col#col.name || Col <- ColDefs], Row).
653
654 more_results_exists(#ok{status = S}) ->
655 435 S band ?SERVER_MORE_RESULTS_EXISTS /= 0;
656 more_results_exists(#error{}) ->
657 9 false; %% No status bits for error
658 more_results_exists(#resultset{status = S}) ->
659 285 S band ?SERVER_MORE_RESULTS_EXISTS /= 0.
660
661 %% @doc Receives NumLeft column definition packets. They are not parsed.
662 %% @see parse_column_definition/1
663 -spec fetch_column_definitions(module(), term(), SeqNum :: integer(),
664 NumLeft :: integer(), Acc :: [binary()]) ->
665 {ok, ColDefPackets :: [binary()], NextSeqNum :: integer()}.
666 fetch_column_definitions(SockModule, Socket, SeqNum, NumLeft, Acc)
667 when NumLeft > 0 ->
668 474 {ok, Packet, SeqNum1} = recv_packet(SockModule, Socket, SeqNum),
669 474 fetch_column_definitions(SockModule, Socket, SeqNum1, NumLeft - 1,
670 [Packet | Acc]);
671 fetch_column_definitions(_SockModule, _Socket, SeqNum, 0, Acc) ->
672 406 {ok, lists:reverse(Acc), SeqNum}.
673
674 %% Parses a packet containing a column definition (part of a result set)
675 parse_column_definition(Data) ->
676 330 {<<"def">>, Rest1} = lenenc_str(Data), %% catalog (always "def")
677 330 {_Schema, Rest2} = lenenc_str(Rest1), %% schema-name
678 330 {_Table, Rest3} = lenenc_str(Rest2), %% virtual table-name
679 330 {_OrgTable, Rest4} = lenenc_str(Rest3), %% physical table-name
680 330 {Name, Rest5} = lenenc_str(Rest4), %% virtual column name
681 330 {_OrgName, Rest6} = lenenc_str(Rest5), %% physical column name
682 330 {16#0c, Rest7} = lenenc_int(Rest6), %% length of the following fields
683 %% (always 0x0c)
684 <<Charset:16/little, %% column character set
685 Length:32/little, %% maximum length of the field
686 Type:8, %% type of the column as defined in Column Type
687 Flags:16/little, %% flags
688 Decimals:8, %% max shown decimal digits:
689 0, %% "filler" %% - 0x00 for integers and static strings
690 0, %% - 0x1f for dynamic strings, double, float
691 330 Rest8/binary>> = Rest7, %% - 0x00 to 0x51 for decimals
692 %% Here, if command was COM_FIELD_LIST {
693 %% default values: lenenc_str
694 %% }
695 330 <<>> = Rest8,
696 330 #col{name = Name, type = Type, charset = Charset, length = Length,
697 decimals = Decimals, flags = Flags}.
698
699 %% @doc Decodes a row using either the text or binary format.
700 -spec decode_row(integer(), [#col{}], binary(), text | binary) -> [term()].
701 decode_row(FieldCount, ColDefs, RowPacket, text) ->
702 169 decode_text_row(FieldCount, ColDefs, RowPacket);
703 decode_row(FieldCount, ColDefs, RowPacket, binary) ->
704 126 decode_binary_row(FieldCount, ColDefs, RowPacket).
705
706 %% -- text protocol --
707
708 -spec decode_text_row(NumColumns :: integer(),
709 ColumnDefinitions :: [#col{}],
710 Data :: binary()) -> [term()].
711 decode_text_row(_NumColumns, ColumnDefs, Data) ->
712 169 decode_text_row_acc(ColumnDefs, Data, []).
713
714 %% parses Data using ColDefs and builds the values Acc.
715 decode_text_row_acc([ColDef | ColDefs], Data, Acc) ->
716 196 case Data of
717 <<16#fb, Rest/binary>> ->
718 %% NULL
719 2 decode_text_row_acc(ColDefs, Rest, [null | Acc]);
720 _ ->
721 %% Every thing except NULL
722 194 {Text, Rest} = lenenc_str(Data),
723 194 Term = decode_text(ColDef, Text),
724 194 decode_text_row_acc(ColDefs, Rest, [Term | Acc])
725 end;
726 decode_text_row_acc([], <<>>, Acc) ->
727 169 lists:reverse(Acc).
728
729 %% @doc When receiving data in the text protocol, we get everything as binaries
730 %% (except NULL). This function is used to parse these string values.
731 decode_text(#col{type = T}, Text)
732 when T == ?TYPE_TINY; T == ?TYPE_SHORT; T == ?TYPE_LONG; T == ?TYPE_LONGLONG;
733 T == ?TYPE_INT24; T == ?TYPE_YEAR ->
734 82 binary_to_integer(Text);
735 decode_text(#col{type = T}, Text)
736 when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
737 T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
738 T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
739 T == ?TYPE_GEOMETRY; T == ?TYPE_JSON ->
740 %% As of MySQL 5.6.21 we receive SET and ENUM values as STRING, i.e. we
741 %% cannot convert them to atom() or sets:set(), etc.
742 47 Text;
743 decode_text(#col{type = ?TYPE_BIT, length = Length}, Text) ->
744 %% Convert to <<_:Length/bitstring>>
745 5 decode_bitstring(Text, Length);
746 decode_text(#col{type = T, decimals = S, length = L}, Text)
747 when T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
748 %% Length is the max number of symbols incl. dot and minus sign, e.g. the
749 %% number of digits plus 2.
750 19 decode_decimal(Text, L - 2, S);
751 decode_text(#col{type = ?TYPE_DATE},
752 <<Y:4/binary, "-", M:2/binary, "-", D:2/binary>>) ->
753 6 {binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)};
754 decode_text(#col{type = ?TYPE_TIME}, Text) ->
755 23 {match, [Sign, Hbin, Mbin, Sbin, Frac]} =
756 re:run(Text,
757 <<"^(-?)(\\d+):(\\d+):(\\d+)(\\.?\\d*)$">>,
758 [{capture, all_but_first, binary}]),
759 23 H = binary_to_integer(Hbin),
760 23 M = binary_to_integer(Mbin),
761 23 S = binary_to_integer(Sbin),
762 23 IsNeg = Sign == <<"-">>,
763 23 Fraction = case Frac of
764 19 <<>> -> 0;
765 2 _ when not IsNeg -> binary_to_float(<<"0", Frac/binary>>);
766 2 _ when IsNeg -> 1 - binary_to_float(<<"0", Frac/binary>>)
767 end,
768 23 Sec1 = H * 3600 + M * 60 + S,
769 23 Sec2 = if IsNeg -> -Sec1; true -> Sec1 end,
770 23 Sec3 = if IsNeg and (Fraction /= 0) -> Sec2 - 1;
771 21 true -> Sec2
772 end,
773 23 {Days, {Hours, Minutes, Seconds}} = calendar:seconds_to_daystime(Sec3),
774 23 {Days, {Hours, Minutes, Seconds + Fraction}};
775 decode_text(#col{type = T},
776 <<Y:4/binary, "-", M:2/binary, "-", D:2/binary, " ",
777 H:2/binary, ":", Mi:2/binary, ":", S:2/binary>>)
778 when T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME ->
779 %% Without fractions.
780 9 {{binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)},
781 {binary_to_integer(H), binary_to_integer(Mi), binary_to_integer(S)}};
782 decode_text(#col{type = T},
783 <<Y:4/binary, "-", M:2/binary, "-", D:2/binary, " ",
784 H:2/binary, ":", Mi:2/binary, ":", FloatS/binary>>)
785 when T == ?TYPE_TIMESTAMP; T == ?TYPE_DATETIME ->
786 %% With fractions.
787 2 {{binary_to_integer(Y), binary_to_integer(M), binary_to_integer(D)},
788 {binary_to_integer(H), binary_to_integer(Mi), binary_to_float(FloatS)}};
789 decode_text(#col{type = T}, Text) when T == ?TYPE_FLOAT;
790 T == ?TYPE_DOUBLE ->
791 28 try binary_to_float(Text)
792 catch error:badarg ->
793 10 try binary_to_integer(Text) of
794 9 Int -> float(Int)
795 catch error:badarg ->
796 %% It is something like "4e75" that must be turned into "4.0e75"
797 1 binary_to_float(binary:replace(Text, <<"e">>, <<".0e">>))
798 end
799 end.
800
801 %% -- binary protocol --
802
803 %% @doc If NumColumns is non-zero, fetches this number of column definitions
804 %% and an EOF packet. Used by prepare/3.
805 fetch_column_definitions_if_any(0, _SockModule, _Socket, SeqNum) ->
806 104 {[], SeqNum};
807 fetch_column_definitions_if_any(N, SockModule, Socket, SeqNum) ->
808 120 {ok, Defs, SeqNum1} = fetch_column_definitions(SockModule, Socket, SeqNum,
809 N, []),
810 120 {ok, ?eof_pattern, SeqNum2} = recv_packet(SockModule, Socket, SeqNum1),
811 120 {Defs, SeqNum2}.
812
813 %% @doc Decodes a packet representing a row in a binary result set.
814 %% It consists of a 0 byte, then a null bitmap, then the values.
815 %% Returns a list of length NumColumns with terms of appropriate types for each
816 %% MySQL type in ColumnTypes.
817 -spec decode_binary_row(NumColumns :: integer(),
818 ColumnDefs :: [#col{}],
819 Data :: binary()) -> [term()].
820 decode_binary_row(NumColumns, ColumnDefs, <<0, Data/binary>>) ->
821 126 {NullBitMap, Rest} = null_bitmap_decode(NumColumns, Data, 2),
822 126 decode_binary_row_acc(ColumnDefs, NullBitMap, Rest, []).
823
824 %% @doc Accumulating helper for decode_binary_row/3.
825 decode_binary_row_acc([_|ColDefs], <<1:1, NullBitMap/bitstring>>, Data, Acc) ->
826 %% NULL
827 1 decode_binary_row_acc(ColDefs, NullBitMap, Data, [null | Acc]);
828 decode_binary_row_acc([ColDef | ColDefs], <<0:1, NullBitMap/bitstring>>, Data,
829 Acc) ->
830 %% Not NULL
831 147 {Term, Rest} = decode_binary(ColDef, Data),
832 147 decode_binary_row_acc(ColDefs, NullBitMap, Rest, [Term | Acc]);
833 decode_binary_row_acc([], _, <<>>, Acc) ->
834 126 lists:reverse(Acc).
835
836 %% @doc Decodes a null bitmap as stored by MySQL and returns it in a strait
837 %% bitstring counting bits from left to right in a tuple with remaining data.
838 %%
839 %% In the MySQL null bitmap the bits are stored counting bytes from the left and
840 %% bits within each byte from the right. (Sort of little endian.)
841 -spec null_bitmap_decode(NumColumns :: integer(), Data :: binary(),
842 BitOffset :: integer()) ->
843 {NullBitstring :: bitstring(), Rest :: binary()}.
844 null_bitmap_decode(NumColumns, Data, BitOffset) ->
845 %% Binary shift right by 3 is equivallent to integer division by 8.
846 127 BitMapLength = (NumColumns + BitOffset + 7) bsr 3,
847 127 <<NullBitstring0:BitMapLength/binary, Rest/binary>> = Data,
848 127 <<_:BitOffset, NullBitstring:NumColumns/bitstring, _/bitstring>> =
849 129 << <<(reverse_byte(B))/binary>> || <<B:1/binary>> <= NullBitstring0 >>,
850 127 {NullBitstring, Rest}.
851
852 %% @doc The reverse of null_bitmap_decode/3. The number of columns is taken to
853 %% be the number of bits in NullBitstring. Returns the MySQL null bitmap as a
854 %% binary (i.e. full bytes). BitOffset is the number of unused bits that should
855 %% be inserted before the other bits.
856 -spec null_bitmap_encode(bitstring(), integer()) -> binary().
857 null_bitmap_encode(NullBitstring, BitOffset) ->
858 84 PayloadLength = bit_size(NullBitstring) + BitOffset,
859 %% Round up to a multiple of 8.
860 84 BitMapLength = (PayloadLength + 7) band bnot 7,
861 84 PadBitsLength = BitMapLength - PayloadLength,
862 84 PaddedBitstring = <<0:BitOffset, NullBitstring/bitstring, 0:PadBitsLength>>,
863 84 << <<(reverse_byte(B))/binary>> || <<B:1/binary>> <= PaddedBitstring >>.
864
865 %% Reverses the bits in a byte.
866 reverse_byte(<<A:1, B:1, C:1, D:1, E:1, F:1, G:1, H:1>>) ->
867 215 <<H:1, G:1, F:1, E:1, D:1, C:1, B:1, A:1>>.
868
869 %% @doc Used for executing prepared statements. The bit offset whould be 0 in
870 %% this case.
871 -spec build_null_bitmap([any()]) -> binary().
872 build_null_bitmap(Values) ->
873 83 Bits = << <<(case V of null -> 1; _ -> 0 end):1>> || V <- Values >>,
874 83 null_bitmap_encode(Bits, 0).
875
876 %% Decodes a value as received in the 'binary protocol' result set.
877 %%
878 %% The types are type constants for the binary protocol, such as
879 %% ProtocolBinary::MYSQL_TYPE_STRING. In the guide "MySQL Internals" these are
880 %% not listed, but we assume that are the same as for the text protocol.
881 -spec decode_binary(ColDef :: #col{}, Data :: binary()) ->
882 {Term :: term(), Rest :: binary()}.
883 decode_binary(#col{type = T}, Data)
884 when T == ?TYPE_STRING; T == ?TYPE_VARCHAR; T == ?TYPE_VAR_STRING;
885 T == ?TYPE_ENUM; T == ?TYPE_SET; T == ?TYPE_LONG_BLOB;
886 T == ?TYPE_MEDIUM_BLOB; T == ?TYPE_BLOB; T == ?TYPE_TINY_BLOB;
887 T == ?TYPE_GEOMETRY; T == ?TYPE_JSON ->
888 %% As of MySQL 5.6.21 we receive SET and ENUM values as STRING, i.e. we
889 %% cannot convert them to atom() or sets:set(), etc.
890 17 lenenc_str(Data);
891 decode_binary(#col{type = ?TYPE_LONGLONG, flags = F},
892 <<Value:64/signed-little, Rest/binary>>)
893 when F band ?UNSIGNED_FLAG == 0 ->
894 7 {Value, Rest};
895 decode_binary(#col{type = ?TYPE_LONGLONG, flags = F},
896 <<Value:64/unsigned-little, Rest/binary>>)
897 when F band ?UNSIGNED_FLAG /= 0 ->
898 2 {Value, Rest};
899 decode_binary(#col{type = T, flags = F},
900 <<Value:32/signed-little, Rest/binary>>)
901 when (T == ?TYPE_LONG orelse T == ?TYPE_INT24) andalso
902 F band ?UNSIGNED_FLAG == 0 ->
903 25 {Value, Rest};
904 decode_binary(#col{type = T, flags = F},
905 <<Value:32/unsigned-little, Rest/binary>>)
906 when (T == ?TYPE_LONG orelse T == ?TYPE_INT24) andalso
907 F band ?UNSIGNED_FLAG /= 0 ->
908 2 {Value, Rest};
909 decode_binary(#col{type = ?TYPE_SHORT, flags = F},
910 <<Value:16/signed-little, Rest/binary>>)
911 when F band ?UNSIGNED_FLAG == 0 ->
912 4 {Value, Rest};
913 decode_binary(#col{type = T, flags = F},
914 <<Value:16/unsigned-little, Rest/binary>>)
915 when (T == ?TYPE_SHORT orelse T == ?TYPE_YEAR) andalso
916 F band ?UNSIGNED_FLAG /= 0 ->
917 3 {Value, Rest};
918 decode_binary(#col{type = ?TYPE_TINY, flags = F},
919 <<Value:8/unsigned, Rest/binary>>)
920 when F band ?UNSIGNED_FLAG /= 0 ->
921 3 {Value, Rest};
922 decode_binary(#col{type = ?TYPE_TINY, flags = F},
923 <<Value:8/signed, Rest/binary>>)
924 when F band ?UNSIGNED_FLAG == 0 ->
925 4 {Value, Rest};
926 decode_binary(#col{type = T, decimals = S, length = L}, Data)
927 when T == ?TYPE_DECIMAL; T == ?TYPE_NEWDECIMAL ->
928 %% Length is the max number of symbols incl. dot and minus sign, e.g. the
929 %% number of digits plus 2.
930 17 {Binary, Rest} = lenenc_str(Data),
931 17 {decode_decimal(Binary, L - 2, S), Rest};
932 decode_binary(#col{type = ?TYPE_DOUBLE},
933 <<Value:64/float-little, Rest/binary>>) ->
934 1 {Value, Rest};
935 decode_binary(#col{type = ?TYPE_FLOAT}, <<0.0:32/float-little, Rest/binary>>) ->
936 %% TYPE_FLOAT conversation fails on math:log10(0.0)
937 1 {0.0, Rest};
938 decode_binary(#col{type = ?TYPE_FLOAT},
939 <<Value:32/float-little, Rest/binary>>) ->
940 %% There is a precision loss when storing and fetching a 32-bit float.
941 %% In the text protocol, it is obviously rounded. Storing 3.14 in a FLOAT
942 %% column and fetching it using the text protocol, we get "3.14" which we
943 %% parse to the Erlang double as close as possible to 3.14. Fetching the
944 %% same value as a binary 32-bit float, we get 3.140000104904175. To achieve
945 %% the same rounding after receiving it as a 32-bit float, we try to do the
946 %% same rounding here as MySQL does when sending it over the text protocol.
947 %%
948 %% This comment explains the idea:
949 %%
950 %% Posted by Geoffrey Downs on March 10 2011 10:26am
951 %%
952 %% Following up... I *think* this is correct for the default float
953 %% columns in mysql:
954 %%
955 %% var yourNumber = some floating point value
956 %% max decimal precision = 10 ^ (-5 + flooring(yourNumber log 10))
957 %% So:
958 %% 0 < x < 10 -> max precision is 0.00001
959 %% 10 <= x < 100 -> max precision is 0.0001
960 %% 100 <= x < 1000 -> max precision is 0.001
961 %% etc.
962 %%
963 %% (From http://dev.mysql.com/doc/refman/5.7/en/problems-with-float.html
964 %% fetched 10 Nov 2014)
965 %%
966 %% The above is almost correct, except for the example in the interval
967 %% 0 < x < 1. There are 6 significant digits also for these numbers.
968 %%
969 %% Now, instead of P = 0.00001 we want the inverse 100000.0 but if we
970 %% compute Factor = 1 / P we get a precision loss, so instead we do this:
971 27 Factor = math:pow(10, flooring(6 - math:log10(abs(Value)))),
972 27 RoundedValue = round(Value * Factor) / Factor,
973 27 {RoundedValue, Rest};
974 decode_binary(#col{type = ?TYPE_BIT, length = Length}, Data) ->
975 4 {Binary, Rest} = lenenc_str(Data),
976 %% Convert to <<_:Length/bitstring>>
977 4 {decode_bitstring(Binary, Length), Rest};
978 decode_binary(#col{type = ?TYPE_DATE}, Data) ->
979 %% Coded in the same way as DATETIME and TIMESTAMP below, but returned in
980 %% a simple triple.
981 5 case lenenc_int(Data) of
982 2 {0, Rest} -> {{0, 0, 0}, Rest};
983 3 {4, <<Y:16/little, M, D, Rest/binary>>} -> {{Y, M, D}, Rest}
984 end;
985 decode_binary(#col{type = T}, Data)
986 when T == ?TYPE_DATETIME; T == ?TYPE_TIMESTAMP ->
987 %% length (1) -- number of bytes following (valid values: 0, 4, 7, 11)
988 9 case lenenc_int(Data) of
989 {0, Rest} ->
990 2 {{{0, 0, 0}, {0, 0, 0}}, Rest};
991 {4, <<Y:16/little, M, D, Rest/binary>>} ->
992 2 {{{Y, M, D}, {0, 0, 0}}, Rest};
993 {7, <<Y:16/little, M, D, H, Mi, S, Rest/binary>>} ->
994 3 {{{Y, M, D}, {H, Mi, S}}, Rest};
995 {11, <<Y:16/little, M, D, H, Mi, S, Micro:32/little, Rest/binary>>} ->
996 2 {{{Y, M, D}, {H, Mi, S + 0.000001 * Micro}}, Rest}
997 end;
998 decode_binary(#col{type = ?TYPE_TIME}, Data) ->
999 %% length (1) -- number of bytes following (valid values: 0, 8, 12)
1000 %% is_negative (1) -- (1 if minus, 0 for plus)
1001 %% days (4) -- days
1002 %% hours (1) -- hours
1003 %% minutes (1) -- minutes
1004 %% seconds (1) -- seconds
1005 %% micro_seconds (4) -- micro-seconds
1006 21 case lenenc_int(Data) of
1007 {0, Rest} ->
1008 2 {{0, {0, 0, 0}}, Rest};
1009 {8, <<0, D:32/little, H, M, S, Rest/binary>>} ->
1010 5 {{D, {H, M, S}}, Rest};
1011 {12, <<0, D:32/little, H, M, S, Micro:32/little, Rest/binary>>} ->
1012 2 {{D, {H, M, S + 0.000001 * Micro}}, Rest};
1013 {8, <<1, D:32/little, H, M, S, Rest/binary>>} ->
1014 %% Negative time. Example: '-00:00:01' --> {-1,{23,59,59}}
1015 10 Seconds = ((D * 24 + H) * 60 + M) * 60 + S,
1016 %Seconds = D * 86400 + calendar:time_to_seconds({H, M, S}),
1017 10 {calendar:seconds_to_daystime(-Seconds), Rest};
1018 {12, <<1, D:32/little, H, M, S, Micro:32/little, Rest/binary>>}
1019 when Micro > 0 ->
1020 %% Negate and convert to seconds, excl fractions
1021 2 Seconds = -(((D * 24 + H) * 60 + M) * 60 + S),
1022 %Seconds = -D * 86400 - calendar:time_to_seconds({H, M, S}),
1023 %% Subtract 1 second for the fractions
1024 2 {Days, {Hours, Minutes, Sec}} =
1025 calendar:seconds_to_daystime(Seconds - 1),
1026 %% Adding the fractions to Sec again makes it a float
1027 2 {{Days, {Hours, Minutes, Sec + 1 - 0.000001 * Micro}}, Rest}
1028 end.
1029
1030 %% @doc Like trunc/1 but towards negative infinity instead of towards zero.
1031 flooring(Value) ->
1032 27 Trunc = trunc(Value),
1033 27 if
1034 21 Trunc =< Value -> Trunc;
1035 6 Trunc > Value -> Trunc - 1 %% for negative values
1036 end.
1037
1038 %% @doc Encodes a term reprenting av value as a binary for use in the binary
1039 %% protocol. As this is used to encode parameters for prepared statements, the
1040 %% encoding is in its required form, namely `<<Type:8, Sign:8, Value/binary>>'.
1041 -spec encode_param(term()) -> {TypeAndSign :: binary(), Data :: binary()}.
1042 encode_param(null) ->
1043 1 {<<?TYPE_NULL, 0>>, <<>>};
1044 encode_param(Value) when is_binary(Value) ->
1045 11 EncLength = lenenc_int_encode(byte_size(Value)),
1046 11 {<<?TYPE_VAR_STRING, 0>>, <<EncLength/binary, Value/binary>>};
1047 encode_param(Value) when is_list(Value) ->
1048 1 encode_param(unicode:characters_to_binary(Value));
1049 encode_param(Value) when is_integer(Value), Value >= 0 ->
1050 %% We send positive integers with the 'unsigned' flag set.
1051 31 if
1052 Value =< 16#ff ->
1053 22 {<<?TYPE_TINY, 16#80>>, <<Value:8>>};
1054 Value =< 16#ffff ->
1055 3 {<<?TYPE_SHORT, 16#80>>, <<Value:16/little>>};
1056 Value =< 16#ffffffff ->
1057 3 {<<?TYPE_LONG, 16#80>>, <<Value:32/little>>};
1058 Value =< 16#ffffffffffffffff ->
1059 2 {<<?TYPE_LONGLONG, 16#80>>, <<Value:64/little>>};
1060 true ->
1061 %% If larger than a 64-bit int we send it as a string. MySQL does
1062 %% silently cast strings in aithmetic expressions. Also, DECIMALs
1063 %% are always sent as strings.
1064 1 encode_param(integer_to_binary(Value))
1065 end;
1066 encode_param(Value) when is_integer(Value), Value < 0 ->
1067 7 if
1068 Value >= -16#80 ->
1069 2 {<<?TYPE_TINY, 0>>, <<Value:8>>};
1070 Value >= -16#8000 ->
1071 1 {<<?TYPE_SHORT, 0>>, <<Value:16/little>>};
1072 Value >= -16#80000000 ->
1073 2 {<<?TYPE_LONG, 0>>, <<Value:32/little>>};
1074 Value >= -16#8000000000000000 ->
1075 1 {<<?TYPE_LONGLONG, 0>>, <<Value:64/little>>};
1076 true ->
1077 1 encode_param(integer_to_binary(Value))
1078 end;
1079 encode_param(Value) when is_float(Value) ->
1080 28 {<<?TYPE_DOUBLE, 0>>, <<Value:64/float-little>>};
1081 encode_param(Value) when is_bitstring(Value) ->
1082 2 Binary = encode_bitstring(Value),
1083 2 EncLength = lenenc_int_encode(byte_size(Binary)),
1084 2 {<<?TYPE_VAR_STRING, 0>>, <<EncLength/binary, Binary/binary>>};
1085 encode_param({Y, M, D}) ->
1086 %% calendar:date()
1087 3 {<<?TYPE_DATE, 0>>, <<4, Y:16/little, M, D>>};
1088 encode_param({{Y, M, D}, {0, 0, 0}}) ->
1089 %% Datetime at midnight
1090 2 {<<?TYPE_DATETIME, 0>>, <<4, Y:16/little, M, D>>};
1091 encode_param({{Y, M, D}, {H, Mi, S}}) when is_integer(S) ->
1092 %% calendar:datetime()
1093 2 {<<?TYPE_DATETIME, 0>>, <<7, Y:16/little, M, D, H, Mi, S>>};
1094 encode_param({{Y, M, D}, {H, Mi, S}}) when is_float(S) ->
1095 %% calendar:datetime() with a float for seconds. This way it looks very
1096 %% similar to a datetime. Microseconds in MySQL timestamps are possible but
1097 %% not very common.
1098 1 Sec = trunc(S),
1099 1 Micro = round(1000000 * (S - Sec)),
1100 1 {<<?TYPE_DATETIME, 0>>, <<11, Y:16/little, M, D, H, Mi, Sec,
1101 Micro:32/little>>};
1102 encode_param({D, {H, M, S}}) when is_integer(S), D >= 0 ->
1103 %% calendar:seconds_to_daystime()
1104 4 {<<?TYPE_TIME, 0>>, <<8, 0, D:32/little, H, M, S>>};
1105 encode_param({D, {H, M, S}}) when is_integer(S), D < 0 ->
1106 %% Convert to seconds, negate and convert back to daystime form.
1107 %% Then set the minus flag.
1108 6 Seconds = ((D * 24 + H) * 60 + M) * 60 + S,
1109 6 {D1, {H1, M1, S1}} = calendar:seconds_to_daystime(-Seconds),
1110 6 {<<?TYPE_TIME, 0>>, <<8, 1, D1:32/little, H1, M1, S1>>};
1111 encode_param({D, {H, M, S}}) when is_float(S), D >= 0 ->
1112 1 S1 = trunc(S),
1113 1 Micro = round(1000000 * (S - S1)),
1114 1 {<<?TYPE_TIME, 0>>, <<12, 0, D:32/little, H, M, S1, Micro:32/little>>};
1115 encode_param({D, {H, M, S}}) when is_float(S), S > 0.0, D < 0 ->
1116 1 IntS = trunc(S),
1117 1 Micro = round(1000000 * (1 - S + IntS)),
1118 1 Seconds = (D * 24 + H) * 3600 + M * 60 + IntS + 1,
1119 1 {D1, {M1, H1, S1}} = calendar:seconds_to_daystime(-Seconds),
1120 1 {<<?TYPE_TIME, 0>>, <<12, 1, D1:32/little, H1, M1, S1, Micro:32/little>>};
1121 encode_param({D, {H, M, 0.0}}) ->
1122 1 encode_param({D, {H, M, 0}}).
1123
1124 %% @doc Checks if the given Parameters can be encoded for use in the
1125 %% binary protocol. Returns `true' if all of the parameters can be
1126 %% encoded, `false' if any of them cannot be encoded.
1127 -spec valid_params([term()]) -> boolean().
1128 valid_params(Values) when is_list(Values) ->
1129 205 lists:all(fun is_valid_param/1, Values).
1130
1131 %% @doc Checks if the given parameter can be encoded for use in the
1132 %% binary protocol.
1133 -spec is_valid_param(term()) -> boolean().
1134 is_valid_param(null) ->
1135 4 true;
1136 is_valid_param(Value) when is_list(Value) ->
1137 12 try
1138 12 unicode:characters_to_binary(Value)
1139 of
1140 Value1 when is_binary(Value1) ->
1141 10 true;
1142 _ErrorOrIncomplete ->
1143 1 false
1144 catch
1145 error:badarg ->
1146 1 false
1147 end;
1148 is_valid_param(Value) when is_number(Value) ->
1149 73 true;
1150 is_valid_param(Value) when is_bitstring(Value) ->
1151 19 true;
1152 is_valid_param({Y, M, D}) ->
1153 9 is_integer(Y) andalso is_integer(M) andalso is_integer(D);
1154 is_valid_param({{Y, M, D}, {H, Mi, S}}) ->
1155 17 is_integer(Y) andalso is_integer(M) andalso is_integer(D) andalso
1156 14 is_integer(H) andalso is_integer(Mi) andalso is_number(S);
1157 is_valid_param({D, {H, M, S}}) ->
1158 22 is_integer(D) andalso
1159 21 is_integer(H) andalso is_integer(M) andalso is_number(S);
1160 is_valid_param(_) ->
1161 9 false.
1162
1163 %% -- Value representation in both the text and binary protocols --
1164
1165 %% @doc Convert to `<<_:Length/bitstring>>'
1166 decode_bitstring(Binary, Length) ->
1167 9 PaddingLength = bit_size(Binary) - Length,
1168 9 <<_:PaddingLength/bitstring, Bitstring:Length/bitstring>> = Binary,
1169 9 Bitstring.
1170
1171 encode_bitstring(Bitstring) ->
1172 2 Size = bit_size(Bitstring),
1173 2 PaddingSize = byte_size(Bitstring) * 8 - Size,
1174 2 <<0:PaddingSize, Bitstring:Size/bitstring>>.
1175
1176 decode_decimal(Bin, _P, 0) ->
1177 8 binary_to_integer(Bin);
1178 decode_decimal(Bin, P, S) when P =< 15, S > 0 ->
1179 16 binary_to_float(Bin);
1180 decode_decimal(Bin, P, S) when P >= 16, S > 0 ->
1181 12 Bin.
1182
1183 %% -- Protocol basics: packets --
1184
1185 %% @doc Wraps Data in packet headers, sends it by calling SockModule:send/2 with
1186 %% Socket and returns {ok, SeqNum1} where SeqNum1 is the next sequence number.
1187 -spec send_packet(module(), term(), Data :: binary(), SeqNum :: integer()) ->
1188 {ok, NextSeqNum :: integer()}.
1189 send_packet(SockModule, Socket, Data, SeqNum) ->
1190 1071 {WithHeaders, SeqNum1} = add_packet_headers(Data, SeqNum),
1191 1071 ok = SockModule:send(Socket, WithHeaders),
1192 1070 {ok, SeqNum1}.
1193
1194 %% @see recv_packet/4
1195 recv_packet(SockModule, Socket, SeqNum) ->
1196 1763 recv_packet(SockModule, Socket, infinity, SeqNum).
1197
1198 %% @doc Receives data by calling SockModule:recv/2 and removes the packet
1199 %% headers. Returns the packet contents and the next packet sequence number.
1200 -spec recv_packet(module(), term(), timeout(), integer() | any) ->
1201 {ok, Data :: binary(), NextSeqNum :: integer()} | {error, term()}.
1202 recv_packet(SockModule, Socket, Timeout, SeqNum) ->
1203 2499 recv_packet(SockModule, Socket, Timeout, SeqNum, <<>>).
1204
1205 %% @doc Accumulating helper for recv_packet/4
1206 -spec recv_packet(module(), term(), timeout(), integer() | any, binary()) ->
1207 {ok, Data :: binary(), NextSeqNum :: integer()} | {error, term()}.
1208 recv_packet(SockModule, Socket, Timeout, ExpectSeqNum, Acc) ->
1209 2499 case SockModule:recv(Socket, 4, Timeout) of
1210 {ok, Header} ->
1211 2464 {Size, SeqNum, More} = parse_packet_header(Header),
1212 2464 true = SeqNum == ExpectSeqNum orelse ExpectSeqNum == any,
1213 2464 {ok, Body} = SockModule:recv(Socket, Size),
1214 2464 Acc1 = <<Acc/binary, Body/binary>>,
1215 2464 NextSeqNum = (SeqNum + 1) band 16#ff,
1216 2464 case More of
1217 2464 false -> {ok, Acc1, NextSeqNum};
1218
:-(
true -> recv_packet(SockModule, Socket, Timeout, NextSeqNum,
1219 Acc1)
1220 end;
1221 {error, Reason} ->
1222 35 {error, Reason}
1223 end.
1224
1225 -spec send_file(module(), term(), Filename :: binary(), AllowedPaths :: [binary()],
1226 SeqNum :: integer()) ->
1227 {ok | {error, Reason}, NextSeqNum :: integer()}
1228 when Reason :: not_allowed
1229 | file:posix()
1230 | badarg
1231 | system_limit.
1232 send_file(SockModule, Socket, Filename, AllowedPaths, SeqNum0) ->
1233 4 {Result, SeqNum1} = case allowed_path(Filename, AllowedPaths) andalso
1234 3 file:open(Filename, [read, raw, binary]) of
1235 false ->
1236 1 {{error, not_allowed}, SeqNum0};
1237 {ok, Handle} ->
1238 2 {ok, SeqNum2} = send_file_chunk(SockModule, Socket, Handle, SeqNum0),
1239 2 ok = file:close(Handle),
1240 2 {ok, SeqNum2};
1241 {error, _Reason} = E ->
1242 1 {E, SeqNum0}
1243 end,
1244 4 {ok, SeqNum3} = send_packet(SockModule, Socket, <<>>, SeqNum1),
1245 4 {Result, SeqNum3}.
1246
1247 -spec allowed_path(binary(), [binary()]) -> boolean().
1248 allowed_path(Path, AllowedPaths) ->
1249 15 valid_path(Path) andalso
1250 12 binary:last(Path) =/= $/ andalso
1251 11 lists:any(
1252 fun
1253 (AllowedPath) when Path =:= AllowedPath ->
1254 2 true;
1255 (AllowedPath) ->
1256 22 Size = byte_size(AllowedPath),
1257 22 HasSlash = binary:last(AllowedPath) =:= $/,
1258 22 case Path of
1259 4 <<AllowedPath:Size/binary, _/binary>> when HasSlash -> true;
1260 1 <<AllowedPath:Size/binary, $/, _/binary>> -> true;
1261 17 _ -> false
1262 end
1263 end,
1264 AllowedPaths
1265 ).
1266
1267 %% @doc Checks if the argument is a valid path.
1268 %%
1269 %% Returns `true' if the argument is an absolute path that does not contain
1270 %% any relative components like `..' or `.', otherwise `false'.
1271 -spec valid_path(term()) -> boolean().
1272 valid_path(Path) when is_binary(Path), byte_size(Path) > 0 ->
1273 26 case filename:pathtype(Path) of
1274 absolute ->
1275 22 valid_abspath(Path);
1276 volumerelative ->
1277
:-(
case Path of
1278 <<$/, _/binary>> ->
1279
:-(
false;
1280 _ ->
1281
:-(
valid_abspath(Path)
1282 end;
1283 relative ->
1284 4 false
1285 end;
1286 valid_path(_Path) ->
1287 2 false.
1288
1289 -spec valid_abspath(<<_:8, _:_*8>>) -> boolean().
1290 valid_abspath(Path) ->
1291 22 lists:all(
1292 fun
1293
:-(
(<<".">>) -> false;
1294 5 (<<"..">>) -> false;
1295 85 (_) -> true
1296 end,
1297 filename:split(Path)
1298 ).
1299
1300 -spec send_file_chunk(module(), term(), Handle :: file:io_device(), SeqNum :: integer()) ->
1301 {ok, NextSeqNum :: integer()}.
1302 send_file_chunk(SockModule, Socket, Handle, SeqNum0) ->
1303 4 case file:read(Handle, 16#ffffff) of
1304 eof ->
1305 2 {ok, SeqNum0};
1306 {ok, <<>>} ->
1307
:-(
send_file_chunk(SockModule, Socket, Handle, SeqNum0);
1308 {ok, Data} ->
1309 2 {ok, SeqNum1} = send_packet(SockModule, Socket, Data, SeqNum0),
1310 2 send_file_chunk(SockModule, Socket, Handle, SeqNum1)
1311 end.
1312
1313 %% @doc Parses a packet header (32 bits) and returns a tuple.
1314 %%
1315 %% The client should first read a header and parse it. Then read PacketLength
1316 %% bytes. If there are more packets, read another header and read a new packet
1317 %% length of payload until there are no more packets. The seq num should
1318 %% increment from 0 and may wrap around at 255 back to 0.
1319 %%
1320 %% When all packets are read and the payload of all packets are concatenated, it
1321 %% can be parsed using parse_response/1, etc. depending on what type of response
1322 %% is expected.
1323 -spec parse_packet_header(PackerHeader :: binary()) ->
1324 {PacketLength :: integer(),
1325 SeqNum :: integer(),
1326 MorePacketsExist :: boolean()}.
1327 parse_packet_header(<<PacketLength:24/little-integer, SeqNum:8/integer>>) ->
1328 2465 {PacketLength, SeqNum, PacketLength == 16#ffffff}.
1329
1330 %% @doc Splits a packet body into chunks and wraps them in headers. The
1331 %% resulting list is ready to be sent to the socket. The result is built as a
1332 %% list to avoid copying large binaries.
1333 -spec add_packet_headers(Data :: binary(), SeqNum :: integer()) ->
1334 {PacketsWithHeaders :: iodata(), NextSeqNum :: integer()}.
1335 add_packet_headers(<<Payload:16#ffffff/binary, Rest/binary>>, SeqNum) ->
1336 4 SeqNum1 = (SeqNum + 1) band 16#ff,
1337 4 {Packets, NextSeqNum} = add_packet_headers(Rest, SeqNum1),
1338 4 Header = <<16#ffffff:24/little, SeqNum:8>>,
1339 4 {[Header, Payload | Packets], NextSeqNum};
1340 add_packet_headers(Bin, SeqNum) when byte_size(Bin) < 16#ffffff ->
1341 1075 NextSeqNum = (SeqNum + 1) band 16#ff,
1342 1075 Header = <<(byte_size(Bin)):24/little, SeqNum:8>>,
1343 1075 {[Header, Bin], NextSeqNum}.
1344
1345 -spec parse_ok_packet(binary()) -> #ok{}.
1346 parse_ok_packet(<<?OK:8, Rest/binary>>) ->
1347 488 {AffectedRows, Rest1} = lenenc_int(Rest),
1348 488 {InsertId, Rest2} = lenenc_int(Rest1),
1349 488 <<StatusFlags:16/little, WarningCount:16/little, Msg/binary>> = Rest2,
1350 %% We have CLIENT_PROTOCOL_41 but not CLIENT_SESSION_TRACK enabled. The
1351 %% protocol is conditional. This is from the protocol documentation:
1352 %%
1353 %% if capabilities & CLIENT_PROTOCOL_41 {
1354 %% int<2> status_flags
1355 %% int<2> warning_count
1356 %% } elseif capabilities & CLIENT_TRANSACTIONS {
1357 %% int<2> status_flags
1358 %% }
1359 %% if capabilities & CLIENT_SESSION_TRACK {
1360 %% string<lenenc> info
1361 %% if status_flags & SERVER_SESSION_STATE_CHANGED {
1362 %% string<lenenc> session_state_changes
1363 %% }
1364 %% } else {
1365 %% string<EOF> info
1366 %% }
1367 488 #ok{affected_rows = AffectedRows,
1368 insert_id = InsertId,
1369 status = StatusFlags,
1370 warning_count = WarningCount,
1371 msg = Msg}.
1372
1373 -spec parse_error_packet(binary()) -> #error{}.
1374 parse_error_packet(<<?ERROR:8, ErrNo:16/little, "#", SQLState:5/binary-unit:8,
1375 Msg/binary>>) ->
1376 %% Error, 4.1 protocol.
1377 %% (Older protocol: <<?ERROR:8, ErrNo:16/little, Msg/binary>>)
1378 19 #error{code = ErrNo, state = SQLState, msg = Msg}.
1379
1380 -spec parse_eof_packet(binary()) -> #eof{}.
1381 parse_eof_packet(<<?EOF:8, NumWarnings:16/little, StatusFlags:16/little>>) ->
1382 %% EOF packet, 4.1 protocol.
1383 %% (Older protocol: <<?EOF:8>>)
1384 572 #eof{status = StatusFlags, warning_count = NumWarnings}.
1385
1386 parse_local_infile_packet(<<?LOCAL_INFILE_REQUEST:8, FileName/binary>>) ->
1387 4 FileName.
1388
1389 -spec parse_auth_method_switch(binary()) -> #auth_method_switch{}.
1390 parse_auth_method_switch(AMSData) ->
1391 52 {AuthPluginName, AuthPluginData} = get_null_terminated_binary(AMSData),
1392 52 #auth_method_switch{
1393 auth_plugin_name = AuthPluginName,
1394 auth_plugin_data = AuthPluginData
1395 }.
1396
1397 -spec parse_auth_more_data(binary()) -> auth_more_data().
1398 parse_auth_more_data(<<3>>) ->
1399 %% With caching_sha2_password authentication, a single 0x03
1400 %% byte signals Fast Auth Success.
1401
:-(
fast_auth_completed;
1402 parse_auth_more_data(<<4>>) ->
1403 %% With caching_sha2_password authentication, a single 0x04
1404 %% byte signals a Full Auth Request.
1405
:-(
full_auth_requested;
1406 parse_auth_more_data(Data) ->
1407 %% With caching_sha2_password authentication, anything
1408 %% other than the above should be the public key of the
1409 %% server.
1410
:-(
PubKey = case public_key:pem_decode(Data) of
1411 [PemEntry = #'SubjectPublicKeyInfo'{}] ->
1412
:-(
public_key:pem_entry_decode(PemEntry);
1413 [PemEntry = #'RSAPublicKey'{}] ->
1414
:-(
PemEntry
1415 end,
1416
:-(
{public_key, PubKey}.
1417
1418 -spec get_null_terminated_binary(binary()) -> {Binary :: binary(),
1419 Rest :: binary()}.
1420 get_null_terminated_binary(In) ->
1421 52 get_null_terminated_binary(In, <<>>).
1422
1423 get_null_terminated_binary(<<0, Rest/binary>>, Acc) ->
1424 52 {Acc, Rest};
1425 get_null_terminated_binary(<<Ch, Rest/binary>>, Acc) ->
1426 1092 get_null_terminated_binary(Rest, <<Acc/binary, Ch>>).
1427
1428 -spec hash_password(AuthMethod, Password, Salt) -> Hash
1429 when AuthMethod :: binary(),
1430 Password :: iodata(),
1431 Salt :: binary(),
1432 Hash :: binary().
1433 hash_password(AuthMethod, Password, Salt) when not is_binary(Password) ->
1434 104 hash_password(AuthMethod, iolist_to_binary(Password), Salt);
1435 hash_password(?authmethod_none, Password, Salt) ->
1436
:-(
hash_password(?authmethod_mysql_native_password, Password, Salt);
1437 hash_password(?authmethod_mysql_native_password, <<>>, _Salt) ->
1438 1 <<>>;
1439 hash_password(?authmethod_mysql_native_password, Password, Salt) ->
1440 %% From the "MySQL Internals" manual:
1441 %% SHA1( password ) XOR SHA1( "20-bytes random data from server" <concat>
1442 %% SHA1( SHA1( password ) ) )
1443 105 Salt1 = trim_salt(Salt),
1444 105 <<Hash1Num:160>> = Hash1 = crypto:hash(sha, Password),
1445 105 Hash2 = crypto:hash(sha, Hash1),
1446 105 <<Hash3Num:160>> = crypto:hash(sha, <<Salt1/binary, Hash2/binary>>),
1447 105 <<(Hash1Num bxor Hash3Num):160>>;
1448 hash_password(?authmethod_caching_sha2_password, <<>>, _Salt) ->
1449 1 <<>>;
1450 hash_password(?authmethod_caching_sha2_password, Password, Salt) ->
1451 %% From https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html
1452 %% (transcribed):
1453 %% SHA256( password ) XOR SHA256( SHA256( SHA256( password ) ) <concat>
1454 %% "20-bytes random data from server" )
1455 1 Salt1 = trim_salt(Salt),
1456 1 <<Hash1Num:256>> = Hash1 = crypto:hash(sha256, Password),
1457 1 Hash2 = crypto:hash(sha256, Hash1),
1458 1 <<Hash3Num:256>> = crypto:hash(sha256, <<Hash2/binary, Salt1/binary>>),
1459 1 <<(Hash1Num bxor Hash3Num):256>>;
1460 hash_password(?authmethod_sha256_password, Password, Salt) ->
1461 %% sha256_password authentication is superseded by
1462 %% caching_sha2_password.
1463
:-(
hash_password(?authmethod_caching_sha2_password, Password, Salt);
1464 hash_password(UnknownAuthMethod, _, _) ->
1465
:-(
error({auth_method, UnknownAuthMethod}).
1466
1467 encrypt_password(Password, Salt, PubKey, ServerVersion)
1468 when is_binary(Password) ->
1469 %% From http://www.dataarchitect.cloud/preparing-your-community-connector-for-mysql-8-part-2-sha256/:
1470 %% "The password is "obfuscated" first by employing a rotating "xor" against
1471 %% the seed bytes that were given to the authentication plugin upon initial
1472 %% handshake [the auth plugin data].
1473 %% [...]
1474 %% Buffer would then be encrypted using the RSA public key the server passed
1475 %% to the client. The resulting buffer would then be passed back to the
1476 %% server."
1477
:-(
Salt1 = trim_salt(Salt),
1478
1479 %% While the article does not mention it, the password must be null-terminated
1480 %% before obfuscation.
1481
:-(
Password1 = <<Password/binary, 0>>,
1482
:-(
Salt2 = case byte_size(Salt1)<byte_size(Password1) of
1483 true ->
1484
:-(
binary:copy(Salt1, (byte_size(Password1) div byte_size(Salt1)) + 1);
1485 false ->
1486
:-(
Salt1
1487 end,
1488
:-(
Size = bit_size(Password1),
1489
:-(
<<PasswordNum:Size>> = Password1,
1490
:-(
<<SaltNum:Size, _/bitstring>> = Salt2,
1491
:-(
Password2 = <<(PasswordNum bxor SaltNum):Size>>,
1492
1493 %% From http://www.dataarchitect.cloud/preparing-your-community-connector-for-mysql-8-part-2-sha256/:
1494 %% "It's important to note that a incompatible change happened in server 8.0.5.
1495 %% Prior to server 8.0.5 the encryption was done using RSA_PKCS1_PADDING.
1496 %% With 8.0.5 it is done with RSA_PKCS1_OAEP_PADDING."
1497
:-(
RsaPadding = case ServerVersion < [8, 0, 5] of
1498
:-(
true -> rsa_pkcs1_padding;
1499
:-(
false -> rsa_pkcs1_oaep_padding
1500 end,
1501 %% The option rsa_pad was renamed to rsa_padding in OTP/22, but rsa_pad
1502 %% is being kept for backwards compatibility.
1503
:-(
public_key:encrypt_public(Password2, PubKey, [{rsa_pad, RsaPadding}]);
1504 encrypt_password(Password, Salt, PubKey, ServerVersion) ->
1505
:-(
encrypt_password(iolist_to_binary(Password), Salt, PubKey, ServerVersion).
1506
1507 trim_salt(<<SaltNoNul:20/binary-unit:8, 0>>) ->
1508 104 SaltNoNul;
1509 trim_salt(Salt = <<_:20/binary-unit:8>>) ->
1510 2 Salt.
1511
1512 %% --- Lowlevel: variable length integers and strings ---
1513
1514 %% lenenc_int/1 decodes length-encoded-integer values
1515 -spec lenenc_int(Input :: binary()) -> {Value :: integer(), Rest :: binary()}.
1516 3841 lenenc_int(<<Value:8, Rest/bits>>) when Value < 251 -> {Value, Rest};
1517 1 lenenc_int(<<16#fc:8, Value:16/little, Rest/binary>>) -> {Value, Rest};
1518 1 lenenc_int(<<16#fd:8, Value:24/little, Rest/binary>>) -> {Value, Rest};
1519 1 lenenc_int(<<16#fe:8, Value:64/little, Rest/binary>>) -> {Value, Rest}.
1520
1521 %% Length-encoded-integer encode. Appends the encoded value to Acc.
1522 %% Values not representable in 64 bits are not accepted.
1523 -spec lenenc_int_encode(0..16#ffffffffffffffff) -> binary().
1524 lenenc_int_encode(Value) when Value >= 0 ->
1525 26 if Value < 251 -> <<Value>>;
1526 1 Value =< 16#ffff -> <<16#fc, Value:16/little>>;
1527 1 Value =< 16#ffffff -> <<16#fd, Value:24/little>>;
1528 1 Value =< 16#ffffffffffffffff -> <<16#fe, Value:64/little>>
1529 end.
1530
1531 %% lenenc_str/1 decodes length-encoded-string values
1532 -spec lenenc_str(Input :: binary()) -> {String :: binary(), Rest :: binary()}.
1533 lenenc_str(Bin) ->
1534 2213 {Length, Rest} = lenenc_int(Bin),
1535 2213 <<String:Length/binary, Rest1/binary>> = Rest,
1536 2213 {String, Rest1}.
1537
1538 %% Length-encoded-string encode. Prefixes the value with a
1539 %% length-encoded-integer denoting its size.
1540 -spec lenenc_str_encode(Input :: binary()) -> binary().
1541 lenenc_str_encode(Bin) ->
1542 9 Length = byte_size(Bin),
1543 9 <<(lenenc_int_encode(Length))/binary, Bin:Length/binary>>.
1544
1545 %% nts/1 decodes a nul-terminated string
1546 -spec nulterm_str(Input :: binary()) -> {String :: binary(), Rest :: binary()}.
1547 nulterm_str(Bin) ->
1548 44 [String, Rest] = binary:split(Bin, <<0>>),
1549 44 {String, Rest}.
1550
1551 -ifdef(TEST).
1552 -include_lib("eunit/include/eunit.hrl").
1553
1554 %% Testing some of the internal functions, mostly the cases we don't cover in
1555 %% other tests.
1556
1557 decode_text_test() ->
1558 %% Int types
1559 1 lists:foreach(fun (T) ->
1560 6 ?assertEqual(1, decode_text(#col{type = T}, <<"1">>))
1561 end,
1562 [?TYPE_TINY, ?TYPE_SHORT, ?TYPE_LONG, ?TYPE_LONGLONG,
1563 ?TYPE_INT24, ?TYPE_YEAR]),
1564
1565 %% BIT
1566 1 <<217>> = decode_text(#col{type = ?TYPE_BIT, length = 8}, <<217>>),
1567
1568 %% Floating point and decimal numbers
1569 1 lists:foreach(fun (T) ->
1570 2 ?assertEqual(3.0, decode_text(#col{type = T}, <<"3.0">>))
1571 end,
1572 [?TYPE_FLOAT, ?TYPE_DOUBLE]),
1573 %% Decimal types
1574 1 lists:foreach(fun (T) ->
1575 2 ColDef = #col{type = T, decimals = 1, length = 4},
1576 2 ?assertMatch(3.0, decode_text(ColDef, <<"3.0">>))
1577 end,
1578 [?TYPE_DECIMAL, ?TYPE_NEWDECIMAL]),
1579 1 ?assertEqual(3.0, decode_text(#col{type = ?TYPE_FLOAT}, <<"3">>)),
1580 1 ?assertEqual(30.0, decode_text(#col{type = ?TYPE_FLOAT}, <<"3e1">>)),
1581 1 ?assertEqual(3, decode_text(#col{type = ?TYPE_LONG}, <<"3">>)),
1582
1583 %% Date and time
1584 1 ?assertEqual({2014, 11, 01},
1585 1 decode_text(#col{type = ?TYPE_DATE}, <<"2014-11-01">>)),
1586 1 ?assertEqual({0, {23, 59, 01}},
1587 1 decode_text(#col{type = ?TYPE_TIME}, <<"23:59:01">>)),
1588 1 ?assertEqual({{2014, 11, 01}, {23, 59, 01}},
1589 decode_text(#col{type = ?TYPE_DATETIME},
1590 1 <<"2014-11-01 23:59:01">>)),
1591 1 ?assertEqual({{2014, 11, 01}, {23, 59, 01}},
1592 decode_text(#col{type = ?TYPE_TIMESTAMP},
1593 1 <<"2014-11-01 23:59:01">>)),
1594
1595 %% Strings and blobs
1596 1 lists:foreach(fun (T) ->
1597 9 ColDef = #col{type = T},
1598 9 ?assertEqual(<<"x">>, decode_text(ColDef, <<"x">>))
1599 end,
1600 [?TYPE_VARCHAR, ?TYPE_ENUM, ?TYPE_TINY_BLOB,
1601 ?TYPE_MEDIUM_BLOB, ?TYPE_LONG_BLOB, ?TYPE_BLOB,
1602 ?TYPE_VAR_STRING, ?TYPE_STRING, ?TYPE_GEOMETRY]),
1603 1 ok.
1604
1605 decode_binary_test() ->
1606 %% Test the special rounding we apply to (single precision) floats.
1607 1 ?assertEqual({1.0, <<>>},
1608 decode_binary(#col{type = ?TYPE_FLOAT},
1609 1 <<1.0:32/float-little>>)),
1610 1 ?assertEqual({0.2, <<>>},
1611 decode_binary(#col{type = ?TYPE_FLOAT},
1612 1 <<0.2:32/float-little>>)),
1613 1 ?assertEqual({-33.3333, <<>>},
1614 decode_binary(#col{type = ?TYPE_FLOAT},
1615 1 <<-33.333333:32/float-little>>)),
1616 1 ?assertEqual({0.000123457, <<>>},
1617 decode_binary(#col{type = ?TYPE_FLOAT},
1618 1 <<0.00012345678:32/float-little>>)),
1619 1 ?assertEqual({1234.57, <<>>},
1620 decode_binary(#col{type = ?TYPE_FLOAT},
1621 1 <<1234.56789:32/float-little>>)),
1622 1 ok.
1623
1624 null_bitmap_test() ->
1625 1 ?assertEqual({<<0, 1:1>>, <<>>}, null_bitmap_decode(9, <<0, 4>>, 2)),
1626 1 ?assertEqual(<<0, 4>>, null_bitmap_encode(<<0, 1:1>>, 2)),
1627 1 ok.
1628
1629 lenenc_int_test() ->
1630 %% decode
1631 1 ?assertEqual({40, <<>>}, lenenc_int(<<40>>)),
1632 1 ?assertEqual({16#ff, <<>>}, lenenc_int(<<16#fc, 255, 0>>)),
1633 1 ?assertEqual({16#33aaff, <<>>}, lenenc_int(<<16#fd, 16#ff, 16#aa, 16#33>>)),
1634 1 ?assertEqual({16#12345678, <<>>}, lenenc_int(<<16#fe, 16#78, 16#56, 16#34,
1635 1 16#12, 0, 0, 0, 0>>)),
1636 %% encode
1637 1 ?assertEqual(<<40>>, lenenc_int_encode(40)),
1638 1 ?assertEqual(<<16#fc, 255, 0>>, lenenc_int_encode(255)),
1639 1 ?assertEqual(<<16#fd, 16#ff, 16#aa, 16#33>>,
1640 1 lenenc_int_encode(16#33aaff)),
1641 1 ?assertEqual(<<16#fe, 16#78, 16#56, 16#34, 16#12, 0, 0, 0, 0>>,
1642 1 lenenc_int_encode(16#12345678)),
1643 1 ok.
1644
1645 lenenc_str_test() ->
1646 1 ?assertEqual({<<"Foo">>, <<"bar">>}, lenenc_str(<<3, "Foobar">>)).
1647
1648 nulterm_test() ->
1649 1 ?assertEqual({<<"Foo">>, <<"bar">>}, nulterm_str(<<"Foo", 0, "bar">>)).
1650
1651 parse_header_test() ->
1652 %% Example from "MySQL Internals", revision 307, section 14.1.3.3 EOF_Packet
1653 1 Packet = <<16#05, 16#00, 16#00, 16#05, 16#fe, 16#00, 16#00, 16#02, 16#00>>,
1654 1 <<Header:4/binary-unit:8, Body/binary>> = Packet,
1655 %% Check header contents and body length
1656 1 ?assertEqual({size(Body), 5, false}, parse_packet_header(Header)),
1657 1 ok.
1658
1659 add_packet_headers_test() ->
1660 1 {Data, 43} = add_packet_headers(<<"foo">>, 42),
1661 1 ?assertEqual(<<3, 0, 0, 42, "foo">>, list_to_binary(Data)).
1662
1663 add_packet_headers_equal_to_0xffffff_test() ->
1664 1 BigBin = binary:copy(<<1>>, 16#ffffff),
1665 1 {Data, 44} = add_packet_headers(BigBin, 42),
1666 1 ?assertEqual(<<16#ff, 16#ff, 16#ff, 42, BigBin/binary,
1667 1 0, 0, 0, 43>>,
1668 1 list_to_binary(Data)).
1669
1670 add_packet_headers_greater_than_0xffffff_test() ->
1671 1 BigBin = binary:copy(<<1>>, 16#ffffff),
1672 1 {Data, 44} = add_packet_headers(<<BigBin/binary, "foo">>, 42),
1673 1 ?assertEqual(<<16#ff, 16#ff, 16#ff, 42, BigBin/binary, 3, 0, 0, 43, "foo">>,
1674 1 list_to_binary(Data)).
1675
1676 add_packet_headers_2_times_greater_than_0xffffff_test() ->
1677 1 BigBin = binary:copy(<<1>>, 16#ffffff),
1678 1 {Data, 45} = add_packet_headers(<<BigBin/binary, BigBin/binary, "foo">>, 42),
1679 1 ?assertEqual(<<16#ff, 16#ff, 16#ff, 42, BigBin/binary,
1680 16#ff, 16#ff, 16#ff, 43, BigBin/binary,
1681 1 3, 0, 0, 44, "foo">>,
1682 1 list_to_binary(Data)).
1683
1684 parse_ok_test() ->
1685 1 Body = <<0, 5, 1, 2, 0, 0, 0, "Foo">>,
1686 1 ?assertEqual(#ok{affected_rows = 5,
1687 insert_id = 1,
1688 status = ?SERVER_STATUS_AUTOCOMMIT,
1689 warning_count = 0,
1690 1 msg = <<"Foo">>},
1691 1 parse_ok_packet(Body)).
1692
1693 parse_error_test() ->
1694 %% Protocol 4.1
1695 1 Body = <<255, 42, 0, "#", "XYZxx", "Foo">>,
1696 1 ?assertEqual(#error{code = 42, state = <<"XYZxx">>, msg = <<"Foo">>},
1697 1 parse_error_packet(Body)),
1698 1 ok.
1699
1700 parse_eof_test() ->
1701 %% Example from "MySQL Internals", revision 307, section 14.1.3.3 EOF_Packet
1702 1 Packet = <<16#05, 16#00, 16#00, 16#05, 16#fe, 16#00, 16#00, 16#02, 16#00>>,
1703 1 <<_Header:4/binary-unit:8, Body/binary>> = Packet,
1704 %% Ignore header. Parse body as an eof_packet.
1705 1 ?assertEqual(#eof{warning_count = 0,
1706 1 status = ?SERVER_STATUS_AUTOCOMMIT},
1707 1 parse_eof_packet(Body)),
1708 1 ok.
1709
1710 hash_password_test() ->
1711 1 ?assertEqual(<<222,207,222,139,41,181,202,13,191,241,
1712 1 234,234,73,127,244,101,205,3,28,251>>,
1713 hash_password(?authmethod_mysql_native_password,
1714 1 <<"foo">>, <<"abcdefghijklmnopqrst">>)),
1715 1 ?assertEqual(<<>>, hash_password(?authmethod_mysql_native_password,
1716 1 <<>>, <<"abcdefghijklmnopqrst">>)),
1717 1 ?assertEqual(<<125,155,142,2,20,139,6,254,65,126,239,
1718 146,107,77,17,8,120,55,247,33,87,16,76,
1719 1 63,128,131,60,188,58,81,171,242>>,
1720 hash_password(?authmethod_caching_sha2_password,
1721 1 <<"foo">>, <<"abcdefghijklmnopqrst">>)),
1722 1 ?assertEqual(<<>>, hash_password(?authmethod_caching_sha2_password,
1723 1 <<>>, <<"abcdefghijklmnopqrst">>)).
1724
1725 valid_params_test() ->
1726 1 ValidParams = [
1727 null,
1728 1,
1729 0.5,
1730 <<>>, <<$x>>, <<0:1>>,
1731
1732 %% valid unicode
1733 [], [$x], [16#E4],
1734
1735 %% valid date
1736 {1, 2, 3},
1737
1738 %% valid time
1739 {1, {2, 3, 4}}, {1, {2, 3, 4.5}},
1740
1741 %% valid datetime
1742 {{1, 2, 3}, {4, 5, 6}}, {{1, 2, 3}, {4, 5, 6.5}}
1743 ],
1744
1745 1 InvalidParams = [
1746 x,
1747 [x],
1748 {},
1749 self(),
1750 make_ref(),
1751
:-(
fun () -> ok end,
1752
1753 %% invalid unicode
1754 [16#FFFFFFFF],
1755
1756 %% invalid date
1757 {x, 1, 2}, {1, x, 2}, {1, 2, x},
1758
1759 %% invalid time
1760 {x, {1, 2, 3}}, {1, {x, 2, 3}},
1761 {1, {2, x, 3}}, {1, {2, 3, x}},
1762
1763 %% invalid datetime
1764 {{x, 1, 2}, {3, 4, 5}}, {{1, x, 2}, {3, 4, 5}},
1765 {{1, 2, x}, {3, 4, 5}}, {{1, 2, 3}, {x, 4, 5}},
1766 {{1, 2, 3}, {4, x, 5}}, {{1, 2, 3}, {4, 5, x}}
1767 ],
1768
1769 1 lists:foreach(
1770 fun (ValidParam) ->
1771 14 ?assert(is_valid_param(ValidParam))
1772 end,
1773 ValidParams),
1774 1 ?assert(valid_params(ValidParams)),
1775
1776 1 lists:foreach(
1777 fun (InvalidParam) ->
1778 20 ?assertNot(is_valid_param(InvalidParam))
1779 end,
1780 InvalidParams),
1781 1 ?assertNot(valid_params(InvalidParams)),
1782 1 ?assertNot(valid_params(ValidParams ++ InvalidParams)).
1783
1784 valid_path_test() ->
1785 1 ValidPaths = [
1786 <<"/">>,
1787 <<"/tmp">>,
1788 <<"/tmp/">>,
1789 <<"/tmp/foo">>
1790 ],
1791 1 InvalidPaths = [
1792 <<>>,
1793 <<"tmp">>,
1794 <<"tmp/">>,
1795 <<"tmp/foo">>,
1796 <<"../tmp">>,
1797 <<"/tmp/..">>,
1798 <<"/tmp/foo/../bar">>,
1799 "/tmp"
1800 ],
1801 1 lists:foreach(
1802 fun (ValidPath) ->
1803 4 ?assert(valid_path(ValidPath))
1804 end,
1805 ValidPaths
1806 ),
1807 1 lists:foreach(
1808 fun (InvalidPath) ->
1809 8 ?assertNot(valid_path(InvalidPath))
1810 end,
1811 InvalidPaths
1812 ).
1813
1814 allowed_path_test() ->
1815 1 AllowedPaths = [
1816 <<"/tmp/foo/file.csv">>,
1817 <<"/tmp/foo/bar/">>,
1818 <<"/tmp/foo/baz">>
1819 ],
1820 1 ValidPaths = [
1821 <<"/tmp/foo/file.csv">>,
1822 <<"/tmp/foo/bar/file.csv">>,
1823 <<"/tmp/foo/baz/file.csv">>,
1824 <<"/tmp/foo/baz">>
1825 ],
1826 1 InvalidPaths = [
1827 <<"/tmp/file.csv">>,
1828 <<"/tmp/foo/other_file.csv">>,
1829 <<"/tmp/foo/other_dir/file.csv">>,
1830 <<"/tmp/foo/../file.csv">>,
1831 <<"/tmp/foo/../bar/file.csv">>,
1832 <<"/tmp/foo/bar/">>,
1833 <<"/tmp/foo/barbaz">>
1834 ],
1835 1 lists:foreach(
1836 fun (ValidPath) ->
1837 4 ?assert(allowed_path(ValidPath, AllowedPaths))
1838 end,
1839 ValidPaths
1840 ),
1841 1 lists:foreach(
1842 fun (InvalidPath) ->
1843 7 ?assertNot(allowed_path(InvalidPath, AllowedPaths))
1844 end,
1845 InvalidPaths
1846 ).
1847
1848 -endif.
1849
Line Hits Source