Skip to content

Commit f10a337

Browse files
committed
Avoid RangeError on integers larger than LONG_LONG
* Use rb_absint_size and rb_absint_singlebit_p on Ruby 2.1 or later. They don't allocate new objects and fast. * Use rb_big_cmp on Ruby 1.9.x and 2.0.0 * Use rb_funcall(bignum, rb_intern("<=>")) on Ruby 1.8 and REE Note that this works on Ruby 2.4.
1 parent f660d51 commit f10a337

File tree

3 files changed

+92
-3
lines changed

3 files changed

+92
-3
lines changed

Diff for: ext/mysql2/extconf.rb

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def add_ssl_defines(header)
2222
$CFLAGS << ' -DNO_SSL_MODE_SUPPORT' if has_no_support
2323
end
2424

25+
# 2.1+
26+
have_func('rb_absint_size')
27+
have_func('rb_absint_singlebit_p')
28+
2529
# 2.0-only
2630
have_header('ruby/thread.h') && have_func('rb_thread_call_without_gvl', 'ruby/thread.h')
2731

@@ -30,6 +34,7 @@ def add_ssl_defines(header)
3034
have_func('rb_wait_for_single_fd')
3135
have_func('rb_hash_dup')
3236
have_func('rb_intern3')
37+
have_func('rb_big_cmp')
3338

3439
# borrowed from mysqlplus
3540
# http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb

Diff for: ext/mysql2/statement.c

+67-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ VALUE cMysql2Statement;
44
extern VALUE mMysql2, cMysql2Error, cBigDecimal, cDateTime, cDate;
55
static VALUE sym_stream, intern_new_with_args, intern_each;
66
static VALUE intern_usec, intern_sec, intern_min, intern_hour, intern_day, intern_month, intern_year, intern_to_s;
7+
#ifndef HAVE_RB_BIG_CMP
8+
static ID id_cmp;
9+
#endif
710

811
#define GET_STATEMENT(self) \
912
mysql_stmt_wrapper *stmt_wrapper; \
@@ -203,6 +206,50 @@ static void set_buffer_for_string(MYSQL_BIND* bind_buffer, unsigned long *length
203206
xfree(length_buffers); \
204207
}
205208

209+
/* return 0 if the given bignum can cast as LONG_LONG, otherwise 1 */
210+
static int my_big2ll(VALUE bignum, LONG_LONG *ptr)
211+
{
212+
unsigned LONG_LONG num;
213+
size_t len;
214+
#ifdef HAVE_RB_ABSINT_SIZE
215+
int nlz_bits = 0;
216+
len = rb_absint_size(bignum, &nlz_bits);
217+
#else
218+
len = RBIGNUM_LEN(bignum) * SIZEOF_BDIGITS;
219+
#endif
220+
if (len > sizeof(LONG_LONG)) goto overflow;
221+
if (RBIGNUM_POSITIVE_P(bignum)) {
222+
num = rb_big2ull(bignum);
223+
if (num > LLONG_MAX)
224+
goto overflow;
225+
*ptr = num;
226+
}
227+
else {
228+
if (len == 8 &&
229+
#ifdef HAVE_RB_ABSINT_SIZE
230+
nlz_bits == 0 &&
231+
#endif
232+
#if defined(HAVE_RB_ABSINT_SIZE) && defined(HAVE_RB_ABSINT_SINGLEBIT_P)
233+
/* Optimized to avoid object allocation for Ruby 2.1+
234+
* only -0x8000000000000000 is safe if `len == 8 && nlz_bits == 0`
235+
*/
236+
!rb_absint_singlebit_p(bignum)
237+
#elif defined(HAVE_RB_BIG_CMP)
238+
rb_big_cmp(bignum, LL2NUM(LLONG_MIN)) == INT2FIX(-1)
239+
#else
240+
/* Ruby 1.8.7 and REE doesn't have rb_big_cmp */
241+
rb_funcall(bignum, id_cmp, 1, LL2NUM(LLONG_MIN)) == INT2FIX(-1)
242+
#endif
243+
) {
244+
goto overflow;
245+
}
246+
*ptr = rb_big2ll(bignum);
247+
}
248+
return 0;
249+
overflow:
250+
return 1;
251+
}
252+
206253
/* call-seq: stmt.execute
207254
*
208255
* Executes the current prepared statement, returns +result+.
@@ -264,9 +311,23 @@ static VALUE execute(int argc, VALUE *argv, VALUE self) {
264311
#endif
265312
break;
266313
case T_BIGNUM:
267-
bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG;
268-
bind_buffers[i].buffer = xmalloc(sizeof(long long int));
269-
*(LONG_LONG*)(bind_buffers[i].buffer) = rb_big2ll(argv[i]);
314+
{
315+
LONG_LONG num;
316+
if (my_big2ll(argv[i], &num) == 0) {
317+
bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG;
318+
bind_buffers[i].buffer = xmalloc(sizeof(long long int));
319+
*(LONG_LONG*)(bind_buffers[i].buffer) = num;
320+
} else {
321+
/* The bignum was larger than we can fit in LONG_LONG, send it as a string */
322+
VALUE rb_val_as_string = rb_big2str(argv[i], 10);
323+
bind_buffers[i].buffer_type = MYSQL_TYPE_NEWDECIMAL;
324+
params_enc[i] = rb_val_as_string;
325+
#ifdef HAVE_RUBY_ENCODING_H
326+
params_enc[i] = rb_str_export_to_enc(params_enc[i], conn_enc);
327+
#endif
328+
set_buffer_for_string(&bind_buffers[i], &length_buffers[i], params_enc[i]);
329+
}
330+
}
270331
break;
271332
case T_FLOAT:
272333
bind_buffers[i].buffer_type = MYSQL_TYPE_DOUBLE;
@@ -500,4 +561,7 @@ void init_mysql2_statement() {
500561
intern_year = rb_intern("year");
501562

502563
intern_to_s = rb_intern("to_s");
564+
#ifndef HAVE_RB_BIG_CMP
565+
id_cmp = rb_intern("<=>");
566+
#endif
503567
}

Diff for: spec/mysql2/statement_spec.rb

+20
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ def stmt_count
6161
expect(rows).to eq([{ "1" => 1 }])
6262
end
6363

64+
it "should handle bignum but in int64_t" do
65+
stmt = @client.prepare('SELECT ? AS max, ? AS min')
66+
int64_max = (1 << 63) - 1
67+
int64_min = -(1 << 63)
68+
result = stmt.execute(int64_max, int64_min)
69+
expect(result.to_a).to eq(['max' => int64_max, 'min' => int64_min])
70+
end
71+
72+
it "should handle bignum but beyond int64_t" do
73+
stmt = @client.prepare('SELECT ? AS max1, ? AS max2, ? AS max3, ? AS min1, ? AS min2, ? AS min3')
74+
int64_max1 = (1 << 63)
75+
int64_max2 = (1 << 64) - 1
76+
int64_max3 = 1 << 64
77+
int64_min1 = -(1 << 63) - 1
78+
int64_min2 = -(1 << 64) + 1
79+
int64_min3 = -0xC000000000000000
80+
result = stmt.execute(int64_max1, int64_max2, int64_max3, int64_min1, int64_min2, int64_min3)
81+
expect(result.to_a).to eq(['max1' => int64_max1, 'max2' => int64_max2, 'max3' => int64_max3, 'min1' => int64_min1, 'min2' => int64_min2, 'min3' => int64_min3])
82+
end
83+
6484
it "should keep its result after other query" do
6585
@client.query 'USE test'
6686
@client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int)'

0 commit comments

Comments
 (0)