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