Skip to content

Commit ba883a5

Browse files
authored
fix(zarr-indexing): reject overflowing integer selectors (#4333)
* fix(zarr-indexing): reject overflowing integer selectors Assisted-by: Codex:gpt-6 * docs(zarr-indexing): number changelog entry for PR 4333 Assisted-by: Codex:gpt-6 * fix(zarr-indexing): type validated integer selectors explicitly Assisted-by: Codex:gpt-6
1 parent fb7ad35 commit ba883a5

3 files changed

Lines changed: 38 additions & 4 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Direct `IndexTransform.oindex` and `IndexTransform.vindex` selections now reject
2+
integer array coordinates outside the `np.intp` range before conversion.
3+
Previously, oversized `uint64` values could wrap to negative coordinates and
4+
silently select a different location in a domain containing negative coordinates.

‎packages/zarr-indexing/src/zarr_indexing/transform.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,7 +1541,7 @@ def _normalize_oindex_selection(
15411541
(indices,) = np.nonzero(sel)
15421542
result.append(indices.astype(np.intp))
15431543
elif isinstance(sel, np.ndarray):
1544-
result.append(sel.astype(np.intp))
1544+
result.append(checked_affine(0, 1, sel))
15451545
elif isinstance(sel, slice):
15461546
result.append(sel)
15471547
elif (scalar := as_scalar_index(sel)) is not None:
@@ -1553,7 +1553,9 @@ def _normalize_oindex_selection(
15531553
(indices,) = np.nonzero(array)
15541554
result.append(indices.astype(np.intp))
15551555
else:
1556-
result.append(np.asarray(sel, dtype=np.intp))
1556+
# Advanced selection validation has already checked the element types.
1557+
integer_array = cast("npt.NDArray[np.integer[Any]]", array)
1558+
result.append(checked_affine(0, 1, integer_array))
15571559
else:
15581560
result.append(sel)
15591561

@@ -1743,9 +1745,11 @@ def _apply_vindex(transform: IndexTransform, selection: Any) -> IndexTransform:
17431745
indices_tuple = np.nonzero(boolean_array)
17441746
processed.extend(indices.astype(np.intp) for indices in indices_tuple)
17451747
elif isinstance(sel, np.ndarray):
1746-
processed.append(sel.astype(np.intp))
1748+
processed.append(checked_affine(0, 1, sel))
17471749
elif isinstance(sel, (list, tuple)):
1748-
processed.append(np.asarray(sel, dtype=np.intp))
1750+
# Advanced selection validation has already checked the element types.
1751+
integer_array = cast("npt.NDArray[np.integer[Any]]", np.asarray(sel))
1752+
processed.append(checked_affine(0, 1, integer_array))
17491753
elif (scalar := as_scalar_index(sel)) is not None:
17501754
processed.append(np.array([scalar], dtype=np.intp))
17511755
else:

‎packages/zarr-indexing/tests/test_transform.py‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,32 @@ def test_vindex_multiple_arrays_preserves_shared_axes(self) -> None:
763763
assert result.output[1].index_array.shape == (2,)
764764

765765

766+
@pytest.mark.parametrize("mode", ["oindex", "vindex"])
767+
@pytest.mark.parametrize("as_list", [False, True])
768+
@pytest.mark.parametrize("value", [2**63, 2**64 - 1])
769+
def test_direct_advanced_index_rejects_unsigned_overflow(
770+
mode: str, as_list: bool, value: int
771+
) -> None:
772+
"""Narrowing must not turn huge positive coordinates into valid negative ones."""
773+
transform = IndexTransform.identity(
774+
IndexDomain(inclusive_min=(np.iinfo(np.intp).min,), exclusive_max=(0,))
775+
)
776+
selector = [value] if as_list else np.array([value], dtype=np.uint64)
777+
with pytest.raises(OverflowError, match="outside np.intp range"):
778+
getattr(transform, mode)[selector]
779+
780+
781+
@pytest.mark.parametrize("mode", ["oindex", "vindex"])
782+
@pytest.mark.parametrize("dtype", ["intp", "uint8", "uint64"])
783+
def test_direct_advanced_index_preserves_valid_coordinates(mode: str, dtype: str) -> None:
784+
transform = IndexTransform.from_shape((5,))
785+
selector = np.array([3, 0, 3], dtype=dtype)
786+
selector.flags.writeable = False
787+
result = getattr(transform, mode)[selector]
788+
np.testing.assert_array_equal(result.apply_many(np.arange(3).reshape(3, 1)), [[3], [0], [3]])
789+
np.testing.assert_array_equal(selector, [3, 0, 3])
790+
791+
766792
@pytest.mark.parametrize("mode", ["oindex", "vindex"])
767793
def test_direct_advanced_index_rejects_float_arrays(mode: str) -> None:
768794
helper = getattr(IndexTransform.from_shape((5,)), mode)

0 commit comments

Comments
 (0)