Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/myxql.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ defmodule MyXQL do
| {:prepare, :force_named | :named | :unnamed}
| {:disconnect_on_error_codes, [atom()]}
| {:enable_cleartext_plugin, boolean()}
| {:local_infile, boolean()}
| DBConnection.start_option()

@type option() :: DBConnection.option()
Expand Down Expand Up @@ -103,6 +104,8 @@ defmodule MyXQL do

* `:enable_cleartext_plugin` - Set to `true` to send password as cleartext (default: `false`)

* `:local_infile` - Set to `true` to enable LOCAL INFILE capability (default: `false`)

The given options are passed down to DBConnection, some of the most commonly used ones are
documented below:

Expand Down
42 changes: 39 additions & 3 deletions lib/myxql/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
:max_packet_size,
:charset,
:collation,
:enable_cleartext_plugin
:enable_cleartext_plugin,
:local_infile
]

@sock_opts [mode: :binary, packet: :raw, active: false]
Expand Down Expand Up @@ -67,7 +68,8 @@
socket_options: (opts[:socket_options] || []) ++ @sock_opts,
charset: Keyword.get(opts, :charset),
collation: Keyword.get(opts, :collation),
enable_cleartext_plugin: Keyword.get(opts, :enable_cleartext_plugin, false)
enable_cleartext_plugin: Keyword.get(opts, :enable_cleartext_plugin, false),
local_infile: Keyword.get(opts, :local_infile, false)
}
end

Expand Down Expand Up @@ -135,7 +137,32 @@

def com_query(client, statement, result_state \\ :single) do
with :ok <- send_com(client, {:com_query, statement}) do
recv_packets(client, &decode_com_query_response/3, :initial, result_state)
case recv_packets(client, &decode_com_query_response/3, :initial, result_state) do
{:ok, {:local_infile, filename}} ->
case handle_local_infile(client, filename) do
:ok ->
recv_packets(client, &decode_com_query_response/3, :initial, result_state)

error ->
error
end

other ->
other
end
end
end

def handle_local_infile(client, filename) do
case File.read(filename) do
{:ok, content} ->
with :ok <- send_packet(client, content, 2),
:ok <- send_packet(client, <<>>, 3) do
:ok
end

{:error, reason} ->

Check warning on line 164 in lib/myxql/client.ex

View workflow job for this annotation

GitHub Actions / test mysql:5.7 1.13/25.3

variable "reason" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 164 in lib/myxql/client.ex

View workflow job for this annotation

GitHub Actions / test mariadb:10.3 1.13/25.3

variable "reason" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 164 in lib/myxql/client.ex

View workflow job for this annotation

GitHub Actions / test mysql:8.0 1.13/25.3

variable "reason" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 164 in lib/myxql/client.ex

View workflow job for this annotation

GitHub Actions / test mysql:5.6 1.13/25.3

variable "reason" is unused (if the variable is not meant to be used, prefix it with an underscore)
send_packet(client, <<>>, 2)
end
end

Expand Down Expand Up @@ -265,6 +292,15 @@
{:many, results} -> {:ok, [result | results]}
end

{:local_infile, filename} ->
case handle_local_infile(client, filename) do
:ok ->
recv_packets(rest, decoder, decoder_state, result_state, timeout, client)

{:error, reason} ->
{:error, reason}
end

{:error, _} = error ->
error
end
Expand Down
12 changes: 12 additions & 0 deletions lib/myxql/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ defmodule MyXQL.Protocol do
])
|> maybe_put_capability_flag(:client_connect_with_db, !is_nil(config.database))
|> maybe_put_capability_flag(:client_ssl, is_list(config.ssl_opts))
|> maybe_put_capability_flag(:client_local_files, config.local_infile)

if config.ssl_opts && !has_capability_flag?(server_capability_flags, :client_ssl) do
{:error, :server_does_not_support_ssl}
Expand Down Expand Up @@ -331,6 +332,12 @@ defmodule MyXQL.Protocol do
{:halt, decode_ok_packet_body(rest)}
end

def decode_com_query_response(<<0xFB, rest::binary>>, "", :initial) do
{filename, _remaining} = take_string_nul(rest)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we can discard the rest here? Or should we assert it is an empty string as well? Or nul even there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According here. The LOCAL INFILE request packet is

  • 1 byte: 0xFB and
  • N bytes: the filename as a NUL-terminated string

So after parsing the filename, there should be nothing left in the packet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we can match on the empty string as second tuple element as well!


{:local_infile, filename}
end

def decode_com_query_response(<<0xFF, rest::binary>>, "", :initial) do
{:halt, decode_err_packet_body(rest)}
end
Expand Down Expand Up @@ -513,6 +520,11 @@ defmodule MyXQL.Protocol do
{:cont, :initial, {:many, [resultset | results]}}
end

defp decode_resultset(<<0xFB, rest::binary>>, _next_data, :initial, _row_decoder) do
{filename, ""} = take_string_nul(rest)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Base on the changes to take_string_nul, it seems you are not expecting a nul here? So maybe we should not call/change said function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we shouldn't change the function!

{:local_infile, filename}
end

defp decode_resultset(payload, _next_data, :initial, _row_decoder) do
{:cont, {:column_defs, decode_int_lenenc(payload), []}}
end
Expand Down
15 changes: 11 additions & 4 deletions lib/myxql/protocol/types.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ defmodule MyXQL.Protocol.Types do
def encode_int_lenenc(int) when int < 0xFFFFFFFFFFFFFFFF, do: <<0xFE, int::uint8()>>

def decode_int_lenenc(binary) do
{integer, ""} = take_int_lenenc(binary)
{integer, _rest} = take_int_lenenc(binary)
integer
end

def take_int_lenenc(<<int::uint1(), rest::binary>>) when int < 251, do: {int, rest}
def take_int_lenenc(<<0xFB, rest::binary>>), do: {nil, rest}
def take_int_lenenc(<<0xFC, int::uint2(), rest::binary>>), do: {int, rest}
def take_int_lenenc(<<0xFD, int::uint3(), rest::binary>>), do: {int, rest}
def take_int_lenenc(<<0xFE, int::uint8(), rest::binary>>), do: {int, rest}
def take_int_lenenc(<<0xFF, rest::binary>>), do: {:error, rest}

# https://dev.mysql.com/doc/internals/en/string.html#packet-Protocol::FixedLengthString
defmacro string(size) do
Expand Down Expand Up @@ -68,8 +70,13 @@ defmodule MyXQL.Protocol.Types do

def take_string_nul(""), do: {nil, ""}

def take_string_nul(binary) do
[string, rest] = :binary.split(binary, <<0>>)
{string, rest}
def take_string_nul(binary) when is_binary(binary) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change still necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tested it without the changes to the method, an exception occurred when using the client. So its necessary

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why? We said the filename is always followed by a nul byte, right? That will always return a two element list:

iex(2)> :binary.split(<<"/", 0>>, [<<0>>])
["/", ""]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I change it I got this error:

stopped: ** (MatchError) no match of right hand side value: ["/Users/iagocavalcante/Workspaces/i9Amazon/i9_processador/25332012000225-pdv_caixa_lancamento-20250618_210733-T-110.txt"]
    (myxql 0.8.0-dev) lib/myxql/protocol/types.ex:74: MyXQL.Protocol.Types.take_string_nul/1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should I do to have this merged?

case :binary.split(binary, <<0>>) do
[string] ->
{string, ""}

[string, rest] ->
{string, rest}
end
end
end
Loading