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:
+33
-16
@@ -325,32 +325,49 @@ case "$cmd" in
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Every item is checked BEFORE anything is written, so a refused path
|
||||
# leaves the blur set exactly as it was.
|
||||
# Items are made booth-relative here; WHETHER each one is an item path is
|
||||
# booth.blur.check_rel's call, the same predicate the web route uses, so
|
||||
# `booth blur g a..b.png` and the operator's click agree. (A `*..*`
|
||||
# substring test here refused `a..b.png`, which the route accepted.)
|
||||
items=()
|
||||
for item in "$@"; do
|
||||
item="${item#"$DATA/$b/"}"; item="${item#/}"
|
||||
case "$item" in
|
||||
*..*) echo "refusing path with '..': $item" >&2; exit 2 ;;
|
||||
esac
|
||||
item="${item#"$DATA/$b/"}"
|
||||
[ -e "$DATA/$b/$item" ] || echo "warning: no such item in $b: $item" >&2
|
||||
items+=("$item")
|
||||
done
|
||||
# ONE WRITER. `.blurred` is a JSON array now (a rel may carry a leading
|
||||
# space or a newline, and the old line format could not round-trip it), and
|
||||
# the service writes it too — so the CLI goes through the same stdlib-only
|
||||
# booth.blur the service does, never a grep/printf of its own. Items travel
|
||||
# as argv, which carries any byte but NUL; an env var or a line would not.
|
||||
# An emptied set removes the file (booth.blur), so `ls -a` still says
|
||||
# whether anything here is blurred at all.
|
||||
# ONE WRITER. `.blurred.json` is a JSON array (a rel may carry a leading
|
||||
# space or a newline, which the old `.blurred` line format could not
|
||||
# round-trip), and the service writes it too, so the CLI goes through the
|
||||
# same stdlib-only booth.blur, never a grep/printf of its own. Items travel
|
||||
# as argv, which carries any byte but NUL. EVERY item is checked before ANY
|
||||
# is written, so a refused path leaves the blur set exactly as it was.
|
||||
# Exit 2: an item path refused. Exit 3: nothing written, and why (the
|
||||
# package is missing, or something that is not a file is in the way).
|
||||
BOOTH_SRC="$(booth_src)" BOOTH_DIR="$DATA/$b" python3 -c '
|
||||
import os, sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, os.environ["BOOTH_SRC"])
|
||||
from booth.blur import set_blurred # stdlib only — no venv needed
|
||||
try:
|
||||
from booth.blur import BlurUnwritable, check_rel, set_blurred # stdlib only
|
||||
except ImportError as exc:
|
||||
src = os.environ["BOOTH_SRC"]
|
||||
sys.stderr.write(f"booth blur: cannot load booth.blur from {src} ({exc}).\n"
|
||||
" Run the booth script from its checkout, beside its booth/ package. Nothing was changed.\n")
|
||||
sys.exit(3)
|
||||
on = sys.argv[1] == "blur"
|
||||
for rel in sys.argv[2:]:
|
||||
set_blurred(Path(os.environ["BOOTH_DIR"]), rel, on)
|
||||
rels = [r.lstrip("/") for r in sys.argv[2:]]
|
||||
for rel in rels:
|
||||
try:
|
||||
check_rel(rel)
|
||||
except ValueError as exc:
|
||||
sys.stderr.write(f"booth blur: refusing {rel!r}: {exc}. Nothing was changed.\n")
|
||||
sys.exit(2)
|
||||
for rel in rels:
|
||||
try:
|
||||
set_blurred(Path(os.environ["BOOTH_DIR"]), rel, on)
|
||||
except BlurUnwritable as exc:
|
||||
sys.stderr.write(f"booth blur: {exc}\n")
|
||||
sys.exit(3)
|
||||
' "$cmd" "${items[@]}"
|
||||
if [ "$cmd" = blur ]; then
|
||||
echo "blurred (cosmetic — still served): $URL/b/$b/"
|
||||
|
||||
Reference in New Issue
Block a user