Skip to content

GBC RTC clobber: distinct sidecar cloud slots + cross-ext write-guard + verify harness#11

Open
terafin wants to merge 3 commits into
feat/scanner-libretro-format-batchfrom
sindri/sidecar-cloud-slot-guard
Open

GBC RTC clobber: distinct sidecar cloud slots + cross-ext write-guard + verify harness#11
terafin wants to merge 3 commits into
feat/scanner-libretro-format-batchfrom
sindri/sidecar-cloud-slot-guard

Conversation

@terafin

@terafin terafin commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

cat <<'EOF'
The cloud-side sibling of the dedup-key/size-classifier work this branch already carries — folded onto it (Idunn option B) so it is ONE branch: classifier + dedup-key (from feat-scanner) + slot-de-collision + write-guard + verify harness. Base shows just my 2 commits.

Bug

A GBC RTC game stores .srm (32KB SRAM) + .rtc (8B clock sidecar); both uploaded under slotName="default" collided as competing versions of one record → /latest returned the newest (.rtc) → the download wrote the 8B .rtc OVER the 32KB .srm (then removed the old variant). Data loss. (Ullr repro, Pokémon Crystal.)

Fix (two layers, save-format spec by Ullr / RSM model by Idunn)

  • ROOT — resolve_slot_name_for_sync: sidecars (.rtc/.mpk/.cpk) → distinct slot "<primary> (<LABEL>)" (default (RTC) / (Controller Pak) / (Cartridge Pak)) — parenthetical, never the ::/: conflict-key delimiter, mirroring the PSX "Memory Card 1/2" precedent. Primaries (incl. container-equiv srm/sav, mcr/vmp/gme) keep the base slot. So .srm/.rtc become separate (rom,slot) records, never competing as versions.
  • GUARD — allow_write: at both download/write sites, never write a canonical whose format crosses the sidecar/primary boundary onto a mismatched target (SIDECAR_FORMATS = {rtc,mpk,cpk}). Sidecars must match exactly; primary↔primary (PSX mcr→vmp etc.) always allowed (no regression). Uses RSM /latests new format field (Option; None on older RSM → prior behavior).

Verification harness (for Ullrs real-save matrix, no network)

  • scanner::classify(path,&bytes) -> {system, format, is_sidecar} (bytes-feedable split of classify_supported_save).
  • explain-sync --fixtures-dir D --cloud-state S [--manifest M]: offline decision-dump — per fixture prints classify · upload-slot · cloud-format · guard (ALLOW/SKIP). Reads fixtures-manifest.tsv (<relpath>\t<identity>) + cloud-state keyed <identity>::<slotName>; identity used as-is (bare SHA1 GB / projection-key N64-pak/PSX).

Gate evidence

  • Size-based gameboy classifier retained (Idunns guardrail): "rtc" => (1..=64) ✓; dedup-key present ✓; slot+guard+harness intact ✓.
  • 62cb895 (feat-preserve-rtc) was redundant vs this branch (same size-classifier + dedup-key, clippy-collapsed) → dropped in the fold; conflict-free.
  • 118 lib tests pass (incl. allow_write matrix, slot-label, classify, no-regression primary↔primary). explain-sync verified end-to-end: legacy collision → .srm guard=SKIP (clobber prevented); fixed → both ALLOW.
  • SSH-signed (siliconspirit), firewall clean, api.rs format field is mine (no overlap).

Base is feat/scanner-libretro-format-batch to show the slot/guard delta cleanly — redirect to intarweb-dev if youd rather gate it as the unified carry.
EOF

terafin and others added 3 commits June 29, 2026 23:38
…car saves

Cloud-side sibling of the local dedup fix in feat-preserve-rtc. A GBC RTC
game stores .srm (32KB SRAM) + .rtc (8B clock sidecar); both uploaded under
the same slotName="default" collided as competing versions of one record, so
/latest returned the newest (.rtc) and the download wrote the 8B .rtc OVER
the 32KB .srm (then removed the old variant). Data loss.

Two layers (save-format spec authored by Ullr; RSM model by Idunn):

- ROOT (resolve_slot_name_for_sync): sidecar exts (.rtc/.mpk/.cpk) now resolve
  to a DISTINCT cloud slot, RSM-native form "<primary> (<LABEL>)" -- e.g.
  "default (RTC)", "default (Controller Pak)", "default (Cartridge Pak)";
  parenthetical, never the conflict-key delimiter, mirroring the PSX
  "Memory Card 1/2" precedent. Primaries (incl. container-equivalent srm/sav,
  mcr/vmp/gme) keep the base slot. So .srm and .rtc become separate (rom,slot)
  records and never compete as versions.

- GUARD (allow_write): at both download/write sites, never write a canonical
  whose format crosses the sidecar/primary boundary onto a mismatched target
  (an 8B .rtc can never land on a .srm). Sidecars must match exactly; primary
  to primary (PSX mcr to vmp etc.) is always allowed (no regression). Uses
  RSM's new /latest format field (Option; None on older RSM -> prior behavior).

Pure seams exposed for verification: is_sidecar, allow_write, sidecar_label,
resolve_slot_name_for_sync. Unit tests cover the logic (guard matrix, slot
labels, no-regression primary-to-primary); the DAT-format/real-save gate is
Ullr's. 109 lib tests pass.

OUT OF SCOPE: N64 battery-format conversion (.srm-combined to .sra/.eep/.fla).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the black-box verification surface Ullr's fixture matrix drives, so the
sidecar slot/guard fix is provable over real save bytes with no network/sync:

- scanner::classify(path, &bytes) -> {system, format, is_sidecar}: split the
  existing classify_supported_save into a bytes-feedable core (size + plain-text
  passed in) so classification is table-driveable from fixtures, not just live
  files. format = bare lowercased ext (matches RSM /latest + the guard).
- `explain-sync --fixtures-dir D --cloud-state S [--manifest M]`: offline (no
  config/auth) subcommand. Reads fixtures-manifest.tsv (<relpath>\t<lower
  identity>) + a cloud-state JSON keyed "<identity>::<slotName>", then per
  fixture prints classify | upload-slot (resolve_slot_name_for_sync) | cloud
  format | write-guard verdict (allow_write). Identity is used AS-IS (bare SHA1
  for GB, projection-key for N64-pak/PSX).

Verified end-to-end against the Crystal case: legacy collision (.rtc latest
under "default") -> .srm guard=SKIP (clobber prevented); fixed (distinct slots)
-> both ALLOW (each round-trips). classify unit test + 110 lib tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the save-format spec (Ullr): the gameboy non-.rtc SRAM arm becomes the
authoritative enum 512 | 2048 | 8192 | 32768 | 65536 | 131072 across all three
helpers. Drops the non-real power-of-two values (1024/4096/16384) and ADDS
131072 — the previous 65536 ceiling silently REJECTED legitimate 128K MBC5
saves (a real classify bug). The .rtc => (1..=64) sidecar arm is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant