fix(items): an entry the walk cannot stat costs that entry, not every page
Path.is_file() swallows a missing entry but propagates EACCES. A directory with read and no execute permission lists its names while every stat under it raises, so one such folder in one booth raised out of booth_items — and list_booths calls that for every booth, taking the index down for all of them. The same blast radius as the unrepresentable-filename case; the same posture applies: such an entry is not a renderable file. Predates R2 (identical on main before the merge); found while folding R2's bug-hunt, where it made landed_at's per-entry skip unreachable.
This commit is contained in:
+11
-1
@@ -283,7 +283,17 @@ def booth_items(booth: Path) -> list[Item]:
|
||||
"""
|
||||
by_rel: dict[str, Path] = {}
|
||||
for p in booth.rglob("*"):
|
||||
if not p.is_file() or p.name.startswith("."):
|
||||
try:
|
||||
# `is_file` swallows a missing entry but PROPAGATES EACCES: a
|
||||
# directory that lists but cannot be searched made every stat under
|
||||
# it raise out of here, and `list_booths` calls this for every
|
||||
# booth — one such folder took down the index for all of them.
|
||||
# An entry nobody can stat is not a renderable file.
|
||||
if not p.is_file():
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
if p.name.startswith("."):
|
||||
continue
|
||||
if is_ask_file(p.name) or is_answer_file(p.name):
|
||||
continue
|
||||
|
||||
@@ -341,6 +341,31 @@ def test_one_unrepresentable_filename_costs_its_own_tile_not_the_booth(tmp_path)
|
||||
assert [it.rel for it in got] == ["ok.png"]
|
||||
|
||||
|
||||
|
||||
def test_a_folder_that_lists_but_cannot_be_searched_costs_its_files_not_the_index(tmp_path):
|
||||
"""Found folding R2's bug-hunt: `Path.is_file()` swallows a missing entry
|
||||
but PROPAGATES EACCES. A directory with read and no execute permission
|
||||
lists its names, and every stat under it raises — so one such folder in
|
||||
one booth took out the index for every booth, the same blast radius as the
|
||||
unrepresentable filename above. Its files are not items.
|
||||
|
||||
Defeating change: calling `is_file()` outside the OSError guard."""
|
||||
b = tmp_path / "b"
|
||||
b.mkdir()
|
||||
(b / "ok.png").write_bytes(b"\x89PNG")
|
||||
sub = b / "d"
|
||||
sub.mkdir()
|
||||
(sub / "x.png").write_bytes(b"\x89PNG")
|
||||
sub.chmod(0o644) # r--: listable, nothing inside stat-able
|
||||
try:
|
||||
with pytest.raises(PermissionError):
|
||||
(sub / "x.png").stat() # the fixture is live, not assumed
|
||||
assert [it.rel for it in booth_items(b)] == ["ok.png"]
|
||||
[row] = list_booths(tmp_path, ttl_seconds=86400)
|
||||
assert row["name"] == "b" and row["count"] == 1
|
||||
finally:
|
||||
sub.chmod(0o755)
|
||||
|
||||
def test_a_huge_caption_sidecar_is_not_read_whole(tmp_path):
|
||||
"""HULDA: `read_text()` pulled the entire sidecar into memory before
|
||||
`[:CAPTION_MAX]` trimmed it, and the handler catches only OSError — so a
|
||||
|
||||
Reference in New Issue
Block a user