Skip to content

Commit 12f82ab

Browse files
committed
BUG19522948: Fix data corruption with TEXT and prepared statements
Using prepared statements while inserting sufficiently large data in a table with TEXT type column, data was getting corrupted. This was due to wrong encoding of data length while sending prepared statement packet. We fix the issue by implementing a new function utils.lc_int(). A unit test has been added for BUG#19522948.
1 parent 761d159 commit 12f82ab

File tree

4 files changed

+81
-8
lines changed

4 files changed

+81
-8
lines changed

lib/mysql/connector/protocol.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -636,24 +636,25 @@ def make_stmt_execute(self, statement_id, data=(), parameters=(),
636636
values.append(packed)
637637
elif isinstance(value, str):
638638
if PY2:
639-
values.append(utils.intstore(len(value)) + value)
639+
values.append(utils.lc_int(len(value)) +
640+
value)
640641
else:
641642
value = value.encode(charset)
642643
values.append(
643-
utils.intstore(len(value)) + value)
644+
utils.lc_int(len(value)) + value)
644645
field_type = FieldType.VARCHAR
645646
elif isinstance(value, bytes):
646-
values.append(utils.intstore(len(value)) + value)
647+
values.append(utils.lc_int(len(value)) + value)
647648
field_type = FieldType.BLOB
648649
elif PY2 and \
649650
isinstance(value, unicode): # pylint: disable=E0602
650651
value = value.encode(charset)
651-
values.append(utils.intstore(len(value)) + value)
652+
values.append(utils.lc_int(len(value)) + value)
652653
field_type = FieldType.VARCHAR
653654
elif isinstance(value, Decimal):
654655
values.append(
655-
utils.intstore(len(str(value).encode(charset))) +
656-
str(value).encode(charset))
656+
utils.lc_int(len(str(value).encode(
657+
charset))) + str(value).encode(charset))
657658
field_type = FieldType.DECIMAL
658659
elif isinstance(value, float):
659660
values.append(struct.pack('d', value))

lib/mysql/connector/utils.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ def int4store(i):
100100

101101
def int8store(i):
102102
"""
103-
Takes an unsigned integer (4 bytes) and packs it as string.
103+
Takes an unsigned integer (8 bytes) and packs it as string.
104104
105105
Returns string.
106106
"""
107107
if i < 0 or i > 18446744073709551616:
108-
raise ValueError('int4store requires 0 <= i <= 2^64')
108+
raise ValueError('int8store requires 0 <= i <= 2^64')
109109
else:
110110
return bytearray(struct.pack('<Q', i))
111111

@@ -136,6 +136,24 @@ def intstore(i):
136136
return formed_string(i)
137137

138138

139+
def lc_int(i):
140+
"""
141+
Takes an unsigned integer and packs it as bytes,
142+
with the information of how much bytes the encoded int takes.
143+
"""
144+
if i < 0 or i > 18446744073709551616:
145+
raise ValueError('Requires 0 <= i <= 2^64')
146+
147+
if i <= 255:
148+
return bytearray(struct.pack('<B', i))
149+
elif i <= 65535:
150+
return b'\xfc' + bytearray(struct.pack('<H', i))
151+
elif i <= 16777215:
152+
return b'\xfd' + bytearray(struct.pack('<I', i)[0:3])
153+
else:
154+
return b'\xfe' + bytearray(struct.pack('<Q', i))
155+
156+
139157
def read_bytes(buf, size):
140158
"""
141159
Reads bytes from a buffer.

tests/test_bugs.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2915,3 +2915,45 @@ def test_dbapi(self):
29152915
type_codes = [row[1] for row in cur.description]
29162916
self.assertEqual(exp, type_codes)
29172917
cur.close()
2918+
2919+
2920+
class BugOra19522948(tests.MySQLConnectorTests):
2921+
"""BUG#19522948: DATA CORRUPTION WITH TEXT FIELDS
2922+
"""
2923+
def setUp(self):
2924+
config = tests.get_mysql_config()
2925+
self.cnx = connection.MySQLConnection(**config)
2926+
self.cur = self.cnx.cursor()
2927+
2928+
self.tbl = 'Bug19522948'
2929+
self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl))
2930+
2931+
create = "CREATE TABLE {0} (c1 LONGTEXT NOT NULL)".format(
2932+
self.tbl
2933+
)
2934+
self.cur.execute(create)
2935+
2936+
def tearDown(self):
2937+
self.cur.execute("DROP TABLE IF EXISTS {0}".format(self.tbl))
2938+
self.cur.close()
2939+
self.cnx.close()
2940+
2941+
def test_row_to_python(self):
2942+
cur = self.cnx.cursor(prepared=True)
2943+
2944+
data = "test_data"*10
2945+
cur.execute("INSERT INTO {0} (c1) VALUES (?)".format(self.tbl), (data,))
2946+
self.cur.execute("SELECT * FROM {0}".format(self.tbl))
2947+
self.assertEqual((data,), self.cur.fetchone())
2948+
self.cur.execute("TRUNCATE TABLE {0}".format(self.tbl))
2949+
2950+
data = "test_data"*1000
2951+
cur.execute("INSERT INTO {0} (c1) VALUES (?)".format(self.tbl), (data,))
2952+
self.cur.execute("SELECT * FROM {0}".format(self.tbl))
2953+
self.assertEqual((data,), self.cur.fetchone())
2954+
self.cur.execute("TRUNCATE TABLE {0}".format(self.tbl))
2955+
2956+
data = "test_data"*10000
2957+
cur.execute("INSERT INTO {0} (c1) VALUES (?)".format(self.tbl), (data,))
2958+
self.cur.execute("SELECT * FROM {0}".format(self.tbl))
2959+
self.assertEqual((data,), self.cur.fetchone())

tests/test_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ def test_intstore(self):
123123
except ValueError as err:
124124
self.fail("intstore failed with 'int{0}store: {1}".format(i, err))
125125

126+
def test_lc_int(self):
127+
prefix = (b'', b'\xfc', b'\xfd', b'\xfe')
128+
try:
129+
for i, j in enumerate((8, 16, 24, 64)):
130+
val = 2 ** (j - 1)
131+
lenenc = utils.lc_int(val)
132+
exp = prefix[i] + \
133+
getattr(utils, 'int{0}store'.format(int(j/8)))(val)
134+
self.assertEqual(exp, lenenc)
135+
except ValueError as err:
136+
self.fail("length_encoded_int failed for size {0}".format(j, err))
137+
126138
def test_read_bytes(self):
127139
"""Read a number of bytes from a buffer"""
128140
buf = bytearray(b"ABCDEFghijklm")

0 commit comments

Comments
 (0)