edit: plan every edit from the file the editor holds, not its path
FileEditor re-opened its path to plan each edit but wrote through the file it held open, and the Python 'r+' handle re-opened the path after every edit to read. When the path came to name another file between edits (a rename or replacement, or a relative path after os.chdir), an edit was laid out from the other file's metadata and written into the held one, corrupting it, and later reads came from the other file (the review's repro: h5py then reports "invalid dataset size, likely file corruption"). The editor now plans from a mapping of its own file (a clone of the held descriptor, dropped before the edit writes) and canonicalises its path at open. New FileEditor::reader() opens the held file anew for reading, without sharing the editor's flock (a mapping of a cloned descriptor holds the lock until unmapped): through /proc/self/fd on Linux, which follows a renamed file; elsewhere by path, refused on Unix when the path no longer names the held file. The Python handle reads through it and keeps no path; a 'w' file is written at the absolute path it was opened with. Tests: edit_tests.rs edits_go_to_the_file_held_not_the_path; test_edit.py test_relative_path_and_chdir and test_path_replaced_between_edits. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -823,3 +823,76 @@ def test_close_releases_the_file(h5py, tmp_path):
|
||||
ds[0] = 1
|
||||
with h5py.File(path, "r+") as g:
|
||||
g["d"][0] = 9
|
||||
|
||||
|
||||
def _two_files(h5py, tmp_path):
|
||||
"""d1/f.h5 and d2/f.h5: same name, different layouts (the review's
|
||||
repro)."""
|
||||
(tmp_path / "d1").mkdir()
|
||||
(tmp_path / "d2").mkdir()
|
||||
with h5py.File(tmp_path / "d1" / "f.h5", "w") as f:
|
||||
f.create_dataset("x", data=np.arange(10, dtype="<i4"))
|
||||
f.create_dataset("big", data=np.full(5000, 1.5))
|
||||
with h5py.File(tmp_path / "d2" / "f.h5", "w") as f:
|
||||
f.create_dataset("pad", data=np.full(3000, 2.5))
|
||||
f.create_dataset("x", data=np.arange(10, dtype="<i4") + 500)
|
||||
return tmp_path / "d1" / "f.h5", tmp_path / "d2" / "f.h5"
|
||||
|
||||
|
||||
def _held_file_edited(h5py, held, other, other_bytes):
|
||||
"""The edits went to `held`, planned from its own metadata; `other`
|
||||
was not touched."""
|
||||
assert other.read_bytes() == other_bytes, "the other file changed"
|
||||
with h5py.File(held, "r") as f:
|
||||
np.testing.assert_array_equal(f["x"][()], np.full(10, 7))
|
||||
np.testing.assert_array_equal(f["big"][()], np.full(5000, 1.5))
|
||||
np.testing.assert_array_equal(f.attrs["note"], np.arange(50.0))
|
||||
h5dump_reads(str(held))
|
||||
|
||||
|
||||
def test_relative_path_and_chdir(h5py, tmp_path, monkeypatch):
|
||||
"""A file opened by a relative path keeps being the file edited and read
|
||||
after os.chdir (an edit was planned from the file the path named in the
|
||||
new directory and written into the one open, corrupting it)."""
|
||||
held, other = _two_files(h5py, tmp_path)
|
||||
other_bytes = other.read_bytes()
|
||||
monkeypatch.chdir(held.parent)
|
||||
f = clawhdf5.File("f.h5", "r+")
|
||||
ds = f["x"]
|
||||
monkeypatch.chdir(other.parent)
|
||||
ds[:] = np.full(10, 7, "<i4")
|
||||
np.testing.assert_array_equal(ds[:5], np.full(5, 7)) # read from the held file
|
||||
f.attrs["note"] = np.arange(50.0)
|
||||
np.testing.assert_array_equal(f["big"][()], np.full(5000, 1.5))
|
||||
assert "pad" not in f
|
||||
f.close()
|
||||
_held_file_edited(h5py, held, other, other_bytes)
|
||||
# 'w' writes where the path named when the file was opened.
|
||||
monkeypatch.chdir(held.parent)
|
||||
w = clawhdf5.File("new.h5", "w")
|
||||
monkeypatch.chdir(other.parent)
|
||||
w.create_dataset("d", data=np.arange(3))
|
||||
w.close()
|
||||
assert (held.parent / "new.h5").exists() and not (other.parent / "new.h5").exists()
|
||||
|
||||
|
||||
def test_path_replaced_between_edits(h5py, tmp_path):
|
||||
"""The path renamed away and another file put in its place between
|
||||
edits: the edits go to the file held open, never mixed with the other."""
|
||||
held, other = _two_files(h5py, tmp_path)
|
||||
path = str(tmp_path / "f.h5")
|
||||
os.replace(held, path)
|
||||
moved = tmp_path / "moved.h5"
|
||||
with clawhdf5.File(path, "r+") as f:
|
||||
ds = f["x"]
|
||||
ds[0] = 7
|
||||
os.replace(path, moved)
|
||||
shutil.copy(other, path)
|
||||
other_bytes = open(path, "rb").read()
|
||||
ds[:] = np.full(10, 7, "<i4")
|
||||
f.attrs["note"] = np.arange(50.0)
|
||||
np.testing.assert_array_equal(f["x"][()], np.full(10, 7))
|
||||
assert "pad" not in f
|
||||
from pathlib import Path
|
||||
_held_file_edited(h5py, moved, Path(path), other_bytes)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user