bdo-toolkit 1.0.0__tar.gz → 1.0.2__tar.gz

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 (90) hide show
  1. {bdo_toolkit-1.0.0/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.2}/PKG-INFO +6 -5
  2. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/README.md +3 -3
  3. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/pyproject.toml +5 -1
  4. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_async_sessions.py +13 -18
  5. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_backend.py +121 -4
  6. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_runtime.py +15 -17
  7. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_deposit_origin.py +579 -155
  8. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_engine.py +9 -0
  9. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_framing.py +29 -7
  10. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_protocol.py +1 -1
  11. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_reassembly.py +20 -7
  12. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_storage_hydration.py +1 -1
  13. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_version.py +1 -1
  14. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/calibration.py +11 -13
  15. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/capture.py +24 -22
  16. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/character_state.py +20 -6
  17. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/events.py +1 -1
  18. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/filters.py +2 -5
  19. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/origin_learning.py +3 -3
  20. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/remote_profiles.py +1 -1
  21. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_discovery.py +1 -1
  22. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/async_session.py +10 -18
  23. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/models.py +1 -1
  24. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/session.py +5 -6
  25. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2/src/bdo_toolkit.egg-info}/PKG-INFO +6 -5
  26. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/SOURCES.txt +1 -0
  27. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_async_sessions.py +1 -4
  28. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration_lifecycle.py +17 -6
  29. bdo_toolkit-1.0.2/tests/test_capture_backend.py +446 -0
  30. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_capture_runtime.py +4 -5
  31. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_character_state.py +179 -3
  32. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_deposit_origin.py +834 -7
  33. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_inventory_snapshots.py +43 -1
  34. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_reassembly.py +133 -0
  35. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_session.py +13 -16
  36. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/LICENSE +0 -0
  37. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/setup.cfg +0 -0
  38. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/__init__.py +0 -0
  39. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_capture_options.py +0 -0
  40. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_profile_runtime.py +0 -0
  41. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_specs.py +0 -0
  42. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
  43. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/cli.py +0 -0
  44. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/diagnostics.py +0 -0
  45. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/item_state.py +0 -0
  46. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/profiles.py +0 -0
  47. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/py.typed +0 -0
  48. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/__init__.py +0 -0
  49. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_constants.py +0 -0
  50. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
  51. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_details.py +0 -0
  52. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
  53. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
  54. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_result.py +0 -0
  55. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_scanner.py +0 -0
  56. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/_validation.py +0 -0
  57. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/solare/replay.py +0 -0
  58. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit/writers.py +0 -0
  59. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
  60. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
  61. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/requires.txt +0 -0
  62. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
  63. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration.py +0 -0
  64. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration_conveniences.py +0 -0
  65. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_calibration_retention.py +0 -0
  66. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_cli.py +0 -0
  67. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_decoder_diagnostics.py +0 -0
  68. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_direction.py +0 -0
  69. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
  70. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_engine.py +0 -0
  71. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_examples_and_writers.py +0 -0
  72. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_explicit_profile.py +0 -0
  73. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_fixture_regressions.py +0 -0
  74. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_hardening.py +0 -0
  75. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_initial_load_storage.py +0 -0
  76. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_item_state.py +0 -0
  77. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_live_capture_session.py +0 -0
  78. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_origin_learning.py +0 -0
  79. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_remote_profiles.py +0 -0
  80. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_api.py +0 -0
  81. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_details.py +0 -0
  82. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_learning.py +0 -0
  83. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_learning_adversarial.py +0 -0
  84. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_real_captures.py +0 -0
  85. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_solare_replay.py +0 -0
  86. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_destination_validation.py +0 -0
  87. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_events.py +0 -0
  88. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_hydration_tracker.py +0 -0
  89. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_storage_unknown_destination_calibration.py +0 -0
  90. {bdo_toolkit-1.0.0 → bdo_toolkit-1.0.2}/tests/test_toolkit_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bdo-toolkit
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Passive, read-only BDO packet parsing toolkit for app developers.
5
5
  Author: bdo-toolkit contributors
