pycyphal2 2.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pycyphal2/_hash.py ADDED
@@ -0,0 +1,204 @@
1
+ """Hash and CRC utilities"""
2
+
3
+ from __future__ import annotations
4
+
5
+ # =====================================================================================================================
6
+ # CRC-32C (Castagnoli)
7
+ # =====================================================================================================================
8
+
9
+ CRC32C_INITIAL = 0xFFFFFFFF
10
+ CRC32C_OUTPUT_XOR = 0xFFFFFFFF
11
+ CRC32C_RESIDUE = 0x48674BC7
12
+ # fmt: off
13
+ _CRC32C_TABLE = [
14
+ 0x00000000, 0xF26B8303, 0xE13B70F7, 0x1350F3F4, 0xC79A971F, 0x35F1141C, 0x26A1E7E8, 0xD4CA64EB,
15
+ 0x8AD958CF, 0x78B2DBCC, 0x6BE22838, 0x9989AB3B, 0x4D43CFD0, 0xBF284CD3, 0xAC78BF27, 0x5E133C24,
16
+ 0x105EC76F, 0xE235446C, 0xF165B798, 0x030E349B, 0xD7C45070, 0x25AFD373, 0x36FF2087, 0xC494A384,
17
+ 0x9A879FA0, 0x68EC1CA3, 0x7BBCEF57, 0x89D76C54, 0x5D1D08BF, 0xAF768BBC, 0xBC267848, 0x4E4DFB4B,
18
+ 0x20BD8EDE, 0xD2D60DDD, 0xC186FE29, 0x33ED7D2A, 0xE72719C1, 0x154C9AC2, 0x061C6936, 0xF477EA35,
19
+ 0xAA64D611, 0x580F5512, 0x4B5FA6E6, 0xB93425E5, 0x6DFE410E, 0x9F95C20D, 0x8CC531F9, 0x7EAEB2FA,
20
+ 0x30E349B1, 0xC288CAB2, 0xD1D83946, 0x23B3BA45, 0xF779DEAE, 0x05125DAD, 0x1642AE59, 0xE4292D5A,
21
+ 0xBA3A117E, 0x4851927D, 0x5B016189, 0xA96AE28A, 0x7DA08661, 0x8FCB0562, 0x9C9BF696, 0x6EF07595,
22
+ 0x417B1DBC, 0xB3109EBF, 0xA0406D4B, 0x522BEE48, 0x86E18AA3, 0x748A09A0, 0x67DAFA54, 0x95B17957,
23
+ 0xCBA24573, 0x39C9C670, 0x2A993584, 0xD8F2B687, 0x0C38D26C, 0xFE53516F, 0xED03A29B, 0x1F682198,
24
+ 0x5125DAD3, 0xA34E59D0, 0xB01EAA24, 0x42752927, 0x96BF4DCC, 0x64D4CECF, 0x77843D3B, 0x85EFBE38,
25
+ 0xDBFC821C, 0x2997011F, 0x3AC7F2EB, 0xC8AC71E8, 0x1C661503, 0xEE0D9600, 0xFD5D65F4, 0x0F36E6F7,
26
+ 0x61C69362, 0x93AD1061, 0x80FDE395, 0x72966096, 0xA65C047D, 0x5437877E, 0x4767748A, 0xB50CF789,
27
+ 0xEB1FCBAD, 0x197448AE, 0x0A24BB5A, 0xF84F3859, 0x2C855CB2, 0xDEEEDFB1, 0xCDBE2C45, 0x3FD5AF46,
28
+ 0x7198540D, 0x83F3D70E, 0x90A324FA, 0x62C8A7F9, 0xB602C312, 0x44694011, 0x5739B3E5, 0xA55230E6,
29
+ 0xFB410CC2, 0x092A8FC1, 0x1A7A7C35, 0xE811FF36, 0x3CDB9BDD, 0xCEB018DE, 0xDDE0EB2A, 0x2F8B6829,
30
+ 0x82F63B78, 0x709DB87B, 0x63CD4B8F, 0x91A6C88C, 0x456CAC67, 0xB7072F64, 0xA457DC90, 0x563C5F93,
31
+ 0x082F63B7, 0xFA44E0B4, 0xE9141340, 0x1B7F9043, 0xCFB5F4A8, 0x3DDE77AB, 0x2E8E845F, 0xDCE5075C,
32
+ 0x92A8FC17, 0x60C37F14, 0x73938CE0, 0x81F80FE3, 0x55326B08, 0xA759E80B, 0xB4091BFF, 0x466298FC,
33
+ 0x1871A4D8, 0xEA1A27DB, 0xF94AD42F, 0x0B21572C, 0xDFEB33C7, 0x2D80B0C4, 0x3ED04330, 0xCCBBC033,
34
+ 0xA24BB5A6, 0x502036A5, 0x4370C551, 0xB11B4652, 0x65D122B9, 0x97BAA1BA, 0x84EA524E, 0x7681D14D,
35
+ 0x2892ED69, 0xDAF96E6A, 0xC9A99D9E, 0x3BC21E9D, 0xEF087A76, 0x1D63F975, 0x0E330A81, 0xFC588982,
36
+ 0xB21572C9, 0x407EF1CA, 0x532E023E, 0xA145813D, 0x758FE5D6, 0x87E466D5, 0x94B49521, 0x66DF1622,
37
+ 0x38CC2A06, 0xCAA7A905, 0xD9F75AF1, 0x2B9CD9F2, 0xFF56BD19, 0x0D3D3E1A, 0x1E6DCDEE, 0xEC064EED,
38
+ 0xC38D26C4, 0x31E6A5C7, 0x22B65633, 0xD0DDD530, 0x0417B1DB, 0xF67C32D8, 0xE52CC12C, 0x1747422F,
39
+ 0x49547E0B, 0xBB3FFD08, 0xA86F0EFC, 0x5A048DFF, 0x8ECEE914, 0x7CA56A17, 0x6FF599E3, 0x9D9E1AE0,
40
+ 0xD3D3E1AB, 0x21B862A8, 0x32E8915C, 0xC083125F, 0x144976B4, 0xE622F5B7, 0xF5720643, 0x07198540,
41
+ 0x590AB964, 0xAB613A67, 0xB831C993, 0x4A5A4A90, 0x9E902E7B, 0x6CFBAD78, 0x7FAB5E8C, 0x8DC0DD8F,
42
+ 0xE330A81A, 0x115B2B19, 0x020BD8ED, 0xF0605BEE, 0x24AA3F05, 0xD6C1BC06, 0xC5914FF2, 0x37FACCF1,
43
+ 0x69E9F0D5, 0x9B8273D6, 0x88D28022, 0x7AB90321, 0xAE7367CA, 0x5C18E4C9, 0x4F48173D, 0xBD23943E,
44
+ 0xF36E6F75, 0x0105EC76, 0x12551F82, 0xE03E9C81, 0x34F4F86A, 0xC69F7B69, 0xD5CF889D, 0x27A40B9E,
45
+ 0x79B737BA, 0x8BDCB4B9, 0x988C474D, 0x6AE7C44E, 0xBE2DA0A5, 0x4C4623A6, 0x5F16D052, 0xAD7D5351,
46
+ ]
47
+ # fmt: on
48
+
49
+
50
+ def crc32c_add(crc: int, data: bytes | memoryview) -> int:
51
+ """CRC-32C (Castagnoli) one update step without the output XOR."""
52
+ for b in data:
53
+ crc = (crc >> 8) ^ _CRC32C_TABLE[b ^ (crc & 0xFF)]
54
+ return crc
55
+
56
+
57
+ def crc32c_full(data: bytes | memoryview) -> int:
58
+ """CRC-32C (Castagnoli) with the output XOR."""
59
+ return crc32c_add(CRC32C_INITIAL, data) ^ CRC32C_OUTPUT_XOR
60
+
61
+
62
+ # =====================================================================================================================
63
+ # CRC-16/CCITT-FALSE
64
+ # =====================================================================================================================
65
+
66
+ CRC16CCITT_FALSE_INITIAL = 0xFFFF
67
+ CRC16CCITT_FALSE_RESIDUE = 0x0000
68
+ # fmt: off
69
+ _CRC16CCITT_FALSE_TABLE = [
70
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C,
71
+ 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318,
72
+ 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4,
73
+ 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630,
74
+ 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4,
75
+ 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969,
76
+ 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF,
77
+ 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
78
+ 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13,
79
+ 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9,
80
+ 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046,
81
+ 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2,
82
+ 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2,
83
+ 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E,
84
+ 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E,
85
+ 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
86
+ 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1,
87
+ 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07,
88
+ 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9,
89
+ 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,
90
+ ]
91
+ # fmt: on
92
+
93
+
94
+ def crc16ccitt_false_add(crc: int, data: bytes | memoryview) -> int:
95
+ """CRC-16/CCITT-FALSE one update step without output post-processing."""
96
+ for b in data:
97
+ crc = ((crc << 8) & 0xFFFF) ^ _CRC16CCITT_FALSE_TABLE[((crc >> 8) ^ b) & 0xFF]
98
+ return crc
99
+
100
+
101
+ def crc16ccitt_false_full(data: bytes | memoryview) -> int:
102
+ """CRC-16/CCITT-FALSE with the standard initial value."""
103
+ return crc16ccitt_false_add(CRC16CCITT_FALSE_INITIAL, data)
104
+
105
+
106
+ # =====================================================================================================================
107
+ # rapidhash V3
108
+ # =====================================================================================================================
109
+
110
+ _RAPID_MASK = 0xFFFFFFFFFFFFFFFF
111
+ _RAPID_SECRET = (
112
+ 0x2D358DCCAA6C78A5,
113
+ 0x8BB84B93962EACC9,
114
+ 0x4B33A62ED433D4A3,
115
+ 0x4D5A2DA51DE1AA47,
116
+ 0xA0761D6478BD642F,
117
+ 0xE7037ED1A0B428DB,
118
+ 0x90ED1765281C388C,
119
+ 0xAAAAAAAAAAAAAAAA,
120
+ )
121
+
122
+
123
+ def _rapid_mum(a: int, b: int) -> tuple[int, int]:
124
+ r = a * b
125
+ return r & _RAPID_MASK, (r >> 64) & _RAPID_MASK
126
+
127
+
128
+ def _rapid_mix(a: int, b: int) -> int:
129
+ lo, hi = _rapid_mum(a, b)
130
+ return lo ^ hi
131
+
132
+
133
+ def _r64(d: bytes, o: int) -> int:
134
+ return int.from_bytes(d[o : o + 8], "little")
135
+
136
+
137
+ def _r32(d: bytes, o: int) -> int:
138
+ return int.from_bytes(d[o : o + 4], "little")
139
+
140
+
141
+ def rapidhash(data: bytes | str) -> int:
142
+ """
143
+ A compliant implementation of rapidhash that matches rapidhash.h that can accept strings directly.
144
+ The eponymous package published PyPI is NOT compatible with rapidhash.h, it must not be used!
145
+ """
146
+ data = data if isinstance(data, bytes) else data.encode("utf8")
147
+ assert isinstance(data, bytes)
148
+ s = _RAPID_SECRET
149
+ n = len(data)
150
+ seed = _rapid_mix(s[2], s[1])
151
+ a = b = 0
152
+ i = n
153
+ p = 0
154
+ if n <= 16:
155
+ if n >= 4:
156
+ seed = (seed ^ n) & _RAPID_MASK
157
+ if n >= 8:
158
+ a = _r64(data, 0)
159
+ b = _r64(data, n - 8)
160
+ else:
161
+ a = _r32(data, 0)
162
+ b = _r32(data, n - 4)
163
+ elif n > 0:
164
+ a = (data[0] << 45) | data[n - 1]
165
+ b = data[n >> 1]
166
+ else:
167
+ if n > 112:
168
+ see1 = see2 = see3 = see4 = see5 = see6 = seed
169
+ while True:
170
+ seed = _rapid_mix(_r64(data, p) ^ s[0], _r64(data, p + 8) ^ seed)
171
+ see1 = _rapid_mix(_r64(data, p + 16) ^ s[1], _r64(data, p + 24) ^ see1)
172
+ see2 = _rapid_mix(_r64(data, p + 32) ^ s[2], _r64(data, p + 40) ^ see2)
173
+ see3 = _rapid_mix(_r64(data, p + 48) ^ s[3], _r64(data, p + 56) ^ see3)
174
+ see4 = _rapid_mix(_r64(data, p + 64) ^ s[4], _r64(data, p + 72) ^ see4)
175
+ see5 = _rapid_mix(_r64(data, p + 80) ^ s[5], _r64(data, p + 88) ^ see5)
176
+ see6 = _rapid_mix(_r64(data, p + 96) ^ s[6], _r64(data, p + 104) ^ see6)
177
+ p += 112
178
+ i -= 112
179
+ if i <= 112:
180
+ break
181
+ seed ^= see1
182
+ see2 ^= see3
183
+ see4 ^= see5
184
+ seed ^= see6
185
+ see2 ^= see4
186
+ seed ^= see2
187
+ if i > 16:
188
+ seed = _rapid_mix(_r64(data, p) ^ s[2], _r64(data, p + 8) ^ seed)
189
+ if i > 32:
190
+ seed = _rapid_mix(_r64(data, p + 16) ^ s[2], _r64(data, p + 24) ^ seed)
191
+ if i > 48:
192
+ seed = _rapid_mix(_r64(data, p + 32) ^ s[1], _r64(data, p + 40) ^ seed)
193
+ if i > 64:
194
+ seed = _rapid_mix(_r64(data, p + 48) ^ s[1], _r64(data, p + 56) ^ seed)
195
+ if i > 80:
196
+ seed = _rapid_mix(_r64(data, p + 64) ^ s[2], _r64(data, p + 72) ^ seed)
197
+ if i > 96:
198
+ seed = _rapid_mix(_r64(data, p + 80) ^ s[1], _r64(data, p + 88) ^ seed)
199
+ a = _r64(data, p + i - 16) ^ i
200
+ b = _r64(data, p + i - 8)
201
+ a ^= s[1]
202
+ b ^= seed
203
+ a, b = _rapid_mum(a, b)
204
+ return _rapid_mix(a ^ s[7], b ^ s[1] ^ i)
pycyphal2/_header.py ADDED
@@ -0,0 +1,349 @@
1
+ from __future__ import annotations
2
+
3
+ import struct
4
+ from dataclasses import dataclass
5
+
6
+ U64_MASK = 0xFFFFFFFFFFFFFFFF
7
+
8
+ HEADER_SIZE = 24
9
+ SEQNO48_MASK = (1 << 48) - 1
10
+ LAGE_MIN = -1
11
+ LAGE_MAX = 35
12
+
13
+
14
+ # =====================================================================================================================
15
+ # MSG headers
16
+ # =====================================================================================================================
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class MsgBeHeader:
21
+ TYPE = 0
22
+
23
+ topic_log_age: int
24
+ topic_evictions: int
25
+ topic_hash: int
26
+ tag: int
27
+
28
+ def serialize(self) -> bytes:
29
+ return _serialize_msg(self.TYPE, self.topic_log_age, self.topic_evictions, self.topic_hash, self.tag)
30
+
31
+ @staticmethod
32
+ def deserialize(buf: bytes | memoryview) -> MsgBeHeader | None:
33
+ r = _deserialize_msg(buf)
34
+ return MsgBeHeader(*r) if r is not None else None
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class MsgRelHeader:
39
+ TYPE = 1
40
+
41
+ topic_log_age: int
42
+ topic_evictions: int
43
+ topic_hash: int
44
+ tag: int
45
+
46
+ def serialize(self) -> bytes:
47
+ return _serialize_msg(self.TYPE, self.topic_log_age, self.topic_evictions, self.topic_hash, self.tag)
48
+
49
+ @staticmethod
50
+ def deserialize(buf: bytes | memoryview) -> MsgRelHeader | None:
51
+ r = _deserialize_msg(buf)
52
+ return MsgRelHeader(*r) if r is not None else None
53
+
54
+
55
+ def _serialize_msg(ty: int, lage: int, evictions: int, topic_hash: int, tag: int) -> bytes:
56
+ buf = bytearray(HEADER_SIZE)
57
+ buf[0] = ty
58
+ buf[3] = lage & 0xFF
59
+ struct.pack_into("<I", buf, 4, evictions & 0xFFFFFFFF)
60
+ struct.pack_into("<Q", buf, 8, topic_hash & U64_MASK)
61
+ struct.pack_into("<Q", buf, 16, tag & U64_MASK)
62
+ return bytes(buf)
63
+
64
+
65
+ def _deserialize_msg(buf: bytes | memoryview) -> tuple[int, int, int, int] | None:
66
+ if len(buf) < HEADER_SIZE:
67
+ return None
68
+ if buf[2] != 0: # incompatibility
69
+ return None
70
+ lage = struct.unpack_from("<b", buf, 3)[0]
71
+ if not (LAGE_MIN <= lage <= LAGE_MAX):
72
+ return None
73
+ evictions = struct.unpack_from("<I", buf, 4)[0]
74
+ topic_hash = struct.unpack_from("<Q", buf, 8)[0]
75
+ tag = struct.unpack_from("<Q", buf, 16)[0]
76
+ return (lage, evictions, topic_hash, tag)
77
+
78
+
79
+ # =====================================================================================================================
80
+ # MSG ACK/NACK headers
81
+ # =====================================================================================================================
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class MsgAckHeader:
86
+ TYPE = 2
87
+
88
+ topic_hash: int
89
+ tag: int
90
+
91
+ def serialize(self) -> bytes:
92
+ return _serialize_msg_ack(self.TYPE, self.topic_hash, self.tag)
93
+
94
+ @staticmethod
95
+ def deserialize(buf: bytes | memoryview) -> MsgAckHeader | None:
96
+ r = _deserialize_msg_ack(buf)
97
+ return MsgAckHeader(*r) if r is not None else None
98
+
99
+
100
+ @dataclass(frozen=True)
101
+ class MsgNackHeader:
102
+ TYPE = 3
103
+
104
+ topic_hash: int
105
+ tag: int
106
+
107
+ def serialize(self) -> bytes:
108
+ return _serialize_msg_ack(self.TYPE, self.topic_hash, self.tag)
109
+
110
+ @staticmethod
111
+ def deserialize(buf: bytes | memoryview) -> MsgNackHeader | None:
112
+ r = _deserialize_msg_ack(buf)
113
+ return MsgNackHeader(*r) if r is not None else None
114
+
115
+
116
+ def _serialize_msg_ack(ty: int, topic_hash: int, tag: int) -> bytes:
117
+ buf = bytearray(HEADER_SIZE)
118
+ buf[0] = ty
119
+ struct.pack_into("<Q", buf, 8, topic_hash & U64_MASK)
120
+ struct.pack_into("<Q", buf, 16, tag & U64_MASK)
121
+ return bytes(buf)
122
+
123
+
124
+ def _deserialize_msg_ack(buf: bytes | memoryview) -> tuple[int, int] | None:
125
+ if len(buf) < HEADER_SIZE:
126
+ return None
127
+ if struct.unpack_from("<I", buf, 4)[0] != 0: # incompatibility
128
+ return None
129
+ topic_hash = struct.unpack_from("<Q", buf, 8)[0]
130
+ tag = struct.unpack_from("<Q", buf, 16)[0]
131
+ return (topic_hash, tag)
132
+
133
+
134
+ # =====================================================================================================================
135
+ # RSP headers (responses)
136
+ # =====================================================================================================================
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class RspBeHeader:
141
+ TYPE = 4
142
+
143
+ tag: int # u8
144
+ seqno: int # u48
145
+ topic_hash: int
146
+ message_tag: int
147
+
148
+ def serialize(self) -> bytes:
149
+ return _serialize_rsp(self.TYPE, self.tag, self.seqno, self.topic_hash, self.message_tag)
150
+
151
+ @staticmethod
152
+ def deserialize(buf: bytes | memoryview) -> RspBeHeader | None:
153
+ r = _deserialize_rsp(buf)
154
+ return RspBeHeader(*r) if r is not None else None
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class RspRelHeader:
159
+ TYPE = 5
160
+
161
+ tag: int
162
+ seqno: int
163
+ topic_hash: int
164
+ message_tag: int
165
+
166
+ def serialize(self) -> bytes:
167
+ return _serialize_rsp(self.TYPE, self.tag, self.seqno, self.topic_hash, self.message_tag)
168
+
169
+ @staticmethod
170
+ def deserialize(buf: bytes | memoryview) -> RspRelHeader | None:
171
+ r = _deserialize_rsp(buf)
172
+ return RspRelHeader(*r) if r is not None else None
173
+
174
+
175
+ # =====================================================================================================================
176
+ # RSP ACK/NACK headers
177
+ # =====================================================================================================================
178
+
179
+
180
+ @dataclass(frozen=True)
181
+ class RspAckHeader:
182
+ TYPE = 6
183
+
184
+ tag: int
185
+ seqno: int
186
+ topic_hash: int
187
+ message_tag: int
188
+
189
+ def serialize(self) -> bytes:
190
+ return _serialize_rsp(self.TYPE, self.tag, self.seqno, self.topic_hash, self.message_tag)
191
+
192
+ @staticmethod
193
+ def deserialize(buf: bytes | memoryview) -> RspAckHeader | None:
194
+ r = _deserialize_rsp(buf)
195
+ return RspAckHeader(*r) if r is not None else None
196
+
197
+
198
+ @dataclass(frozen=True)
199
+ class RspNackHeader:
200
+ TYPE = 7
201
+
202
+ tag: int
203
+ seqno: int
204
+ topic_hash: int
205
+ message_tag: int
206
+
207
+ def serialize(self) -> bytes:
208
+ return _serialize_rsp(self.TYPE, self.tag, self.seqno, self.topic_hash, self.message_tag)
209
+
210
+ @staticmethod
211
+ def deserialize(buf: bytes | memoryview) -> RspNackHeader | None:
212
+ r = _deserialize_rsp(buf)
213
+ return RspNackHeader(*r) if r is not None else None
214
+
215
+
216
+ def _serialize_rsp(ty: int, tag: int, seqno: int, topic_hash: int, message_tag: int) -> bytes:
217
+ buf = bytearray(HEADER_SIZE)
218
+ buf[0] = ty
219
+ buf[1] = tag & 0xFF
220
+ seqno48 = seqno & SEQNO48_MASK
221
+ for i in range(6):
222
+ buf[2 + i] = (seqno48 >> (i * 8)) & 0xFF
223
+ struct.pack_into("<Q", buf, 8, topic_hash & U64_MASK)
224
+ struct.pack_into("<Q", buf, 16, message_tag & U64_MASK)
225
+ return bytes(buf)
226
+
227
+
228
+ def _deserialize_rsp(buf: bytes | memoryview) -> tuple[int, int, int, int] | None:
229
+ if len(buf) < HEADER_SIZE:
230
+ return None
231
+ tag = buf[1]
232
+ seqno = 0
233
+ for i in range(6):
234
+ seqno |= buf[2 + i] << (i * 8)
235
+ topic_hash = struct.unpack_from("<Q", buf, 8)[0]
236
+ message_tag = struct.unpack_from("<Q", buf, 16)[0]
237
+ return (tag, seqno, topic_hash, message_tag)
238
+
239
+
240
+ # =====================================================================================================================
241
+ # GOSSIP header
242
+ # =====================================================================================================================
243
+
244
+
245
+ @dataclass(frozen=True)
246
+ class GossipHeader:
247
+ TYPE = 8
248
+
249
+ topic_log_age: int
250
+ topic_hash: int
251
+ topic_evictions: int
252
+ name_len: int
253
+
254
+ def serialize(self) -> bytes:
255
+ buf = bytearray(HEADER_SIZE)
256
+ buf[0] = self.TYPE
257
+ buf[3] = self.topic_log_age & 0xFF
258
+ struct.pack_into("<Q", buf, 8, self.topic_hash & U64_MASK)
259
+ struct.pack_into("<I", buf, 16, self.topic_evictions & 0xFFFFFFFF)
260
+ buf[23] = self.name_len & 0xFF
261
+ return bytes(buf)
262
+
263
+ @staticmethod
264
+ def deserialize(buf: bytes | memoryview) -> GossipHeader | None:
265
+ if len(buf) < HEADER_SIZE:
266
+ return None
267
+ if struct.unpack_from("<I", buf, 4)[0] != 0:
268
+ return None
269
+ lage = struct.unpack_from("<b", buf, 3)[0]
270
+ if not (LAGE_MIN <= lage <= LAGE_MAX):
271
+ return None
272
+ topic_hash = struct.unpack_from("<Q", buf, 8)[0]
273
+ evictions = struct.unpack_from("<I", buf, 16)[0]
274
+ name_len = buf[23]
275
+ return GossipHeader(lage, topic_hash, evictions, name_len)
276
+
277
+
278
+ # =====================================================================================================================
279
+ # SCOUT header
280
+ # =====================================================================================================================
281
+
282
+
283
+ @dataclass(frozen=True)
284
+ class ScoutHeader:
285
+ TYPE = 9
286
+
287
+ pattern_len: int
288
+
289
+ def serialize(self) -> bytes:
290
+ buf = bytearray(HEADER_SIZE)
291
+ buf[0] = self.TYPE
292
+ buf[23] = self.pattern_len & 0xFF
293
+ return bytes(buf)
294
+
295
+ @staticmethod
296
+ def deserialize(buf: bytes | memoryview) -> ScoutHeader | None:
297
+ if len(buf) < HEADER_SIZE:
298
+ return None
299
+ if struct.unpack_from("<I", buf, 4)[0] != 0:
300
+ return None
301
+ if struct.unpack_from("<Q", buf, 8)[0] != 0:
302
+ return None
303
+ return ScoutHeader(buf[23])
304
+
305
+
306
+ # =====================================================================================================================
307
+ # Dispatcher
308
+ # =====================================================================================================================
309
+
310
+ HeaderType = (
311
+ MsgBeHeader
312
+ | MsgRelHeader
313
+ | MsgAckHeader
314
+ | MsgNackHeader
315
+ | RspBeHeader
316
+ | RspRelHeader
317
+ | RspAckHeader
318
+ | RspNackHeader
319
+ | GossipHeader
320
+ | ScoutHeader
321
+ )
322
+
323
+
324
+ def deserialize_header(buf: bytes | memoryview) -> HeaderType | None:
325
+ """Deserialize a 24-byte session-layer header. Returns None on validation failure."""
326
+ if len(buf) < 1:
327
+ return None
328
+ ty = buf[0]
329
+ if ty == 0:
330
+ return MsgBeHeader.deserialize(buf)
331
+ if ty == 1:
332
+ return MsgRelHeader.deserialize(buf)
333
+ if ty == 2:
334
+ return MsgAckHeader.deserialize(buf)
335
+ if ty == 3:
336
+ return MsgNackHeader.deserialize(buf)
337
+ if ty == 4:
338
+ return RspBeHeader.deserialize(buf)
339
+ if ty == 5:
340
+ return RspRelHeader.deserialize(buf)
341
+ if ty == 6:
342
+ return RspAckHeader.deserialize(buf)
343
+ if ty == 7:
344
+ return RspNackHeader.deserialize(buf)
345
+ if ty == 8:
346
+ return GossipHeader.deserialize(buf)
347
+ if ty == 9:
348
+ return ScoutHeader.deserialize(buf)
349
+ return None