Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pymysql/protocol.py: 31%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# Python implementation of low level MySQL client-server protocol
2# http://dev.mysql.com/doc/internals/en/client-server-protocol.html
4import struct
5import sys
7from . import err
8from .charset import MBLENGTH
9from .constants import FIELD_TYPE, SERVER_STATUS
11DEBUG = False
13NULL_COLUMN = 251
14UNSIGNED_CHAR_COLUMN = 251
15UNSIGNED_SHORT_COLUMN = 252
16UNSIGNED_INT24_COLUMN = 253
17UNSIGNED_INT64_COLUMN = 254
20def dump_packet(data): # pragma: no cover
21 def printable(data):
22 if 32 <= data < 127:
23 return chr(data)
24 return "."
26 try:
27 print("packet length:", len(data))
28 for i in range(1, 7):
29 f = sys._getframe(i)
30 print("call[%d]: %s (line %d)" % (i, f.f_code.co_name, f.f_lineno))
31 print("-" * 66)
32 except ValueError:
33 pass
34 dump_data = [data[i : i + 16] for i in range(0, min(len(data), 256), 16)]
35 for d in dump_data:
36 print(
37 " ".join(f"{x:02X}" for x in d)
38 + " " * (16 - len(d))
39 + " " * 2
40 + "".join(printable(x) for x in d)
41 )
42 print("-" * 66)
43 print()
46class MysqlPacket:
47 """Representation of a MySQL response packet.
49 Provides an interface for reading/parsing the packet results.
50 """
52 __slots__ = ("_data", "_position")
54 def __init__(self, data, encoding):
55 self._position = 0
56 self._data = data
58 def get_all_data(self):
59 return self._data
61 def read(self, size):
62 """Read the first 'size' bytes in packet and advance cursor past them."""
63 result = self._data[self._position : (self._position + size)]
64 if len(result) != size:
65 error = (
66 "Result length not requested length:\n"
67 f"Expected={size}. Actual={len(result)}. Position: {self._position}. Data Length: {len(self._data)}"
68 )
69 if DEBUG:
70 print(error)
71 self.dump()
72 raise AssertionError(error)
73 self._position += size
74 return result
76 def read_all(self):
77 """Read all remaining data in the packet.
79 (Subsequent read() will return errors.)
80 """
81 result = self._data[self._position :]
82 self._position = None # ensure no subsequent read()
83 return result
85 def advance(self, length):
86 """Advance the cursor in data buffer 'length' bytes."""
87 new_position = self._position + length
88 if new_position < 0 or new_position > len(self._data):
89 raise Exception(
90 f"Invalid advance amount ({length}) for cursor. Position={new_position}"
91 )
92 self._position = new_position
94 def rewind(self, position=0):
95 """Set the position of the data buffer cursor to 'position'."""
96 if position < 0 or position > len(self._data):
97 raise Exception("Invalid position to rewind cursor to: %s." % position)
98 self._position = position
100 def get_bytes(self, position, length=1):
101 """Get 'length' bytes starting at 'position'.
103 Position is start of payload (first four packet header bytes are not
104 included) starting at index '0'.
106 No error checking is done. If requesting outside end of buffer
107 an empty string (or string shorter than 'length') may be returned!
108 """
109 return self._data[position : (position + length)]
111 def read_uint8(self):
112 result = self._data[self._position]
113 self._position += 1
114 return result
116 def read_uint16(self):
117 result = struct.unpack_from("<H", self._data, self._position)[0]
118 self._position += 2
119 return result
121 def read_uint24(self):
122 low, high = struct.unpack_from("<HB", self._data, self._position)
123 self._position += 3
124 return low + (high << 16)
126 def read_uint32(self):
127 result = struct.unpack_from("<I", self._data, self._position)[0]
128 self._position += 4
129 return result
131 def read_uint64(self):
132 result = struct.unpack_from("<Q", self._data, self._position)[0]
133 self._position += 8
134 return result
136 def read_string(self):
137 end_pos = self._data.find(b"\0", self._position)
138 if end_pos < 0:
139 return None
140 result = self._data[self._position : end_pos]
141 self._position = end_pos + 1
142 return result
144 def read_length_encoded_integer(self):
145 """Read a 'Length Coded Binary' number from the data buffer.
147 Length coded numbers can be anywhere from 1 to 9 bytes depending
148 on the value of the first byte.
149 """
150 c = self._data[self._position]
151 self._position += 1
152 if c == NULL_COLUMN:
153 return None
154 if c < UNSIGNED_CHAR_COLUMN:
155 return c
156 elif c == UNSIGNED_SHORT_COLUMN:
157 return self.read_uint16()
158 elif c == UNSIGNED_INT24_COLUMN:
159 return self.read_uint24()
160 elif c == UNSIGNED_INT64_COLUMN:
161 return self.read_uint64()
163 def read_length_coded_string(self):
164 """Read a 'Length Coded String' from the data buffer.
166 A 'Length Coded String' consists first of a length coded
167 (unsigned, positive) integer represented in 1-9 bytes followed by
168 that many bytes of binary data. (For example "cat" would be "3cat".)
169 """
170 length = self.read_length_encoded_integer()
171 if length is None:
172 return None
173 return self.read(length)
175 def read_struct(self, fmt):
176 s = struct.Struct(fmt)
177 result = s.unpack_from(self._data, self._position)
178 self._position += s.size
179 return result
181 def is_ok_packet(self):
182 # https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
183 return self._data[0] == 0 and len(self._data) >= 7
185 def is_eof_packet(self):
186 # http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-EOF_Packet
187 # Caution: \xFE may be LengthEncodedInteger.
188 # If \xFE is LengthEncodedInteger header, 8bytes followed.
189 return self._data[0] == 0xFE and len(self._data) < 9
191 def is_auth_switch_request(self):
192 # http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
193 return self._data[0] == 0xFE
195 def is_extra_auth_data(self):
196 # https://dev.mysql.com/doc/internals/en/successful-authentication.html
197 return self._data[0] == 1
199 def is_resultset_packet(self):
200 field_count = self._data[0]
201 return 1 <= field_count <= 250
203 def is_load_local_packet(self):
204 return self._data[0] == 0xFB
206 def is_error_packet(self):
207 return self._data[0] == 0xFF
209 def check_error(self):
210 if self.is_error_packet():
211 self.raise_for_error()
213 def raise_for_error(self):
214 self.rewind()
215 self.advance(1) # field_count == error (we already know that)
216 errno = self.read_uint16()
217 if DEBUG:
218 print("errno =", errno)
219 err.raise_mysql_exception(self._data)
221 def dump(self):
222 dump_packet(self._data)
225class FieldDescriptorPacket(MysqlPacket):
226 """A MysqlPacket that represents a specific column's metadata in the result.
228 Parsing is automatically done and the results are exported via public
229 attributes on the class such as: db, table_name, name, length, type_code.
230 """
232 def __init__(self, data, encoding):
233 MysqlPacket.__init__(self, data, encoding)
234 self._parse_field_descriptor(encoding)
236 def _parse_field_descriptor(self, encoding):
237 """Parse the 'Field Descriptor' (Metadata) packet.
239 This is compatible with MySQL 4.1+ (not compatible with MySQL 4.0).
240 """
241 self.catalog = self.read_length_coded_string()
242 self.db = self.read_length_coded_string()
243 self.table_name = self.read_length_coded_string().decode(encoding)
244 self.org_table = self.read_length_coded_string().decode(encoding)
245 self.name = self.read_length_coded_string().decode(encoding)
246 self.org_name = self.read_length_coded_string().decode(encoding)
247 (
248 self.charsetnr,
249 self.length,
250 self.type_code,
251 self.flags,
252 self.scale,
253 ) = self.read_struct("<xHIBHBxx")
254 # 'default' is a length coded binary and is still in the buffer?
255 # not used for normal result sets...
257 def description(self):
258 """Provides a 7-item tuple compatible with the Python PEP249 DB Spec."""
259 return (
260 self.name,
261 self.type_code,
262 None, # TODO: display_length; should this be self.length?
263 self.get_column_length(), # 'internal_size'
264 self.get_column_length(), # 'precision' # TODO: why!?!?
265 self.scale,
266 self.flags % 2 == 0,
267 )
269 def get_column_length(self):
270 if self.type_code == FIELD_TYPE.VAR_STRING:
271 mblen = MBLENGTH.get(self.charsetnr, 1)
272 return self.length // mblen
273 return self.length
275 def __str__(self):
276 return f"{self.__class__} {self.db!r}.{self.table_name!r}.{self.name!r}, type={self.type_code}, flags={self.flags:x}"
279class OKPacketWrapper:
280 """
281 OK Packet Wrapper. It uses an existing packet object, and wraps
282 around it, exposing useful variables while still providing access
283 to the original packet objects variables and methods.
284 """
286 def __init__(self, from_packet):
287 if not from_packet.is_ok_packet():
288 raise ValueError(
289 "Cannot create "
290 + str(self.__class__.__name__)
291 + " object from invalid packet type"
292 )
294 self.packet = from_packet
295 self.packet.advance(1)
297 self.affected_rows = self.packet.read_length_encoded_integer()
298 self.insert_id = self.packet.read_length_encoded_integer()
299 self.server_status, self.warning_count = self.read_struct("<HH")
300 self.message = self.packet.read_all()
301 self.has_next = self.server_status & SERVER_STATUS.SERVER_MORE_RESULTS_EXISTS
303 def __getattr__(self, key):
304 return getattr(self.packet, key)
307class EOFPacketWrapper:
308 """
309 EOF Packet Wrapper. It uses an existing packet object, and wraps
310 around it, exposing useful variables while still providing access
311 to the original packet objects variables and methods.
312 """
314 def __init__(self, from_packet):
315 if not from_packet.is_eof_packet():
316 raise ValueError(
317 f"Cannot create '{self.__class__}' object from invalid packet type"
318 )
320 self.packet = from_packet
321 self.warning_count, self.server_status = self.packet.read_struct("<xhh")
322 if DEBUG:
323 print("server_status=", self.server_status)
324 self.has_next = self.server_status & SERVER_STATUS.SERVER_MORE_RESULTS_EXISTS
326 def __getattr__(self, key):
327 return getattr(self.packet, key)
330class LoadLocalPacketWrapper:
331 """
332 Load Local Packet Wrapper. It uses an existing packet object, and wraps
333 around it, exposing useful variables while still providing access
334 to the original packet objects variables and methods.
335 """
337 def __init__(self, from_packet):
338 if not from_packet.is_load_local_packet():
339 raise ValueError(
340 f"Cannot create '{self.__class__}' object from invalid packet type"
341 )
343 self.packet = from_packet
344 self.filename = self.packet.get_all_data()[1:]
345 if DEBUG:
346 print("filename=", self.filename)
348 def __getattr__(self, key):
349 return getattr(self.packet, key)