bdo-toolkit 1.0.0__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.
Files changed (48) hide show
  1. bdo_toolkit/__init__.py +87 -0
  2. bdo_toolkit/_async_sessions.py +651 -0
  3. bdo_toolkit/_capture_backend.py +194 -0
  4. bdo_toolkit/_capture_options.py +68 -0
  5. bdo_toolkit/_capture_runtime.py +626 -0
  6. bdo_toolkit/_deposit_origin.py +1599 -0
  7. bdo_toolkit/_engine.py +327 -0
  8. bdo_toolkit/_framing.py +904 -0
  9. bdo_toolkit/_profile_runtime.py +157 -0
  10. bdo_toolkit/_protocol.py +386 -0
  11. bdo_toolkit/_reassembly.py +654 -0
  12. bdo_toolkit/_specs.py +285 -0
  13. bdo_toolkit/_storage_destination_validation.py +167 -0
  14. bdo_toolkit/_storage_hydration.py +241 -0
  15. bdo_toolkit/_version.py +3 -0
  16. bdo_toolkit/calibration.py +3223 -0
  17. bdo_toolkit/capture.py +1713 -0
  18. bdo_toolkit/character_state.py +3506 -0
  19. bdo_toolkit/cli.py +948 -0
  20. bdo_toolkit/diagnostics.py +51 -0
  21. bdo_toolkit/events.py +214 -0
  22. bdo_toolkit/filters.py +105 -0
  23. bdo_toolkit/item_state.py +48 -0
  24. bdo_toolkit/origin_learning.py +779 -0
  25. bdo_toolkit/profiles.py +370 -0
  26. bdo_toolkit/py.typed +1 -0
  27. bdo_toolkit/remote_profiles.py +358 -0
  28. bdo_toolkit/solare/__init__.py +50 -0
  29. bdo_toolkit/solare/_constants.py +94 -0
  30. bdo_toolkit/solare/_detail_learning.py +1437 -0
  31. bdo_toolkit/solare/_details.py +796 -0
  32. bdo_toolkit/solare/_discovery.py +1212 -0
  33. bdo_toolkit/solare/_live_tracker.py +472 -0
  34. bdo_toolkit/solare/_replay_capture.py +182 -0
  35. bdo_toolkit/solare/_result.py +441 -0
  36. bdo_toolkit/solare/_scanner.py +203 -0
  37. bdo_toolkit/solare/_validation.py +11 -0
  38. bdo_toolkit/solare/async_session.py +444 -0
  39. bdo_toolkit/solare/models.py +806 -0
  40. bdo_toolkit/solare/replay.py +62 -0
  41. bdo_toolkit/solare/session.py +1051 -0
  42. bdo_toolkit/writers.py +30 -0
  43. bdo_toolkit-1.0.0.dist-info/METADATA +143 -0
  44. bdo_toolkit-1.0.0.dist-info/RECORD +48 -0
  45. bdo_toolkit-1.0.0.dist-info/WHEEL +5 -0
  46. bdo_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  47. bdo_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  48. bdo_toolkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,441 @@
