Track the exact regular files owned by each completed and in-flight peer download instead of sweeping every non-reserved path after cancellation. Bind the record to the canonical games directory, publish pending ownership before payload mutation, and use the final version.ini rename as the recovery commit point. Make replacement, cancellation, and startup recovery preserve unknown files and install state while removing stale or partial downloader-owned bytes. Add a new-format baseline so legacy discarded sentinels cannot make partially modified payloads ready, sync payload and journal state in transaction order, and serialize startup recovery against operation admission. Document ambiguous legacy target adoption, portable alias transitions, and the other ownership tradeoffs in the refactor decision log. Test Plan: - `just clippy` -- passed - `just test` -- passed (182 peer-core tests plus the full workspace) - `just fmt` -- Rust, TOML, and Prettier formatting completed; the command then stopped on 40 pre-existing rumdl findings in unrelated Markdown content - `git diff --cached --check` -- passed
225 lines
11 KiB
Markdown
225 lines
11 KiB
Markdown
# Peer authentication refactor decisions
|
|
|
|
This file records implementation choices that are not fully determined by
|
|
`PEER_AUTH_PLAN.md`, especially choices where every available option has a
|
|
meaningful downside.
|
|
|
|
## 2026-08-09 — Bound protocol-7 download descriptions generously
|
|
|
|
**TL;DR:** Until catalog manifests replace remote descriptions, accept at most
|
|
100,000 entries, 1 TiB per ordinary file, 64 KiB for the in-memory `version.ini`
|
|
sentinel, 255 bytes per path component, 900 bytes per relative path, and 16 TiB
|
|
in aggregate for one game. Resolved destinations are capped at 1,000 platform
|
|
path units.
|
|
|
|
The plan requires count, per-file, and aggregate limits but intentionally does
|
|
not prescribe values. These limits are far above the expected game and CLI
|
|
fixture sizes while bounding allocation, planning work, and arithmetic.
|
|
|
|
Alternatives:
|
|
|
|
- Lower product-sized limits would reject abusive inputs earlier, but risk
|
|
inventing constraints that real large games or mod packs exceed.
|
|
- Limits near `usize`/`u64` maxima avoid practical compatibility concerns, but
|
|
do not meaningfully bound memory or work.
|
|
- Configurable limits add settings and test combinations without a current user
|
|
need; protocol safety limits should be consistent between installations.
|
|
|
|
## 2026-08-09 — Track download ownership; never infer it from unknown files
|
|
|
|
**TL;DR:** Persist an atomic per-game set of successfully downloaded paths under
|
|
application state. On replacement, remove only paths in the previous owned set
|
|
that are absent from the new authoritative manifest. If old or corrupt state has
|
|
no trustworthy set, preserve unknown root files.
|
|
|
|
The existing game directory has no provenance information. Deleting every
|
|
non-reserved path would remove stale downloads, but could also delete a user's
|
|
own files and contradict the plan's preservation rule. The first update after
|
|
upgrading may therefore leave legacy stale files; subsequent successful
|
|
downloads have exact ownership and can clean safely.
|
|
|
|
Alternatives:
|
|
|
|
- Bootstrap ownership from the current directory. This is seamless, but can
|
|
permanently misclassify user files as Lanspread-owned and later delete them.
|
|
- Delete every non-reserved file missing from the new manifest. This is simple
|
|
and cleans legacy state completely, but is unsafe for user files.
|
|
- Delete only familiar archive suffixes such as `.eti`. This avoids most user
|
|
files, but bakes package naming guesses into the security boundary and cannot
|
|
clean general catalog payloads.
|
|
- Never clean stale files. This is safest for user data, but fails the explicit
|
|
successful-replacement requirement and can keep serving obsolete payload.
|
|
|
|
## 2026-08-09 — Generate production manifests outside the source checkout
|
|
|
|
**TL;DR:** Provide a small deterministic generator binary that runs next to the
|
|
canonical production packages and writes the JSON manifest artifacts that are
|
|
then stored and packaged with Lanspread.
|
|
|
|
This checkout has catalog metadata for 186 games but canonical package bytes
|
|
only for a small CLI fixture set. Hashes cannot be derived honestly without the
|
|
bytes. Keeping the generator here makes the format reproducible without
|
|
requiring production game packages in Git.
|
|
|
|
Alternatives:
|
|
|
|
- Store production packages in this repository. That would make generation
|
|
self-contained, but is impractical for size and distribution reasons.
|
|
- Generate hashes at application runtime. This would make local peer bytes the
|
|
authority, defeating catalog-owned verification and adding startup work.
|
|
- Accept hashes announced by peers. This is easy to deploy, but provides no
|
|
protection against a peer that supplies both the bytes and their claimed hash.
|
|
|
|
## 2026-08-09 — Bind ownership records to one canonical games directory
|
|
|
|
**TL;DR:** A per-game ownership record includes an opaque, exact identity for
|
|
the canonical configured games directory. A record from another directory is
|
|
ignored rather than authorizing deletion in the current one.
|
|
|
|
The application can switch games directories while retaining the same state
|
|
directory. Keying ownership only by `game_id` would let provenance learned in
|
|
one tree delete an unrelated same-named file in another tree. The opaque key is
|
|
derived from the platform-native canonical path representation so non-Unicode
|
|
paths do not need a lossy conversion.
|
|
|
|
Alternatives:
|
|
|
|
- Clear every ownership record whenever the setting changes. This is safe, but
|
|
loses useful cleanup history when the user switches back to an earlier tree.
|
|
- Nest ownership records under a games-directory key. This makes the separation
|
|
structural, but complicates all existing per-game state layout and migration.
|
|
- Store the canonical path as JSON text. This is easy to inspect, but cannot
|
|
represent every valid native path without a lossy conversion.
|
|
|
|
## 2026-08-09 — Journal pending download ownership across crashes
|
|
|
|
**TL;DR:** Persist both the last committed file set and a write-ahead pending
|
|
set. Park the old `version.ini` before recording pending ownership, commit the
|
|
new sentinel only after transfer and stale cleanup, then finalize the ledger.
|
|
|
|
A committed-only ledger cannot distinguish a brand-new partial download from
|
|
user files after a crash. The pending set makes cancellation and recovery exact.
|
|
Parking the old sentinel first removes an ambiguity during recovery: while a
|
|
pending set exists, a regular `version.ini` can only be the newly committed
|
|
sentinel. A crash before the pending write instead leaves the parked sentinel,
|
|
which recovery restores.
|
|
|
|
Alternatives:
|
|
|
|
- Record pending before parking the old sentinel. This minimizes time without a
|
|
root sentinel, but recovery cannot tell an old sentinel from a landed commit.
|
|
- Keep only committed ownership. This is smaller, but leaks partial files from a
|
|
crashed first download because they have no trustworthy provenance.
|
|
- Put every downloaded file through a separate staging tree. This gives a clean
|
|
promotion point, but can require another full game's worth of disk and a large
|
|
multi-file transaction mechanism.
|
|
|
|
## 2026-08-09 — Manifest target paths become download-owned
|
|
|
|
**TL;DR:** Once a validated download transaction starts, every regular file path
|
|
in that manifest is treated as downloader-owned for abort and recovery. Unknown
|
|
paths outside the manifest remain untouched.
|
|
|
|
Without prior ownership state, an existing file at an exact manifest target is
|
|
ambiguous. Treating the authoritative target namespace as download-owned keeps
|
|
legacy upgrades working and makes partial-transfer cleanup deterministic. A
|
|
user-created extra file is preserved, but a user file placed at an exact package
|
|
path may be replaced or removed by the download.
|
|
|
|
Alternatives:
|
|
|
|
- Reject every untracked existing target. This preserves ambiguous files, but
|
|
prevents the first post-upgrade update of existing legacy downloads.
|
|
- Snapshot and restore untracked targets. This preserves their bytes, but adds
|
|
unbounded backup space and another crash-consistent transaction.
|
|
- Leave pre-existing untracked targets after abort. This avoids deletion, but
|
|
the transfer may already have truncated or partially overwritten them, so it
|
|
does not actually preserve their contents.
|
|
|
|
## 2026-08-09 — Ownership-record rename is its publication point
|
|
|
|
**TL;DR:** Once an ownership-record temporary file has been synced and renamed,
|
|
the application treats it as published. A subsequent parent-directory sync
|
|
failure is logged but is not reported as a pre-publication failure.
|
|
|
|
Rolling back after the rename could restore the old `version.ini` beside a
|
|
visible pending record. Recovery would then mistake that old sentinel for the
|
|
new download's commit point. Treating rename as publication keeps every
|
|
observable state unambiguous; the parent sync still runs to improve power-loss
|
|
durability.
|
|
|
|
Alternatives:
|
|
|
|
- Return a phase-aware error that forces every caller to distinguish failures
|
|
before and after rename. This is explicit, but spreads a subtle transaction
|
|
protocol across all ownership callers without changing their post-rename
|
|
action.
|
|
- Try to rename the old record back after a sync failure. That adds another
|
|
fallible mutation and can still leave either name visible after a crash.
|
|
- Treat every sync failure as fatal and leave the new record in place. This
|
|
sounds stricter, but callers could then perform the unsafe sentinel rollback
|
|
unless the error also carries publication state.
|
|
|
|
## 2026-08-09 — Reject cross-version portable path aliases
|
|
|
|
**TL;DR:** An update is rejected before mutation if an owned path changes only
|
|
by a portable alias, such as `Data.eti` to `data.eti`.
|
|
|
|
On a case-insensitive filesystem, deleting the old spelling after transfer can
|
|
delete the newly written destination. On a case-sensitive filesystem, keeping
|
|
both spellings leaves stale package data. Rejecting the ambiguous transition is
|
|
rare, deterministic, and safe on every supported filesystem.
|
|
|
|
Alternatives:
|
|
|
|
- Compare exact paths only. This correctly removes the old path on Linux, but
|
|
can delete current data on Windows and default macOS filesystems.
|
|
- Treat alias-equivalent paths as identical everywhere. This protects
|
|
case-insensitive filesystems, but leaks the old spelling where both names can
|
|
coexist.
|
|
- Detect filesystem case behavior and rename through a temporary name. This can
|
|
support case-only catalog changes, but adds another crash-consistent mutation
|
|
protocol for a catalog shape the publisher can avoid.
|
|
|
|
## 2026-08-09 — Ownership cleanup preserves all directories
|
|
|
|
**TL;DR:** The download ledger authorizes deletion of exact regular files only;
|
|
cleanup does not prune their now-empty parent directories.
|
|
|
|
An empty directory may have existed before Lanspread placed an owned file in it,
|
|
and the current ledger cannot prove directory provenance. Leaving an empty
|
|
directory is harmless, while deleting a user-created directory violates the
|
|
fail-safe ownership boundary.
|
|
|
|
Alternatives:
|
|
|
|
- Delete every empty ancestor of an owned file. This keeps roots tidy but can
|
|
delete a user-owned empty directory.
|
|
- Journal directory ownership and creation state. This supports exact pruning,
|
|
but expands the crash protocol and still needs a policy for pre-existing
|
|
manifest directories.
|
|
|
|
## 2026-08-09 — A baseline record distinguishes new and legacy scratch
|
|
|
|
**TL;DR:** Before parking an existing `version.ini`, the new downloader durably
|
|
writes at least an empty ownership record. Recovery restores a parked sentinel
|
|
only when that valid baseline exists; scratch beside a missing or invalid record
|
|
is discarded without making payload bytes ready.
|
|
|
|
Older builds used `.version.ini.discarded` but could leave it behind after they
|
|
had already truncated or overwritten payload files. The scratch filename alone
|
|
therefore cannot prove that mutation never began. The baseline makes every park
|
|
performed by the new transaction format distinguishable.
|
|
|
|
Alternatives:
|
|
|
|
- Restore every discarded sentinel when no journal exists. This preserves a
|
|
clean crash between park and journaling, but can advertise a legacy partial
|
|
payload as a complete game.
|
|
- Never restore a discarded sentinel. This is fail-safe for upgrades, but would
|
|
unnecessarily discard a known-clean sentinel after a new-format pre-journal
|
|
crash.
|
|
- Introduce another dedicated phase-marker file. This is equally expressive, but
|
|
adds a second persistent transaction artifact where an empty valid ledger
|
|
already provides the needed proof.
|