Skip to content

Commit a61e359

Browse files
committed
Bug fixes for dbwire
1 parent 2acc8a5 commit a61e359

4 files changed

Lines changed: 226 additions & 68 deletions

File tree

extra/dbwire/clickhouse.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,34 @@
2525
from extra.dbwire import OperationalError
2626
from extra.dbwire import ProgrammingError
2727

28+
# TabSeparated backslash escapes -> the literal byte they denote
29+
_ESCAPE = {ord("t"): 9, ord("n"): 10, ord("r"): 13, ord("0"): 0, ord("b"): 8,
30+
ord("f"): 12, ord("a"): 7, ord("v"): 11, ord("\\"): 92, ord("'"): 39}
31+
2832
def _unescape(value):
29-
if value == "\\N":
33+
# value: the raw bytes of one TSV field -> None (\N) or the unescaped bytes. Operates on bytes because a
34+
# String/FixedString column can hold arbitrary non-UTF-8 data, which a whole-body utf-8 decode would destroy.
35+
if value == b"\\N":
3036
return None
31-
if "\\" not in value:
37+
if b"\\" not in value:
3238
return value
33-
out = []
34-
i = 0
35-
n = len(value)
39+
src, out, i, n = bytearray(value), bytearray(), 0, len(value)
3640
while i < n:
37-
ch = value[i]
38-
if ch == "\\" and i + 1 < n:
39-
nxt = value[i + 1]
40-
out.append({"t": "\t", "n": "\n", "r": "\r", "0": "\0", "b": "\b", "f": "\f", "a": "\a", "v": "\v", "\\": "\\", "'": "'"}.get(nxt, nxt))
41-
i += 2
41+
c = src[i]
42+
if c == 0x5c and i + 1 < n: # backslash
43+
out.append(_ESCAPE.get(src[i + 1], src[i + 1])); i += 2
4244
else:
43-
out.append(ch)
44-
i += 1
45-
return "".join(out)
45+
out.append(c); i += 1
46+
return bytes(out)
47+
48+
def _decode_cell(value):
49+
# keep text as str; hand back raw bytes only when a value is not valid UTF-8 (sqlmap then hex-encodes it)
50+
if value is None:
51+
return None
52+
try:
53+
return value.decode("utf-8")
54+
except UnicodeDecodeError:
55+
return value
4656

4757
class Cursor(object):
4858
def __init__(self, connection):
@@ -99,7 +109,7 @@ def close(self):
99109
def _query(self, query):
100110
req = Request(self._url, data=query.encode("utf-8"), headers=self._headers)
101111
try:
102-
body = urlopen(req, timeout=self._timeout).read().decode("utf-8", "replace")
112+
body = urlopen(req, timeout=self._timeout).read() # bytes: column data may be non-UTF-8
103113
except HTTPError as ex:
104114
raise ProgrammingError("(remote) %s" % ex.read().decode("utf-8", "replace").strip())
105115
except URLError as ex:
@@ -109,13 +119,13 @@ def _query(self, query):
109119

110120
if not body:
111121
return None, []
112-
lines = body.split("\n")
113-
if lines and lines[-1] == "":
122+
lines = body.split(b"\n")
123+
if lines and lines[-1] == b"":
114124
lines.pop()
115125
if not lines:
116126
return None, []
117-
description = [(name, None, None, None, None, None, None) for name in (_unescape(_) for _ in lines[0].split("\t"))]
118-
rows = [tuple(_unescape(_) for _ in line.split("\t")) for line in lines[1:]]
127+
description = [(name, None, None, None, None, None, None) for name in (_decode_cell(_unescape(_)) for _ in lines[0].split(b"\t"))]
128+
rows = [tuple(_decode_cell(_unescape(_)) for _ in line.split(b"\t")) for line in lines[1:]]
119129
return description, rows
120130

121131
def connect(host=None, port=8123, user=None, password=None, database=None, connect_timeout=None, **kwargs):

extra/dbwire/mysql.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535

