Security audit finding EXP2-SEC-04. `validate_game_id` in the wire
protocol only enforced non-blank and a 255-byte maximum, so a request
such as `StreamInstall { game_id: "../../x" }` decoded successfully and
was passed on to the transfer layer. Every consumer resolves the ID
against the local catalog before touching the filesystem, so this was
not exploitable, but the protocol boundary is the right place to state
what a game ID is: the name of one catalog directory.
Requests and library snapshots now fail validation when the game ID
contains `/`, `\`, any Unicode control character (including NUL), or is
exactly `.` or `..`. A new `ControlValidationError::InvalidPathComponent`
variant reports the rejection. Embedded dots such as "game..v1" remain
valid because the catalog validators accept them.
Test plan: `just test` exercises the rejected forms plus an accepted ID
with embedded dots.
Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg