Fix 2.5.0 parsing errors - #8
Open
scharissis wants to merge 1 commit into
Open
Conversation
Patch 2.5.0 (game build 48652) changed the .rec format in two independent places. Replays that exercise either one fail to parse entirely. The per-player trailing list: the four bytes between players are a count, not padding. They were zero in every build up to 46673, which is why they read as padding; 48652 populates the list with one 6-byte record (u32 pbgid, u16 slot) per entry. Consuming only the count leaves the reader inside a record, so the next player's UTF-16 name length is read from the middle of a pbgid. Parsing the list properly needs no version gate: a count of zero consumes exactly the four bytes the old code did. The message tick's leading u32 is a kind, not a message count. Zero meant an empty tick and anything else meant "read a 20-byte header, then that many messages". 48652 introduced kind 2, whose whole body is four bytes — smaller than the header — so the read ran off the end of a length-delimited payload. Dispatching on the kind and skipping unknown kinds by their own declared length means the next new kind cannot break parsing either.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Patch 2.5.0 (game build 48652) changed the
.recformat in two independent places. This is the same fix I sent to vault as ryantaylor/vault#38, rebased onto cohlib — you mentioned there that contributions should go here now.Why you haven't seen this. cohlib's corpus already contains 48652 replays and they parse fine, which is the same reason cohdb sees no failures: the first of the two changes only bites when a particular list is non-empty, and it is empty in every replay cohdb would have ingested. It is not automatch-vs-lobby — my 2.4.2 corpus includes 8-human custom games with an empty list. Measured over 41 local replays, the list is populated per-human-player only on builds >= 48652.
1. The per-player trailing list
The four bytes between players are a count, not padding. They were zero in every build up to 46673, which is why they read as padding; 48652 populates the list with one 6-byte record (
u32pbgid,u16slot) per entry, consistent with the patch's lobby and battlegroup-selection rework.Consuming only the count leaves the reader inside a record, so the next player's UTF-16 name length is read from the middle of a pbgid — a 16-million-character name, then
Eof.Parsing the list properly (
length_count(le_u32, take(6u32))) needs no version gate: a count of zero consumes exactly the four bytes the old code did.2. The message tick's leading
u32is a kind, not a countMessageTickpeeked the firstu32and treated it as a number of chat messages: zero meant an empty tick, anything else meant "read a 20-byte header, then that many messages". 48652 introduced kind 2, whose whole body is four bytes — smaller than the header — so the read ran off the end of a length-delimited payload.Across a 2.4.2 corpus that leading value is only ever
1or0, so "kind" and "count" were indistinguishable from data alone. 2.5.0 settles it: two messages do not fit in four bytes. The fix dispatches on the kind and skips any kind it does not know by that kind's own declared length, so the next new kind cannot break parsing either.Evidence
cargo test -p cohlibnew_failure.rec, which fails identically onmainmaincargo clippy -p replay -- -D warnings,cargo fmt --checkBuilds covered by the local replays: 42217, 44736, 46673, 48652, 48791, 48837.
Two unrelated things I noticed while testing, happy to split into issues if you'd prefer:
replaypanics on some real replays, thoughCLAUDE.mdsays it doesn't. Onmaintoday a campaign replay hitscrates/replay/src/player.rs:142:66—unwrap()onErr("Invalid faction type americans_campaign!"). This matters more than usual for wasm consumers, where a panic is a trap rather than a returnableErr(in practice it surfaces as a catchableRuntimeError: unreachableand the module stays usable, so it isn't fatal — just opaque whereFaction's own error type would be exact). I see #7 makes the wire-format path explicitly panicking; might be worth settling what the contract is and havingCLAUDE.mdmatch.There's no LICENSE file or
licensekey, so cohlib is all rights reserved by default. I suspect that's an oversight — vault was MIT. It's currently the one thing stopping us depending on cohlib downstream.