Skip to content

Fix 2.5.0 parsing errors - #8

Open
scharissis wants to merge 1 commit into
ryantaylor:mainfrom
scharissis:fix-2.5.0-parsing
Open

Fix 2.5.0 parsing errors#8
scharissis wants to merge 1 commit into
ryantaylor:mainfrom
scharissis:fix-2.5.0-parsing

Conversation

@scharissis

Copy link
Copy Markdown

Patch 2.5.0 (game build 48652) changed the .rec format 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 (u32 pbgid, u16 slot) 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 u32 is a kind, not a count

MessageTick peeked the first u32 and 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 1 or 0, 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

Check Result
cargo test -p cohlib 41 passed, 0 failed
cohlib's 22 checked-in replays, output before vs after 21 byte-identical; the 22nd is new_failure.rec, which fails identically on main
Chat lines over that corpus, before vs after 58 / 58, identical
15 local replays on builds 48652 / 48791 / 48837 that fail on main all 15 parse
9 replays on 46673, plus 10 on 42217 / 44736 byte-identical before and after
cargo clippy -p replay -- -D warnings, cargo fmt --check clean

Builds 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:

replay panics on some real replays, though CLAUDE.md says it doesn't. On main today a campaign replay hits crates/replay/src/player.rs:142:66unwrap() on Err("Invalid faction type americans_campaign!"). This matters more than usual for wasm consumers, where a panic is a trap rather than a returnable Err (in practice it surfaces as a catchable RuntimeError: unreachable and the module stays usable, so it isn't fatal — just opaque where Faction'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 having CLAUDE.md match.

There's no LICENSE file or license key, 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.

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.
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