test_msg_codec.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. # tests/test_msg_codec.py
  2. """
  3. cpp_reversed/msg_codec 算法库的 pytest 测试套件
  4. 覆盖: hdr_codec / frame / xxtea / msg_codec 顶层 API / 抓包分析场景
  5. """
  6. import os
  7. import sys
  8. import struct
  9. import pytest
  10. # 让 pytest 能 import cpp_reversed 模块
  11. CPP_REV = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "cpp_reversed"))
  12. if CPP_REV not in sys.path:
  13. sys.path.insert(0, CPP_REV)
  14. # 兼容 protocol_bot (用于 cross-verify)
  15. PROTO_BOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "protocol_bot"))
  16. if PROTO_BOT not in sys.path:
  17. sys.path.insert(0, PROTO_BOT)
  18. import hdr_codec
  19. import xxtea
  20. import frame as frame_mod
  21. import msg_codec
  22. # ============== hdr_codec: 24B 头编解码 ==============
  23. class TestHdrCodec:
  24. """测试 24B 头编解码 (Encode6BitBuf 算法核心)"""
  25. def test_encode_24b_to_32b(self):
  26. plain = b"\x00" * 24
  27. enc = hdr_codec.encode_header(plain)
  28. assert len(enc) == 32
  29. assert all(0x3C <= b <= 0x7B for b in enc), "编码字节非可打印"
  30. def test_decode_32b_to_24b(self):
  31. plain = b"\x00" * 24
  32. enc = hdr_codec.encode_header(plain)
  33. dec = hdr_codec.decode_header(enc)
  34. assert dec == plain
  35. def test_roundtrip_random(self):
  36. import random
  37. random.seed(42)
  38. for _ in range(50):
  39. plain = bytes(random.randint(0, 255) for _ in range(24))
  40. enc = hdr_codec.encode_header(plain)
  41. dec = hdr_codec.decode_header(enc)
  42. assert dec == plain, f"roundtrip fail: {plain.hex()} → {enc.hex()} → {dec.hex()}"
  43. def test_parse_header_legacy_24b(self):
  44. # 24B 旧格式: recog(4) + p1(4) + p2(4) + p3(4) + p4(4) + msgId(2) + 2 字节
  45. hdr = bytearray(24)
  46. struct.pack_into("<I", hdr, 0, 900000) # recog
  47. struct.pack_into("<I", hdr, 4, 12345) # p1
  48. struct.pack_into("<I", hdr, 8, 37504) # p2
  49. struct.pack_into("<H", hdr, 20, 101) # msgId
  50. fields = hdr_codec.parse_header(bytes(hdr))
  51. assert fields["msgId"] == 101
  52. assert fields["recog"] == 900000
  53. assert fields["param1"] == 12345
  54. assert fields["param2"] == 37504
  55. # ============== frame: 36B 头 + body 完整帧 ==============
  56. class TestFrame:
  57. """测试 36B wire format 头 + body 完整帧"""
  58. def test_pack_frame_basic(self):
  59. packed = frame_mod.pack_frame(101, 900000, 0, 0, 0, 0, b"")
  60. assert len(packed) == 48 # 48B 头 + 0B body
  61. def test_pack_with_body(self):
  62. body = b"hello world" * 10 # 110B
  63. packed = frame_mod.pack_frame(101, 900000, 0, 0, 0, 0, body)
  64. assert len(packed) == 48 + len(body)
  65. def test_roundtrip_basic(self):
  66. f = frame_mod.Frame(msg_id=101, recog=900000, p1=12345, p2=37504, p3=0, tail=0, body=b"test")
  67. packed = f.pack()
  68. frames, rest = frame_mod.unpack_stream(packed)
  69. assert rest == b""
  70. assert len(frames) == 1
  71. g = frames[0]
  72. assert g.msg_id == 101
  73. assert g.recog == 900000
  74. assert g.body == b"test"
  75. def test_roundtrip_random_50(self):
  76. import random
  77. random.seed(0xC0FFEE)
  78. for i in range(50):
  79. msg_id = random.randint(0, 0x1FFE)
  80. recog = random.randint(0, 0x7FFFFFFF)
  81. p1 = random.randint(0, 0x7FFFFFFF)
  82. p2 = random.randint(0, 0x7FFFFFFF)
  83. p3 = random.randint(0, 0x7FFFFFFF)
  84. tail = random.randint(0, 9)
  85. body = bytes(random.randint(0, 255) for _ in range(random.randint(0, 200)))
  86. f = frame_mod.Frame(msg_id, recog, p1, p2, p3, tail, body)
  87. packed = f.pack()
  88. frames, rest = frame_mod.unpack_stream(packed)
  89. assert rest == b"", f"iter {i}: rest non-empty"
  90. assert len(frames) == 1, f"iter {i}: wrong frame count"
  91. g = frames[0]
  92. assert g.msg_id == msg_id
  93. assert g.recog == recog
  94. assert g.p1 == p1
  95. assert g.p2 == p2
  96. assert g.p3 == p3
  97. assert g.tail == tail
  98. assert g.body == body
  99. def test_unpack_partial(self):
  100. f = frame_mod.Frame(msg_id=529, recog=0, p1=0, p2=0, p3=0, tail=1, body=b"key32abc")
  101. packed = f.pack()
  102. frames, rest = frame_mod.unpack_stream(packed[:-5])
  103. assert frames == []
  104. assert rest == packed[:-5]
  105. def test_unpack_multi_packet(self):
  106. f1 = frame_mod.Frame(msg_id=20, recog=0, p1=0, p2=0, p3=0, tail=9, body=b"login_payload")
  107. f2 = frame_mod.Frame(msg_id=529, recog=0, p1=0, p2=0, p3=0, tail=1, body=b"key32abc")
  108. multi = f1.pack() + f2.pack()
  109. frames, rest = frame_mod.unpack_stream(multi)
  110. assert rest == b""
  111. assert len(frames) == 2
  112. assert frames[0].msg_id == 20
  113. assert frames[1].msg_id == 529
  114. assert frames[1].body == b"key32abc"
  115. def test_wire_format_interop_with_protocol_bot(self):
  116. """与 protocol_bot/encode6bitbuf_final 字节级一致 (向后兼容)"""
  117. from encode6bitbuf_final import build_frame, encode6
  118. head36 = frame_mod.HDR_STRUCT.pack(0, 101, 900000, 0, 0, 0, 0, 0, 0)
  119. assert frame_mod._encode_full(head36) == encode6(head36)
  120. # build_frame 缺省 body=空
  121. assert frame_mod.pack_frame(101, 900000, 0, 0, 0, 0, b"") == build_frame(101, 900000, 0, 0, 0)
  122. # ============== xxtea: 加解密 ==============
  123. class TestXXTEA:
  124. """测试 XXTEA 加解密算法"""
  125. KEY = b"0123456789abcdef"
  126. def test_known_vector_zero(self):
  127. # 16B zeros + zero key → e6c8d5ff070fb6e498a534f7ac03e399
  128. c = xxtea.encrypt(b"\x00" * 16, b"\x00" * 16)
  129. assert c.hex() == "e6c8d5ff070fb6e498a534f7ac03e399"
  130. def test_roundtrip_8b(self):
  131. plain = b"\x12" * 8
  132. c = xxtea.encrypt(plain, self.KEY)
  133. d = xxtea.decrypt(c, self.KEY)
  134. assert d == plain
  135. def test_roundtrip_16b(self):
  136. plain = bytes.fromhex("123456789abcdef0deadbeefcafebabe")
  137. c = xxtea.encrypt(plain, self.KEY)
  138. d = xxtea.decrypt(c, self.KEY)
  139. assert d == plain
  140. def test_roundtrip_32b(self):
  141. plain = b"Hello XXTEA!!! " # 16B
  142. c = xxtea.encrypt(plain, self.KEY)
  143. d = xxtea.decrypt(c, self.KEY)
  144. assert d == plain
  145. def test_padded_roundtrip(self):
  146. for plain in [b"", b"x", b"xy", b"Hello", b"HelloWorld12"]:
  147. enc = xxtea.encrypt_padded(plain, self.KEY)
  148. dec = xxtea.decrypt_padded(enc, self.KEY)
  149. assert dec == plain, f"padded roundtrip fail: {plain!r}"
  150. def test_empty(self):
  151. c = xxtea.encrypt(b"", self.KEY)
  152. d = xxtea.decrypt(c, self.KEY)
  153. assert d == b""
  154. def test_invalid_key_length(self):
  155. with pytest.raises(ValueError, match="key 必须是 16 字节"):
  156. xxtea.encrypt(b"\x00" * 8, b"short")
  157. def test_invalid_plain_length(self):
  158. with pytest.raises(ValueError, match="plain 长度必须是 8 倍数"):
  159. xxtea.encrypt(b"7bytes!", self.KEY)
  160. def test_cross_verify_with_third_party(self):
  161. """与第三方 xxtea 6.2.0 包 cross-verify (需先 pip install xxtea)"""
  162. try:
  163. import xxtea as third_party
  164. except ImportError:
  165. pytest.skip("xxtea 第三方包未安装")
  166. test_cases = [
  167. (b"\x00" * 16, b"\x00" * 16),
  168. (b"\xff" * 16, b"\x00" * 16),
  169. (bytes.fromhex("123456789abcdef0deadbeefcafebabe"),
  170. bytes.fromhex("0102030405060708090a0b0c0d0e0f10")),
  171. ]
  172. for plain, key in test_cases:
  173. mine = xxtea.encrypt(plain, key)
  174. theirs = third_party.encrypt(plain, key)
  175. assert mine == theirs, f"cross-verify fail: {plain.hex()} / {key.hex()}"
  176. # ============== msg_codec: 顶层 API ==============
  177. class TestMsgCodec:
  178. """测试 msg_codec 顶层 API"""
  179. def test_send_basic(self):
  180. wire = msg_codec.send(101, 900000, 0, 0, 0, b"hello", 0)
  181. assert len(wire) == 48 + 5
  182. def test_receive_roundtrip(self):
  183. wire = msg_codec.send(20, 0, 0, 0, 0, b"*#test/1/2/3", 9)
  184. frames, rest = msg_codec.receive(wire)
  185. assert rest == b""
  186. assert len(frames) == 1
  187. assert frames[0].msg_id == 20
  188. assert frames[0].body == b"*#test/1/2/3"
  189. def test_frame_to_dict(self):
  190. f = frame_mod.Frame(101, 900000, 12345, 37504, 0, 0, b"hello world!")
  191. d = msg_codec.frame_to_dict(f)
  192. assert d["msg_id"] == 101
  193. assert d["recog"] == 900000
  194. assert d["body_len"] == 12
  195. assert d["body_preview"] == b"hello world!".hex()
  196. def test_decode_captured_48b_wire(self):
  197. wire = msg_codec.send(101, 900000, 12345, 37504, 0, b"x" * 32)
  198. d = msg_codec.decode_captured(wire[:48])
  199. assert d["msgId"] == 101
  200. assert d["recog"] == 900000
  201. assert d["p1"] == 12345
  202. assert d["p2"] == 37504
  203. assert d["body_len"] == 32
  204. def test_decode_captured_32b_legacy(self):
  205. # 构造 24B 旧格式明文头
  206. plain = bytearray(24)
  207. struct.pack_into("<I", plain, 0, 900000)
  208. struct.pack_into("<I", plain, 4, 12345)
  209. struct.pack_into("<I", plain, 8, 37504)
  210. struct.pack_into("<H", plain, 20, 101)
  211. enc = hdr_codec.encode_header(bytes(plain))
  212. d = msg_codec.decode_captured(enc)
  213. assert d["msgId"] == 101
  214. assert d["recog"] == 900000
  215. assert d["param1"] == 12345
  216. assert d["param2"] == 37504
  217. def test_decode_captured_too_short(self):
  218. with pytest.raises(ValueError, match="至少需要 32B"):
  219. msg_codec.decode_captured(b"\x00" * 10)
  220. def test_xxtea_aliases(self):
  221. c = msg_codec.xxtea_encrypt(b"test1234", self.KEY)
  222. d = msg_codec.xxtea_decrypt(c, self.KEY)
  223. assert d == b"test1234"
  224. def test_padded_aliases(self):
  225. c = msg_codec.encrypt_padded(b"test", self.KEY)
  226. d = msg_codec.decrypt_padded(c, self.KEY)
  227. assert d == b"test"
  228. @property
  229. def KEY(self):
  230. return b"0123456789abcdef"
  231. # ============== rsa: 接口契约 ==============
  232. class TestRSAStub:
  233. """测试 rsa 接口契约 (密钥未提取, 抛 NotImplementedError)"""
  234. def test_encrypt_pub_raises(self):
  235. with pytest.raises(NotImplementedError, match="未从二进制"):
  236. msg_codec.encrypt_pub(b"test")
  237. def test_decrypt_pri_raises(self):
  238. with pytest.raises(NotImplementedError, match="未反推|未从二进制"):
  239. msg_codec.decrypt_pri(b"\x00" * 256)
  240. # ============== 集成测试: 模拟完整抓包场景 ==============
  241. class TestIntegration:
  242. """端到端: 模拟一次 client→server→client 的完整协议流"""
  243. def test_create_role_full_flow(self):
  244. """模拟 §11.4 创建角色完整调用链 (创角 CS101 → 成功 SC521)"""
  245. from msg_codec import send, receive, MSG_CS_CREATE_ROLE, MSG_SC_CREATE_ROLE_OK
  246. # 1. client 创角上行
  247. body_cs = b'{"account":"z1098210167","chr":"\xe8\x90\x8c\xe6\x96\xb0","job":0,"sex":1,"server_name":"\xe6\xaf\x94\xe5\xa5\x87","sdkid":"37504","secsdk":"0"}'
  248. cs_wire = send(MSG_CS_CREATE_ROLE, 900000, 12345, 37504, 0, body_cs)
  249. # 2. server 回包 (手动构造 SC521 成功响应)
  250. body_sc = b"\xe8\x90\x8c\xe6\x96\xb0/12345" # 名字/roleID
  251. sc_wire = send(MSG_SC_CREATE_ROLE_OK, 0, 12345, 0, 0, body_sc)
  252. # 3. client 端解析 (抓包视角)
  253. cs_frames, cs_rest = receive(cs_wire)
  254. sc_frames, sc_rest = receive(sc_wire)
  255. assert cs_rest == sc_rest == b""
  256. assert cs_frames[0].msg_id == MSG_CS_CREATE_ROLE
  257. assert sc_frames[0].msg_id == MSG_SC_CREATE_ROLE_OK
  258. assert sc_frames[0].body == body_sc
  259. def test_tcp_fragmented_recv(self):
  260. """模拟 TCP 跨两次 recv 的粘包/半包"""
  261. body = b"x" * 200
  262. wire = msg_codec.send(529, 0, 0, 0, 0, body, 0)
  263. # 第一次 recv 拿到前 100 字节
  264. buf1 = wire[:100]
  265. frames, rest = msg_codec.receive(buf1)
  266. assert frames == []
  267. assert rest == buf1 # 不够一帧, 全部留存
  268. # 第二次 recv 拿到剩余
  269. buf2 = wire[100:]
  270. frames, rest = msg_codec.receive(rest + buf2)
  271. assert rest == b""
  272. assert len(frames) == 1
  273. assert frames[0].body == body
  274. def test_batch_message_concat(self):
  275. """批量包 (-1) 多个 msg 拼接在一起"""
  276. msgs = [
  277. frame_mod.Frame(20, 0, 0, 0, 0, 0, b"msg1_payload"),
  278. frame_mod.Frame(101, 900000, 0, 0, 0, 0, b"msg2_payload_longer"),
  279. frame_mod.Frame(529, 0, 0, 0, 0, 0, b"msg3"),
  280. ]
  281. # 模拟批量包: 把多个 frame body 串起来, 外层 -1 帧
  282. concat_body = b"".join(m.pack() for m in msgs)
  283. batch_wire = frame_mod.pack_frame(-1, 0, 0, 0, 0, 0, concat_body)
  284. # 解外层: 得到批量包 body
  285. frames, _ = msg_codec.receive(batch_wire)
  286. assert len(frames) == 1
  287. assert frames[0].msg_id == -1
  288. assert len(frames[0].body) == len(concat_body)
  289. if __name__ == "__main__":
  290. sys.exit(pytest.main([__file__, "-v", "--tb=short"]))