Files
live-hub-py/tests/test_huya_struct_parse_resilience.py
T
yml2213 a9d711a7e9 fix(huya): 结构体尾部字段容错解析(服务端新增 ZERO 字段不再打挂整个 RPC)
真机现象(2026-10-15,外部 CK 导入后查询角色):checkUserBindGameAccount 响应结构
尾部多了 tag6(ZERO 类型 0x6c),共享的 read_struct 读到非 STRUCT_END 直接抛
「期望 STRUCT_END, 实际 0x0c」,整个 RPC 返回 None —— 工作台「查询角色」永远失败。

- taf_protocol:read_struct 收尾改为按 TAF 语义跳过未建模的尾部字段(上限 32 个,
  防止解析错位时无限吞字节);新增 skip_to_struct_end()
- frame_decoder / shop_structs:抽出 decode_struct_bytes 与 ShopRawRsp.from_payload,
  WSS 与 HTTP 两条通道共用一份通用解码逻辑(wss_client 同步改用)

验证:tests/test_huya_struct_parse_resilience.py 用真机响应字节回归(含 tag6=ZERO 的
响应能解析出角色「泪雨全是谎言」);按 AGENTS.md 反向验证 —— 注掉
skip_to_struct_end() 即复现线上报错「期望 STRUCT_END, 实际 0x0c」,还原后恢复。
全量 pytest 通过。
2026-10-15 15:21:34 +08:00

65 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""TAF 结构体解析韧性:尾部未建模字段(服务端新增/ZERO)与通用响应回落。
回归来源(2026-10-15 真机):`checkUserBindGameAccount` 响应结构尾部多了 tag6
(ZERO 类型),旧解析直接抛 "期望 STRUCT_END, 实际 0x0c",导致整个 RPC 返回 None、
工作台「查询角色」永远失败。修法:按 TAF 语义跳过未建模的尾部字段。
"""
from core.huya.activity_structs import CheckUserBindGameAccountResp
from core.huya.shop_structs import ShopRawRsp
from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType
# 真机响应(checkUserBindGameAccount,UID 1199665170519,bActId 9271)
BIND_RESP_HEX = (
"0a0100c816002a0001100120013a060016002602717136004612e6b3aae6958fe6849fe"
"79a84e59b9ee5bf86560066000b4a06001612e6b3aae99ba8e585a8e698afe8b08ee8a8"
"802604e6898b51360be88bb9e69e9c28694f53294600560066000b526a2595806c0b36"
"0ce5928ce5b9b3e7b2bee88bb1460050070b"
)
def test_bind_response_with_zero_tail_field_parses():
stream = TafInputStream(bytes.fromhex(BIND_RESP_HEX))
tag, dtype = stream.read_head()
assert (tag, dtype) == (0, TafType.STRUCT_BEGIN)
resp = CheckUserBindGameAccountResp()
resp.read_from(stream)
assert resp.status == 200
assert resp.gameName == "和平精英"
assert resp.accountData.isBindAcount == 1
assert resp.accountData.isBindRole == 1
assert resp.accountData.gameAccount.nick == "泪敏感的回忆"
assert resp.accountData.gameRole.roleName == "泪雨全是谎言"
assert resp.accountData.gameRole.areaName == "手Q"
assert resp.accountData.gameRole.platName == "苹果(iOS)"
# 外层结构正常收尾(内层 tag6=ZERO 已由 read_struct 跳过)
_end_tag, end_type = stream.read_head()
assert end_type == TafType.STRUCT_END
def test_skip_to_struct_end_skips_zero_field():
output = TafOutputStream()
output.write_struct_begin(0)
output.write_int32(0, 1)
output.write_int32(6, 0) # ZERO 类型(0x6c)
output.write_struct_end()
stream = TafInputStream(output.get_bytes())
stream.read_head() # STRUCT_BEGIN
assert stream.read_int32(0) == 1
assert stream.skip_to_struct_end() == 1
_tag, dtype = stream.read_head()
assert dtype == TafType.STRUCT_END
def test_shop_raw_rsp_from_payload_flat_and_nested():
flat = ShopRawRsp.from_payload({"tag0": 200, "tag1": "success"})
assert (flat.code, flat.message) == (200, "success")
nested = ShopRawRsp.from_payload({"tag0": {"tag0": 200, "tag1": "成功"}})
assert (nested.code, nested.message) == (200, "成功")
assert ShopRawRsp.from_payload(None).code == 0