|
@@ -0,0 +1,125 @@
|
|
|
|
|
+# Real TCP Wire Codec Implementation Plan
|
|
|
|
|
+
|
|
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
|
+
|
|
|
|
|
+**Goal:** Replace the speculative 24-byte/XXTEA TCP transport with the verified 36-byte header encoded as a 48-byte wire header.
|
|
|
|
|
+
|
|
|
|
|
+**Architecture:** A pure `encode6` module embeds the verified 256-byte client substitution table. `framing` packs nine signed little-endian words, encodes only that 36-byte header, then appends the plaintext body. `TcpClient` reads the 48-byte wire boundary and owns a monotonically increasing diagnostic sequence.
|
|
|
|
|
+
|
|
|
|
|
+**Tech Stack:** Python 3.12+, `asyncio`, `struct`, Pydantic, pytest, pytest-asyncio.
|
|
|
|
|
+
|
|
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-18-cq2-real-tcp-wire-codec-design.md`
|
|
|
|
|
+
|
|
|
|
|
+## Global Constraints
|
|
|
|
|
+
|
|
|
|
|
+- The wire shape is `encode6(header36) + body`; do not apply XXTEA to TCP frames.
|
|
|
|
|
+- The real header is `<9i`: `pre, msg_id, recog, param1, param2, param3, codec, body_len, sequence`.
|
|
|
|
|
+- Export `PLAIN_HEADER_SIZE = 36`, `WIRE_HEADER_SIZE = 48`, and compatibility `HEADER_SIZE = WIRE_HEADER_SIZE`.
|
|
|
|
|
+- `codec != 0` raises `ValueError('unsupported body codec: N')`; it must never return compressed bytes as a body.
|
|
|
|
|
+- `initial_sequence` defaults to `0`; that value is not evidence of a production login initial sequence.
|
|
|
|
|
+- Do not add live game login, credentials, captures, inferred CS2001 payloads, or body decompression.
|
|
|
|
|
+
|
|
|
|
|
+### Task 1: Establish a runnable baseline
|
|
|
|
|
+
|
|
|
|
|
+**Files:** no repository source changes.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Install the local test runner:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+../../.venv/bin/python -m pip install 'pytest>=9.1.1' 'pytest-asyncio>=1.4.0' 'pydantic>=2.13.5'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Run and record the pre-change baseline:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+PYTHONPATH=cq2_sdk/src ../../.venv/bin/python -m pytest cq2_sdk/tests/unit/test_framing.py cq2_sdk/tests/unit/test_tcp_client.py -q
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Confirm `.venv` and cache files are still ignored with `git status --short --ignored`.
|
|
|
|
|
+
|
|
|
|
|
+### Task 2: Implement the verified table-driven 6-bit codec
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Create: `cq2_sdk/src/cq2_sdk/transport/encode6.py`
|
|
|
|
|
+- Modify: `cq2_sdk/tests/unit/test_framing.py`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:** `encode6(src: bytes) -> bytes`; `decode6(data: bytes, *, expected_size: int | None = None) -> bytes`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Write tests first using literal, hand-verified heartbeat values:
|
|
|
|
|
+
|
|
|
|
|
+```python
|
|
|
|
|
+plain = bytes.fromhex(
|
|
|
|
|
+ '0000000060ea0000b02daf6a000000000000000095000000000000000000000070000000'
|
|
|
|
|
+)
|
|
|
|
|
+wire = bytes.fromhex(
|
|
|
|
|
+ '5d5452415d4c67445d545078495e55525d5452415d545241'
|
|
|
|
|
+ '5d54507a5d5452415d5452415d5452415d54527b5d545241'
|
|
|
|
|
+)
|
|
|
|
|
+assert encode6(plain) == wire
|
|
|
|
|
+assert decode6(wire, expected_size=36) == plain
|
|
|
|
|
+with pytest.raises(ValueError, match='outside 6-bit wire range'):
|
|
|
|
|
+ decode6(b'<~')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Run `pytest test_framing.py -k 'encode6 or decode6'` and confirm collection fails because the module does not exist.
|
|
|
|
|
+- [ ] Add the pure codec. Embed the first 256 bytes of `docs/逆向成果-0918/Encode6BitBuf_替换表.bin` (verified SHA-256 `bd8e0d6b48cff81a2e25e497a86e5e5ef33fedab7cb4af6a1e0db05ddd59fe1e`) as `ENC_TABLE`; build the inverse table in memory. Reject bytes outside `0x3c..0x7b` and an incorrect `expected_size`.
|
|
|
|
|
+- [ ] Re-run the selected tests until green, then commit:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add cq2_sdk/src/cq2_sdk/transport/encode6.py cq2_sdk/tests/unit/test_framing.py
|
|
|
|
|
+git commit -m 'feat: add verified 6-bit header codec'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 3: Convert framing to the nine-word wire contract
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `cq2_sdk/src/cq2_sdk/transport/framing.py`
|
|
|
|
|
+- Modify: `cq2_sdk/src/cq2_sdk/transport/__init__.py`
|
|
|
|
|
+- Modify: `cq2_sdk/tests/unit/test_framing.py`
|
|
|
|
|
+- Modify: `cq2_sdk/tests/unit/test_recorder.py`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:** `FrameHeader(pre, msg_id, recog, param1, param2, param3, codec, body_len, sequence)`, `encode_frame`, `decode_header`, `decode_frame`.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Replace the old 24-byte tests with failing behavior tests asserting: a body-bearing frame has `WIRE_HEADER_SIZE + len(body)` bytes; a round trip preserves `pre`, `sequence`, and body; a truncated body raises; and `codec=1` raises `unsupported body codec: 1`.
|
|
|
|
|
+- [ ] Update the recorder server test to read 48 bytes, use `decode_header`, and send its response through `encode_frame`; do not hand-unpack `<6i`.
|
|
|
|
|
+- [ ] Run `PYTHONPATH=cq2_sdk/src ../../.venv/bin/python -m pytest cq2_sdk/tests/unit/test_framing.py cq2_sdk/tests/unit/test_recorder.py -q` and confirm RED because old framing lacks fields and retains a 24-byte header.
|
|
|
|
|
+- [ ] Implement `PLAIN_HEADER_FORMAT = '<9i'`, `PLAIN_HEADER_SIZE = 36`, `WIRE_HEADER_SIZE = 48`; pack all nine words, encode exactly the plain header, append the body, and decode the 48-byte header with `decode6(..., expected_size=36)`.
|
|
|
|
|
+- [ ] Re-export new constants, run the affected tests until green, then commit:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add cq2_sdk/src/cq2_sdk/transport/framing.py cq2_sdk/src/cq2_sdk/transport/__init__.py cq2_sdk/tests/unit/test_framing.py cq2_sdk/tests/unit/test_recorder.py
|
|
|
|
|
+git commit -m 'feat: encode verified TCP wire frames'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 4: Update TCP buffering and sequence allocation
|
|
|
|
|
+
|
|
|
|
|
+**Files:**
|
|
|
|
|
+- Modify: `cq2_sdk/src/cq2_sdk/transport/tcp.py`
|
|
|
|
|
+- Modify: `cq2_sdk/tests/unit/test_tcp_client.py`
|
|
|
|
|
+- Modify: `cq2_sdk/src/cq2_sdk/client.py`
|
|
|
|
|
+
|
|
|
|
|
+**Interfaces:** `TcpClient(host, port, *, initial_sequence: int = 0)`; existing six-argument `send` remains unchanged.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Write failing loopback tests that: (1) send an encoded frame in slices `[:11]`, `[11:48]`, `[48:]` and assert the client receives the full header/body; (2) decode two outgoing 48-byte headers server-side and assert a client with `initial_sequence=9` emits sequences `9` then `10`.
|
|
|
|
|
+- [ ] Run `PYTHONPATH=cq2_sdk/src ../../.venv/bin/python -m pytest cq2_sdk/tests/unit/test_tcp_client.py -q` and confirm the new tests are RED because the client reads 24 bytes and has no sequence counter.
|
|
|
|
|
+- [ ] Remove the `encryption_key` argument and `xxtea` call. Buffer until `WIRE_HEADER_SIZE + body_len`; use `_next_sequence` in `encode_frame`, then increment only after frame construction. Update `client.py` to say frame layout is implemented while CS2001 body, true initial sequence, body codec, and state machine remain unverified.
|
|
|
|
|
+- [ ] Re-run TCP tests until green, then commit:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+git add cq2_sdk/src/cq2_sdk/transport/tcp.py cq2_sdk/src/cq2_sdk/client.py cq2_sdk/tests/unit/test_tcp_client.py
|
|
|
|
|
+git commit -m 'feat: read real TCP wire frames'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Task 5: Regression verification and review
|
|
|
|
|
+
|
|
|
|
|
+**Files:** no new source files unless a test finds a defect.
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Run:
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+PYTHONPATH=cq2_sdk/src ../../.venv/bin/python -m pytest cq2_sdk/tests/unit/test_framing.py cq2_sdk/tests/unit/test_tcp_client.py cq2_sdk/tests/unit/test_recorder.py cq2_sdk/tests/unit/test_game_login.py -q
|
|
|
|
|
+git diff master...HEAD --check
|
|
|
|
|
+git status --short
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- [ ] Confirm no token/capture/`.env` file is staged and request review with the test output and the explicit non-goals above.
|