3636
_MAX_PACKET = 0x1000000
3737
_MAX_MESSAGE_LENGTH = 0x40000000 # cap on a (re-assembled) payload, to bound a hostile/corrupt stream
38-
_BINARY_CHARSET = 63 # collation id 63 == 'binary' (BLOB/BINARY/VARBINARY columns)
38+
_BINARY_CHARSET = 63 # collation id 63 == 'binary'
39+
# field types for which charset==63 genuinely denotes raw bytes (BLOB/BINARY/VARBINARY/BIT/GEOMETRY family).
40+
# Numeric & temporal columns ALSO report charset 63 in the text protocol, but carry their ASCII text form -
41+
# they must be decoded, not returned as bytes (else -d hexifies e.g. the int 12345 to '3132333435').
42+
_BINARY_TYPES = frozenset((15, 16, 249, 250, 251, 252, 253, 254, 255)) # VARCHAR,BIT,*BLOB,VAR_STRING,STRING,GEOMETRY
3943

4044
def _xor(a, b):
4145
if str is bytes: # Python 2
@@ -210,8 +214,9 @@ def _read_query_response(self):
210214
_, off = _lenc_str(cpay, off) # org_name
211215
_, off = _lenc_int(cpay, off) # length of the fixed-length block (0x0c)
212216
charset = struct.unpack("<H", cpay[off:off + 2])[0]
213-
description.append((name.decode("utf-8", "replace"), None, None, None, None, None, None))
214-
binary.append(charset == _BINARY_CHARSET)
217+
col_type = _u8(cpay, off + 6) # fixed block: charset(2) column_length(4) type(1) flags(2) ...
218+
description.append((name.decode("utf-8", "replace"), col_type, None, None, None, None, None))
219+
binary.append(charset == _BINARY_CHARSET and col_type in _BINARY_TYPES)
215220

216221
_read_packet(self._sock) # EOF after the column definitions
217222

extra/dbwire/tds.py

Lines changed: 189 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -151,35 +151,192 @@ def _decode_datetime(raw):
151151
import datetime
152152
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, milliseconds=ticks * 10.0 / 3.0))
153153

