Fix: foreach_get/set does not work on multidimensional arrays
The foreach_get/foreach_set methods of bpy_prop_array get/set the entire contents of the array, but they were checking that the length of the input sequence was equal to the length of the current array dimension rather than the total length of all dimensions of the array. This would read/write memory after the end of the passed in sequence when the property was a multidimensional array. Performing `foreach_get` with a Python list with length matching the length of the current dimension of a multidimensional array would crash a debug build due to the trailing pad bytes of the temporarily allocated array being overwritten. This patch fixes `pyprop_array_foreach_getset` by changing the function used to get the expected sequence size, to the RNA function that gets the total length of the array across all its dimensions. The tests have be updated to additionally test multidimensional array properties. Pull Request: https://projects.blender.org/blender/blender/pulls/116457
This commit is contained in:
committed by
Brecht Van Lommel
parent
9c7b3659e2
commit
5139a9c064
@@ -50,78 +50,128 @@ def seq_items_as_dims(data):
|
||||
class TestPropArray(unittest.TestCase):
|
||||
def setUp(self):
|
||||
id_type.test_array_f = FloatVectorProperty(size=10)
|
||||
id_type.test_array_f_2d = FloatVectorProperty(size=(4, 1))
|
||||
id_type.test_array_f_3d = FloatVectorProperty(size=(3, 2, 4))
|
||||
id_type.test_array_i = IntVectorProperty(size=10)
|
||||
scene = bpy.context.scene
|
||||
self.array_f = scene.test_array_f
|
||||
self.array_i = scene.test_array_i
|
||||
id_type.test_array_i_2d = IntVectorProperty(size=(4, 1))
|
||||
id_type.test_array_i_3d = IntVectorProperty(size=(3, 2, 4))
|
||||
|
||||
def tearDown(self):
|
||||
del id_type.test_array_f
|
||||
del id_type.test_array_f_2d
|
||||
del id_type.test_array_f_3d
|
||||
del id_type.test_array_i
|
||||
del id_type.test_array_i_2d
|
||||
del id_type.test_array_i_3d
|
||||
|
||||
@staticmethod
|
||||
def parse_test_args(prop_array_first_dim, prop_type, prop_size):
|
||||
match prop_type:
|
||||
case 'INT':
|
||||
expected_dtype = np.int32
|
||||
wrong_kind_dtype = np.float32
|
||||
wrong_size_dtype = np.int64
|
||||
case 'FLOAT':
|
||||
expected_dtype = np.float32
|
||||
wrong_kind_dtype = np.int32
|
||||
wrong_size_dtype = np.float64
|
||||
case _:
|
||||
raise AssertionError("Unexpected property type '%s'" % prop_type)
|
||||
|
||||
expected_length = np.prod(prop_size)
|
||||
num_dims = len(prop_size)
|
||||
|
||||
assert expected_length > 0
|
||||
too_short_length = expected_length - 1
|
||||
|
||||
match num_dims:
|
||||
case 1:
|
||||
def get_flat_iterable_all_dimensions():
|
||||
return prop_array_first_dim[:]
|
||||
case 2:
|
||||
def get_flat_iterable_all_dimensions():
|
||||
return (flat_elem for array_1d in prop_array_first_dim[:] for flat_elem in array_1d[:])
|
||||
case 3:
|
||||
def get_flat_iterable_all_dimensions():
|
||||
return (flat_elem
|
||||
for array_2d in prop_array_first_dim[:]
|
||||
for array_1d in array_2d[:]
|
||||
for flat_elem in array_1d[:])
|
||||
case _:
|
||||
raise AssertionError("Number of dimensions must be 1, 2 or 3, but was %i" % num_dims)
|
||||
|
||||
return (expected_dtype, wrong_kind_dtype, wrong_size_dtype, expected_length, too_short_length,
|
||||
get_flat_iterable_all_dimensions)
|
||||
|
||||
def do_test_foreach_getset_current_dimension(self, prop_array, expected_dtype, wrong_kind_dtype, wrong_size_dtype,
|
||||
expected_length, too_short_length, get_flat_iterable_all_dimensions):
|
||||
with self.assertRaises(TypeError):
|
||||
prop_array.foreach_set(range(too_short_length))
|
||||
|
||||
prop_array.foreach_set(range(5, 5 + expected_length))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
prop_array.foreach_set(np.arange(too_short_length, dtype=expected_dtype))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
prop_array.foreach_set(np.arange(expected_length, dtype=wrong_size_dtype))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
prop_array.foreach_get(np.arange(expected_length, dtype=wrong_kind_dtype))
|
||||
|
||||
a = np.arange(expected_length, dtype=expected_dtype)
|
||||
prop_array.foreach_set(a)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
prop_array.foreach_set(a[:too_short_length])
|
||||
|
||||
for v1, v2 in zip(a, get_flat_iterable_all_dimensions()):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
b = np.empty(expected_length, dtype=expected_dtype)
|
||||
prop_array.foreach_get(b)
|
||||
for v1, v2 in zip(a, b):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
b = [None] * expected_length
|
||||
prop_array.foreach_get(b)
|
||||
for v1, v2 in zip(a, b):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
def do_test_foreach_getset(self, prop_array, prop_type, prop_size):
|
||||
if not isinstance(prop_size, (tuple, list)):
|
||||
prop_size = (prop_size,)
|
||||
num_dimensions = len(prop_size)
|
||||
|
||||
test_args = self.parse_test_args(prop_array, prop_type, prop_size)
|
||||
|
||||
# Test that foreach_get/foreach_set work, and work the same regardless of the current dimension/sub-array being
|
||||
# accessed.
|
||||
self.do_test_foreach_getset_current_dimension(prop_array, *test_args)
|
||||
if num_dimensions > 1:
|
||||
for i in range(prop_size[0]):
|
||||
self.do_test_foreach_getset_current_dimension(prop_array[i], *test_args)
|
||||
if num_dimensions > 2:
|
||||
for j in range(prop_size[1]):
|
||||
self.do_test_foreach_getset_current_dimension(prop_array[i][j], *test_args)
|
||||
|
||||
def test_foreach_getset_i(self):
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_i.foreach_set(range(5))
|
||||
|
||||
self.array_i.foreach_set(range(5, 15))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_i.foreach_set(np.arange(5, dtype=np.int32))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_i.foreach_set(np.arange(10, dtype=np.int64))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_i.foreach_get(np.arange(10, dtype=np.float32))
|
||||
|
||||
a = np.arange(10, dtype=np.int32)
|
||||
self.array_i.foreach_set(a)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_i.foreach_set(a[:5])
|
||||
|
||||
for v1, v2 in zip(a, self.array_i[:]):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
b = np.empty(10, dtype=np.int32)
|
||||
self.array_i.foreach_get(b)
|
||||
for v1, v2 in zip(a, b):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
b = [None] * 10
|
||||
self.array_f.foreach_get(b)
|
||||
for v1, v2 in zip(a, b):
|
||||
self.assertEqual(v1, v2)
|
||||
self.do_test_foreach_getset(id_inst.test_array_i, 'INT', 10)
|
||||
|
||||
def test_foreach_getset_f(self):
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_i.foreach_set(range(5))
|
||||
self.do_test_foreach_getset(id_inst.test_array_f, 'FLOAT', 10)
|
||||
|
||||
self.array_f.foreach_set(range(5, 15))
|
||||
def test_foreach_getset_i_2d(self):
|
||||
self.do_test_foreach_getset(id_inst.test_array_i_2d, 'INT', (4, 1))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_f.foreach_set(np.arange(5, dtype=np.float32))
|
||||
def test_foreach_getset_f_2d(self):
|
||||
self.do_test_foreach_getset(id_inst.test_array_f_2d, 'FLOAT', (4, 1))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_f.foreach_set(np.arange(10, dtype=np.int32))
|
||||
def test_foreach_getset_i_3d(self):
|
||||
self.do_test_foreach_getset(id_inst.test_array_i_3d, 'INT', (3, 2, 4))
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
self.array_f.foreach_get(np.arange(10, dtype=np.float64))
|
||||
|
||||
a = np.arange(10, dtype=np.float32)
|
||||
self.array_f.foreach_set(a)
|
||||
for v1, v2 in zip(a, self.array_f[:]):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
b = np.empty(10, dtype=np.float32)
|
||||
self.array_f.foreach_get(b)
|
||||
for v1, v2 in zip(a, b):
|
||||
self.assertEqual(v1, v2)
|
||||
|
||||
b = [None] * 10
|
||||
self.array_f.foreach_get(b)
|
||||
for v1, v2 in zip(a, b):
|
||||
self.assertEqual(v1, v2)
|
||||
def test_foreach_getset_f_3d(self):
|
||||
self.do_test_foreach_getset(id_inst.test_array_f_3d, 'FLOAT', (3, 2, 4))
|
||||
|
||||
|
||||
class TestPropArrayMultiDimensional(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user