The heid bug-hunt panel on 4cfbce5 (hulda, regin, kimi; groa timed out) found
four real defects in the round-trip fix, and three of its arms converged on the
worst: it re-created the bug it existed to fix.
- Two names, never a sniffed file (3/3). JSON went into the OLD `.blurred`, and
the reader guessed the format from the bytes, so a legacy file whose one line
is an item named `["a.png"]` read as {"a.png"} and blurred the neighbour. The
set now lives in `.blurred.json`, JSON only. The legacy `.blurred` is read as
lines only, and only while `.blurred.json` is absent; the first write retires
it, after the new file is in place.
- A planted directory is a 409, not a 500 (2/3 plus a third angle, executed by
the seat). The reader was hardened against it and the writer was not:
os.replace and unlink raised IsADirectoryError through the route. Now the
writer is judged by its reader: set_blurred re-reads after writing and raises
BlurUnwritable unless the set on disk is the set asked for. That one check
covers a directory at either name, a permission and a race.
- A lone surrogate is dropped on read (hulda, executed). `"\ud800"` is a valid
JSON string that no filename can produce, and the UTF-8 encode raised on it
at every later write.
- The writer respects the reader's size cap (2/3). Nothing capped the write,
and the reader reads an oversized file as EMPTY, which reveals everything.
- One predicate, check_rel, for the route and the CLI (2/3). The CLI's `*..*`
substring guard refused `a..b.png`, which the route accepts. It also refuses
an empty path now (regin, kimi), and every item is checked before any is
written.
- `booth blur` fails closed, with a message and exit 3, when its package is
missing (kimi), as `link` already does.
Declined, with reasons: the Item positional-constructor break (booth_items is
the only constructor, INV-1), the fdopen fd leak and the short read (not
constructible on a local filesystem, and the `.seen` shape), and
unreadable-reads-as-revealed (blur is cosmetic; the `.seen` posture).
blur_storage.toml: 20/20 proved. One row came back VACUOUS on its first run,
because `set() or X` is X, and was rewritten before counting.
213 lines
7.2 KiB
TOML
213 lines
7.2 KiB
TOML
# Per-item blur storage: `.blurred` round-trips any rel, whoever writes it.
|
|
# The fix for the wrong-item write the heid bug-hunt found through r2b merge 1
|
|
# (a stripped rel blurred its neighbour), operator-ruled 2026-09-23. Every row
|
|
# is a change tests/test_blur.py claims to forbid.
|
|
#
|
|
# NOT here, on purpose: the S_ISREG guard in read_blurred. With O_NONBLOCK a
|
|
# FIFO opens and reads as EOF, a symlink is already refused by O_NOFOLLOW, and a
|
|
# device node needs root to plant, so no test here can see that guard go. It
|
|
# stays as the `.seen` shape, and it is not claimed as a proven falsifier.
|
|
|
|
unit = "blur storage round-trip"
|
|
|
|
[[mutation]]
|
|
label = "the writer strips the rel (the old line format's loss)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_leading_space_rel_round_trips"
|
|
old = '''
|
|
current.add(rel)'''
|
|
new = '''
|
|
current.add(rel.strip())'''
|
|
|
|
[[mutation]]
|
|
label = "the route strips `f` before writing (the reported wrong-item write)"
|
|
file = "booth/app.py"
|
|
test = "tests/test_blur.py::test_the_blur_route_blurs_exactly_the_item_it_names"
|
|
old = '''
|
|
rel = f.lstrip("/")'''
|
|
new = '''
|
|
rel = f.strip().lstrip("/")'''
|
|
|
|
[[mutation]]
|
|
label = "a JSON-only reader: every live line-format file un-blurs on deploy"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_the_legacy_line_format_still_reads"
|
|
old = '''
|
|
return {ln.strip() for ln in text.splitlines() if ln.strip()}'''
|
|
new = '''
|
|
return set()'''
|
|
|
|
[[mutation]]
|
|
label = "a legacy file that is not JSON reads as nothing instead of falling back"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_legacy_rel_that_starts_with_a_bracket_still_reads"
|
|
old = '''
|
|
text = raw.decode("utf-8", "surrogateescape")
|
|
return {ln.strip()'''
|
|
new = '''
|
|
text = raw.decode("utf-8", "surrogateescape")
|
|
try:
|
|
json.loads(text)
|
|
except ValueError:
|
|
return set()
|
|
return {ln.strip()'''
|
|
|
|
[[mutation]]
|
|
label = "a FIFO blocks the read (no O_NONBLOCK)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_fifo_blur_file_does_not_block_the_read"
|
|
old = '''
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)'''
|
|
new = '''
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)'''
|
|
|
|
[[mutation]]
|
|
label = "the read follows a planted symlink (no O_NOFOLLOW)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_symlinked_blur_file_is_not_followed_on_read"
|
|
old = '''
|
|
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)'''
|
|
new = '''
|
|
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK)'''
|
|
|
|
[[mutation]]
|
|
label = "the write goes through a planted symlink instead of replacing it"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_write_replaces_a_planted_symlink_rather_than_writing_through_it"
|
|
old = '''
|
|
os.replace(tmp, path)'''
|
|
new = '''
|
|
path.write_bytes(Path(tmp).read_bytes()); os.unlink(tmp)'''
|
|
|
|
[[mutation]]
|
|
label = "the stored order is not the stated one (invariant 6)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_the_file_is_a_json_array_in_sorted_order"
|
|
old = '''
|
|
body = json.dumps(sorted(current), ensure_ascii=False)'''
|
|
new = '''
|
|
body = json.dumps(sorted(current, reverse=True), ensure_ascii=False)'''
|
|
|
|
[[mutation]]
|
|
label = "the CLI ignores the verb: `unblur` blurs"
|
|
file = "scripts/booth"
|
|
test = "tests/test_blur.py::test_the_cli_writes_the_format_the_service_reads"
|
|
old = '''
|
|
on = sys.argv[1] == "blur"'''
|
|
new = '''
|
|
on = True'''
|
|
|
|
[[mutation]]
|
|
label = "the CLI writes past a refused '..' path (the shared predicate loses its component check)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_the_cli_still_refuses_a_dotdot_path"
|
|
old = '''
|
|
if not rel or rel.startswith("/") or ".." in rel.split("/"):'''
|
|
new = '''
|
|
if not rel or rel.startswith("/"):'''
|
|
|
|
[[mutation]]
|
|
label = "the item's own blur is the composed one (booth fog leaks into it)"
|
|
file = "booth/items.py"
|
|
test = "tests/test_blur.py::test_the_item_record_carries_its_own_blur_apart_from_the_booths"
|
|
old = '''
|
|
blurred_self=rel in blurred,'''
|
|
new = '''
|
|
blurred_self=rel in blurred or booth_blur,'''
|
|
|
|
[[mutation]]
|
|
label = "app.py reads the blur file a second time (invariant 3)"
|
|
file = "booth/app.py"
|
|
test = "tests/test_blur.py::test_app_py_never_reads_the_blur_file_itself"
|
|
old = '''
|
|
out = []
|
|
for it in booth_items(child):'''
|
|
new = '''
|
|
out = []
|
|
read_blurred(child)
|
|
for it in booth_items(child):'''
|
|
|
|
# ---- the heid bug-hunt on this change (hulda, regin, kimi), folded -------------
|
|
|
|
[[mutation]]
|
|
label = "the legacy file is sniffed for JSON again (a `[\"a.png\"]` line blurs the neighbour)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_legacy_line_that_is_valid_json_still_reads_as_a_line"
|
|
old = '''
|
|
text = raw.decode("utf-8", "surrogateescape")
|
|
return {ln.strip()'''
|
|
new = '''
|
|
text = raw.decode("utf-8", "surrogateescape")
|
|
try:
|
|
d = json.loads(text)
|
|
if isinstance(d, list):
|
|
return {r for r in d if isinstance(r, str)}
|
|
except ValueError:
|
|
pass
|
|
return {ln.strip()'''
|
|
|
|
[[mutation]]
|
|
label = "no postcondition: a planted directory's OSError is swallowed as success"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_planted_directory_at_the_blur_file_is_a_refusal_not_a_crash"
|
|
old = '''
|
|
if read_blurred(booth) != current:'''
|
|
new = '''
|
|
if False:'''
|
|
|
|
[[mutation]]
|
|
label = "the route turns a disk-state refusal into a 500"
|
|
file = "booth/app.py"
|
|
test = "tests/test_blur.py::test_the_route_answers_a_planted_directory_with_409"
|
|
old = '''
|
|
raise HTTPException(status_code=409, detail=str(exc))'''
|
|
new = '''
|
|
raise'''
|
|
|
|
[[mutation]]
|
|
label = "a lone surrogate from a planted file reaches the writer"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_a_lone_surrogate_in_the_file_is_skipped_and_writes_still_work"
|
|
old = '''
|
|
return {r for r in data if isinstance(r, str) and r and _encodable(r)}'''
|
|
new = '''
|
|
return {r for r in data if isinstance(r, str) and r}'''
|
|
|
|
[[mutation]]
|
|
label = "the writer writes a set the reader would refuse and read as nothing"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_the_writer_never_writes_a_set_the_reader_would_refuse"
|
|
old = '''
|
|
if len(body) > BLUR_MAX_BYTES:'''
|
|
new = '''
|
|
if False:'''
|
|
|
|
[[mutation]]
|
|
label = "an empty item path is accepted and stored"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_the_cli_refuses_an_empty_item_path_before_writing"
|
|
old = '''
|
|
if not rel or rel.startswith("/") or ".." in rel.split("/"):'''
|
|
new = '''
|
|
if rel.startswith("/") or ".." in rel.split("/"):'''
|
|
|
|
[[mutation]]
|
|
label = "a double dot INSIDE a name is refused (the old `*..*` substring rule)"
|
|
file = "booth/blur.py"
|
|
test = "tests/test_blur.py::test_the_cli_accepts_a_double_dot_inside_a_name"
|
|
old = '''
|
|
if not rel or rel.startswith("/") or ".." in rel.split("/"):'''
|
|
new = '''
|
|
if not rel or rel.startswith("/") or ".." in rel:'''
|
|
|
|
[[mutation]]
|
|
label = "the CLI dies with a traceback when its package is missing"
|
|
file = "scripts/booth"
|
|
test = "tests/test_blur.py::test_the_cli_fails_closed_without_its_package"
|
|
old = '''
|
|
except ImportError as exc:
|
|
src = os.environ["BOOTH_SRC"]'''
|
|
new = '''
|
|
except ZeroDivisionError as exc:
|
|
src = os.environ["BOOTH_SRC"]'''
|