154+
def _decode_money(raw):
155+
if len(raw) == 4:
156+
v = struct.unpack("<i", raw)[0]
157+
else: # 8 bytes: signed high dword, unsigned low dword
158+
hi, lo = struct.unpack("<iI", raw)
159+
v = (hi << 32) | lo
160+
return "%.4f" % (v / 10000.0)
161+
162+
def _decode_numeric(raw, scale):
163+
sign = bytearray(raw)[0] # 1 == positive, 0 == negative; magnitude is little-endian
164+
magnitude = 0
165+
for b in reversed(bytearray(raw[1:])):
166+
magnitude = (magnitude << 8) | b
167+
value = magnitude if sign else -magnitude
168+
if scale:
169+
s = "%0*d" % (scale + 1, abs(value))
170+
return ("-" if value < 0 else "") + s[:-scale] + "." + s[-scale:]
171+
return str(value)
172+
173+
def _decode_guid(raw):
174+
a, b, c = struct.unpack("<IHH", raw[:8]) # Data1/2/3 little-endian, Data4 big-endian (mixed-endian GUID)
175+
d = raw[8:]
176+
return "%08X-%04X-%04X-%s-%s" % (a, b, c,
177+
"".join("%02X" % x for x in bytearray(d[:2])), "".join("%02X" % x for x in bytearray(d[2:])))
178+
179+
def _decode_temporal(t, scale, raw):
180+
# DATE 0x28 / TIME 0x29 / DATETIME2 0x2a / DATETIMEOFFSET 0x2b; time is scaled 10^-scale second units,
181+
# rendered with the column's exact fractional precision (Python datetime only holds microseconds)
182+
import datetime
183+
offset = None
184+
if t == 0x2b: # trailing 2-byte signed offset (minutes from UTC); value bytes are stored as UTC
185+
offset = struct.unpack("<h", raw[-2:])[0]
186+
raw = raw[:-2]
187+
has_date = t in (0x28, 0x2a, 0x2b)
188+
has_time = t != 0x28
189+
date_bytes = raw[-3:] if has_date else b""
190+
time_bytes = (raw[:-3] if has_date else raw) if has_time else b""
191+
days = struct.unpack("<I", date_bytes + b"\x00")[0] if has_date else 0
192+
base = datetime.datetime(1, 1, 1) + datetime.timedelta(days=days)
193+
frac = ""
194+
if has_time:
195+
ticks = struct.unpack("<Q", time_bytes + b"\x00" * (8 - len(time_bytes)))[0]
196+
base += datetime.timedelta(seconds=ticks // (10 ** scale))
197+
if scale:
198+
frac = "." + ("%0*d" % (scale, ticks % (10 ** scale)))
199+
if offset is not None:
200+
base += datetime.timedelta(minutes=offset)
201+
if t == 0x28:
202+
return "%s" % base.date()
203+
if t == 0x29:
204+
return "%s" % base.time() + frac
205+
s = "%s" % base + frac
206+
if offset is not None:
207+
sign, mins = ("+", offset) if offset >= 0 else ("-", -offset)
208+
s += " %s%02d:%02d" % (sign, mins // 60, mins % 60)
209+
return s
210+
211+
# SQL Server COLLATION -> Python codec. The 5-byte collation is a little-endian uint32 (low 20 bits = LCID)
212+
# plus a 1-byte sort id: a non-zero sort id fixes the code page, else the LCID does. Only single-byte / DBCS
213+
# code pages need a codec (NVARCHAR is UTF-16, handled separately). Derived from pytds; default cp1252 (the
214+
# stock SQL_Latin1_General code page - NOT latin-1, whose 0x80-0x9F differ, corrupting e.g. the euro sign).
215+
_LCID_CP = {
216+
0x405: "cp1250", 0x40e: "cp1250", 0x415: "cp1250", 0x418: "cp1250", 0x41a: "cp1250", 0x41b: "cp1250",
217+
0x41c: "cp1250", 0x424: "cp1250", 0x402: "cp1251", 0x419: "cp1251", 0x422: "cp1251", 0x423: "cp1251",
218+
0x42f: "cp1251", 0x408: "cp1253", 0x41f: "cp1254", 0x42c: "cp1254", 0x443: "cp1254", 0x40d: "cp1255",
219+
0x401: "cp1256", 0x420: "cp1256", 0x429: "cp1256", 0x425: "cp1257", 0x426: "cp1257", 0x427: "cp1257",
220+
0x42a: "cp1258", 0x41e: "cp874", 0x411: "cp932", 0x804: "cp936", 0x1004: "cp936", 0x412: "cp949",
221+
0x404: "cp950", 0xc04: "cp950", 0x1404: "cp950",
222+
}
223+
224+
def _sortid_cp(sid):
225+
if 30 <= sid <= 34:
226+
return "cp437"
227+
if 40 <= sid <= 44 or sid == 49 or 55 <= sid <= 61:
228+
return "cp850"
229+
if sid in (51, 52, 53, 54) or 183 <= sid <= 186:
230+
return "cp1252"
231+
if 80 <= sid <= 96:
232+
return "cp1250"
233+
if 104 <= sid <= 108:
234+
return "cp1251"
235+
if 112 <= sid <= 124:
236+
return "cp1253"
237+
if 128 <= sid <= 130:
238+
return "cp1254"
239+
if 136 <= sid <= 138:
240+
return "cp1255"
241+
if 144 <= sid <= 146:
242+
return "cp1256"
243+
if 152 <= sid <= 160:
244+
return "cp1257"
245+
return None
246+
247+
def _collation_codec(collation):
248+
if not collation or len(collation) < 5:
249+
return "cp1252"
250+
lump = struct.unpack("<I", collation[:4])[0]
251+
sid = bytearray(collation)[4]
252+
if sid:
253+
return _sortid_cp(sid) or "cp1252"
254+
return _LCID_CP.get(lump & 0xfffff, "cp1252")
255+
256+
def _decode_variant(body):
257+
# SQL_VARIANT value: base type (1) | property-bytes count (1) | type-specific metadata | value bytes
258+
b = bytearray(body)
259+
base, propbytes = b[0], b[1]
260+
meta, val = body[2:2 + propbytes], body[2 + propbytes:]
261+
if base == 0x30:
262+
return str(bytearray(val)[0])
263+
if base == 0x32:
264+
return "1" if bytearray(val)[0] else "0"
265+
if base == 0x34:
266+
return str(struct.unpack("<h", val)[0])
267+
if base == 0x38:
268+
return str(struct.unpack("<i", val)[0])
269+
if base == 0x7f:
270+
return str(struct.unpack("<q", val)[0])
271+
if base == 0x3b:
272+
return repr(struct.unpack("<f", val)[0])
273+
if base == 0x3e:
274+
return repr(struct.unpack("<d", val)[0])
275+
if base in (0x3c, 0x7a):
276+
return _decode_money(val)
277+
if base == 0x3d:
278+
return _decode_datetime(val)
279+
if base == 0x24:
280+
return _decode_guid(val)
281+
if base in (0x6a, 0x6c): # decimal/numeric: metadata = precision, scale
282+
return _decode_numeric(val, bytearray(meta)[1])
283+
if base in (0xe7, 0xef):
284+
return val.decode("utf-16-le", "replace")
285+
if base in (0xa5, 0xad):
286+
return val # binary -> raw bytes
287+
if base in (0xa7, 0xaf): # (var)char: metadata = 5-byte collation + 2-byte max length
288+
return val.decode(_collation_codec(meta[:5]), "replace")
289+
if base == 0x28:
290+
return _decode_temporal(base, 0, val)
291+
if base in (0x29, 0x2a, 0x2b): # metadata = scale
292+
return _decode_temporal(base, bytearray(meta)[0], val)
293+
return "".join("%02x" % x for x in bytearray(val)) # unknown base type -> hex (never desyncs)
294+
154295
class _Column(object):
155-
__slots__ = ("name", "type", "size", "scale", "binary")
296+
__slots__ = ("name", "type", "size", "scale", "binary", "collation")
156297

157298
def _parse_type_info(data, off):
158299
col = _Column()
159300
col.type = _u8(data, off); off += 1
160-
col.size, col.scale, col.binary = 0, 0, False
301+
col.size, col.scale, col.binary, col.collation = 0, 0, False, None
161302
t = col.type
162303
if t in (0x30, 0x32, 0x34, 0x38, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x7a, 0x7f, 0x1f):
163304
pass # fixed-length types, size implied by type
164-
elif t in (0x26, 0x68, 0x6d, 0x6e, 0x6f, 0x24, 0x2e, 0x37): # INTN/BITN/FLTN/MONEYN/DATETIMN/GUID
305+
elif t in (0x26, 0x68, 0x6d, 0x6e, 0x6f, 0x24): # INTN/BITN/FLTN/MONEYN/DATETIMN/GUID
165306
col.size = _u8(data, off); off += 1
166-
elif t in (0x6a, 0x6c): # DECIMALN / NUMERICN
307+
elif t in (0x6a, 0x6c, 0x37, 0x3f): # DECIMALN/NUMERICN + legacy DECIMAL/NUMERIC (size, precision, scale)
167308
col.size = _u8(data, off); off += 1
168309
off += 1 # precision
169310
col.scale = _u8(data, off); off += 1
170311
elif t in (0xa7, 0xaf, 0xe7, 0xef): # (BIG)VARCHAR/CHAR, N(VAR)CHAR
171312
col.size = struct.unpack("<H", data[off:off + 2])[0]; off += 2
172-
off += 5 # collation
313+
col.collation = data[off:off + 5]; off += 5
173314
elif t in (0xa5, 0xad): # (BIG)VARBINARY / BINARY
174315
col.size = struct.unpack("<H", data[off:off + 2])[0]; off += 2
175316
col.binary = True
176317
elif t in (0x28, 0x29, 0x2a, 0x2b): # DATE/TIME/DATETIME2/DATETIMEOFFSET
177318
if t != 0x28:
178319
col.scale = _u8(data, off); off += 1
320+
elif t == 0xf0: # UDT (CLR geometry/geography/hierarchyid) - value arrives as PLP raw bytes
321+
off += 2 # max byte size
322+
for _ in range(3): # db, schema, type name: B_VARCHAR
323+
off += 1 + _u8(data, off) * 2
324+
off += 2 + struct.unpack("<H", data[off:off + 2])[0] * 2 # assembly-qualified name: US_VARCHAR
325+
col.binary = True
326+
elif t == 0xf1: # XML (value arrives PLP-encoded UTF-16, no size in TYPE_INFO)
327+
if _u8(data, off): # schema-present: B_VARCHAR dbname, B_VARCHAR owner, US_VARCHAR collection
328+
off += 1
329+
for _ in range(2):
330+
off += 1 + _u8(data, off) * 2
331+
off += 2 + struct.unpack("<H", data[off:off + 2])[0] * 2
332+
else:
333+
off += 1
334+
elif t == 0x62: # SQL_VARIANT (4-byte max length; per-value base type carried in the body)
335+
col.size = struct.unpack("<i", data[off:off + 4])[0]; off += 4
179336
elif t in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE
180337
col.size = struct.unpack("<i", data[off:off + 4])[0]; off += 4
181338
if t in (0x23, 0x63):
182-
off += 5 # collation
339+
col.collation = data[off:off + 5]; off += 5
183340
col.binary = (t == 0x22)
184341
# table name (num parts + parts) follows in COLMETADATA for these; handled by caller via name read
185342
else:
@@ -219,10 +376,9 @@ def _decode_value(col, data, off):
219376
if t == 0x3e:
220377
return repr(struct.unpack("<d", data[off:off + 8])[0]), off + 8
221378
if t == 0x7a: # MONEY4 (4 bytes)
222-
return "%.4f" % (struct.unpack("<i", data[off:off + 4])[0] / 10000.0), off + 4
223-
if t == 0x3c: # MONEY (8 bytes: high 4 then low 4)
224-
hi, lo = struct.unpack("<iI", data[off:off + 8])
225-
return "%.4f" % (((hi << 32) | lo) / 10000.0), off + 8
379+
return _decode_money(data[off:off + 4]), off + 4
380+
if t == 0x3c: # MONEY (8 bytes: high dword then low dword)
381+
return _decode_money(data[off:off + 8]), off + 8
226382
if t == 0x3d: # DATETIME (8 bytes)
227383
return _decode_datetime(data[off:off + 8]), off + 8
228384
if t == 0x3a: # DATETIM4 / smalldatetime (2-byte days since 1900 + 2-byte minutes)
@@ -245,7 +401,7 @@ def _decode_value(col, data, off):
245401
return raw, off # binary -> raw bytes
246402
if t in (0xe7, 0xef):
247403
return raw.decode("utf-16-le", "replace"), off
248-
return raw.decode("latin-1"), off # (var)char is the collation's single-byte codepage; latin-1 round-trips every byte losslessly
404+
return raw.decode(_collation_codec(col.collation), "replace"), off # (var)char: the collation's code page
249405

250406
if t in (0x23, 0x63, 0x22): # TEXT/NTEXT/IMAGE: 1-byte textptr len (0 = NULL) then textptr+timestamp then 4-byte len
251407
ptr_len = _u8(data, off); off += 1
@@ -258,7 +414,21 @@ def _decode_value(col, data, off):
258414
return raw, off
259415
if t == 0x63:
260416
return raw.decode("utf-16-le", "replace"), off
261-
return raw.decode("latin-1"), off # TEXT is single-byte codepage; latin-1 is lossless
417+
return raw.decode(_collation_codec(col.collation), "replace"), off # TEXT: the collation's code page
418+
419+
if t == 0xf1: # XML: PLP-encoded UTF-16-LE
420+
raw, off = _read_plp(data, off)
421+
return (raw.decode("utf-16-le", "replace") if raw is not None else None), off
422+
423+
if t == 0xf0: # UDT (geometry/geography/hierarchyid): PLP raw bytes -> sqlmap hex-encodes them
424+
return _read_plp(data, off)
425+
426+
if t == 0x62: # SQL_VARIANT: 4-byte total length (0 = NULL) then a self-describing value body
427+
(total,) = struct.unpack("<i", data[off:off + 4]); off += 4
428+
if total <= 0:
429+
return None, off
430+
body, off = data[off:off + total], off + total
431+
return _decode_variant(body), off
262432

263433
# nullable / length-prefixed numeric & misc
264434
(n,) = struct.unpack("<B", data[off:off + 1]); off += 1
@@ -272,47 +442,20 @@ def _decode_value(col, data, off):
272442
if t == 0x6d: # FLTN
273443
return repr(struct.unpack("<f" if n == 4 else "<d", raw)[0]), off
274444
if t in (0x6e, 0x3d, 0x7a): # MONEYN / MONEY / MONEY4
275-
if n == 4:
276-
v = struct.unpack("<i", raw)[0]
277-
else:
278-
hi, lo = struct.unpack("<ii", raw)
279-
v = (hi << 32) | (lo & 0xffffffff)
280-
return "%.4f" % (v / 10000.0), off
281-
if t in (0x6a, 0x6c): # DECIMALN / NUMERICN
282-
sign = bytearray(raw)[0]
283-
magnitude = 0
284-
for b in reversed(bytearray(raw[1:])):
285-
magnitude = (magnitude << 8) | b
286-
value = magnitude if sign else -magnitude
287-
if col.scale:
288-
s = "%0*d" % (col.scale + 1, value if value >= 0 else -value)
289-
s = ("-" if value < 0 else "") + s[:-col.scale] + "." + s[-col.scale:]
290-
return s, off
291-
return str(value), off
445+
return _decode_money(raw), off
446+
if t in (0x6a, 0x6c, 0x37, 0x3f): # DECIMALN / NUMERICN (+ legacy DECIMAL / NUMERIC)
447+
return _decode_numeric(raw, col.scale), off
292448
if t == 0x24: # GUID
293-
a, b, c = struct.unpack("<IHH", raw[:8])
294-
d = raw[8:]
295-
return ("%08X-%04X-%04X-%s-%s" % (a, b, c, "".join("%02X" % x for x in bytearray(d[:2])), "".join("%02X" % x for x in bytearray(d[2:])))), off
449+
return _decode_guid(raw), off
296450
if t == 0x6f: # DATETIMN (n=8 datetime, n=4 smalldatetime)
297451
if n == 8:
298452
return _decode_datetime(raw), off
299453
import datetime
300454
days, mins = struct.unpack("<HH", raw)
301455
return "%s" % (datetime.datetime(1900, 1, 1) + datetime.timedelta(days=days, minutes=mins)), off
302-
if t == 0x28: # DATE (3 bytes: days since 0001-01-01)
303-
import datetime
304-
days = struct.unpack("<I", raw[:3] + b"\x00")[0]
305-
return "%s" % (datetime.date(1, 1, 1) + datetime.timedelta(days=days)), off
306-
if t in (0x2a, 0x29): # DATETIME2 (time + 3-byte date) / TIME (time only), time = scaled 10^-scale s units
307-
import datetime
308-
date_bytes = raw[-3:] if t == 0x2a else b"\x00\x00\x00"
309-
time_bytes = raw[:-3] if t == 0x2a else raw
310-
days = struct.unpack("<I", date_bytes + b"\x00")[0]
311-
ticks = struct.unpack("<Q", time_bytes + b"\x00" * (8 - len(time_bytes)))[0]
312-
seconds = ticks / (10.0 ** (col.scale or 7))
313-
base = datetime.datetime(1, 1, 1) + datetime.timedelta(days=days, seconds=seconds)
314-
return ("%s" % base if t == 0x2a else "%s" % base.time()), off
315-
# unknown date/time layout: return the raw hex as a last resort (never desyncs)
456+
if t in (0x28, 0x29, 0x2a, 0x2b): # DATE / TIME / DATETIME2 / DATETIMEOFFSET
457+
return _decode_temporal(t, col.scale, raw), off
458+
# unknown layout: return the raw hex as a last resort (never desyncs)
316459
return "".join("%02x" % x for x in bytearray(raw)), off
317460

318461
def _parse_tokens(sock, login=False):

0 commit comments

Comments
 (0)