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/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