6
6
  License-Expression: MIT
@@ -12,9 +12,10 @@ Keywords: bdo,packets,parse,pcap,passive
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
15
16
  Classifier: Programming Language :: Python :: 3 :: Only
16
17
  Classifier: Typing :: Typed
17
- Requires-Python: >=3.10
18
+ Requires-Python: >=3.14
18
19
  Description-Content-Type: text/markdown
19
20
  License-File: LICENSE
20
21
  Requires-Dist: scapy>=2.5
@@ -27,7 +28,7 @@ Dynamic: license-file
27
28
  # bdo-toolkit
28
29
 
29
30
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
30
- [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
31
+ [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
31
32
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
32
33
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
33
34
  [![License: MIT](https://img.shields.io/badge/license-MIT-2f855a?style=flat-square)](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
@@ -62,7 +63,7 @@ services is unknown.
62
63
  python -m pip install bdo-toolkit
63
64
  ```
64
65
 
65
- Python 3.10 or newer is required. Before live capture, complete
66
+ Python 3.14 or newer is required. Before live capture, complete
66
67
  [Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
67
68
  including Npcap on Windows and permission to capture on the selected interface.
68
69
  Offline PCAP and PCAPNG replay does not require Npcap.
@@ -134,7 +135,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
134
135
  ```
135
136
 
136
137
  CI runs tests, type checking, wheel construction, and a CLI smoke test on
137
- Ubuntu and Windows with Python 3.10 and 3.14. Tests that require private
138
+ Ubuntu and Windows with Python 3.14. Tests that require private
138
139
  game-session captures skip when those local fixtures are absent.
139
140
 
140
141
  ## License
@@ -1,7 +1,7 @@
1
1
  # bdo-toolkit
2
2
 
3
3
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
4
- [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
4
+ [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
5
5
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
6
6
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](https://ychwu.github.io/bdo-toolkit/#stability)
7
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-2f855a?style=flat-square)](https://github.com/ychwu/bdo-toolkit/blob/main/LICENSE)
@@ -36,7 +36,7 @@ services is unknown.
36
36
  python -m pip install bdo-toolkit
37
37
  ```
38
38
 
39
- Python 3.10 or newer is required. Before live capture, complete
39
+ Python 3.14 or newer is required. Before live capture, complete
40
40
  [Installation & setup](https://ychwu.github.io/bdo-toolkit/#capture-foundation),
41
41
  including Npcap on Windows and permission to capture on the selected interface.
42
42
  Offline PCAP and PCAPNG replay does not require Npcap.
@@ -108,7 +108,7 @@ python -m pip wheel . --no-deps --wheel-dir dist
108
108
  ```
109
109
 
110
110
  CI runs tests, type checking, wheel construction, and a CLI smoke test on
111
- Ubuntu and Windows with Python 3.10 and 3.14. Tests that require private
111
+ Ubuntu and Windows with Python 3.14. Tests that require private
112
112
  game-session captures skip when those local fixtures are absent.
113
113
 
114
114
  ## License
@@ -7,7 +7,7 @@ name = "bdo-toolkit"
7
7
  dynamic = ["version"]
8
8
  description = "Passive, read-only BDO packet parsing toolkit for app developers."
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.14"
11
11
  dependencies = [
12
12
  "scapy>=2.5",
13
13
  ]
@@ -21,6 +21,7 @@ classifiers = [
21
21
  "Development Status :: 5 - Production/Stable",
22
22
  "Operating System :: OS Independent",
23
23
  "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3.14",
24
25
  "Programming Language :: Python :: 3 :: Only",
25
26
  "Typing :: Typed",
26
27
  ]
@@ -47,6 +48,9 @@ where = ["src"]
47
48
  [tool.pytest.ini_options]
48
49
  testpaths = ["tests"]
49
50
 
51
+ [tool.mypy]
52
+ python_version = "3.14"
53
+
50
54
  [tool.setuptools.package-data]
51
55
  bdo_toolkit = ["py.typed"]
52
56
 
@@ -14,7 +14,7 @@ from concurrent.futures import ThreadPoolExecutor
14
14
  from functools import partial
15
15
  from pathlib import Path
16
16
  from types import TracebackType
17
- from typing import AsyncIterator, Callable, Optional, TypeVar
17
+ from typing import AsyncIterator, Callable, Optional
18
18
 
19
19
  from ._capture_runtime import CaptureEndpoint, _attach_cleanup_owner
20
20
  from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
@@ -33,10 +33,7 @@ from .origin_learning import CompanionObservation
33
33
  from .profiles import OpcodeProfile
34
34
 
35
35
 
36
- T = TypeVar("T")
37
-
38
-
39
- async def _wait_ignoring_cancellation(future: asyncio.Future[T]) -> T:
36
+ async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
40
37
  """Wait for an already-started operation, even after caller cancellation."""
41
38
 
42
39
  while not future.done():
@@ -51,14 +48,14 @@ async def _wait_ignoring_cancellation(future: asyncio.Future[T]) -> T:
51
48
  return future.result()
52
49
 
53
50
 
54
- async def _await_preserving_future(future: asyncio.Future[T]) -> T:
51
+ async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
55
52
  """Await without cancelling or wrapping the submitted worker future."""
56
53
 
57
54
  await asyncio.wait((future,))
58
55
  return future.result()
59
56
 
60
57
 
61
- def _thread_task(function: Callable[[], T]) -> asyncio.Task[T]:
58
+ def _thread_task[T](function: Callable[[], T]) -> asyncio.Task[T]:
62
59
  return asyncio.create_task(asyncio.to_thread(function))
63
60
 
64
61
 
@@ -143,7 +140,7 @@ class AsyncLiveCaptureSession:
143
140
 
144
141
  self._session.raise_if_failed()
145
142
 
146
- def _submit(self, function: Callable[[], T]) -> asyncio.Future[T]:
143
+ def _submit[T](self, function: Callable[[], T]) -> asyncio.Future[T]:
147
144
  if self._executor_closed:
148
145
  raise RuntimeError("async live capture session is already closed")
149
146
  loop = asyncio.get_running_loop()
@@ -380,11 +377,10 @@ class AsyncLiveCaptureSession:
380
377
  self,
381
378
  context="async live item capture context",
382
379
  )
383
- if hasattr(exc_value, "add_note"):
384
- exc_value.add_note(
385
- "async live item capture context cleanup also failed: "
386
- f"{cleanup_error!r}"
387
- )
380
+ exc_value.add_note(
381
+ "async live item capture context cleanup also failed: "
382
+ f"{cleanup_error!r}"
383
+ )
388
384
 
389
385
 
390
386
  class AsyncCalibrationSession:
@@ -644,8 +640,7 @@ class AsyncCalibrationSession:
644
640
  self,
645
641
  context="async live calibration context",
646
642
  )
647
- if hasattr(exc_value, "add_note"):
648
- exc_value.add_note(
649
- "async live calibration context cleanup also failed: "
650
- f"{cleanup_error!r}"
651
- )
643
+ exc_value.add_note(
644
+ "async live calibration context cleanup also failed: "
645
+ f"{cleanup_error!r}"
646
+ )
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from dataclasses import dataclass
6
6
  from pathlib import Path
7
- from typing import Iterable, Iterator, Optional, Protocol
7
+ from typing import Any, Iterable, Iterator, Optional, Protocol
8
8
 
9
9
 
10
10
  class SegmentConsumer(Protocol):
@@ -93,21 +93,138 @@ def validate_server_ports(ports: Iterable[int]) -> tuple[int, ...]:
93
93
  return tuple(normalized)
94
94
 
95
95
 
96
+ def _required_header_integer(value: Any, field_name: str) -> int:
97
+ """Return one decoded header field or reject an incomplete dissection."""
98
+ if value is None:
99
+ raise ValueError(f"IPv4/TCP {field_name} is unavailable")
100
+ try:
101
+ return int(value)
102
+ except (TypeError, ValueError, OverflowError) as exc:
103
+ raise ValueError(f"IPv4/TCP {field_name} is invalid: {value!r}") from exc
104
+
105
+
106
+ def _validate_unfragmented_ipv4_tcp(ip: Any) -> None:
107
+ """Reject IP states that cannot be handed to TCP reassembly safely."""
108
+ version = _required_header_integer(getattr(ip, "version", None), "version")
109
+ if version != 4:
110
+ raise ValueError(f"expected IPv4 version 4, got {version}")
111
+
112
+ fragment_offset = _required_header_integer(
113
+ getattr(ip, "frag", None),
114
+ "fragment offset",
115
+ )
116
+ flags = _required_header_integer(getattr(ip, "flags", None), "flags")
117
+ if fragment_offset != 0 or flags & 0x01:
118
+ raise ValueError(
119
+ "fragmented IPv4/TCP packets are unsupported; "
120
+ "TCP reassembly requires a complete IP datagram"
121
+ )
122
+
123
+
124
+ def _captured_ipv4_length(ip: Any) -> int:
125
+ """Return captured bytes without reserializing ordinary Scapy packets."""
126
+ original = getattr(ip, "original", None)
127
+ if isinstance(original, (bytes, bytearray, memoryview)) and original:
128
+ return len(original)
129
+ return len(bytes(ip))
130
+
131
+
132
+ def _extract_ipv4_tcp_payload(ip: Any, tcp: Any) -> bytes:
133
+ """Return only application bytes declared by complete IPv4/TCP headers.
134
+
135
+ Scapy exposes link-layer padding through ``tcp.payload`` even though those
136
+ bytes lie beyond the IPv4 total length. The wire header lengths are the
137
+ authority at this boundary so padding can never advance TCP sequence state.
138
+ """
139
+ total_length = _required_header_integer(
140
+ getattr(ip, "len", None),
141
+ "total length",
142
+ )
143
+ ip_header_words = _required_header_integer(
144
+ getattr(ip, "ihl", None),
145
+ "header length",
146
+ )
147
+ tcp_header_words = _required_header_integer(
148
+ getattr(tcp, "dataofs", None),
149
+ "TCP header length",
150
+ )
151
+
152
+ if not 5 <= ip_header_words <= 15:
153
+ raise ValueError(
154
+ f"invalid IPv4 header length: {ip_header_words} 32-bit words"
155
+ )
156
+ if not 5 <= tcp_header_words <= 15:
157
+ raise ValueError(
158
+ f"invalid TCP header length: {tcp_header_words} 32-bit words"
159
+ )
160
+ if not 0 <= total_length <= 0xFFFF:
161
+ raise ValueError(f"invalid IPv4 total length: {total_length}")
162
+
163
+ captured_length = _captured_ipv4_length(ip)
164
+ if captured_length < total_length:
165
+ raise ValueError(
166
+ "truncated IPv4 packet: "
167
+ f"header declares {total_length} bytes, capture has {captured_length}"
168
+ )
169
+
170
+ header_length = (ip_header_words + tcp_header_words) * 4
171
+ if total_length < header_length:
172
+ raise ValueError(
173
+ "invalid IPv4/TCP header lengths: "
174
+ f"total length {total_length} is smaller than {header_length}"
175
+ )
176
+
177
+ declared_length = total_length - header_length
178
+ available = bytes(getattr(tcp, "payload", b""))
179
+ if len(available) < declared_length:
180
+ raise ValueError(
181
+ "truncated IPv4/TCP payload: "
182
+ f"header declares {declared_length} bytes, capture has {len(available)}"
183
+ )
184
+ return available[:declared_length]
185
+
186
+
187
+ def _consumer_server_ports(engine: SegmentConsumer) -> Optional[frozenset[int]]:
188
+ """Read the validated port set exposed by each built-in consumer."""
189
+ ports = getattr(engine, "server_ports", None)
190
+ if ports is None:
191
+ # Solare's internal collector predates the shared ``server_ports`` name.
192
+ ports = getattr(engine, "ports", None)
193
+ if ports is None:
194
+ return None
195
+ return frozenset(ports)
196
+
197
+
96
198
  def make_packet_handler(engine: SegmentConsumer):
97
199
  IP, TCP, _, _, _ = import_scapy()
200
+ server_ports = _consumer_server_ports(engine)
98
201
 
99
202
  def handle(packet) -> None:
100
- if IP not in packet or TCP not in packet:
203
+ if IP not in packet:
101
204
  return
102
205
 
103
206
  ip = packet[IP]
207
+ protocol = _required_header_integer(getattr(ip, "proto", None), "protocol")
208
+ if protocol != 6:
209
+ return
210
+
211
+ if TCP not in packet:
212
+ # Non-initial fragments and severely truncated headers have no
213
+ # source port, so they cannot be attributed to a selected flow.
214
+ return
215
+
104
216
  tcp = packet[TCP]
105
- payload = bytes(tcp.payload)
217
+ source_port = int(tcp.sport)
218
+ if server_ports is not None and source_port not in server_ports:
219
+ return
220
+
221
+ _validate_unfragmented_ipv4_tcp(ip)
222
+ payload = _extract_ipv4_tcp_payload(ip, tcp)
106
223
  flags = int(tcp.flags)
107
224
 
108
225
  engine.process_tcp_segment(
109
226
  source_ip=str(ip.src),
110
- source_port=int(tcp.sport),
227
+ source_port=source_port,
111
228
  destination_ip=str(ip.dst),
112
229
  destination_port=int(tcp.dport),
113
230
  sequence=int(tcp.seq),
@@ -64,18 +64,17 @@ def _attach_cleanup_owner(
64
64
  # Built-in and ordinary application exceptions expose __dict__. Keep
65
65
  # the original failure identity even for an exotic immutable subtype.
66
66
  pass
67
- if hasattr(error, "add_note"):
68
- if attached:
69
- error.add_note(
70
- f"{context} cleanup is incomplete; retry stop() on "
71
- "exception.cleanup_owner"
72
- )
73
- else:
74
- error.add_note(
75
- f"{context} cleanup is incomplete; this exception subtype "
76
- "cannot expose cleanup_owner, so retain and stop the original "
77
- "session object"
78
- )
67
+ if attached:
68
+ error.add_note(
69
+ f"{context} cleanup is incomplete; retry stop() on "
70
+ "exception.cleanup_owner"
71
+ )
72
+ else:
73
+ error.add_note(
74
+ f"{context} cleanup is incomplete; this exception subtype "
75
+ "cannot expose cleanup_owner, so retain and stop the original "
76
+ "session object"
77
+ )
79
78
 
80
79
 
81
80
  @dataclass(frozen=True)
@@ -456,11 +455,10 @@ class LivePacketCapture:
456
455
  # Startup remains the primary failure, but incomplete
457
456
  # cleanup must be explicit in its traceback and remains
458
457
  # retryable through stop().
459
- if hasattr(exc, "add_note"):
460
- exc.add_note(
461
- "live capture startup cleanup also failed: "
462
- f"{cleanup_error!r}"
463
- )
458
+ exc.add_note(
459
+ "live capture startup cleanup also failed: "
460
+ f"{cleanup_error!r}"
461
+ )
464
462
  if not self._cleanup_incomplete:
465
463
  self._capture = None
466
464
  self._capture_socket = None