1
+ """Turn opcode-agnostic structural discovery into immutable public models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Optional
7
+
8
+ from bdo_toolkit._protocol import BDOFrame
9
+
10
+ from ._constants import class_name
11
+ from ._detail_learning import learn_unknown_solare_details
12
+ from ._details import (
13
+ _overall_layout_for,
14
+ _rich_layout_for,
15
+ decode_solare_details,
16
+ decode_solare_overall_details,
17
+ )
18
+ from ._discovery import (
19
+ DiscoveredSolareFamily,
20
+ SolareDiscoveryResult,
21
+ discover_solare,
22
+ family_frames,
23
+ )
24
+ from ._validation import validate_retain_raw_extensions
25
+ from .models import (
26
+ SolareCaptureHealth,
27
+ SolareCaptureResult,
28
+ SolareClass,
29
+ SolareDetectionStatus,
30
+ SolareEvidence,
31
+ SolareFamilyLayout,
32
+ SolareLeaderboardSnapshot,
33
+ SolareOverallEntry,
34
+ SolarePlayer,
35
+ solare_snapshot_id,
36
+ )
37
+
38
+
39
+ def _family_layout(
40
+ family: Optional[DiscoveredSolareFamily],
41
+ *,
42
+ detail_layout_id: Optional[str] = None,
43
+ ) -> Optional[SolareFamilyLayout]:
44
+ if family is None:
45
+ return None
46
+ return SolareFamilyLayout(
47
+ role=family.role,
48
+ opcode=family.opcode,
49
+ message_length=family.frame_length,
50
+ message_count=family.frame_count,
51
+ record_stride=family.record_stride,
52
+ name_offset=family.name_offset,
53
+ rank_offset=family.rank_offset,
54
+ class_offset=family.class_offset,
55
+ detail_layout_id=detail_layout_id,
56
+ )
57
+
58
+
59
+ def _ranking_players(rich: DiscoveredSolareFamily) -> tuple[SolarePlayer, ...]:
60
+ players = (
61
+ SolarePlayer(
62
+ name=name,
63
+ global_rank=rank,
64
+ primary_class=SolareClass(code=class_code, name=class_name(class_code)),
65
+ )
66
+ for name, rank, class_code in zip(
67
+ rich.names,
68
+ rich.ranks,
69
+ rich.class_codes,
70
+ )
71
+ )
72
+ return tuple(sorted(players, key=lambda player: player.global_rank))
73
+
74
+
75
+ def _overall_entries(
76
+ overall: DiscoveredSolareFamily,
77
+ ) -> tuple[SolareOverallEntry, ...]:
78
+ return tuple(
79
+ SolareOverallEntry(
80
+ name=name,
81
+ global_rank=rank,
82
+ )
83
+ for name, rank in zip(overall.names, overall.ranks)
84
+ )
85
+
86
+
87
+ def _details_match_discovery(
88
+ players: tuple[SolarePlayer, ...],
89
+ rich: DiscoveredSolareFamily,
90
+ ) -> bool:
91
+ discovered = tuple(
92
+ sorted(
93
+ zip(rich.ranks, rich.names, rich.class_codes),
94
+ key=lambda row: row[0],
95
+ )
96
+ )
97
+ decoded = tuple(
98
+ (player.global_rank, player.name, player.primary_class.code)
99
+ for player in sorted(players, key=lambda player: player.global_rank)
100
+ )
101
+ return decoded == discovered
102
+
103
+
104
+ def _overall_details_match_discovery(
105
+ entries: tuple[SolareOverallEntry, ...],
106
+ overall: DiscoveredSolareFamily,
107
+ ) -> bool:
108
+ discovered = tuple(
109
+ sorted(
110
+ zip(overall.ranks, overall.names),
111
+ key=lambda row: row[0],
112
+ )
113
+ )
114
+ decoded = tuple(
115
+ (entry.global_rank, entry.name)
116
+ for entry in sorted(entries, key=lambda entry: entry.global_rank)
117
+ )
118
+ return decoded == discovered
119
+
120
+
121
+ def _message_for_status(status: SolareDetectionStatus) -> str:
122
+ messages = {
123
+ SolareDetectionStatus.COMPLETE: (
124
+ "structurally confirmed Solare leaderboard without a known opcode"
125
+ ),
126
+ SolareDetectionStatus.DETECTED_INCOMPLETE: (
127
+ "leaderboard traffic was recognized, but the complete snapshot "
128
+ "was not captured"
129
+ ),
130
+ SolareDetectionStatus.RICH_CANDIDATE: (
131
+ "a complete class-balanced ranked table was found, but the "
132
+ "independent overall top-100 cross-check was not captured"
133
+ ),
134
+ SolareDetectionStatus.RANKED_PARTIAL: (
135
+ "ranked leaderboard structure was found, but the class-balanced "
136
+ "table and overall cross-check were incomplete"
137
+ ),
138
+ SolareDetectionStatus.MENU_CONTEXT: (
139
+ "Solare menu/history-like traffic was found, not a leaderboard"
140
+ ),
141
+ SolareDetectionStatus.INCONCLUSIVE: (
142
+ "no complete structural Solare signature was found"
143
+ ),
144
+ SolareDetectionStatus.NO_TRAFFIC: (
145
+ "no inbound payload from the selected game-server ports was found"
146
+ ),
147
+ }
148
+ return messages[status]
149
+
150
+
151
+ def _resource_loss_summary(health: SolareCaptureHealth) -> Optional[str]:
152
+ """Describe bounded live-pipeline losses that invalidate acquisition.
153
+
154
+ This deliberately covers only losses introduced by the toolkit's bounded
155
+ packet/reassembly pipeline. Pcap drops and TCP gaps keep their broader
156
+ capture-integrity wording below.
157
+ """
158
+
159
+ losses: list[str] = []
160
+ if health.packet_queue_overflows:
161
+ count = health.packet_queue_overflows
162
+ losses.append(
163
+ "packet queue overflow discarded "
164
+ f"{count} captured packet{'s' if count != 1 else ''} before decoding"
165
+ )
166
+ if health.flow_state_evictions:
167
+ count = health.flow_state_evictions
168
+ losses.append(
169
+ "forced flow-state eviction removed "
170
+ f"{count} active TCP reassembly state{'s' if count != 1 else ''}"
171
+ )
172
+ if not losses:
173
+ return None
174
+ if len(losses) == 1:
175
+ return losses[0]
176
+ return f"{losses[0]} and {losses[1]}"
177
+
178
+
179
+ def _capture_loss_summary(health: SolareCaptureHealth) -> Optional[str]:
180
+ """Name every observed acquisition loss, including pre-confirmation gaps."""
181
+
182
+ losses: list[str] = []
183
+ resource_loss = _resource_loss_summary(health)
184
+ if resource_loss is not None:
185
+ losses.append(resource_loss)
186
+ if health.tcp_gap_resets:
187
+ count = health.tcp_gap_resets
188
+ losses.append(
189
+ "TCP reassembly reset "
190
+ f"{count} time{'s' if count != 1 else ''} after an unresolved "
191
+ "sequence gap"
192
+ )
193
+ if health.pcap_dropped not in (None, 0):
194
+ backend_drop_count = health.pcap_dropped
195
+ assert backend_drop_count is not None
196
+ losses.append(
197
+ "the capture backend reported "
198
+ f"{backend_drop_count} dropped "
199
+ f"packet{'s' if backend_drop_count != 1 else ''}"
200
+ )
201
+ if health.pcap_interface_dropped not in (None, 0):
202
+ interface_drop_count = health.pcap_interface_dropped
203
+ assert interface_drop_count is not None
204
+ losses.append(
205
+ "the capture interface reported "
206
+ f"{interface_drop_count} dropped "
207
+ f"packet{'s' if interface_drop_count != 1 else ''}"
208
+ )
209
+ if not losses:
210
+ return None
211
+ if len(losses) == 1:
212
+ return losses[0]
213
+ return "; ".join(losses[:-1]) + f"; and {losses[-1]}"
214
+
215
+
216
+ def _noncomplete_message(
217
+ status: SolareDetectionStatus,
218
+ health: SolareCaptureHealth,
219
+ *,
220
+ integrity_rejected: bool,
221
+ ) -> str:
222
+ capture_loss = _capture_loss_summary(health)
223
+ if capture_loss is not None:
224
+ retry = (
225
+ "Retry after reducing capture load"
226
+ if _resource_loss_summary(health) is not None
227
+ else "Retry with a fresh capture"
228
+ )
229
+ if integrity_rejected:
230
+ return (
231
+ "the structural tables matched, but acquisition integrity was "
232
+ f"lost because {capture_loss}; snapshot publication was "
233
+ f"withheld. {retry}"
234
+ )
235
+ return (
236
+ f"{_message_for_status(status)}; acquisition integrity was lost "
237
+ f"because {capture_loss}. {retry}"
238
+ )
239
+
240
+ if integrity_rejected:
241
+ return (
242
+ "the structural tables matched, but capture gaps or reported "
243
+ "packet drops made acquisition integrity uncertain; snapshot "
244
+ "publication was withheld"
245
+ )
246
+
247
+ message = _message_for_status(status)
248
+ if health.candidate_history_rolled_over and health.capture_is_clean:
249
+ return (
250
+ f"{message}; the bounded candidate history rolled over after older "
251
+ "frames were evaluated and discarded. Retry while opening the "
252
+ "leaderboard promptly"
253
+ )
254
+ return message
255
+
256
+
257
+ def _evidence(
258
+ discovery: SolareDiscoveryResult,
259
+ health: SolareCaptureHealth,
260
+ *,
261
+ class_detail_layout_id: Optional[str] = None,
262
+ overall_detail_layout_id: Optional[str] = None,
263
+ ) -> SolareEvidence:
264
+ ranked = discovery.ranked_candidate
265
+ overall = discovery.overall or discovery.partial_overall
266
+ rich = discovery.rich
267
+ return SolareEvidence(
268
+ scanned_messages=discovery.scanned_frames,
269
+ candidate_families=discovery.candidate_families,
270
+ ranked_players=ranked.record_count if ranked is not None else 0,
271
+ class_group_counts=rich.class_counts if rich is not None else (),
272
+ overall_players=overall.record_count if overall is not None else 0,
273
+ exact_cross_check=discovery.exact_cross_check,
274
+ rich_layout=_family_layout(
275
+ rich,
276
+ detail_layout_id=class_detail_layout_id,
277
+ ),
278
+ overall_layout=_family_layout(
279
+ overall,
280
+ detail_layout_id=overall_detail_layout_id,
281
+ ),
282
+ health=health,
283
+ )
284
+
285
+
286
+ def build_solare_result(
287
+ frames: Iterable[BDOFrame],
288
+ health: SolareCaptureHealth,
289
+ *,
290
+ retain_raw_extensions: bool = False,
291
+ _discovery: Optional[SolareDiscoveryResult] = None,
292
+ ) -> SolareCaptureResult:
293
+ """Classify collected generic frames and construct a fail-closed result.
294
+
295
+ A snapshot is returned only after the rich 620-player class table and an
296
+ independent complete overall top 100 agree on every shared rank and name.
297
+ Deeper detail decoding is optional: an unfamiliar geometry may use bounded
298
+ ephemeral inference after confirmation, while every unsupported or
299
+ ambiguous capability is still omitted from the ranking snapshot.
300
+ """
301
+
302
+ retain_raw_extensions = validate_retain_raw_extensions(
303
+ retain_raw_extensions
304
+ )
305
+ frame_tuple = tuple(frames)
306
+ discovery = _discovery or discover_solare(frame_tuple)
307
+ integrity_rejected = discovery.confirmed and not health.capture_is_clean
308
+ if not frame_tuple and health.payload_segments == 0:
309
+ status = SolareDetectionStatus.NO_TRAFFIC
310
+ elif integrity_rejected:
311
+ status = SolareDetectionStatus.DETECTED_INCOMPLETE
312
+ else:
313
+ status = SolareDetectionStatus(discovery.status)
314
+
315
+ if status is not SolareDetectionStatus.COMPLETE:
316
+ evidence = _evidence(discovery, health)
317
+ message = _noncomplete_message(
318
+ status,
319
+ health,
320
+ integrity_rejected=integrity_rejected,
321
+ )
322
+ return SolareCaptureResult(
323
+ status=status,
324
+ evidence=evidence,
325
+ message=message,
326
+ )
327
+
328
+ rich = discovery.rich
329
+ overall = discovery.overall
330
+ assert rich is not None
331
+ assert overall is not None
332
+
333
+ players = _ranking_players(rich)
334
+ overall_entries = _overall_entries(overall)
335
+ class_table_capabilities = {"rankings"}
336
+ overall_capabilities = {"rankings"}
337
+ class_detail_layout_id: Optional[str] = None
338
+ overall_detail_layout_id: Optional[str] = None
339
+ rich_frames = family_frames(frame_tuple, rich)
340
+ overall_frames = family_frames(frame_tuple, overall)
341
+ class_details = decode_solare_details(
342
+ rich_frames,
343
+ rich,
344
+ retain_raw_extensions=retain_raw_extensions,
345
+ )
346
+ overall_details = decode_solare_overall_details(
347
+ overall_frames,
348
+ overall,
349
+ retain_raw_extensions=retain_raw_extensions,
350
+ )
351
+ learn_rich = _rich_layout_for(rich) is None
352
+ learn_overall = _overall_layout_for(overall) is None
353
+ if learn_rich or learn_overall:
354
+ known_rich_elos: Optional[dict[str, int]] = None
355
+ if (
356
+ not learn_rich
357
+ and class_details is not None
358
+ and "elo" in class_details.capabilities
359
+ and all(player.elo is not None for player in class_details.players)
360
+ ):
361
+ known_rich_elos = {
362
+ player.name: player.elo
363
+ for player in class_details.players
364
+ if player.elo is not None
365
+ }
366
+ known_overall_elos: Optional[dict[str, int]] = None
367
+ if (
368
+ not learn_overall
369
+ and overall_details is not None
370
+ and "elo" in overall_details.capabilities
371
+ and all(entry.elo is not None for entry in overall_details.entries)
372
+ ):
373
+ known_overall_elos = {
374
+ entry.name: entry.elo
375
+ for entry in overall_details.entries
376
+ if entry.elo is not None
377
+ }
378
+ learned_class, learned_overall = learn_unknown_solare_details(
379
+ rich_frames,
380
+ rich,
381
+ overall_frames,
382
+ overall,
383
+ learn_rich=learn_rich,
384
+ learn_overall=learn_overall,
385
+ retain_raw_extensions=retain_raw_extensions,
386
+ known_rich_elos=known_rich_elos,
387
+ known_overall_elos=known_overall_elos,
388
+ )
389
+ if class_details is None and learn_rich:
390
+ class_details = learned_class
391
+ if overall_details is None and learn_overall:
392
+ overall_details = learned_overall
393
+
394
+ if class_details is not None and _details_match_discovery(
395
+ class_details.players,
396
+ rich,
397
+ ):
398
+ players = tuple(
399
+ sorted(class_details.players, key=lambda player: player.global_rank)
400
+ )
401
+ class_detail_layout_id = class_details.layout_id
402
+ class_table_capabilities.update(class_details.capabilities)
403
+
404
+ if overall_details is not None and _overall_details_match_discovery(
405
+ overall_details.entries,
406
+ overall,
407
+ ):
408
+ overall_entries = tuple(
409
+ sorted(
410
+ overall_details.entries,
411
+ key=lambda entry: entry.global_rank,
412
+ )
413
+ )
414
+ overall_detail_layout_id = overall_details.layout_id
415
+ overall_capabilities.update(overall_details.capabilities)
416
+
417
+ evidence = _evidence(
418
+ discovery,
419
+ health,
420
+ class_detail_layout_id=class_detail_layout_id,
421
+ overall_detail_layout_id=overall_detail_layout_id,
422
+ )
423
+ snapshot = SolareLeaderboardSnapshot(
424
+ snapshot_id=solare_snapshot_id(
425
+ players,
426
+ overall_top_100=overall_entries,
427
+ ),
428
+ observed_at=max(
429
+ frame.context.timestamp for frame in (*rich_frames, *overall_frames)
430
+ ),
431
+ players=players,
432
+ overall_top_100=overall_entries,
433
+ class_table_capabilities=frozenset(class_table_capabilities),
434
+ overall_capabilities=frozenset(overall_capabilities),
435
+ )
436
+ return SolareCaptureResult(
437
+ status=status,
438
+ evidence=evidence,
439
+ snapshot=snapshot,
440
+ message=_message_for_status(status),
441
+ )
@@ -0,0 +1,203 @@
1
+ """Midstream-safe generic BDO framing used by Solare structural discovery."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Callable, Optional
6
+
7
+ from bdo_toolkit._protocol import BDOFrame, PacketContext
8
+
9
+ from ._constants import (
10
+ BDO_HEADER_SIZE,
11
+ BDO_MAX_FRAME_SIZE,
12
+ DISCOVERY_MAX_FRAME_LENGTH,
13
+ DISCOVERY_MIN_FRAME_LENGTH,
14
+ DISCOVERY_SYNC_BUFFER_LIMIT,
15
+ DISCOVERY_SYNC_HEADERS,
16
+ )
17
+
18
+
19
+ def _u16le(data: bytes | bytearray, offset: int) -> int:
20
+ return int.from_bytes(data[offset : offset + 2], "little")
21
+
22
+
23
+ class SolareDiscoveryStreamScanner:
24
+ """Emit generic frames without trusting a known Solare opcode.
25
+
26
+ Live capture can attach in the middle of an application message. The
27
+ scanner therefore requires either two equal large-family headers or three
28
+ consecutive generic flag-zero headers before it declares synchronization.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ callback: Callable[[BDOFrame], None],
34
+ reset_callback: Optional[Callable[[], None]] = None,
35
+ ) -> None:
36
+ self._callback = callback
37
+ self._reset_callback = reset_callback
38
+ self._buffer = bytearray()
39
+ self._buffer_start_sequence: Optional[int] = None
40
+ self._locked = False
41
+ self._frame_index = 0
42
+
43
+ def reset(self) -> None:
44
+ self._buffer.clear()
45
+ self._buffer_start_sequence = None
46
+ self._locked = False
47
+ if self._reset_callback is not None:
48
+ self._reset_callback()
49
+
50
+ def can_anchor_at_start(self, data: bytes) -> bool:
51
+ """Return whether byte zero already satisfies the discovery sync proof."""
52
+
53
+ def header_length(offset: int) -> Optional[int]:
54
+ if offset + BDO_HEADER_SIZE > len(data):
55
+ return None
56
+ message_length = _u16le(data, offset)
57
+ if not BDO_HEADER_SIZE <= message_length <= BDO_MAX_FRAME_SIZE:
58
+ return -1
59
+ if data[offset + 2] != 0:
60
+ return -1
61
+ return message_length
62
+
63
+ position = 0
64
+ generic_chain = True
65
+ for index in range(DISCOVERY_SYNC_HEADERS):
66
+ message_length = header_length(position)
67
+ if message_length is None or message_length < BDO_HEADER_SIZE:
68
+ generic_chain = False
69
+ break
70
+ if index != DISCOVERY_SYNC_HEADERS - 1:
71
+ if position + message_length > len(data):
72
+ generic_chain = False
73
+ break
74
+ position += message_length
75
+ if generic_chain:
76
+ return True
77
+
78
+ first_length = header_length(0)
79
+ if first_length is None or not (
80
+ DISCOVERY_MIN_FRAME_LENGTH
81
+ <= first_length
82
+ <= DISCOVERY_MAX_FRAME_LENGTH
83
+ ):
84
+ return False
85
+ second_offset = first_length
86
+ return (
87
+ header_length(second_offset) == first_length
88
+ and second_offset + BDO_HEADER_SIZE <= len(data)
89
+ and _u16le(data, 3) == _u16le(data, second_offset + 3)
90
+ )
91
+
92
+ def feed(self, data: bytes, context: PacketContext) -> None:
93
+ if not data:
94
+ return
95
+ if not self._buffer:
96
+ self._buffer_start_sequence = context.stream_start
97
+ self._buffer.extend(data)
98
+ self._scan(context)
99
+
100
+ def scan_standalone(self, data: bytes, context: PacketContext) -> None:
101
+ # FlowManager invokes this for overlap/retransmission bytes already
102
+ # represented in the primary stream. Ignoring them prevents duplicate
103
+ # discovery frames.
104
+ return
105
+
106
+ def _discard_prefix(self, byte_count: int) -> None:
107
+ if byte_count <= 0:
108
+ return
109
+ del self._buffer[:byte_count]
110
+ if self._buffer_start_sequence is not None:
111
+ self._buffer_start_sequence += byte_count
112
+ if not self._buffer:
113
+ self._buffer_start_sequence = None
114
+
115
+ def _header_length(self, offset: int) -> Optional[int]:
116
+ if offset + BDO_HEADER_SIZE > len(self._buffer):
117
+ return None
118
+ message_length = _u16le(self._buffer, offset)
119
+ if not BDO_HEADER_SIZE <= message_length <= BDO_MAX_FRAME_SIZE:
120
+ return -1
121
+ if self._buffer[offset + 2] != 0:
122
+ return -1
123
+ return message_length
124
+
125
+ def _has_sync_chain(self, offset: int) -> Optional[bool]:
126
+ position = offset
127
+ for index in range(DISCOVERY_SYNC_HEADERS):
128
+ message_length = self._header_length(position)
129
+ if message_length is None:
130
+ return None
131
+ if message_length < BDO_HEADER_SIZE:
132
+ return False
133
+ if index == DISCOVERY_SYNC_HEADERS - 1:
134
+ return True
135
+ if position + message_length > len(self._buffer):
136
+ return None
137
+ position += message_length
138
+ return True
139
+
140
+ def _find_sync_offset(self) -> Optional[int]:
141
+ final_offset = len(self._buffer) - BDO_HEADER_SIZE
142
+ for offset in range(max(0, final_offset + 1)):
143
+ if self._has_sync_chain(offset) is True:
144
+ return offset
145
+ return None
146
+
147
+ def _find_repeated_large_family_offset(self) -> Optional[int]:
148
+ final_offset = len(self._buffer) - BDO_HEADER_SIZE
149
+ for offset in range(max(0, final_offset + 1)):
150
+ message_length = self._header_length(offset)
151
+ if message_length is None or not (
152
+ DISCOVERY_MIN_FRAME_LENGTH
153
+ <= message_length
154
+ <= DISCOVERY_MAX_FRAME_LENGTH
155
+ ):
156
+ continue
157
+ next_offset = offset + message_length
158
+ next_length = self._header_length(next_offset)
159
+ if next_length != message_length:
160
+ continue
161
+ if _u16le(self._buffer, offset + 3) != _u16le(
162
+ self._buffer, next_offset + 3
163
+ ):
164
+ continue
165
+ return offset
166
+ return None
167
+
168
+ def _scan(self, context: PacketContext) -> None:
169
+ while True:
170
+ if not self._locked:
171
+ sync_offset = self._find_repeated_large_family_offset()
172
+ if sync_offset is None:
173
+ sync_offset = self._find_sync_offset()
174
+ if sync_offset is None:
175
+ if len(self._buffer) > DISCOVERY_SYNC_BUFFER_LIMIT:
176
+ self._discard_prefix(
177
+ len(self._buffer) - DISCOVERY_SYNC_BUFFER_LIMIT
178
+ )
179
+ return
180
+ if sync_offset:
181
+ self._discard_prefix(sync_offset)
182
+ self._locked = True
183
+
184
+ message_length = self._header_length(0)
185
+ if message_length is None:
186
+ return
187
+ if message_length < BDO_HEADER_SIZE:
188
+ self._locked = False
189
+ self._discard_prefix(1)
190
+ continue
191
+ if message_length > len(self._buffer):
192
+ return
193
+
194
+ message = bytes(self._buffer[:message_length])
195
+ frame = BDOFrame(
196
+ index=self._frame_index,
197
+ message=message,
198
+ context=context,
199
+ stream_sequence=self._buffer_start_sequence,
200
+ )
201
+ self._frame_index += 1
202
+ self._callback(frame)
203
+ self._discard_prefix(message_length)
@@ -0,0 +1,11 @@
1
+ """Shared validation for public Arena of Solare acquisition options."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def validate_retain_raw_extensions(value: object) -> bool:
7
+ """Return a validated raw-retention flag with one consistent contract."""
8
+
9
+ if not isinstance(value, bool):
10
+ raise TypeError("retain_raw_extensions must be a boolean")
11
+ return value