Repository: zarr-developers/zarr-python

Issue:
`keys`/`items` yields two diferent results between v2 and v3 on `ZipStore`
### Zarr version

0c154c327e92a40648b3dd95e9e9d9c71d3d53a3

### Numcodecs version

0.15.0

### Python Version

3.12

### Operating System

Mac ARM

### Installation

`uv`

### Description

I have an admittedly old zarr zip store where the `keys` from one of the subgroups appears to be yielding itself in zarr v3 (erroneously as it errors out on access) but correctly in zarr v2.  I can't reproduce this just by creating groups in v3 or v2, with either the current main branch or the final v2 commit.

It's possible the file is corrupted or something, tough to say since it is a zip store.  The same problem does not happen when I unzip the store and either visually examine its contents or when I actually open the store using `zarr.open` and then run `.keys()`

### Steps to reproduce

[adata.zarr.zip](https://github.com/user-attachments/files/18534376/adata.zarr.zip)

For the above file one should run in v2:

```python
import zarr
g = zarr.open('adata.zarr.zip', mode="r")
list(g["obsm"].keys()) # ['array', 'df', 'sparse']
g["obsm"]["obsm"] # not there!
```

and then

```python
import zarr
from zarr.storage import ZipStore
g = zarr.open(ZipStore('/Users/ilangold/Projects/Theis/anndata/tests/data/archives/v0.7.0/adata.zarr.zip'), mode="r")
list(g["obsm"].keys()) # ['array', 'df', 'sparse', 'obsm']
g["obsm"]["obsm"] # also not there!
```

### Additional output

_No response_

Discussion hints:
d-v-b: can you add a test that would have failed on `main`, but passes after this fix? Ideally this would be a modification of one of the shared store test cases.
d-v-b: you don't need to upload anything -- modify one of these [test functions](https://github.com/zarr-developers/zarr-python/blob/458299857141a5470ba3956d8a1607f52ac33857/src/zarr/testing/store.py#L368-L421) to generate a condition that triggers the error. 
martindurant: (you can, of course, use builtin zipfile to generate a possibly problematic object - that can be done in memory or tmpdir)
d-v-b: in the interest of speed, you could follow @martindurant's advice and add a test case that creates a problematic zip instance to the zip-store tests: https://github.com/zarr-developers/zarr-python/blob/main/tests/test_store/test_zip.py, we could then figure out how to generalize this to the base class if we feel the need.
ilan-gold: Done!

Solution patch:
diff --git a/changes/2758.bugfix.rst b/changes/2758.bugfix.rst
new file mode 100644
index 00000000..6b80f8a6
--- /dev/null
+++ b/changes/2758.bugfix.rst
@@ -0,0 +1,1 @@
+Fix zip-store path checking for stores with directories listed as files.
\ No newline at end of file
diff --git a/src/zarr/storage/_zip.py b/src/zarr/storage/_zip.py
index 51bb702c..bbfe6c67 100644
--- a/src/zarr/storage/_zip.py
+++ b/src/zarr/storage/_zip.py
@@ -283,7 +283,7 @@ class ZipStore(Store):
                     yield key
         else:
             for key in keys:
-                if key.startswith(prefix + "/") and key != prefix:
+                if key.startswith(prefix + "/") and key.strip("/") != prefix:
                     k = key.removeprefix(prefix + "/").split("/")[0]
                     if k not in seen:
                         seen.add(k)


Test patch:
diff --git a/tests/test_store/test_zip.py b/tests/test_store/test_zip.py
index a83327d9..83965610 100644
--- a/tests/test_store/test_zip.py
+++ b/tests/test_store/test_zip.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import os
+import shutil
 import tempfile
 import zipfile
 from typing import TYPE_CHECKING
@@ -14,6 +15,7 @@ from zarr.storage import ZipStore
 from zarr.testing.store import StoreTests
 
 if TYPE_CHECKING:
+    from pathlib import Path
     from typing import Any
 
 
@@ -111,3 +113,15 @@ class TestZipStore(StoreTests[ZipStore, cpu.Buffer]):
         kws = {**store_kwargs, "mode": zip_mode}
         store = await self.store_cls.open(**kws)
         assert store.read_only == read_only
+
+    def test_externally_zipped_store(self, tmp_path: Path) -> None:
+        # See: https://github.com/zarr-developers/zarr-python/issues/2757
+        zarr_path = tmp_path / "foo.zarr"
+        root = zarr.open_group(store=zarr_path, mode="w")
+        root.require_group("foo")
+        root["foo"]["bar"] = np.array([1])
+        shutil.make_archive(zarr_path, "zip", zarr_path)
+        zip_path = tmp_path / "foo.zarr.zip"
+        zipped = zarr.open_group(ZipStore(zip_path, mode="r"), mode="r")
+        assert list(zipped.keys()) == list(root.keys())
+        assert list(zipped["foo"].keys()) == list(root["foo"].keys())