Enforce the two-entry .tcade contract in the package reader - #27
Conversation
Review 1P1 — Directory-marked required entries are still accepted
Test/coverage residuals
Method: reviewed only Verdict: NEEDS ATTENTION |
Review 1 on #27: rootEntries matched the two required names but never checked the entry type. A zip also marks directories through external attributes (MS-DOS bit 0x10, Unix mode bits) with no trailing slash required, so a crafted archive could present termcade.toml or game.wasm as a directory and pass. The two required entries must now be regular files by FileInfo semantics, which cover both directory encodings and every other non-regular mode; a symlink- or device-marked game.wasm has no meaning in a two-file package either. Tested per required name with fixtures built via CreateHeader + SetMode, which WritePackage cannot produce. Refs aviorstudio/termcade-be#42
Review 1 replyFixed in Reachable cases now cover Residual coverage: unexpected and nested names are rejected before their modes matter, so type assertions intentionally bind only to the two accepted names. Encryption is checked first and reports encryption if combined with a non-regular mode. Corrupt-CRC and oversized entry behavior remains in the existing read/limit paths. |
Review 2No findings. Review 1 disposition verified: Method: static review of the exact pinned range Verdict: APPROVE |
Review 2 replyNo findings require changes. The residual gap is explicitly dispositioned: Disposition: Review 2 is approving. |
docs/packaging.md has always said a .tcade is exactly termcade.toml and game.wasm at its root, but the reader only looked up those two names and silently ignored everything else: extra files, duplicate entries (zip permits repeated names), nested lookalikes like assets/game.wasm, and directory entries all passed through. An encrypted entry failed later with a misleading error. readPackage now classifies every entry in the central directory before reading anything: the two root names exactly once each, anything nested or unexpected rejected by name, encrypted entries (flag bit 0) rejected explicitly, and the existing size limits and wasm-magic check retained. Tested per violation with fixtures built via raw zip construction, since WritePackage cannot produce any of them. Refs aviorstudio/termcade-be#42
Review 1 on #27: rootEntries matched the two required names but never checked the entry type. A zip also marks directories through external attributes (MS-DOS bit 0x10, Unix mode bits) with no trailing slash required, so a crafted archive could present termcade.toml or game.wasm as a directory and pass. The two required entries must now be regular files by FileInfo semantics, which cover both directory encodings and every other non-regular mode; a symlink- or device-marked game.wasm has no meaning in a two-file package either. Tested per required name with fixtures built via CreateHeader + SetMode, which WritePackage cannot produce. Refs aviorstudio/termcade-be#42
f7b53b9 to
ca1db6c
Compare
Refs aviorstudio/termcade-be#42
What
docs/packaging.mdhas always said a.tcadeis exactlytermcade.tomlandgame.wasmat its root. The reader only looked those two names up and silently ignored everything else:assets/game.wasmand directory entries were ignored,ReadAllyields empty content with a nil error, surfacing asgame.wasm is not a wasm module).readPackagenow classifies every entry in the central directory before reading anything: the two root names exactly once each, nested and unexpected entries rejected by name, encrypted entries (general-purpose flag bit 0) rejected explicitly, missing entries reported as before. The existing security bounds are retained: 1 MiB manifest limit, 64 MiB wasm limit, wasm magic check.This is the public
manifestmodule the registry validates uploads against, so the arcade and the marketplace keep deciding by one piece of code.Tests
New
TestPackageEntryContractbuilds fixtures with raw zip construction (duplicates viaWriter.Create, encryption flag viaCreateRaw) thatWritePackagecannot produce:Gate proven fallible: run against the previous implementation (stash of
manifest/package.go), the third-root-entry fixture and six sibling cases fail — e.g.TestPackageEntryContract/extra_root_entry: accepted, want rejection containing "unexpected package entry \"README.md\"". Restored code passes all ten subtests. The two missing-entry cases already failed before this change (pre-existing behavior, retained).Verification (all from this worktree, go1.26.5)
gofmt -l .— cleango vet ./... && go vet ./sdk/...— cleango test -race -count=1 -timeout 10m ./... ./sdk/...— all pass, incl.internal/pluginwasm E2E (87.9s, guests built from the working tree; E2E skips only under-short, not passed here)GOWORK=off go build ./... && GOWORK=off go vet ./...— clean (consumer resolution)GOWORK=off go test -count=1 ./manifest/ ./internal/starter/andGOWORK=off go test -count=1 -timeout 10m ./internal/plugin/— pass (host + manifest as an external consumer resolves them)unzip -l;internal/starterreads them throughReadPackageand passes.After merge
The registry (aviorstudio/termcade-be#43) consumes
github.com/aviorstudio/termcade/manifestas a versioned module. After this merges, cut the normal arcade release (Release workflow, patch bump → v0.0.8; v0.0.7 is the current tag) so the strict validator is published through the usual gates. No tag created here.