Skip to content

Commit 5fac050

Browse files
committed
Merge pull request #446 from sodabrew/local_infile_handler
Local local infile handler
2 parents 5f5372c + 9e56c07 commit 5fac050

File tree

7 files changed

+178
-5
lines changed

7 files changed

+178
-5
lines changed

ext/mysql2/client.c

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#include <mysql2_ext.h>
2-
#include <client.h>
2+
33
#include <errno.h>
44
#ifndef _WIN32
55
#include <sys/socket.h>
@@ -146,9 +146,13 @@ static VALUE rb_raise_mysql2_error(mysql_client_wrapper *wrapper) {
146146

147147
static void *nogvl_init(void *ptr) {
148148
MYSQL *client;
149+
mysql_client_wrapper *wrapper = (mysql_client_wrapper *)ptr;
149150

150151
/* may initialize embedded server and read /etc/services off disk */
151-
client = mysql_init((MYSQL *)ptr);
152+
client = mysql_init(wrapper->client);
153+
154+
if (client) mysql2_set_local_infile(client, wrapper);
155+
152156
return (void*)(client ? Qtrue : Qfalse);
153157
}
154158

@@ -1124,7 +1128,7 @@ static VALUE set_read_default_group(VALUE self, VALUE value) {
11241128
static VALUE initialize_ext(VALUE self) {
11251129
GET_CLIENT(self);
11261130

1127-
if ((VALUE)rb_thread_call_without_gvl(nogvl_init, wrapper->client, RUBY_UBF_IO, 0) == Qfalse) {
1131+
if ((VALUE)rb_thread_call_without_gvl(nogvl_init, wrapper, RUBY_UBF_IO, 0) == Qfalse) {
11281132
/* TODO: warning - not enough memory? */
11291133
return rb_raise_mysql2_error(wrapper);
11301134
}
@@ -1139,7 +1143,7 @@ void init_mysql2_client() {
11391143
int i;
11401144
int dots = 0;
11411145
const char *lib = mysql_get_client_info();
1142-
1146+
11431147
for (i = 0; lib[i] != 0 && MYSQL_LINK_VERSION[i] != 0; i++) {
11441148
if (lib[i] == '.') {
11451149
dots++;

ext/mysql2/infile.c

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#include <mysql2_ext.h>
2+
3+
#include <errno.h>
4+
#include <unistd.h>
5+
6+
#define ERROR_LEN 1024
7+
typedef struct
8+
{
9+
int fd;
10+
char *filename;
11+
char error[ERROR_LEN];
12+
mysql_client_wrapper *wrapper;
13+
} mysql2_local_infile_data;
14+
15+
/* MySQL calls this function when a user begins a LOAD DATA LOCAL INFILE query.
16+
*
17+
* Allocate a data struct and pass it back through the data pointer.
18+
*
19+
* Returns:
20+
* 0 on success
21+
* 1 on error
22+
*/
23+
static int
24+
mysql2_local_infile_init(void **ptr, const char *filename, void *userdata)
25+
{
26+
mysql2_local_infile_data *data = malloc(sizeof(mysql2_local_infile_data));
27+
if (!data) return 1;
28+
29+
*ptr = data;
30+
data->error[0] = 0;
31+
data->wrapper = userdata;
32+
33+
data->filename = strdup(filename);
34+
if (!data->filename) {
35+
snprintf(data->error, ERROR_LEN, "%s: %s", strerror(errno), filename);
36+
return 1;
37+
}
38+
39+
data->fd = open(filename, O_RDONLY);
40+
if (data->fd < 0) {
41+
snprintf(data->error, ERROR_LEN, "%s: %s", strerror(errno), filename);
42+
return 1;
43+
}
44+
45+
return 0;
46+
}
47+
48+
/* MySQL calls this function to read data from the local file.
49+
*
50+
* Returns:
51+
* > 0 number of bytes read
52+
* == 0 end of file
53+
* < 0 error
54+
*/
55+
static int
56+
mysql2_local_infile_read(void *ptr, char *buf, uint buf_len)
57+
{
58+
int count;
59+
mysql2_local_infile_data *data = (mysql2_local_infile_data *)ptr;
60+
61+
count = (int)read(data->fd, buf, buf_len);
62+
if (count < 0) {
63+
snprintf(data->error, ERROR_LEN, "%s: %s", strerror(errno), data->filename);
64+
}
65+
66+
return count;
67+
}
68+
69+
/* MySQL calls this function when we're done with the LOCAL INFILE query.
70+
*
71+
* ptr will be null if the init function failed.
72+
*/
73+
static void
74+
mysql2_local_infile_end(void *ptr)
75+
{
76+
mysql2_local_infile_data *data = (mysql2_local_infile_data *)ptr;
77+
if (data) {
78+
if (data->fd >= 0)
79+
close(data->fd);
80+
if (data->filename)
81+
free(data->filename);
82+
free(data);
83+
}
84+
}
85+
86+
/* MySQL calls this function if any of the functions above returned an error.
87+
*
88+
* This function is called even if init failed, with whatever ptr value
89+
* init has set, regardless of the return value of the init function.
90+
*
91+
* Returns:
92+
* Error message number (see http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html)
93+
*/
94+
static int
95+
mysql2_local_infile_error(void *ptr, char *error_msg, uint error_msg_len)
96+
{
97+
mysql2_local_infile_data *data = (mysql2_local_infile_data *) ptr;
98+
99+
if (data) {
100+
snprintf(error_msg, error_msg_len, "%s", data->error);
101+
return CR_UNKNOWN_ERROR;
102+
}
103+
104+
snprintf(error_msg, error_msg_len, "Out of memory");
105+
return CR_OUT_OF_MEMORY;
106+
}
107+
108+
/* Tell MySQL Client to use our own local_infile functions.
109+
* This is both due to bugginess in the default handlers,
110+
* and to improve the Rubyness of the handlers here.
111+
*/
112+
void mysql2_set_local_infile(MYSQL *mysql, void *userdata)
113+
{
114+
mysql_set_local_infile_handler(mysql,
115+
mysql2_local_infile_init,
116+
mysql2_local_infile_read,
117+
mysql2_local_infile_end,
118+
mysql2_local_infile_error, userdata);
119+
}

ext/mysql2/infile.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
void mysql2_set_local_infile(MYSQL *mysql, void *userdata);

ext/mysql2/mysql2_ext.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ typedef unsigned int uint;
4141

4242
#include <client.h>
4343
#include <result.h>
44+
#include <infile.h>
4445

4546
#endif

ext/mysql2/result.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <mysql2_ext.h>
2+
23
#include <stdint.h>
34

45
#include "mysql_enc_to_ruby.h"

spec/mysql2/client_spec.rb

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ def connect *args
7878
end
7979

8080
it "should be able to connect via SSL options" do
81-
pending("DON'T WORRY, THIS TEST PASSES :) - but is machine-specific. You need to have MySQL running with SSL configured and enabled. Then update the paths in this test to your needs and remove the pending state.")
81+
ssl = @client.query "SHOW VARIABLES LIKE 'have_%ssl'"
82+
ssl_enabled = ssl.any? {|x| x['Value'] == 'ENABLED'}
83+
pending("DON'T WORRY, THIS TEST PASSES - but SSL is not enabled in your MySQL daemon.") unless ssl_enabled
84+
pending("DON'T WORRY, THIS TEST PASSES - but you must update the SSL cert paths in this test and remove this pending state.")
8285
ssl_client = nil
8386
lambda {
8487
ssl_client = Mysql2::Client.new(
@@ -197,6 +200,49 @@ def connect *args
197200
end
198201
end
199202

203+
context ":local_infile" do
204+
before(:all) do
205+
@client_i = Mysql2::Client.new DatabaseCredentials['root'].merge(:local_infile => true)
206+
local = @client_i.query "SHOW VARIABLES LIKE 'local_infile'"
207+
local_enabled = local.any? {|x| x['Value'] == 'ON'}
208+
pending("DON'T WORRY, THIS TEST PASSES - but LOCAL INFILE is not enabled in your MySQL daemon.") unless local_enabled
209+
210+
@client_i.query %[
211+
CREATE TABLE IF NOT EXISTS infileTest (
212+
id MEDIUMINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
213+
foo VARCHAR(10),
214+
bar MEDIUMTEXT
215+
)
216+
]
217+
end
218+
219+
after(:all) do
220+
@client_i.query "DROP TABLE infileTest"
221+
end
222+
223+
it "should raise an error when local_infile is disabled" do
224+
client = Mysql2::Client.new DatabaseCredentials['root'].merge(:local_infile => false)
225+
lambda {
226+
client.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest"
227+
}.should raise_error(Mysql2::Error, %r{command is not allowed})
228+
end
229+
230+
it "should raise an error when a non-existent file is loaded" do
231+
lambda {
232+
@client_i.query "LOAD DATA LOCAL INFILE 'this/file/is/not/here' INTO TABLE infileTest"
233+
}.should_not raise_error(Mysql2::Error, %r{file not found: this/file/is/not/here})
234+
end
235+
236+
it "should LOAD DATA LOCAL INFILE" do
237+
@client_i.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest"
238+
info = @client_i.query_info
239+
info.should eql({:records => 1, :deleted => 0, :skipped => 0, :warnings => 0})
240+
241+
result = @client_i.query "SELECT * FROM infileTest"
242+
result.first.should eql({'id' => 1, 'foo' => 'Hello', 'bar' => 'World'})
243+
end
244+
end
245+
200246
it "should expect connect_timeout to be a positive integer" do
201247
lambda {
202248
Mysql2::Client.new(:connect_timeout => -1)

spec/test_data

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
\N Hello World

0 commit comments

Comments
 (0)