kkprotocol 0.1.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.
- kkprotocol/__init__.py +105 -0
- kkprotocol/builder.py +1005 -0
- kkprotocol/codec.py +804 -0
- kkprotocol/compiler.py +182 -0
- kkprotocol/contrib/__init__.py +8 -0
- kkprotocol/contrib/artnet.py +55 -0
- kkprotocol/contrib/bacnet_mstp.py +110 -0
- kkprotocol/contrib/cctalk.py +53 -0
- kkprotocol/contrib/cjt188.py +57 -0
- kkprotocol/contrib/dlt645.py +81 -0
- kkprotocol/contrib/hdlc.py +47 -0
- kkprotocol/contrib/mbus.py +61 -0
- kkprotocol/contrib/modbus_ascii.py +59 -0
- kkprotocol/contrib/nmea.py +59 -0
- kkprotocol/contrib/slip.py +38 -0
- kkprotocol/contrib/xmodem.py +74 -0
- kkprotocol/crc.py +103 -0
- kkprotocol/errors.py +48 -0
- kkprotocol/example_device.py +186 -0
- kkprotocol/example_modbus_rtu.py +176 -0
- kkprotocol/example_modbus_rtu_multi.py +391 -0
- kkprotocol/fields.py +167 -0
- kkprotocol/frame.py +114 -0
- kkprotocol/frame_codec.py +50 -0
- kkprotocol/framing.py +190 -0
- kkprotocol/length.py +151 -0
- kkprotocol/pack.py +222 -0
- kkprotocol/py.typed +0 -0
- kkprotocol/spec.py +494 -0
- kkprotocol-0.1.0.dist-info/METADATA +1015 -0
- kkprotocol-0.1.0.dist-info/RECORD +34 -0
- kkprotocol-0.1.0.dist-info/WHEEL +5 -0
- kkprotocol-0.1.0.dist-info/licenses/LICENSE +21 -0
- kkprotocol-0.1.0.dist-info/top_level.txt +1 -0
kkprotocol/fields.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""字段规则:用字段类代替字典,获得完整的 IDE 智能提示。
|
|
3
|
+
|
|
4
|
+
三种写法都支持(推荐第一种):
|
|
5
|
+
|
|
6
|
+
1. 工厂方法,最短且有类型提示::
|
|
7
|
+
|
|
8
|
+
Field.u8("header", fixed=0x55)
|
|
9
|
+
Field.u16("ser")
|
|
10
|
+
Field.raw("data", "{len.val}-10")
|
|
11
|
+
Field.checksum("crc", 2, group="{len}{ver}{data}")
|
|
12
|
+
|
|
13
|
+
2. 完整构造::
|
|
14
|
+
|
|
15
|
+
Field(key="header", length=1, fmt="B", fixed=0x55)
|
|
16
|
+
|
|
17
|
+
3. 旧式字典(兼容既有代码)::
|
|
18
|
+
|
|
19
|
+
{"key": "header", "len": 1, "fmt": "B", "fixed": 0x55}
|
|
20
|
+
|
|
21
|
+
字段属性说明:
|
|
22
|
+
|
|
23
|
+
``key``
|
|
24
|
+
字段名,帧内唯一。
|
|
25
|
+
``length``
|
|
26
|
+
长度。取值:``int``、``"10"``、表达式 ``"{len.val}-10"``、
|
|
27
|
+
回调 ``callable(fields) -> int``,或 ``None`` 表示“取到帧尾为止”
|
|
28
|
+
(只能用于分隔符定界,长度定界下编译期就会报错)。
|
|
29
|
+
表达式里 ``{字段.属性}`` 的属性只能是 ``val``(数值)、``len``/``bytes``
|
|
30
|
+
(该字段实际字节数)。
|
|
31
|
+
``fmt``
|
|
32
|
+
``struct`` 格式串,必须只解析出一个值;为空表示字段值就是原始 ``bytes``。
|
|
33
|
+
多字节数值请显式带字节序前缀(``!``/``<``),避免原生对齐差异。
|
|
34
|
+
``fixed``
|
|
35
|
+
期望常量。``int`` 与解析值比较,``bytes`` 与原始字节比较,``None`` 表示不校验。
|
|
36
|
+
``group``
|
|
37
|
+
校验字段专用:参与校验的字段模板,如 ``"{len}{ver}{data}"``。
|
|
38
|
+
给出了 ``group`` 就自动视为校验字段,``crc`` 可省略;省略 ``group`` 时
|
|
39
|
+
校验它前面所有字段。
|
|
40
|
+
``crc``
|
|
41
|
+
显式标记为校验字段(配合 ``crc_imp`` 使用)。
|
|
42
|
+
``desc``
|
|
43
|
+
仅注释用,方便阅读规则表。
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
50
|
+
|
|
51
|
+
__all__ = ["Field"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class Field:
|
|
56
|
+
"""一个字段的声明式规则。"""
|
|
57
|
+
|
|
58
|
+
key: str
|
|
59
|
+
length: Optional[Union[int, str, Callable[[Dict[str, Any]], int]]] = 0
|
|
60
|
+
fmt: str = ""
|
|
61
|
+
fixed: Any = None
|
|
62
|
+
group: Optional[str] = None
|
|
63
|
+
crc: bool = False
|
|
64
|
+
desc: str = ""
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------ 工厂
|
|
67
|
+
@classmethod
|
|
68
|
+
def u8(cls, key: str, *, fixed: Optional[int] = None, desc: str = "") -> "Field":
|
|
69
|
+
"""1 字节无符号整数。"""
|
|
70
|
+
return cls(key=key, length=1, fmt="B", fixed=fixed, desc=desc)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def u16(
|
|
74
|
+
cls,
|
|
75
|
+
key: str,
|
|
76
|
+
*,
|
|
77
|
+
fixed: Optional[int] = None,
|
|
78
|
+
little_endian: bool = False,
|
|
79
|
+
desc: str = "",
|
|
80
|
+
) -> "Field":
|
|
81
|
+
"""2 字节无符号整数,默认大端。"""
|
|
82
|
+
return cls(
|
|
83
|
+
key=key, length=2, fmt="<H" if little_endian else "!H", fixed=fixed, desc=desc
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def u32(
|
|
88
|
+
cls,
|
|
89
|
+
key: str,
|
|
90
|
+
*,
|
|
91
|
+
fixed: Optional[int] = None,
|
|
92
|
+
little_endian: bool = False,
|
|
93
|
+
desc: str = "",
|
|
94
|
+
) -> "Field":
|
|
95
|
+
"""4 字节无符号整数,默认大端。"""
|
|
96
|
+
return cls(
|
|
97
|
+
key=key, length=4, fmt="<I" if little_endian else "!I", fixed=fixed, desc=desc
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def u64(
|
|
102
|
+
cls,
|
|
103
|
+
key: str,
|
|
104
|
+
*,
|
|
105
|
+
fixed: Optional[int] = None,
|
|
106
|
+
little_endian: bool = False,
|
|
107
|
+
desc: str = "",
|
|
108
|
+
) -> "Field":
|
|
109
|
+
"""8 字节无符号整数,默认大端。"""
|
|
110
|
+
return cls(
|
|
111
|
+
key=key, length=8, fmt="<Q" if little_endian else "!Q", fixed=fixed, desc=desc
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def raw(
|
|
116
|
+
cls,
|
|
117
|
+
key: str,
|
|
118
|
+
length: Optional[Union[int, str, Callable[[Dict[str, Any]], int]]] = 0,
|
|
119
|
+
*,
|
|
120
|
+
fixed: Optional[bytes] = None,
|
|
121
|
+
desc: str = "",
|
|
122
|
+
) -> "Field":
|
|
123
|
+
"""原始字节字段(不做 struct 解包,值即 bytes)。"""
|
|
124
|
+
return cls(key=key, length=length, fmt="", fixed=fixed, desc=desc)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def rest(cls, key: str, *, desc: str = "") -> "Field":
|
|
128
|
+
"""“取到帧尾为止”的字段(仅分隔符定界可用,行协议常用)。"""
|
|
129
|
+
return cls(key=key, length=None, fmt="", desc=desc)
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def checksum(
|
|
133
|
+
cls,
|
|
134
|
+
key: str,
|
|
135
|
+
length: Union[int, str] = 2,
|
|
136
|
+
*,
|
|
137
|
+
group: Optional[str] = None,
|
|
138
|
+
fmt: Optional[str] = None,
|
|
139
|
+
desc: str = "",
|
|
140
|
+
) -> "Field":
|
|
141
|
+
"""校验字段(CRC/累加和等),校验算法由 ``crc_imp`` 提供。
|
|
142
|
+
|
|
143
|
+
``fmt`` 省略时按 ``length`` 自动选(1 字节 ``B``、2 字节 ``!H``、
|
|
144
|
+
4 字节 ``!I``、8 字节 ``!Q``,其他情况按原始字节处理)。
|
|
145
|
+
"""
|
|
146
|
+
if fmt is None:
|
|
147
|
+
fmt = {1: "B", 2: "!H", 4: "!I", 8: "!Q"}.get(
|
|
148
|
+
length if isinstance(length, int) else -1, ""
|
|
149
|
+
)
|
|
150
|
+
return cls(key=key, length=length, fmt=fmt, group=group, crc=True, desc=desc)
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------------ 兼容
|
|
153
|
+
@classmethod
|
|
154
|
+
def from_dict(cls, rule: Dict[str, Any]) -> "Field":
|
|
155
|
+
"""把旧式字典规则转成字段类(内部编译也走这里)。"""
|
|
156
|
+
if not isinstance(rule, dict):
|
|
157
|
+
raise TypeError("规则必须是 dict 或 Field,实际 {}".format(type(rule).__name__))
|
|
158
|
+
spec_len = rule["len"] if "len" in rule else rule.get("length", 0)
|
|
159
|
+
return cls(
|
|
160
|
+
key=rule.get("key"),
|
|
161
|
+
length=spec_len,
|
|
162
|
+
fmt=rule.get("fmt") or "",
|
|
163
|
+
fixed=rule.get("fixed"),
|
|
164
|
+
group=rule.get("group"),
|
|
165
|
+
crc=bool(rule.get("crc", False)),
|
|
166
|
+
desc=rule.get("desc", ""),
|
|
167
|
+
)
|
kkprotocol/frame.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""解析结果:字段值与整帧。"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
__all__ = ["FieldValue", "Frame"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class FieldValue:
|
|
14
|
+
"""单个字段的解析结果。
|
|
15
|
+
|
|
16
|
+
``raw`` 是字段原始字节(定界模式下为**反转义后**的字节),``val`` 是 ``fmt``
|
|
17
|
+
解出的值;``fmt`` 为空时 ``val`` 就是 ``raw`` 本身。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
key: str
|
|
21
|
+
raw: bytes
|
|
22
|
+
val: Any
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def length(self) -> int:
|
|
26
|
+
"""该字段实际占用的字节数。"""
|
|
27
|
+
return len(self.raw)
|
|
28
|
+
|
|
29
|
+
def __len__(self) -> int:
|
|
30
|
+
return len(self.raw)
|
|
31
|
+
|
|
32
|
+
def __int__(self) -> int:
|
|
33
|
+
if not isinstance(self.val, int):
|
|
34
|
+
raise TypeError("字段 {} 的值不是整数(fmt 为空时值为 bytes)".format(self.key))
|
|
35
|
+
return int(self.val)
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str: # pragma: no cover - 调试展示
|
|
38
|
+
return "FieldValue(key={!r}, len={}, val={!r})".format(self.key, len(self.raw), self.val)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Frame:
|
|
43
|
+
"""一帧完整数据的解析结果。
|
|
44
|
+
|
|
45
|
+
* ``raw``:解析器交给规则的字节(定界模式下已去掉定界符并完成反转义)
|
|
46
|
+
* ``wire``:这一帧在流中实际占用的字节(含定界符与转义字节),便于抓包比对
|
|
47
|
+
* ``framing``:本帧使用的定界方式,``"length"`` 或 ``"delimiter"``
|
|
48
|
+
* ``device_id``:该帧携带的设备标识(由 ``device_field`` 指定字段得出),
|
|
49
|
+
未配置 ``device_field`` 时为 ``None``
|
|
50
|
+
* ``device_mismatch``:与已绑定设备号不一致(仅在 ``enforce_device_id=False`` 时还会返回)
|
|
51
|
+
* ``learned_device``:本帧是否触发了设备号自动学习
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
raw: bytes
|
|
55
|
+
fields: Dict[str, FieldValue] = field(default_factory=dict)
|
|
56
|
+
wire: Optional[bytes] = None
|
|
57
|
+
framing: str = "length"
|
|
58
|
+
device_id: Any = None
|
|
59
|
+
device_mismatch: bool = False
|
|
60
|
+
learned_device: bool = False
|
|
61
|
+
|
|
62
|
+
def __getitem__(self, key: str) -> FieldValue:
|
|
63
|
+
return self.fields[key]
|
|
64
|
+
|
|
65
|
+
def __contains__(self, key: str) -> bool:
|
|
66
|
+
return key in self.fields
|
|
67
|
+
|
|
68
|
+
def __len__(self) -> int:
|
|
69
|
+
return len(self.raw)
|
|
70
|
+
|
|
71
|
+
def keys(self):
|
|
72
|
+
return self.fields.keys()
|
|
73
|
+
|
|
74
|
+
def value(self, key: str, default: Any = None) -> Any:
|
|
75
|
+
"""取字段值;字段不存在时返回 default。"""
|
|
76
|
+
fv = self.fields.get(key)
|
|
77
|
+
return default if fv is None else fv.val
|
|
78
|
+
|
|
79
|
+
def raw_of(self, key: str) -> bytes:
|
|
80
|
+
"""取字段原始字节。"""
|
|
81
|
+
return self.fields[key].raw
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------ 整帧字节
|
|
84
|
+
def to_bytes(self, wire: bool = True) -> bytes:
|
|
85
|
+
"""整帧字节。
|
|
86
|
+
|
|
87
|
+
``wire=True``(默认)返回这一帧在流里实际占用的字节:长度定界就是
|
|
88
|
+
``55 AA ...`` 的原文,分隔符定界还包含首尾标志位和转义字节。
|
|
89
|
+
``wire=False`` 返回交给规则解析的净帧字节(已去定界符、已反转义)。
|
|
90
|
+
"""
|
|
91
|
+
if wire and self.wire is not None:
|
|
92
|
+
return self.wire
|
|
93
|
+
return self.raw
|
|
94
|
+
|
|
95
|
+
def to_hex(self, sep: str = " ", upper: bool = True, wire: bool = True) -> str:
|
|
96
|
+
"""整帧的十六进制字符串,默认形如 ``"55 AA 00 59 02 00 ..."``。
|
|
97
|
+
|
|
98
|
+
``sep=""`` 得到 ``"55AA00590200..."``,``upper=False`` 得到小写。
|
|
99
|
+
"""
|
|
100
|
+
data = self.to_bytes(wire)
|
|
101
|
+
text = data.hex() if not sep else data.hex(sep)
|
|
102
|
+
return text.upper() if upper else text
|
|
103
|
+
|
|
104
|
+
def __bytes__(self) -> bytes:
|
|
105
|
+
return self.to_bytes()
|
|
106
|
+
|
|
107
|
+
def as_dict(self) -> Dict[str, Any]:
|
|
108
|
+
"""{字段名: 值} 的扁平字典,便于直接消费。"""
|
|
109
|
+
return {k: v.val for k, v in self.fields.items()}
|
|
110
|
+
|
|
111
|
+
def __repr__(self) -> str: # pragma: no cover - 调试展示
|
|
112
|
+
return "Frame(framing={}, len={}, fields={})".format(
|
|
113
|
+
self.framing, len(self.raw), list(self.fields)
|
|
114
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""兼容旧导入路径(v1 的 ``kkprotocol.frame_codec``)。
|
|
3
|
+
|
|
4
|
+
新代码请直接 ``from kkprotocol import ...``;这个模块只做转发,
|
|
5
|
+
并把旧类名 ``AbstractProtocolBaseFilter`` 映射到 ``AbstractFrameCodec``。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .codec import AbstractFrameCodec, CodecStats
|
|
11
|
+
from .compiler import CompiledField, compile_rules
|
|
12
|
+
from .errors import FrameCodecError, FrameRuleError
|
|
13
|
+
from .fields import Field
|
|
14
|
+
from .frame import FieldValue, Frame
|
|
15
|
+
from .framing import (
|
|
16
|
+
AUTO_FRAMING,
|
|
17
|
+
DELIMITER_FRAMING,
|
|
18
|
+
LENGTH_FRAMING,
|
|
19
|
+
DelimiterFraming,
|
|
20
|
+
escape_bytes,
|
|
21
|
+
find_end_delimiter,
|
|
22
|
+
unescape_bytes,
|
|
23
|
+
wrap_frame,
|
|
24
|
+
)
|
|
25
|
+
from .length import LengthExpr
|
|
26
|
+
|
|
27
|
+
# 旧类名兼容
|
|
28
|
+
AbstractProtocolBaseFilter = AbstractFrameCodec
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AbstractFrameCodec",
|
|
32
|
+
"AbstractProtocolBaseFilter",
|
|
33
|
+
"AUTO_FRAMING",
|
|
34
|
+
"CodecStats",
|
|
35
|
+
"CompiledField",
|
|
36
|
+
"DELIMITER_FRAMING",
|
|
37
|
+
"DelimiterFraming",
|
|
38
|
+
"Field",
|
|
39
|
+
"FieldValue",
|
|
40
|
+
"Frame",
|
|
41
|
+
"FrameCodecError",
|
|
42
|
+
"FrameRuleError",
|
|
43
|
+
"LENGTH_FRAMING",
|
|
44
|
+
"LengthExpr",
|
|
45
|
+
"compile_rules",
|
|
46
|
+
"escape_bytes",
|
|
47
|
+
"find_end_delimiter",
|
|
48
|
+
"unescape_bytes",
|
|
49
|
+
"wrap_frame",
|
|
50
|
+
]
|
kkprotocol/framing.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""定界方式:长度前缀定界 与 分隔符定界(含字节转义 / 透明传输)。
|
|
3
|
+
|
|
4
|
+
两种定界的本质区别:
|
|
5
|
+
|
|
6
|
+
* ``length``(长度定界):先读长度字段,再按长度取字节。净荷里出现任何字节都
|
|
7
|
+
无害,**不需要转义**;但长度字段一旦被破坏,整条流会跟着错位。
|
|
8
|
+
* ``delimiter``(分隔符定界):按特殊字节(如 ``0x7E``)切帧。净荷里若出现同样的
|
|
9
|
+
字节会误判帧边界,因此**必须转义**;此时长度字段只能作为“参考值/校验项”,
|
|
10
|
+
不能当作切帧依据(转义会让线上长度和净荷长度不一致)。
|
|
11
|
+
|
|
12
|
+
``auto`` 模式会先用长度定界尝试解析,失败再试分隔符定界,命中一次后锁定策略。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import Dict, Mapping, Optional, Union
|
|
19
|
+
|
|
20
|
+
from .errors import FrameRuleError
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"LENGTH_FRAMING",
|
|
24
|
+
"DELIMITER_FRAMING",
|
|
25
|
+
"AUTO_FRAMING",
|
|
26
|
+
"FRAMING_MODES",
|
|
27
|
+
"DelimiterFraming",
|
|
28
|
+
"escape_bytes",
|
|
29
|
+
"unescape_bytes",
|
|
30
|
+
"find_end_delimiter",
|
|
31
|
+
"wrap_frame",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
LENGTH_FRAMING = "length"
|
|
35
|
+
DELIMITER_FRAMING = "delimiter"
|
|
36
|
+
AUTO_FRAMING = "auto"
|
|
37
|
+
FRAMING_MODES = frozenset({LENGTH_FRAMING, DELIMITER_FRAMING, AUTO_FRAMING})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _check_byte(name: str, value: int) -> None:
|
|
41
|
+
if isinstance(value, bool) or not isinstance(value, int) or not 0 <= value <= 0xFF:
|
|
42
|
+
raise FrameRuleError("{} 必须是 0..255 的整数,实际 {!r}".format(name, value))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class DelimiterFraming:
|
|
47
|
+
"""分隔符定界配置。
|
|
48
|
+
|
|
49
|
+
:param end: 帧尾定界字节(必须)
|
|
50
|
+
:param start: 帧头定界字节,可省略。``start == end`` 表示首尾共用一个标志
|
|
51
|
+
(PPP/HDLC 风格);两者都配置时,帧内容取两者之间。
|
|
52
|
+
:param escape: 转义字节;``None`` 表示不做转义
|
|
53
|
+
:param escape_xor: 转义字节后一字节与原始字节的异或值(PPP/HDLC 为 0x20)
|
|
54
|
+
:param escape_map: 显式映射(原始字节 -> 转义后第二字节),用于 SLIP 这类
|
|
55
|
+
非异或方案;未命中的字节仍按 ``escape_xor`` 还原
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
end: int
|
|
59
|
+
start: Optional[int] = None
|
|
60
|
+
escape: Optional[int] = None
|
|
61
|
+
escape_xor: int = 0x20
|
|
62
|
+
escape_map: Optional[Mapping[int, int]] = None
|
|
63
|
+
|
|
64
|
+
_map: Dict[int, int] = field(init=False, repr=False, compare=False, default_factory=dict)
|
|
65
|
+
_reverse: Dict[int, int] = field(init=False, repr=False, compare=False, default_factory=dict)
|
|
66
|
+
_targets: frozenset = field(init=False, repr=False, compare=False, default_factory=frozenset)
|
|
67
|
+
|
|
68
|
+
def __post_init__(self) -> None:
|
|
69
|
+
_check_byte("end", self.end)
|
|
70
|
+
if self.start is not None:
|
|
71
|
+
_check_byte("start", self.start)
|
|
72
|
+
if self.escape is not None:
|
|
73
|
+
_check_byte("escape", self.escape)
|
|
74
|
+
_check_byte("escape_xor", self.escape_xor)
|
|
75
|
+
|
|
76
|
+
if self.escape is not None and self.escape in (self.end, self.start):
|
|
77
|
+
raise FrameRuleError("转义字节不能与定界符相同(会造成歧义)")
|
|
78
|
+
|
|
79
|
+
forward: Dict[int, int] = {}
|
|
80
|
+
reverse: Dict[int, int] = {}
|
|
81
|
+
if self.escape_map:
|
|
82
|
+
for original, second in self.escape_map.items():
|
|
83
|
+
_check_byte("escape_map 键", original)
|
|
84
|
+
_check_byte("escape_map 值", second)
|
|
85
|
+
if second in reverse:
|
|
86
|
+
raise FrameRuleError("escape_map 中第二字节 {!r} 重复".format(second))
|
|
87
|
+
forward[original] = second
|
|
88
|
+
reverse[second] = original
|
|
89
|
+
|
|
90
|
+
targets = set()
|
|
91
|
+
if self.escape is not None:
|
|
92
|
+
for value in (self.start, self.end, self.escape):
|
|
93
|
+
if value is not None:
|
|
94
|
+
targets.add(value)
|
|
95
|
+
for target in list(targets):
|
|
96
|
+
second = reverse.get(target, target ^ self.escape_xor)
|
|
97
|
+
if second in targets and reverse.get(second) != target:
|
|
98
|
+
raise FrameRuleError(
|
|
99
|
+
"转义后字节 {:#04x} 与定界/转义字节冲突,请显式提供 escape_map".format(second)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
object.__setattr__(self, "_map", forward)
|
|
103
|
+
object.__setattr__(self, "_reverse", reverse)
|
|
104
|
+
object.__setattr__(self, "_targets", frozenset(targets))
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------ 查询
|
|
107
|
+
@property
|
|
108
|
+
def targets(self) -> frozenset:
|
|
109
|
+
"""必须被转义的字节集合(帧头/帧尾/转义字节自身)。"""
|
|
110
|
+
return self._targets
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def reversed_map(self) -> Dict[int, int]:
|
|
114
|
+
"""转义后第二字节 -> 原始字节。"""
|
|
115
|
+
return self._reverse
|
|
116
|
+
|
|
117
|
+
def second_byte(self, original: int) -> int:
|
|
118
|
+
"""原始字节转义后跟在转义字节后面的那一字节。"""
|
|
119
|
+
return self._map.get(original, original ^ self.escape_xor)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def escape_bytes(data: Union[bytes, bytearray], spec: DelimiterFraming) -> bytes:
|
|
123
|
+
"""按配置转义(发送方向)。"""
|
|
124
|
+
if spec.escape is None:
|
|
125
|
+
return bytes(data)
|
|
126
|
+
targets = spec.targets
|
|
127
|
+
escape = spec.escape
|
|
128
|
+
out = bytearray()
|
|
129
|
+
for byte in data:
|
|
130
|
+
if byte in targets:
|
|
131
|
+
out.append(escape)
|
|
132
|
+
out.append(spec.second_byte(byte))
|
|
133
|
+
else:
|
|
134
|
+
out.append(byte)
|
|
135
|
+
return bytes(out)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def unescape_bytes(data: Union[bytes, bytearray], spec: DelimiterFraming) -> bytes:
|
|
139
|
+
"""按配置反转义。遇到悬空转义字节(帧被截断)或非法转义序列抛 ``ValueError``。"""
|
|
140
|
+
if spec.escape is None:
|
|
141
|
+
return bytes(data)
|
|
142
|
+
escape = spec.escape
|
|
143
|
+
reverse = spec.reversed_map
|
|
144
|
+
xor = spec.escape_xor
|
|
145
|
+
out = bytearray()
|
|
146
|
+
index = 0
|
|
147
|
+
size = len(data)
|
|
148
|
+
while index < size:
|
|
149
|
+
byte = data[index]
|
|
150
|
+
if byte == escape:
|
|
151
|
+
if index + 1 >= size:
|
|
152
|
+
raise ValueError("帧以转义字节 0x{:02X} 结尾,数据被截断".format(escape))
|
|
153
|
+
second = data[index + 1]
|
|
154
|
+
original = reverse.get(second)
|
|
155
|
+
out.append(second ^ xor if original is None else original)
|
|
156
|
+
index += 2
|
|
157
|
+
else:
|
|
158
|
+
out.append(byte)
|
|
159
|
+
index += 1
|
|
160
|
+
return bytes(out)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def find_end_delimiter(
|
|
164
|
+
buf: Union[bytes, bytearray], start: int, spec: DelimiterFraming
|
|
165
|
+
) -> Optional[int]:
|
|
166
|
+
"""从 start 起找第一个**未被转义**的帧尾定界字节,返回其下标。"""
|
|
167
|
+
end = spec.end
|
|
168
|
+
escape = spec.escape
|
|
169
|
+
index = start
|
|
170
|
+
size = len(buf)
|
|
171
|
+
if escape is None:
|
|
172
|
+
found = bytes(buf[index:]).find(bytes([end]))
|
|
173
|
+
return None if found < 0 else index + found
|
|
174
|
+
while index < size:
|
|
175
|
+
byte = buf[index]
|
|
176
|
+
if byte == escape:
|
|
177
|
+
index += 2
|
|
178
|
+
continue
|
|
179
|
+
if byte == end:
|
|
180
|
+
return index
|
|
181
|
+
index += 1
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def wrap_frame(payload: Union[bytes, bytearray], spec: DelimiterFraming) -> bytes:
|
|
186
|
+
"""发送方向:把净荷按分隔符/转义规则封装成线上字节(便于构造测试与设备下行)。"""
|
|
187
|
+
body = escape_bytes(payload, spec)
|
|
188
|
+
if spec.start is not None:
|
|
189
|
+
return bytes([spec.start]) + body + bytes([spec.end])
|
|
190
|
+
return body + bytes([spec.end])
|
kkprotocol/length.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""长度表达式:``{len.val}-1-1-2-4-2`` 的编译与求值。
|
|
3
|
+
|
|
4
|
+
用 AST 白名单求值,**不使用 eval**,因此表达式无法执行任意代码。
|
|
5
|
+
编译期做常量折叠(``-1-1-2-4-2`` 折叠成 ``-10``),解析期只剩一次减法。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import itertools
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Dict, Tuple
|
|
14
|
+
|
|
15
|
+
from .errors import FrameInvalid, FrameRuleError
|
|
16
|
+
|
|
17
|
+
__all__ = ["LengthExpr"]
|
|
18
|
+
|
|
19
|
+
_REF_RE = re.compile(r"\{\s*([A-Za-z_]\w*)\s*\.\s*([A-Za-z_]\w*)\s*\}")
|
|
20
|
+
_GROUP_REF_RE = re.compile(r"\{\s*([A-Za-z_]\w*)\s*\}")
|
|
21
|
+
_LEN_ATTRS = frozenset({"len", "length", "bytes", "raw"})
|
|
22
|
+
|
|
23
|
+
_BIN_OPS = {
|
|
24
|
+
ast.Add: lambda a, b: a + b,
|
|
25
|
+
ast.Sub: lambda a, b: a - b,
|
|
26
|
+
ast.Mult: lambda a, b: a * b,
|
|
27
|
+
ast.FloorDiv: lambda a, b: a // b,
|
|
28
|
+
ast.Mod: lambda a, b: a % b,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _eval_ast(node: ast.AST, values: Dict[str, int]) -> int:
|
|
33
|
+
if isinstance(node, ast.Constant):
|
|
34
|
+
return int(node.value)
|
|
35
|
+
if isinstance(node, ast.Name):
|
|
36
|
+
return values[node.id]
|
|
37
|
+
if isinstance(node, ast.UnaryOp):
|
|
38
|
+
if isinstance(node.op, ast.USub):
|
|
39
|
+
return -_eval_ast(node.operand, values)
|
|
40
|
+
return _eval_ast(node.operand, values)
|
|
41
|
+
if isinstance(node, ast.BinOp):
|
|
42
|
+
op = _BIN_OPS[type(node.op)]
|
|
43
|
+
try:
|
|
44
|
+
return op(_eval_ast(node.left, values), _eval_ast(node.right, values))
|
|
45
|
+
except ZeroDivisionError:
|
|
46
|
+
raise FrameInvalid("长度表达式除零") from None
|
|
47
|
+
raise FrameRuleError( # pragma: no cover - 编译期已拦截
|
|
48
|
+
"长度表达式含不支持的语法节点 {}".format(type(node).__name__)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _fold(node: ast.AST) -> ast.AST:
|
|
53
|
+
"""编译期常量折叠。"""
|
|
54
|
+
if isinstance(node, ast.BinOp):
|
|
55
|
+
left = _fold(node.left)
|
|
56
|
+
right = _fold(node.right)
|
|
57
|
+
if isinstance(left, ast.Constant) and isinstance(right, ast.Constant):
|
|
58
|
+
return ast.Constant(value=_BIN_OPS[type(node.op)](left.value, right.value))
|
|
59
|
+
return ast.BinOp(left=left, op=node.op, right=right)
|
|
60
|
+
if isinstance(node, ast.UnaryOp) and isinstance(node.operand, ast.Constant):
|
|
61
|
+
value = node.operand.value
|
|
62
|
+
return ast.Constant(value=-value if isinstance(node.op, ast.USub) else value)
|
|
63
|
+
return node
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class LengthExpr:
|
|
67
|
+
"""一个已编译的长度表达式。"""
|
|
68
|
+
|
|
69
|
+
__slots__ = ("source", "_tree", "_names", "_refs")
|
|
70
|
+
|
|
71
|
+
def __init__(self, source: str) -> None:
|
|
72
|
+
self.source = source
|
|
73
|
+
names: Dict[str, Tuple[str, str]] = {}
|
|
74
|
+
counter = itertools.count()
|
|
75
|
+
|
|
76
|
+
def _replace(match: "re.Match[str]") -> str:
|
|
77
|
+
token = "_v{}".format(next(counter))
|
|
78
|
+
names[token] = (match.group(1), match.group(2))
|
|
79
|
+
return token
|
|
80
|
+
|
|
81
|
+
expr = _REF_RE.sub(_replace, source)
|
|
82
|
+
if not names:
|
|
83
|
+
raise FrameRuleError(
|
|
84
|
+
"长度 {!r} 既不是整数也不是含 {{字段.属性}} 的表达式".format(source)
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
tree = ast.parse(expr, mode="eval")
|
|
88
|
+
except SyntaxError as exc:
|
|
89
|
+
raise FrameRuleError("长度表达式 {!r} 语法错误: {}".format(source, exc.msg)) from exc
|
|
90
|
+
self._check(tree)
|
|
91
|
+
try:
|
|
92
|
+
folded = _fold(tree.body)
|
|
93
|
+
except ZeroDivisionError:
|
|
94
|
+
raise FrameRuleError("长度表达式 {!r} 含常量除零".format(source)) from None
|
|
95
|
+
self._tree = ast.Expression(body=folded)
|
|
96
|
+
self._names = names
|
|
97
|
+
self._refs = tuple((token, key, attr) for token, (key, attr) in names.items())
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------ 校验
|
|
100
|
+
@property
|
|
101
|
+
def refs(self) -> Tuple[Tuple[str, str], ...]:
|
|
102
|
+
return tuple(self._names.values())
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _check(node: ast.AST) -> None:
|
|
106
|
+
if isinstance(node, ast.Expression):
|
|
107
|
+
LengthExpr._check(node.body)
|
|
108
|
+
return
|
|
109
|
+
if isinstance(node, ast.Constant):
|
|
110
|
+
if isinstance(node.value, bool) or not isinstance(node.value, int):
|
|
111
|
+
raise FrameRuleError("长度表达式只允许整数常量,发现 {!r}".format(node.value))
|
|
112
|
+
return
|
|
113
|
+
if isinstance(node, ast.Name):
|
|
114
|
+
return
|
|
115
|
+
if isinstance(node, ast.UnaryOp):
|
|
116
|
+
if not isinstance(node.op, (ast.USub, ast.UAdd)):
|
|
117
|
+
raise FrameRuleError("长度表达式不支持的运算符 {}".format(type(node.op).__name__))
|
|
118
|
+
LengthExpr._check(node.operand)
|
|
119
|
+
return
|
|
120
|
+
if isinstance(node, ast.BinOp):
|
|
121
|
+
if type(node.op) not in _BIN_OPS:
|
|
122
|
+
raise FrameRuleError(
|
|
123
|
+
"长度表达式不支持的运算符 {}(只允许 + - * // %,除法请用 //)".format(
|
|
124
|
+
type(node.op).__name__
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
LengthExpr._check(node.left)
|
|
128
|
+
LengthExpr._check(node.right)
|
|
129
|
+
return
|
|
130
|
+
raise FrameRuleError("长度表达式含不支持的语法节点 {}".format(type(node).__name__))
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------ 求值
|
|
133
|
+
def evaluate(self, fields: Dict[str, Any]) -> int:
|
|
134
|
+
values: Dict[str, int] = {}
|
|
135
|
+
for token, key, attr in self._refs:
|
|
136
|
+
fv = fields.get(key)
|
|
137
|
+
if fv is None: # pragma: no cover - 编译期已保证只引用前置字段
|
|
138
|
+
raise FrameInvalid("长度表达式引用了未解析字段 {!r}".format(key))
|
|
139
|
+
if attr == "val":
|
|
140
|
+
val = fv.val
|
|
141
|
+
if isinstance(val, bool) or not isinstance(val, int):
|
|
142
|
+
raise FrameInvalid(
|
|
143
|
+
"长度表达式引用了字段 {!r} 的 val,但它不是整数".format(key)
|
|
144
|
+
)
|
|
145
|
+
values[token] = int(val)
|
|
146
|
+
else:
|
|
147
|
+
values[token] = len(fv.raw)
|
|
148
|
+
result = _eval_ast(self._tree.body, values)
|
|
149
|
+
if isinstance(result, bool) or not isinstance(result, int): # pragma: no cover
|
|
150
|
+
raise FrameInvalid("长度表达式 {!r} 结果不是整数".format(self.source))
|
|
151
|
+
return result
|