magpie-bridge 0.1.0__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.
- magpie_bridge-0.1.0/LICENSE +21 -0
- magpie_bridge-0.1.0/PKG-INFO +133 -0
- magpie_bridge-0.1.0/README.md +106 -0
- magpie_bridge-0.1.0/pyproject.toml +42 -0
- magpie_bridge-0.1.0/setup.cfg +4 -0
- magpie_bridge-0.1.0/src/magpie_bridge/__init__.py +47 -0
- magpie_bridge-0.1.0/src/magpie_bridge/constants.py +34 -0
- magpie_bridge-0.1.0/src/magpie_bridge/errors.py +5 -0
- magpie_bridge-0.1.0/src/magpie_bridge/protocol.py +225 -0
- magpie_bridge-0.1.0/src/magpie_bridge.egg-info/PKG-INFO +133 -0
- magpie_bridge-0.1.0/src/magpie_bridge.egg-info/SOURCES.txt +12 -0
- magpie_bridge-0.1.0/src/magpie_bridge.egg-info/dependency_links.txt +1 -0
- magpie_bridge-0.1.0/src/magpie_bridge.egg-info/top_level.txt +1 -0
- magpie_bridge-0.1.0/tests/test_protocol.py +150 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Albert Moky
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: magpie_bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reliable UDP Relay Network protocol library
|
|
5
|
+
Author: Albert Moky
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/moky/magpie-bridge
|
|
8
|
+
Project-URL: Repository, https://github.com/moky/magpie-bridge
|
|
9
|
+
Project-URL: Issues, https://github.com/moky/magpie-bridge/issues
|
|
10
|
+
Keywords: udp,relay,network,protocol,magpie-bridge
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Internet
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
# magpie-bridge (Python)
|
|
29
|
+
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://pypi.org/project/magpie-bridge/)
|
|
32
|
+
|
|
33
|
+
Reliable UDP Relay Network — wire protocol library for Python.
|
|
34
|
+
|
|
35
|
+
This package implements the Magpie Bridge protocol codec: packet packing,
|
|
36
|
+
unpacking, validation, message segmentation, and acknowledgement helpers.
|
|
37
|
+
|
|
38
|
+
> [中文文档](README.zh-CN.md)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install magpie-bridge
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/moky/magpie-bridge.git
|
|
50
|
+
cd magpie-bridge/python
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from magpie_bridge import Packet, split_message, build_ack, TYPE_MINI
|
|
58
|
+
|
|
59
|
+
# Build and send a small packet
|
|
60
|
+
pkt = Packet(TYPE_MINI, target_bid=42, source_bid=7, sn=1, body=b"hello")
|
|
61
|
+
data = pkt.pack() # bytes ready for UDP send
|
|
62
|
+
|
|
63
|
+
# Parse a received packet
|
|
64
|
+
received = Packet.unpack(data)
|
|
65
|
+
print(received.body) # b"hello"
|
|
66
|
+
|
|
67
|
+
# Split a large message into segments
|
|
68
|
+
message = b"x" * 5000
|
|
69
|
+
segments = split_message(message, sn=2, source_bid=7, target_bid=42)
|
|
70
|
+
for seg in segments:
|
|
71
|
+
send(seg.pack())
|
|
72
|
+
|
|
73
|
+
# Build an acknowledgement
|
|
74
|
+
ack = build_ack(received)
|
|
75
|
+
send(ack.pack()) # body=b"OK", bids swapped
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Protocol Overview
|
|
79
|
+
|
|
80
|
+
| Field | Size | Description |
|
|
81
|
+
| ----------------- | ------ | ------------------------------------ |
|
|
82
|
+
| Magic Code | 4 B | `MB\x00\x01` (Magpie Bridge v1) |
|
|
83
|
+
| Type | 1 B | packet type + parameter width |
|
|
84
|
+
| Head Length | 1 B | 20 / 22 / 24 / 28 |
|
|
85
|
+
| Body Length | 2 B | body size in octets |
|
|
86
|
+
| Target Bridge ID | 4 B | destination bid |
|
|
87
|
+
| Source Bridge ID | 4 B | originator bid |
|
|
88
|
+
| Serial Number | 4 B | message identity |
|
|
89
|
+
| index / count | 0/2/4/8 B | segment ordinal / total segments |
|
|
90
|
+
| Body | 0..1200 B | application payload |
|
|
91
|
+
|
|
92
|
+
- Maximum UDP payload (MSS): **1232** octets (1280 - 40 - 8, dual-stack safe).
|
|
93
|
+
- Maximum body size: **1200** octets.
|
|
94
|
+
- See `../docs/design-protocol.md` for the full specification.
|
|
95
|
+
|
|
96
|
+
## API Reference
|
|
97
|
+
|
|
98
|
+
### `Packet`
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
Packet(type, target_bid, source_bid, sn, index=None, count=None, body=b"")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `pack() -> bytes` — serialize.
|
|
105
|
+
- `Packet.unpack(data: bytes) -> Packet` — parse.
|
|
106
|
+
- `validate() -> bool` — check whether the packet is well-formed.
|
|
107
|
+
- `message_id -> int` — deduplication key (`sn` for data packets,
|
|
108
|
+
`(sn << (type & 0x0F)) | index` for ACKs).
|
|
109
|
+
- `head_length`, `param_width`, `is_ack` — derived properties.
|
|
110
|
+
|
|
111
|
+
### Helpers
|
|
112
|
+
|
|
113
|
+
- `select_type(message_size) -> int` — choose Type by message size.
|
|
114
|
+
- `split_message(data, sn, source_bid, target_bid) -> list[Packet]`.
|
|
115
|
+
- `build_ack(packet) -> Packet` — build a `b"OK"` acknowledgement.
|
|
116
|
+
- `build_control(body, sn, source_bid=0, target_bid=0) -> Packet` — build a
|
|
117
|
+
control packet (SYN/ACK/PING/PONG/FIN) with Type 0.
|
|
118
|
+
|
|
119
|
+
### Exceptions
|
|
120
|
+
|
|
121
|
+
- `ProtocolError` — raised on invalid packets or parsing failures.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cd python
|
|
127
|
+
pip install -e ".[dev]" # or: pip install pytest
|
|
128
|
+
pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# magpie-bridge (Python)
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://pypi.org/project/magpie-bridge/)
|
|
5
|
+
|
|
6
|
+
Reliable UDP Relay Network — wire protocol library for Python.
|
|
7
|
+
|
|
8
|
+
This package implements the Magpie Bridge protocol codec: packet packing,
|
|
9
|
+
unpacking, validation, message segmentation, and acknowledgement helpers.
|
|
10
|
+
|
|
11
|
+
> [中文文档](README.zh-CN.md)
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install magpie-bridge
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install from source:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/moky/magpie-bridge.git
|
|
23
|
+
cd magpie-bridge/python
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from magpie_bridge import Packet, split_message, build_ack, TYPE_MINI
|
|
31
|
+
|
|
32
|
+
# Build and send a small packet
|
|
33
|
+
pkt = Packet(TYPE_MINI, target_bid=42, source_bid=7, sn=1, body=b"hello")
|
|
34
|
+
data = pkt.pack() # bytes ready for UDP send
|
|
35
|
+
|
|
36
|
+
# Parse a received packet
|
|
37
|
+
received = Packet.unpack(data)
|
|
38
|
+
print(received.body) # b"hello"
|
|
39
|
+
|
|
40
|
+
# Split a large message into segments
|
|
41
|
+
message = b"x" * 5000
|
|
42
|
+
segments = split_message(message, sn=2, source_bid=7, target_bid=42)
|
|
43
|
+
for seg in segments:
|
|
44
|
+
send(seg.pack())
|
|
45
|
+
|
|
46
|
+
# Build an acknowledgement
|
|
47
|
+
ack = build_ack(received)
|
|
48
|
+
send(ack.pack()) # body=b"OK", bids swapped
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Protocol Overview
|
|
52
|
+
|
|
53
|
+
| Field | Size | Description |
|
|
54
|
+
| ----------------- | ------ | ------------------------------------ |
|
|
55
|
+
| Magic Code | 4 B | `MB\x00\x01` (Magpie Bridge v1) |
|
|
56
|
+
| Type | 1 B | packet type + parameter width |
|
|
57
|
+
| Head Length | 1 B | 20 / 22 / 24 / 28 |
|
|
58
|
+
| Body Length | 2 B | body size in octets |
|
|
59
|
+
| Target Bridge ID | 4 B | destination bid |
|
|
60
|
+
| Source Bridge ID | 4 B | originator bid |
|
|
61
|
+
| Serial Number | 4 B | message identity |
|
|
62
|
+
| index / count | 0/2/4/8 B | segment ordinal / total segments |
|
|
63
|
+
| Body | 0..1200 B | application payload |
|
|
64
|
+
|
|
65
|
+
- Maximum UDP payload (MSS): **1232** octets (1280 - 40 - 8, dual-stack safe).
|
|
66
|
+
- Maximum body size: **1200** octets.
|
|
67
|
+
- See `../docs/design-protocol.md` for the full specification.
|
|
68
|
+
|
|
69
|
+
## API Reference
|
|
70
|
+
|
|
71
|
+
### `Packet`
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
Packet(type, target_bid, source_bid, sn, index=None, count=None, body=b"")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `pack() -> bytes` — serialize.
|
|
78
|
+
- `Packet.unpack(data: bytes) -> Packet` — parse.
|
|
79
|
+
- `validate() -> bool` — check whether the packet is well-formed.
|
|
80
|
+
- `message_id -> int` — deduplication key (`sn` for data packets,
|
|
81
|
+
`(sn << (type & 0x0F)) | index` for ACKs).
|
|
82
|
+
- `head_length`, `param_width`, `is_ack` — derived properties.
|
|
83
|
+
|
|
84
|
+
### Helpers
|
|
85
|
+
|
|
86
|
+
- `select_type(message_size) -> int` — choose Type by message size.
|
|
87
|
+
- `split_message(data, sn, source_bid, target_bid) -> list[Packet]`.
|
|
88
|
+
- `build_ack(packet) -> Packet` — build a `b"OK"` acknowledgement.
|
|
89
|
+
- `build_control(body, sn, source_bid=0, target_bid=0) -> Packet` — build a
|
|
90
|
+
control packet (SYN/ACK/PING/PONG/FIN) with Type 0.
|
|
91
|
+
|
|
92
|
+
### Exceptions
|
|
93
|
+
|
|
94
|
+
- `ProtocolError` — raised on invalid packets or parsing failures.
|
|
95
|
+
|
|
96
|
+
## Development
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
cd python
|
|
100
|
+
pip install -e ".[dev]" # or: pip install pytest
|
|
101
|
+
pytest
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "magpie_bridge"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Reliable UDP Relay Network protocol library"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Albert Moky"},
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Internet",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Topic :: System :: Networking",
|
|
29
|
+
]
|
|
30
|
+
keywords = ["udp", "relay", "network", "protocol", "magpie-bridge"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/moky/magpie-bridge"
|
|
34
|
+
Repository = "https://github.com/moky/magpie-bridge"
|
|
35
|
+
Issues = "https://github.com/moky/magpie-bridge/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Magpie Bridge protocol library.
|
|
2
|
+
|
|
3
|
+
Reliable UDP Relay Network wire protocol codec.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .constants import (
|
|
7
|
+
ACK_BIT,
|
|
8
|
+
HEAD_FIXED,
|
|
9
|
+
MAGIC,
|
|
10
|
+
MAX_BODY,
|
|
11
|
+
MSS,
|
|
12
|
+
TYPE_ACK_LARGE,
|
|
13
|
+
TYPE_ACK_MEDIUM,
|
|
14
|
+
TYPE_ACK_MINI,
|
|
15
|
+
TYPE_ACK_SMALL,
|
|
16
|
+
TYPE_LARGE,
|
|
17
|
+
TYPE_MEDIUM,
|
|
18
|
+
TYPE_MINI,
|
|
19
|
+
TYPE_SMALL,
|
|
20
|
+
)
|
|
21
|
+
from .errors import ProtocolError
|
|
22
|
+
from .protocol import Packet, build_ack, build_control, select_type, split_message
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"ACK_BIT",
|
|
29
|
+
"HEAD_FIXED",
|
|
30
|
+
"MAGIC",
|
|
31
|
+
"MAX_BODY",
|
|
32
|
+
"MSS",
|
|
33
|
+
"Packet",
|
|
34
|
+
"ProtocolError",
|
|
35
|
+
"TYPE_ACK_LARGE",
|
|
36
|
+
"TYPE_ACK_MEDIUM",
|
|
37
|
+
"TYPE_ACK_MINI",
|
|
38
|
+
"TYPE_ACK_SMALL",
|
|
39
|
+
"TYPE_LARGE",
|
|
40
|
+
"TYPE_MEDIUM",
|
|
41
|
+
"TYPE_MINI",
|
|
42
|
+
"TYPE_SMALL",
|
|
43
|
+
"build_ack",
|
|
44
|
+
"build_control",
|
|
45
|
+
"select_type",
|
|
46
|
+
"split_message",
|
|
47
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Protocol constants for Magpie Bridge."""
|
|
2
|
+
|
|
3
|
+
# Magic code: 'M', 'B', 0x00, 0x01 (Magpie Bridge v1)
|
|
4
|
+
MAGIC = b"MB\x00\x01"
|
|
5
|
+
|
|
6
|
+
# Fixed head size (octets), before the optional parameter area
|
|
7
|
+
HEAD_FIXED = 20
|
|
8
|
+
|
|
9
|
+
# Maximum UDP payload size (octets), dual-stack safe: 1280 - 40 - 8
|
|
10
|
+
MSS = 1232
|
|
11
|
+
|
|
12
|
+
# Maximum body size (octets): MSS - 32 (reserved head budget)
|
|
13
|
+
MAX_BODY = 1200
|
|
14
|
+
|
|
15
|
+
# High bit of Type marks an acknowledgement (reply) packet
|
|
16
|
+
ACK_BIT = 0x80
|
|
17
|
+
|
|
18
|
+
# Data packet types (low nibble = parameter width in octets)
|
|
19
|
+
TYPE_MINI = 0x00 # no parameter area, miniature single packet
|
|
20
|
+
TYPE_SMALL = 0x01 # 1-octet parameters, 1 <= K < 256
|
|
21
|
+
TYPE_MEDIUM = 0x02 # 2-octet parameters, 256 <= K < 65536
|
|
22
|
+
TYPE_LARGE = 0x04 # 4-octet parameters, 65536 <= K < 2^32
|
|
23
|
+
|
|
24
|
+
# Acknowledgement packet types
|
|
25
|
+
TYPE_ACK_MINI = 0x80
|
|
26
|
+
TYPE_ACK_SMALL = 0x81
|
|
27
|
+
TYPE_ACK_MEDIUM = 0x82
|
|
28
|
+
TYPE_ACK_LARGE = 0x84
|
|
29
|
+
|
|
30
|
+
# Mapping from low nibble to parameter width (octets)
|
|
31
|
+
PARAM_WIDTH = {0: 0, 1: 1, 2: 2, 4: 4}
|
|
32
|
+
|
|
33
|
+
# Mapping from low nibble to total head length (octets)
|
|
34
|
+
HEAD_LENGTH = {0: 20, 1: 22, 2: 24, 4: 28}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Magpie Bridge protocol packet codec.
|
|
2
|
+
|
|
3
|
+
Implements packing, unpacking, validation and helpers for the wire protocol
|
|
4
|
+
defined in docs/design-protocol.md.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import struct
|
|
8
|
+
from typing import List, Optional
|
|
9
|
+
|
|
10
|
+
from .constants import (
|
|
11
|
+
ACK_BIT,
|
|
12
|
+
HEAD_FIXED,
|
|
13
|
+
MAGIC,
|
|
14
|
+
MAX_BODY,
|
|
15
|
+
MSS,
|
|
16
|
+
PARAM_WIDTH,
|
|
17
|
+
TYPE_LARGE,
|
|
18
|
+
TYPE_MEDIUM,
|
|
19
|
+
TYPE_MINI,
|
|
20
|
+
TYPE_SMALL,
|
|
21
|
+
)
|
|
22
|
+
from .errors import ProtocolError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Packet:
|
|
26
|
+
"""A single Magpie Bridge protocol frame (head + body)."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
type: int = TYPE_MINI,
|
|
31
|
+
target_bid: int = 0,
|
|
32
|
+
source_bid: int = 0,
|
|
33
|
+
sn: int = 0,
|
|
34
|
+
index: Optional[int] = None,
|
|
35
|
+
count: Optional[int] = None,
|
|
36
|
+
body: bytes = b"",
|
|
37
|
+
) -> None:
|
|
38
|
+
self.type = type
|
|
39
|
+
self.target_bid = target_bid
|
|
40
|
+
self.source_bid = source_bid
|
|
41
|
+
self.sn = sn
|
|
42
|
+
self.index = index
|
|
43
|
+
self.count = count
|
|
44
|
+
self.body = bytes(body)
|
|
45
|
+
|
|
46
|
+
# -- derived properties ------------------------------------------------
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def param_width(self) -> int:
|
|
50
|
+
"""Width (octets) of each parameter (index, count)."""
|
|
51
|
+
low = self.type & 0x0F
|
|
52
|
+
if low not in PARAM_WIDTH:
|
|
53
|
+
raise ProtocolError(f"invalid type low nibble: {low}")
|
|
54
|
+
return PARAM_WIDTH[low]
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def head_length(self) -> int:
|
|
58
|
+
"""Total head length including the parameter area."""
|
|
59
|
+
return HEAD_FIXED + 2 * self.param_width
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_ack(self) -> bool:
|
|
63
|
+
"""True when the high bit of Type is set."""
|
|
64
|
+
return bool(self.type & ACK_BIT)
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def message_id(self) -> int:
|
|
68
|
+
"""Deduplication key combining sn and index.
|
|
69
|
+
|
|
70
|
+
For data packets the key is sn alone; for acknowledgement packets
|
|
71
|
+
the key is (sn << (type & 0x0F)) | index.
|
|
72
|
+
"""
|
|
73
|
+
if self.type & ACK_BIT == 0:
|
|
74
|
+
return self.sn
|
|
75
|
+
return (self.sn << (self.type & 0x0F)) | (self.index or 0)
|
|
76
|
+
|
|
77
|
+
# -- pack / unpack -----------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def pack(self) -> bytes:
|
|
80
|
+
"""Serialize the packet to bytes. Raises ProtocolError on invalid state."""
|
|
81
|
+
w = self.param_width
|
|
82
|
+
body = self.body
|
|
83
|
+
|
|
84
|
+
if len(body) > MAX_BODY:
|
|
85
|
+
raise ProtocolError(f"body too large: {len(body)} > {MAX_BODY}")
|
|
86
|
+
if self.head_length + len(body) > MSS:
|
|
87
|
+
raise ProtocolError(
|
|
88
|
+
f"packet too large: {self.head_length + len(body)} > {MSS}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if w:
|
|
92
|
+
if self.index is None or self.count is None:
|
|
93
|
+
raise ProtocolError("index and count are required for segmented packets")
|
|
94
|
+
if not (0 <= self.index < self.count):
|
|
95
|
+
raise ProtocolError(
|
|
96
|
+
f"invalid index/count: {self.index}/{self.count}"
|
|
97
|
+
)
|
|
98
|
+
max_val = (1 << (8 * w)) - 1
|
|
99
|
+
if self.index > max_val or self.count > max_val:
|
|
100
|
+
raise ProtocolError("index/count exceeds parameter width")
|
|
101
|
+
|
|
102
|
+
head = bytearray()
|
|
103
|
+
head += MAGIC
|
|
104
|
+
head.append(self.type & 0xFF)
|
|
105
|
+
head.append(self.head_length & 0xFF)
|
|
106
|
+
head += struct.pack(">H", len(body))
|
|
107
|
+
head += struct.pack(
|
|
108
|
+
">III",
|
|
109
|
+
self.target_bid & 0xFFFFFFFF,
|
|
110
|
+
self.source_bid & 0xFFFFFFFF,
|
|
111
|
+
self.sn & 0xFFFFFFFF,
|
|
112
|
+
)
|
|
113
|
+
if w:
|
|
114
|
+
head += self.index.to_bytes(w, "big")
|
|
115
|
+
head += self.count.to_bytes(w, "big")
|
|
116
|
+
return bytes(head) + body
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def unpack(cls, data: bytes) -> "Packet":
|
|
120
|
+
"""Parse bytes into a Packet. Raises ProtocolError on malformed input."""
|
|
121
|
+
if len(data) < HEAD_FIXED:
|
|
122
|
+
raise ProtocolError(f"packet too short: {len(data)} < {HEAD_FIXED}")
|
|
123
|
+
if data[0:4] != MAGIC:
|
|
124
|
+
raise ProtocolError("bad magic code")
|
|
125
|
+
|
|
126
|
+
type_ = data[4]
|
|
127
|
+
head_len = data[5]
|
|
128
|
+
body_len = struct.unpack(">H", data[6:8])[0]
|
|
129
|
+
|
|
130
|
+
if head_len + body_len != len(data):
|
|
131
|
+
raise ProtocolError(
|
|
132
|
+
f"length mismatch: head={head_len} body={body_len} total={len(data)}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
low = type_ & 0x0F
|
|
136
|
+
if low not in PARAM_WIDTH:
|
|
137
|
+
raise ProtocolError(f"invalid type low nibble: {low}")
|
|
138
|
+
w = PARAM_WIDTH[low]
|
|
139
|
+
expected_head = HEAD_FIXED + 2 * w
|
|
140
|
+
if head_len != expected_head:
|
|
141
|
+
raise ProtocolError(
|
|
142
|
+
f"head length mismatch: {head_len} != {expected_head}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
target, source, sn = struct.unpack(">III", data[8:20])
|
|
146
|
+
index = count = None
|
|
147
|
+
if w:
|
|
148
|
+
index = int.from_bytes(data[20:20 + w], "big")
|
|
149
|
+
count = int.from_bytes(data[20 + w:20 + 2 * w], "big")
|
|
150
|
+
|
|
151
|
+
body = data[head_len:head_len + body_len]
|
|
152
|
+
return cls(type_, target, source, sn, index, count, body)
|
|
153
|
+
|
|
154
|
+
def validate(self) -> bool:
|
|
155
|
+
"""Return True if the packet can be packed without error."""
|
|
156
|
+
try:
|
|
157
|
+
self.pack()
|
|
158
|
+
return True
|
|
159
|
+
except ProtocolError:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
return (
|
|
164
|
+
f"Packet(type=0x{self.type:02x}, target={self.target_bid}, "
|
|
165
|
+
f"source={self.source_bid}, sn={self.sn}, "
|
|
166
|
+
f"index={self.index}, count={self.count}, body={len(self.body)}B)"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# -- helpers ---------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def select_type(message_size: int) -> int:
|
|
174
|
+
"""Select the data packet Type for a message of size K (octets)."""
|
|
175
|
+
if message_size <= 0:
|
|
176
|
+
return TYPE_MINI
|
|
177
|
+
if message_size < 256:
|
|
178
|
+
return TYPE_SMALL
|
|
179
|
+
if message_size < 65536:
|
|
180
|
+
return TYPE_MEDIUM
|
|
181
|
+
return TYPE_LARGE
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def split_message(
|
|
185
|
+
data: bytes,
|
|
186
|
+
sn: int,
|
|
187
|
+
source_bid: int,
|
|
188
|
+
target_bid: int,
|
|
189
|
+
) -> List[Packet]:
|
|
190
|
+
"""Split a message into one or more packets (max MAX_BODY octets each)."""
|
|
191
|
+
if not data:
|
|
192
|
+
return [Packet(TYPE_MINI, target_bid, source_bid, sn, body=b"")]
|
|
193
|
+
|
|
194
|
+
type_ = select_type(len(data))
|
|
195
|
+
count = (len(data) + MAX_BODY - 1) // MAX_BODY
|
|
196
|
+
packets: List[Packet] = []
|
|
197
|
+
for i in range(count):
|
|
198
|
+
chunk = data[i * MAX_BODY:(i + 1) * MAX_BODY]
|
|
199
|
+
packets.append(
|
|
200
|
+
Packet(type_, target_bid, source_bid, sn, i, count, chunk)
|
|
201
|
+
)
|
|
202
|
+
return packets
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def build_ack(packet: Packet) -> Packet:
|
|
206
|
+
"""Build a data acknowledgement (body = b'OK') for the given packet."""
|
|
207
|
+
return Packet(
|
|
208
|
+
type=packet.type | ACK_BIT,
|
|
209
|
+
target_bid=packet.source_bid,
|
|
210
|
+
source_bid=packet.target_bid,
|
|
211
|
+
sn=packet.sn,
|
|
212
|
+
index=packet.index,
|
|
213
|
+
count=packet.count,
|
|
214
|
+
body=b"OK",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def build_control(
|
|
219
|
+
body: bytes,
|
|
220
|
+
sn: int,
|
|
221
|
+
source_bid: int = 0,
|
|
222
|
+
target_bid: int = 0,
|
|
223
|
+
) -> Packet:
|
|
224
|
+
"""Build a control packet (SYN/ACK/PING/PONG/FIN) using Type 0 (mini)."""
|
|
225
|
+
return Packet(TYPE_MINI, target_bid, source_bid, sn, body=body)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: magpie-bridge
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reliable UDP Relay Network protocol library
|
|
5
|
+
Author: Albert Moky
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/moky/magpie-bridge
|
|
8
|
+
Project-URL: Repository, https://github.com/moky/magpie-bridge
|
|
9
|
+
Project-URL: Issues, https://github.com/moky/magpie-bridge/issues
|
|
10
|
+
Keywords: udp,relay,network,protocol,magpie-bridge
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Internet
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Requires-Python: >=3.8
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
|
|
28
|
+
# magpie-bridge (Python)
|
|
29
|
+
|
|
30
|
+
[](https://opensource.org/licenses/MIT)
|
|
31
|
+
[](https://pypi.org/project/magpie-bridge/)
|
|
32
|
+
|
|
33
|
+
Reliable UDP Relay Network — wire protocol library for Python.
|
|
34
|
+
|
|
35
|
+
This package implements the Magpie Bridge protocol codec: packet packing,
|
|
36
|
+
unpacking, validation, message segmentation, and acknowledgement helpers.
|
|
37
|
+
|
|
38
|
+
> [中文文档](README.zh-CN.md)
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install magpie-bridge
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/moky/magpie-bridge.git
|
|
50
|
+
cd magpie-bridge/python
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from magpie_bridge import Packet, split_message, build_ack, TYPE_MINI
|
|
58
|
+
|
|
59
|
+
# Build and send a small packet
|
|
60
|
+
pkt = Packet(TYPE_MINI, target_bid=42, source_bid=7, sn=1, body=b"hello")
|
|
61
|
+
data = pkt.pack() # bytes ready for UDP send
|
|
62
|
+
|
|
63
|
+
# Parse a received packet
|
|
64
|
+
received = Packet.unpack(data)
|
|
65
|
+
print(received.body) # b"hello"
|
|
66
|
+
|
|
67
|
+
# Split a large message into segments
|
|
68
|
+
message = b"x" * 5000
|
|
69
|
+
segments = split_message(message, sn=2, source_bid=7, target_bid=42)
|
|
70
|
+
for seg in segments:
|
|
71
|
+
send(seg.pack())
|
|
72
|
+
|
|
73
|
+
# Build an acknowledgement
|
|
74
|
+
ack = build_ack(received)
|
|
75
|
+
send(ack.pack()) # body=b"OK", bids swapped
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Protocol Overview
|
|
79
|
+
|
|
80
|
+
| Field | Size | Description |
|
|
81
|
+
| ----------------- | ------ | ------------------------------------ |
|
|
82
|
+
| Magic Code | 4 B | `MB\x00\x01` (Magpie Bridge v1) |
|
|
83
|
+
| Type | 1 B | packet type + parameter width |
|
|
84
|
+
| Head Length | 1 B | 20 / 22 / 24 / 28 |
|
|
85
|
+
| Body Length | 2 B | body size in octets |
|
|
86
|
+
| Target Bridge ID | 4 B | destination bid |
|
|
87
|
+
| Source Bridge ID | 4 B | originator bid |
|
|
88
|
+
| Serial Number | 4 B | message identity |
|
|
89
|
+
| index / count | 0/2/4/8 B | segment ordinal / total segments |
|
|
90
|
+
| Body | 0..1200 B | application payload |
|
|
91
|
+
|
|
92
|
+
- Maximum UDP payload (MSS): **1232** octets (1280 - 40 - 8, dual-stack safe).
|
|
93
|
+
- Maximum body size: **1200** octets.
|
|
94
|
+
- See `../docs/design-protocol.md` for the full specification.
|
|
95
|
+
|
|
96
|
+
## API Reference
|
|
97
|
+
|
|
98
|
+
### `Packet`
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
Packet(type, target_bid, source_bid, sn, index=None, count=None, body=b"")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `pack() -> bytes` — serialize.
|
|
105
|
+
- `Packet.unpack(data: bytes) -> Packet` — parse.
|
|
106
|
+
- `validate() -> bool` — check whether the packet is well-formed.
|
|
107
|
+
- `message_id -> int` — deduplication key (`sn` for data packets,
|
|
108
|
+
`(sn << (type & 0x0F)) | index` for ACKs).
|
|
109
|
+
- `head_length`, `param_width`, `is_ack` — derived properties.
|
|
110
|
+
|
|
111
|
+
### Helpers
|
|
112
|
+
|
|
113
|
+
- `select_type(message_size) -> int` — choose Type by message size.
|
|
114
|
+
- `split_message(data, sn, source_bid, target_bid) -> list[Packet]`.
|
|
115
|
+
- `build_ack(packet) -> Packet` — build a `b"OK"` acknowledgement.
|
|
116
|
+
- `build_control(body, sn, source_bid=0, target_bid=0) -> Packet` — build a
|
|
117
|
+
control packet (SYN/ACK/PING/PONG/FIN) with Type 0.
|
|
118
|
+
|
|
119
|
+
### Exceptions
|
|
120
|
+
|
|
121
|
+
- `ProtocolError` — raised on invalid packets or parsing failures.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
cd python
|
|
127
|
+
pip install -e ".[dev]" # or: pip install pytest
|
|
128
|
+
pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/magpie_bridge/__init__.py
|
|
5
|
+
src/magpie_bridge/constants.py
|
|
6
|
+
src/magpie_bridge/errors.py
|
|
7
|
+
src/magpie_bridge/protocol.py
|
|
8
|
+
src/magpie_bridge.egg-info/PKG-INFO
|
|
9
|
+
src/magpie_bridge.egg-info/SOURCES.txt
|
|
10
|
+
src/magpie_bridge.egg-info/dependency_links.txt
|
|
11
|
+
src/magpie_bridge.egg-info/top_level.txt
|
|
12
|
+
tests/test_protocol.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
magpie_bridge
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Tests for the Magpie Bridge protocol codec."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from magpie_bridge import (
|
|
6
|
+
MAX_BODY,
|
|
7
|
+
MSS,
|
|
8
|
+
Packet,
|
|
9
|
+
ProtocolError,
|
|
10
|
+
TYPE_LARGE,
|
|
11
|
+
TYPE_MEDIUM,
|
|
12
|
+
TYPE_MINI,
|
|
13
|
+
TYPE_SMALL,
|
|
14
|
+
build_ack,
|
|
15
|
+
build_control,
|
|
16
|
+
select_type,
|
|
17
|
+
split_message,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestPacketPackUnpack:
|
|
22
|
+
def test_mini_packet_roundtrip(self):
|
|
23
|
+
pkt = Packet(TYPE_MINI, 1, 2, 100, body=b"hello")
|
|
24
|
+
data = pkt.pack()
|
|
25
|
+
assert data[0:4] == b"MB\x00\x01"
|
|
26
|
+
assert len(data) == 20 + 5
|
|
27
|
+
restored = Packet.unpack(data)
|
|
28
|
+
assert restored.type == TYPE_MINI
|
|
29
|
+
assert restored.target_bid == 1
|
|
30
|
+
assert restored.source_bid == 2
|
|
31
|
+
assert restored.sn == 100
|
|
32
|
+
assert restored.index is None
|
|
33
|
+
assert restored.count is None
|
|
34
|
+
assert restored.body == b"hello"
|
|
35
|
+
|
|
36
|
+
def test_segmented_packet_roundtrip(self):
|
|
37
|
+
pkt = Packet(TYPE_SMALL, 10, 20, 5, index=0, count=3, body=b"abc")
|
|
38
|
+
data = pkt.pack()
|
|
39
|
+
assert len(data) == 22 + 3
|
|
40
|
+
restored = Packet.unpack(data)
|
|
41
|
+
assert restored.type == TYPE_SMALL
|
|
42
|
+
assert restored.head_length == 22
|
|
43
|
+
assert restored.index == 0
|
|
44
|
+
assert restored.count == 3
|
|
45
|
+
assert restored.body == b"abc"
|
|
46
|
+
|
|
47
|
+
def test_medium_and_large_widths(self):
|
|
48
|
+
for type_, width in [(TYPE_MEDIUM, 2), (TYPE_LARGE, 4)]:
|
|
49
|
+
pkt = Packet(type_, 1, 2, 7, index=5, count=10, body=b"x")
|
|
50
|
+
data = pkt.pack()
|
|
51
|
+
assert len(data) == 20 + 2 * width + 1
|
|
52
|
+
restored = Packet.unpack(data)
|
|
53
|
+
assert restored.index == 5
|
|
54
|
+
assert restored.count == 10
|
|
55
|
+
|
|
56
|
+
def test_bad_magic_raises(self):
|
|
57
|
+
data = b"XXXX" + b"\x00" * 16
|
|
58
|
+
with pytest.raises(ProtocolError):
|
|
59
|
+
Packet.unpack(data)
|
|
60
|
+
|
|
61
|
+
def test_too_short_raises(self):
|
|
62
|
+
with pytest.raises(ProtocolError):
|
|
63
|
+
Packet.unpack(b"MB\x00\x01")
|
|
64
|
+
|
|
65
|
+
def test_length_mismatch_raises(self):
|
|
66
|
+
pkt = Packet(TYPE_MINI, 1, 2, 1, body=b"ok")
|
|
67
|
+
data = pkt.pack()
|
|
68
|
+
with pytest.raises(ProtocolError):
|
|
69
|
+
Packet.unpack(data + b"extra")
|
|
70
|
+
|
|
71
|
+
def test_body_too_large_raises(self):
|
|
72
|
+
pkt = Packet(TYPE_MINI, 1, 2, 1, body=b"x" * (MAX_BODY + 1))
|
|
73
|
+
with pytest.raises(ProtocolError):
|
|
74
|
+
pkt.pack()
|
|
75
|
+
|
|
76
|
+
def test_invalid_index_raises(self):
|
|
77
|
+
pkt = Packet(TYPE_SMALL, 1, 2, 1, index=5, count=3, body=b"x")
|
|
78
|
+
with pytest.raises(ProtocolError):
|
|
79
|
+
pkt.pack()
|
|
80
|
+
|
|
81
|
+
def test_validate(self):
|
|
82
|
+
good = Packet(TYPE_MINI, 1, 2, 1, body=b"ok")
|
|
83
|
+
bad = Packet(TYPE_SMALL, 1, 2, 1, index=5, count=3, body=b"x")
|
|
84
|
+
assert good.validate() is True
|
|
85
|
+
assert bad.validate() is False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestHelpers:
|
|
89
|
+
def test_select_type(self):
|
|
90
|
+
assert select_type(0) == TYPE_MINI
|
|
91
|
+
assert select_type(100) == TYPE_SMALL
|
|
92
|
+
assert select_type(255) == TYPE_SMALL
|
|
93
|
+
assert select_type(256) == TYPE_MEDIUM
|
|
94
|
+
assert select_type(65535) == TYPE_MEDIUM
|
|
95
|
+
assert select_type(65536) == TYPE_LARGE
|
|
96
|
+
|
|
97
|
+
def test_split_message_single(self):
|
|
98
|
+
data = b"hello world"
|
|
99
|
+
packets = split_message(data, 42, 1, 2)
|
|
100
|
+
assert len(packets) == 1
|
|
101
|
+
assert packets[0].count == 1
|
|
102
|
+
assert packets[0].index == 0
|
|
103
|
+
assert packets[0].body == data
|
|
104
|
+
|
|
105
|
+
def test_split_message_empty(self):
|
|
106
|
+
packets = split_message(b"", 1, 1, 2)
|
|
107
|
+
assert len(packets) == 1
|
|
108
|
+
assert packets[0].type == TYPE_MINI
|
|
109
|
+
assert packets[0].body == b""
|
|
110
|
+
|
|
111
|
+
def test_split_message_multiple(self):
|
|
112
|
+
data = b"x" * (MAX_BODY * 2 + 10)
|
|
113
|
+
packets = split_message(data, 7, 1, 2)
|
|
114
|
+
assert len(packets) == 3
|
|
115
|
+
assert packets[0].count == 3
|
|
116
|
+
assert packets[-1].body == b"x" * 10
|
|
117
|
+
for i, pkt in enumerate(packets):
|
|
118
|
+
assert pkt.index == i
|
|
119
|
+
assert pkt.sn == 7
|
|
120
|
+
|
|
121
|
+
def test_build_ack(self):
|
|
122
|
+
pkt = Packet(TYPE_SMALL, 10, 20, 5, index=1, count=3, body=b"abc")
|
|
123
|
+
ack = build_ack(pkt)
|
|
124
|
+
assert ack.type == 0x81
|
|
125
|
+
assert ack.target_bid == 20
|
|
126
|
+
assert ack.source_bid == 10
|
|
127
|
+
assert ack.sn == 5
|
|
128
|
+
assert ack.index == 1
|
|
129
|
+
assert ack.count == 3
|
|
130
|
+
assert ack.body == b"OK"
|
|
131
|
+
|
|
132
|
+
def test_build_control(self):
|
|
133
|
+
pkt = build_control(b"SYN", 1, source_bid=0, target_bid=0)
|
|
134
|
+
assert pkt.type == TYPE_MINI
|
|
135
|
+
assert pkt.body == b"SYN"
|
|
136
|
+
assert pkt.head_length == 20
|
|
137
|
+
|
|
138
|
+
def test_message_id_data_packet(self):
|
|
139
|
+
pkt = Packet(TYPE_SMALL, 1, 2, 100, index=3, count=5, body=b"x")
|
|
140
|
+
assert pkt.message_id == 100
|
|
141
|
+
|
|
142
|
+
def test_message_id_ack_packet(self):
|
|
143
|
+
pkt = Packet(0x81, 1, 2, 100, index=3, count=5, body=b"OK")
|
|
144
|
+
assert pkt.message_id == (100 << 1) | 3
|
|
145
|
+
|
|
146
|
+
def test_max_body_fits_mss(self):
|
|
147
|
+
body = b"y" * MAX_BODY
|
|
148
|
+
pkt = Packet(TYPE_LARGE, 1, 2, 1, index=0, count=1, body=body)
|
|
149
|
+
data = pkt.pack()
|
|
150
|
+
assert len(data) <= MSS
|