protolib 0.2.2__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.
- protolib/__init__.py +76 -0
- protolib/conditions.py +245 -0
- protolib/core.py +884 -0
- protolib/errors.py +40 -0
- protolib/framer.py +85 -0
- protolib/io.py +71 -0
- protolib/loader.py +214 -0
- protolib/nbt.py +293 -0
- protolib/primitives.py +390 -0
- protolib-0.2.2.dist-info/METADATA +593 -0
- protolib-0.2.2.dist-info/RECORD +13 -0
- protolib-0.2.2.dist-info/WHEEL +5 -0
- protolib-0.2.2.dist-info/top_level.txt +1 -0
protolib/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
protolib
|
|
3
|
+
|
|
4
|
+
Libreria propia (no oficial, no depende de la `protodef` de npm/PyPI)
|
|
5
|
+
para definir, leer y escribir protocolos binarios a partir de una
|
|
6
|
+
definicion de protocolo en JSON (estilo node-minecraft-protocol /
|
|
7
|
+
node-protodef) o en YAML (sintaxis propia, mas legible).
|
|
8
|
+
|
|
9
|
+
Uso basico (JSON, formato minecraft-data sin modificar):
|
|
10
|
+
|
|
11
|
+
from protolib import Protocol
|
|
12
|
+
|
|
13
|
+
proto = Protocol("protocol.json")
|
|
14
|
+
pkt = proto.parse_packet("play", "toServer", raw_bytes)
|
|
15
|
+
print(pkt.name, pkt.params)
|
|
16
|
+
|
|
17
|
+
data = proto.serialize_packet("play", "toClient", "keep_alive", {"keepAliveId": 1})
|
|
18
|
+
|
|
19
|
+
Uso con YAML (sintaxis shorthand, ver protolib/loader.py):
|
|
20
|
+
|
|
21
|
+
proto = Protocol("protocol.yml")
|
|
22
|
+
|
|
23
|
+
Tambien aceptan dict ya parseado, o el contenido en memoria como string:
|
|
24
|
+
|
|
25
|
+
proto = Protocol({"types": {...}})
|
|
26
|
+
proto = Protocol(yaml_text, fmt="yaml")
|
|
27
|
+
|
|
28
|
+
Migrar un protocol.json existente a .yml:
|
|
29
|
+
|
|
30
|
+
from protolib.loader import protocol_dict_to_yaml
|
|
31
|
+
import json
|
|
32
|
+
raw = json.load(open("protocol.json"))
|
|
33
|
+
open("protocol.yml", "w").write(protocol_dict_to_yaml(raw))
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from .core import Protocol, ParsedPacket, Scope
|
|
37
|
+
from .io import Reader, Writer, BufferUnderrun
|
|
38
|
+
from .primitives import PRIMITIVES, Primitive, make_fixed_utf16be_string
|
|
39
|
+
from .errors import (
|
|
40
|
+
ProtolibError,
|
|
41
|
+
UnknownTypeError,
|
|
42
|
+
InvalidTypeDefinition,
|
|
43
|
+
SwitchCaseNotFound,
|
|
44
|
+
ConditionError,
|
|
45
|
+
)
|
|
46
|
+
from .conditions import eval_condition
|
|
47
|
+
from .framer import PacketFramer
|
|
48
|
+
from .loader import load_protocol_dict, protocol_dict_to_yaml, LoaderError
|
|
49
|
+
from .nbt import read_nbt, write_nbt, NBTError
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"Protocol",
|
|
53
|
+
"ParsedPacket",
|
|
54
|
+
"Scope",
|
|
55
|
+
"Reader",
|
|
56
|
+
"Writer",
|
|
57
|
+
"BufferUnderrun",
|
|
58
|
+
"PRIMITIVES",
|
|
59
|
+
"Primitive",
|
|
60
|
+
"make_fixed_utf16be_string",
|
|
61
|
+
"ProtolibError",
|
|
62
|
+
"UnknownTypeError",
|
|
63
|
+
"InvalidTypeDefinition",
|
|
64
|
+
"SwitchCaseNotFound",
|
|
65
|
+
"ConditionError",
|
|
66
|
+
"eval_condition",
|
|
67
|
+
"PacketFramer",
|
|
68
|
+
"load_protocol_dict",
|
|
69
|
+
"protocol_dict_to_yaml",
|
|
70
|
+
"LoaderError",
|
|
71
|
+
"read_nbt",
|
|
72
|
+
"write_nbt",
|
|
73
|
+
"NBTError",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
__version__ = "0.2.2"
|
protolib/conditions.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
protolib/conditions.py
|
|
3
|
+
|
|
4
|
+
Evaluador de las expresiones de condición usadas en campos `condition` de
|
|
5
|
+
los containers (estilo protodef: "fields.x === 1 && fields.y !== 0").
|
|
6
|
+
|
|
7
|
+
Deliberadamente NO usa eval()/exec(): parsea un subconjunto chico y seguro
|
|
8
|
+
de JS-like boolean expressions sobre el dict `fields` del container actual
|
|
9
|
+
(y opcionalmente `$root` / `$parent` para referenciar el contexto padre).
|
|
10
|
+
|
|
11
|
+
Gramática soportada:
|
|
12
|
+
expr := or_expr
|
|
13
|
+
or_expr := and_expr ( '||' and_expr )*
|
|
14
|
+
and_expr := comparison ( '&&' comparison )*
|
|
15
|
+
comparison := operand ( ('===' | '!==' | '==' | '!=' | '>=' | '<=' | '>' | '<') operand )?
|
|
16
|
+
operand := path | literal | '(' expr ')'
|
|
17
|
+
path := ('fields' | '$root' | '$parent') ('.' NAME | '[' INT ']')*
|
|
18
|
+
literal := INT | FLOAT | STRING | 'true' | 'false' | 'null'
|
|
19
|
+
|
|
20
|
+
Si la condición es un solo `operand` sin operador de comparación, se evalúa
|
|
21
|
+
su "truthiness" (igual que en JS/Python).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
_TOKEN_RE = re.compile(
|
|
30
|
+
r"""
|
|
31
|
+
\s*(?:
|
|
32
|
+
(?P<op>===|!==|==|!=|>=|<=|&&|\|\||>|<|\(|\))
|
|
33
|
+
| (?P<num>-?\d+\.\d+|-?\d+)
|
|
34
|
+
| (?P<str>'[^']*'|"[^"]*")
|
|
35
|
+
| (?P<ident>[A-Za-z_$][A-Za-z0-9_]*)
|
|
36
|
+
| (?P<dot>\.)
|
|
37
|
+
| (?P<lbracket>\[)
|
|
38
|
+
| (?P<rbracket>\])
|
|
39
|
+
)
|
|
40
|
+
""",
|
|
41
|
+
re.VERBOSE,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _Token:
|
|
46
|
+
__slots__ = ("kind", "value")
|
|
47
|
+
|
|
48
|
+
def __init__(self, kind: str, value: str):
|
|
49
|
+
self.kind = kind
|
|
50
|
+
self.value = value
|
|
51
|
+
|
|
52
|
+
def __repr__(self):
|
|
53
|
+
return f"Token({self.kind!r}, {self.value!r})"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _tokenize(expr: str) -> list[_Token]:
|
|
57
|
+
tokens: list[_Token] = []
|
|
58
|
+
pos = 0
|
|
59
|
+
while pos < len(expr):
|
|
60
|
+
m = _TOKEN_RE.match(expr, pos)
|
|
61
|
+
if not m or m.end() == pos:
|
|
62
|
+
stripped = expr[pos:].strip()
|
|
63
|
+
if not stripped:
|
|
64
|
+
break
|
|
65
|
+
raise ValueError(f"token inesperado en posición {pos}: {expr[pos:pos + 10]!r}")
|
|
66
|
+
pos = m.end()
|
|
67
|
+
kind = m.lastgroup
|
|
68
|
+
value = m.group(kind)
|
|
69
|
+
tokens.append(_Token(kind, value))
|
|
70
|
+
return tokens
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _Parser:
|
|
74
|
+
"""Recursive-descent parser sobre la lista de tokens. Evalúa directamente
|
|
75
|
+
(no construye un AST separado) porque la gramática es chica y no hace
|
|
76
|
+
falta reusar el árbol."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, tokens: list[_Token], context: dict[str, Any]):
|
|
79
|
+
self.tokens = tokens
|
|
80
|
+
self.pos = 0
|
|
81
|
+
self.context = context # { 'fields': {...}, '$root': {...}, '$parent': {...} }
|
|
82
|
+
|
|
83
|
+
def _peek(self) -> _Token | None:
|
|
84
|
+
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
|
|
85
|
+
|
|
86
|
+
def _advance(self) -> _Token:
|
|
87
|
+
tok = self.tokens[self.pos]
|
|
88
|
+
self.pos += 1
|
|
89
|
+
return tok
|
|
90
|
+
|
|
91
|
+
def _expect_op(self, value: str) -> None:
|
|
92
|
+
tok = self._peek()
|
|
93
|
+
if not tok or tok.value != value:
|
|
94
|
+
raise ValueError(f"se esperaba '{value}'")
|
|
95
|
+
self._advance()
|
|
96
|
+
|
|
97
|
+
def parse_expr(self) -> Any:
|
|
98
|
+
return self._parse_or()
|
|
99
|
+
|
|
100
|
+
def _parse_or(self) -> Any:
|
|
101
|
+
left = self._parse_and()
|
|
102
|
+
while self._peek() and self._peek().value == "||":
|
|
103
|
+
self._advance()
|
|
104
|
+
right = self._parse_and()
|
|
105
|
+
left = bool(left) or bool(right)
|
|
106
|
+
return left
|
|
107
|
+
|
|
108
|
+
def _parse_and(self) -> Any:
|
|
109
|
+
left = self._parse_comparison()
|
|
110
|
+
while self._peek() and self._peek().value == "&&":
|
|
111
|
+
self._advance()
|
|
112
|
+
right = self._parse_comparison()
|
|
113
|
+
left = bool(left) and bool(right)
|
|
114
|
+
return left
|
|
115
|
+
|
|
116
|
+
def _parse_comparison(self) -> Any:
|
|
117
|
+
left = self._parse_operand()
|
|
118
|
+
tok = self._peek()
|
|
119
|
+
if tok and tok.kind == "op" and tok.value in ("===", "!==", "==", "!=", ">=", "<=", ">", "<"):
|
|
120
|
+
op = self._advance().value
|
|
121
|
+
right = self._parse_operand()
|
|
122
|
+
if op == "==":
|
|
123
|
+
return _loose_eq(left, right)
|
|
124
|
+
if op == "!=":
|
|
125
|
+
return not _loose_eq(left, right)
|
|
126
|
+
if op == "===":
|
|
127
|
+
return left == right
|
|
128
|
+
if op == "!==":
|
|
129
|
+
return left != right
|
|
130
|
+
if op == ">=":
|
|
131
|
+
return left >= right
|
|
132
|
+
if op == "<=":
|
|
133
|
+
return left <= right
|
|
134
|
+
if op == ">":
|
|
135
|
+
return left > right
|
|
136
|
+
if op == "<":
|
|
137
|
+
return left < right
|
|
138
|
+
return left
|
|
139
|
+
|
|
140
|
+
def _parse_operand(self) -> Any:
|
|
141
|
+
tok = self._peek()
|
|
142
|
+
if tok is None:
|
|
143
|
+
raise ValueError("expresión incompleta")
|
|
144
|
+
|
|
145
|
+
if tok.value == "(":
|
|
146
|
+
self._advance()
|
|
147
|
+
value = self.parse_expr()
|
|
148
|
+
self._expect_op(")")
|
|
149
|
+
return value
|
|
150
|
+
|
|
151
|
+
if tok.kind == "num":
|
|
152
|
+
self._advance()
|
|
153
|
+
return float(tok.value) if "." in tok.value else int(tok.value)
|
|
154
|
+
|
|
155
|
+
if tok.kind == "str":
|
|
156
|
+
self._advance()
|
|
157
|
+
return tok.value[1:-1]
|
|
158
|
+
|
|
159
|
+
if tok.kind == "ident":
|
|
160
|
+
return self._parse_path()
|
|
161
|
+
|
|
162
|
+
raise ValueError(f"operando inesperado: {tok!r}")
|
|
163
|
+
|
|
164
|
+
def _parse_path(self) -> Any:
|
|
165
|
+
tok = self._advance()
|
|
166
|
+
name = tok.value
|
|
167
|
+
|
|
168
|
+
if name == "true":
|
|
169
|
+
return True
|
|
170
|
+
if name == "false":
|
|
171
|
+
return False
|
|
172
|
+
if name == "null" or name == "undefined":
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
if name not in self.context:
|
|
176
|
+
# nombre de raíz desconocido (no es 'fields'/'$root'/'$parent'):
|
|
177
|
+
# se interpreta como string-constante por tolerancia, igual que
|
|
178
|
+
# protodef original tolera ciertos identificadores sueltos.
|
|
179
|
+
current = None
|
|
180
|
+
else:
|
|
181
|
+
current = self.context[name]
|
|
182
|
+
|
|
183
|
+
while self._peek() and self._peek().kind in ("dot", "lbracket"):
|
|
184
|
+
sep = self._advance()
|
|
185
|
+
if sep.kind == "dot":
|
|
186
|
+
key_tok = self._advance()
|
|
187
|
+
key = key_tok.value
|
|
188
|
+
current = current.get(key) if isinstance(current, dict) else None
|
|
189
|
+
else: # lbracket
|
|
190
|
+
idx_tok = self._advance()
|
|
191
|
+
self._expect_op("]")
|
|
192
|
+
idx = int(idx_tok.value)
|
|
193
|
+
current = current[idx] if isinstance(current, (list, tuple)) else None
|
|
194
|
+
|
|
195
|
+
return current
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _loose_eq(left: Any, right: Any) -> bool:
|
|
199
|
+
"""
|
|
200
|
+
Coerción estilo JS '==' -- SOLO para el caso relevante acá: comparar
|
|
201
|
+
un número (leído del wire) contra un string numérico (típico cuando
|
|
202
|
+
las keys de un switch/mapping vienen de YAML/JSON como "0", "0x1f",
|
|
203
|
+
etc.). Cualquier otro par de tipos se compara tal cual, sin
|
|
204
|
+
coerciones raras estilo JS ([] == false, etc. -- no hace falta
|
|
205
|
+
replicar TODA la tabla de coerción de JS, solo lo que un protocolo
|
|
206
|
+
binario real necesita).
|
|
207
|
+
"""
|
|
208
|
+
if left == right:
|
|
209
|
+
return True
|
|
210
|
+
if isinstance(left, bool) or isinstance(right, bool):
|
|
211
|
+
return left == right
|
|
212
|
+
if isinstance(left, (int, float)) and isinstance(right, str):
|
|
213
|
+
return _try_parse_number(right) == left
|
|
214
|
+
if isinstance(right, (int, float)) and isinstance(left, str):
|
|
215
|
+
return _try_parse_number(left) == right
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _try_parse_number(s: str) -> Any:
|
|
220
|
+
try:
|
|
221
|
+
if s.lower().startswith("0x"):
|
|
222
|
+
return int(s, 16)
|
|
223
|
+
return int(s)
|
|
224
|
+
except ValueError:
|
|
225
|
+
try:
|
|
226
|
+
return float(s)
|
|
227
|
+
except ValueError:
|
|
228
|
+
return object() # no matchea nada, evita falsos positivos
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def eval_condition(expr: str, fields: dict[str, Any],
|
|
232
|
+
root: dict[str, Any] | None = None,
|
|
233
|
+
parent: dict[str, Any] | None = None) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Evalúa una expresión de condición tipo protodef contra el dict `fields`
|
|
236
|
+
del container actual. `root` y `parent` son opcionales, para condiciones
|
|
237
|
+
que referencian `$root.algo` o `$parent.algo`.
|
|
238
|
+
"""
|
|
239
|
+
context = {"fields": fields, "$root": root or {}, "$parent": parent or {}}
|
|
240
|
+
tokens = _tokenize(expr)
|
|
241
|
+
parser = _Parser(tokens, context)
|
|
242
|
+
result = parser.parse_expr()
|
|
243
|
+
if parser.pos != len(tokens):
|
|
244
|
+
raise ValueError(f"tokens sobrantes al final de la expresión: {expr!r}")
|
|
245
|
+
return bool(result)
|