fix(blur): fold the heid bug-hunt: two file names, a reader-judged writer, one predicate
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.
This commit is contained in:
@@ -66,9 +66,9 @@ that list in the same commit.
|
||||
No database. `ls ~/booth-data` tells you everything the service knows.
|
||||
|
||||
Per-booth operator state is a **dotfile inside the booth**: `.forever` (keep),
|
||||
`.viewed` (last deliberate look — U4's "viewing is activity"), `.blurred` (the
|
||||
per-item blur set, a JSON ARRAY — see below), `.seen` (R2: rels looked at full
|
||||
size, a JSON ARRAY), `.blurbooth` (the whole booth fogged — a MARKER like `.forever`, not
|
||||
`.viewed` (last deliberate look — U4's "viewing is activity"), `.blurred.json`
|
||||
(the per-item blur set, a JSON ARRAY — see below; the legacy `.blurred` is
|
||||
read-only), `.seen` (R2: rels looked at full size, a JSON ARRAY), `.blurbooth` (the whole booth fogged — a MARKER like `.forever`, not
|
||||
JSON, because a boolean has no rels to round-trip), `.marks.json` + `.marks.lock` (judgment), `.pins` (link-board pin
|
||||
ids), `.uploaded` (upload-booth marker). `booth_items()` skips `name.startswith(".")`, so a new
|
||||
dotfile costs nothing in item counts, galleries or zips. That skip is why the
|
||||
@@ -79,19 +79,30 @@ inventing a sidecar-per-item.
|
||||
O_NONBLOCK` with an `S_ISREG` check and a size cap.** A rel may carry a leading
|
||||
space or a newline, and line-stripped storage does not round-trip it: `.blurred`
|
||||
was one stripped rel per line, and blurring `" a.png"` blurred `a.png` instead.
|
||||
`.seen` was written as JSON for exactly that reason (design-dev, R2), and
|
||||
`.blurred` now matches it (`booth/blur.py`). The open flags mean a planted
|
||||
symlink is refused and a FIFO cannot hang the read, which is the outage in
|
||||
`persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md`. **Any new dotfile
|
||||
inherits that shape.** `.blurred`'s reader still accepts the old line format,
|
||||
so a booth written before the change keeps its blur until its next write
|
||||
upgrades the file. Do not remove that fallback while a line-format file can
|
||||
still exist.
|
||||
`.seen` was written as JSON for exactly that reason (design-dev, R2), and the
|
||||
blur set now matches it in `.blurred.json` (`booth/blur.py`). The open flags
|
||||
mean a planted symlink is refused and a FIFO cannot hang the read, which is the
|
||||
outage in `persistent-memory.d/2026-09-22-size-cap-opened-a-hang.md`. **Any new
|
||||
dotfile inherits that shape.**
|
||||
|
||||
**A dotfile with two writers has ONE implementation of the writer.** `.blurred`
|
||||
is written by the service and by `booth blur`, and both call
|
||||
`booth.blur.set_blurred`; the CLI used to keep a grep/printf writer of its own,
|
||||
and two writers of one format is how the formats drift apart.
|
||||
⚠ **A format change gets a NEW NAME, never a sniffed file.** The first cut of
|
||||
the blur fix wrote JSON into `.blurred` and guessed the format from the bytes;
|
||||
a legacy file whose one line is an item literally named `["a.png"]` parses as
|
||||
JSON and blurs the neighbour, the bug being fixed (heid bug-hunt, 3 of 3). So
|
||||
`.blurred.json` is JSON only, the legacy `.blurred` is lines only and read only
|
||||
while `.blurred.json` is absent, and the first write retires it. Do not remove
|
||||
that legacy read while a line-format file can still exist.
|
||||
|
||||
**A writer is judged by its reader.** `set_blurred` re-reads after writing and
|
||||
raises `BlurUnwritable` unless the reader returns exactly the set asked for.
|
||||
One postcondition covers a planted directory, a permission and a race without
|
||||
a branch per way the disk can be wrong; the route answers it 409, never 500.
|
||||
|
||||
**A dotfile with two writers has ONE implementation of the writer, and one
|
||||
predicate for its keys.** The blur set is written by the service and by `booth
|
||||
blur`; both call `booth.blur.set_blurred`, and both ask `check_rel` what an
|
||||
item path is. The CLI used to keep a grep/printf writer and a `*..*` guard of
|
||||
its own, which refused `a..b.png` where the route accepted it.
|
||||
|
||||
### 3. One resolver for item facts
|
||||
|
||||
|
||||
Reference in New Issue
Block a user