Skip to content

Commit 1e272ed

Browse files
committed
NumpyParser: reject column encryption; fix double column_type() call; add Cython parser tests
- NumpyParser.parse_rows() now raises NotImplementedError when column_encryption_policy is set, as NumpyParser does not support column encryption (reviewer feedback from dkropachev). - Fix redundant ce_policy.column_type(coldesc) call in TupleRowParser.unpack_col_encrypted_row(); reuse the already-assigned col_type variable. - Add CythonParserTest covering ListParser and NumpyParser paths.
1 parent af8525b commit 1e272ed

3 files changed

Lines changed: 124 additions & 1 deletion

File tree

cassandra/numpy_parser.pyx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ cdef class NumpyParser(ColumnParser):
8181
cdef ArrDesc[::1] array_descs
8282
cdef ArrDesc *arrs
8383

84+
if desc.column_encryption_policy:
85+
raise NotImplementedError(
86+
"NumpyParser does not support column encryption")
87+
8488
rowcount = read_int(reader)
8589
array_descs, arrays = make_arrays(desc, rowcount)
8690
arrs = &array_descs[0]

cassandra/obj_parser.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ cdef class TupleRowParser(RowParser):
8989
col_type = ce_policy.column_type(coldesc)
9090
decrypted_bytes = ce_policy.decrypt(coldesc, to_bytes(&buf))
9191
PyBytes_AsStringAndSize(decrypted_bytes, &newbuf.ptr, &newbuf.size)
92-
deserializer = find_deserializer(ce_policy.column_type(coldesc))
92+
deserializer = find_deserializer(col_type)
9393
val = from_binary(deserializer, &newbuf, desc.protocol_version)
9494
else:
9595
val = from_binary(deserializer, &buf, desc.protocol_version)

tests/unit/test_protocol.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,122 @@ def test_optimization_efficiency(self):
320320
# The key is we avoid checking 'column_encryption_policy and ...' 200 times
321321
self.assertEqual(mock_policy.contains_column.call_count, 200,
322322
"contains_column should be called for each value when policy exists")
323+
324+
325+
class CythonParserTest(unittest.TestCase):
326+
"""
327+
Tests for the Cython fast-path parsers (ListParser, TupleRowParser)
328+
to verify the column_encryption_policy optimization in obj_parser.pyx.
329+
"""
330+
331+
def _build_binary_rows(self, rows):
332+
"""
333+
Build a binary buffer containing encoded rows.
334+
335+
Each row is a list of (size, raw_bytes) pairs.
336+
Prepends a 4-byte big-endian row count.
337+
"""
338+
import struct
339+
data = struct.pack('>i', len(rows))
340+
for row in rows:
341+
for raw in row:
342+
if raw is None:
343+
data += struct.pack('>i', -1) # NULL
344+
else:
345+
data += struct.pack('>i', len(raw)) + raw
346+
return data
347+
348+
def _make_parse_desc(self, column_encryption_policy=None):
349+
from cassandra.parsing import ParseDesc
350+
from cassandra.deserializers import make_deserializers
351+
from cassandra.policies import ColDesc
352+
353+
colnames = ['col1', 'col2']
354+
coltypes = [Int32Type, UTF8Type]
355+
coldescs = [ColDesc('ks', 'tbl', 'col1'), ColDesc('ks', 'tbl', 'col2')]
356+
deserializers = make_deserializers(coltypes)
357+
return ParseDesc(colnames, coltypes, column_encryption_policy,
358+
coldescs, deserializers, ProtocolVersion.V4)
359+
360+
def _int32_bytes(self, val):
361+
import struct
362+
return struct.pack('>i', val)
363+
364+
def test_list_parser_without_encryption(self):
365+
"""ListParser decodes rows correctly without encryption policy."""
366+
from cassandra.bytesio import BytesIOReader
367+
from cassandra.obj_parser import ListParser
368+
369+
desc = self._make_parse_desc(column_encryption_policy=None)
370+
data = self._build_binary_rows([
371+
[self._int32_bytes(42), b'hello'],
372+
[self._int32_bytes(100), b'world'],
373+
])
374+
reader = BytesIOReader(data)
375+
result = ListParser().parse_rows(reader, desc)
376+
377+
self.assertEqual(len(result), 2)
378+
self.assertEqual(result[0], (42, 'hello'))
379+
self.assertEqual(result[1], (100, 'world'))
380+
381+
def test_list_parser_with_encryption_no_encrypted_cols(self):
382+
"""ListParser decodes rows correctly when policy exists but no columns are encrypted."""
383+
from cassandra.bytesio import BytesIOReader
384+
from cassandra.obj_parser import ListParser
385+
386+
mock_policy = Mock()
387+
mock_policy.contains_column = Mock(return_value=False)
388+
389+
desc = self._make_parse_desc(column_encryption_policy=mock_policy)
390+
data = self._build_binary_rows([
391+
[self._int32_bytes(42), b'hello'],
392+
])
393+
reader = BytesIOReader(data)
394+
result = ListParser().parse_rows(reader, desc)
395+
396+
self.assertEqual(len(result), 1)
397+
self.assertEqual(result[0], (42, 'hello'))
398+
# 1 row * 2 columns = 2 calls
399+
self.assertEqual(mock_policy.contains_column.call_count, 2)
400+
401+
def test_list_parser_with_encrypted_column(self):
402+
"""ListParser decodes rows with an encrypted column (mock decrypt is identity)."""
403+
from cassandra.bytesio import BytesIOReader
404+
from cassandra.obj_parser import ListParser
405+
from cassandra.deserializers import find_deserializer
406+
407+
mock_policy = Mock()
408+
mock_policy.contains_column = Mock(
409+
side_effect=lambda cd: cd.col == 'col1')
410+
mock_policy.column_type = Mock(return_value=Int32Type)
411+
# decrypt returns the raw bytes unchanged (identity)
412+
mock_policy.decrypt = Mock(side_effect=lambda cd, val: val)
413+
414+
desc = self._make_parse_desc(column_encryption_policy=mock_policy)
415+
data = self._build_binary_rows([
416+
[self._int32_bytes(7), b'test'],
417+
])
418+
reader = BytesIOReader(data)
419+
result = ListParser().parse_rows(reader, desc)
420+
421+
self.assertEqual(len(result), 1)
422+
self.assertEqual(result[0], (7, 'test'))
423+
self.assertEqual(mock_policy.decrypt.call_count, 1)
424+
self.assertEqual(mock_policy.column_type.call_count, 1)
425+
426+
def test_numpy_parser_rejects_encryption(self):
427+
"""NumpyParser raises NotImplementedError when column_encryption_policy is set."""
428+
try:
429+
from cassandra.numpy_parser import NumpyParser
430+
except ImportError:
431+
self.skipTest("NumPy or numpy_parser not available")
432+
433+
from cassandra.bytesio import BytesIOReader
434+
435+
mock_policy = Mock()
436+
desc = self._make_parse_desc(column_encryption_policy=mock_policy)
437+
data = self._build_binary_rows([[self._int32_bytes(1), b'x']])
438+
reader = BytesIOReader(data)
439+
440+
with self.assertRaises(NotImplementedError):
441+
NumpyParser().parse_rows(reader, desc)

0 commit comments

Comments
 (0)