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/core.py
ADDED
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
"""
|
|
2
|
+
protolib/core.py
|
|
3
|
+
|
|
4
|
+
Motor central de la librería. La clase Protocol carga un protocol.json
|
|
5
|
+
(o un dict de tipos equivalente) y sabe:
|
|
6
|
+
|
|
7
|
+
protocol.read_type(type_def, reader, scope, fields) -> valor python
|
|
8
|
+
protocol.write_type(type_def, value, writer, scope, fields)
|
|
9
|
+
|
|
10
|
+
protocol.parse_packet(state, direction, data: bytes) -> ParsedPacket
|
|
11
|
+
protocol.serialize_packet(state, direction, name, params: dict) -> bytes
|
|
12
|
+
|
|
13
|
+
Tipos compuestos soportados (definidos como ["tipoBase", opciones]):
|
|
14
|
+
container - lista ordenada de campos con nombre
|
|
15
|
+
array - lista homogénea, con count fijo, countType, o
|
|
16
|
+
count-referenciando-otro-campo
|
|
17
|
+
switch - elige el tipo según el valor de otro campo (compareTo)
|
|
18
|
+
mapper - traduce un entero crudo a un nombre simbólico (y viceversa)
|
|
19
|
+
option - valor opcional precedido por un bool ("presente?")
|
|
20
|
+
bitfield - empaqueta/desempaqueta sub-campos de N bits cada uno
|
|
21
|
+
bitflags - entero interpretado como conjunto de flags con nombre
|
|
22
|
+
buffer - bytes crudos, con longitud fija o por countType/count
|
|
23
|
+
pstring - string con longitud prefijada por countType (varint, u16, etc)
|
|
24
|
+
entityMetadataLoop - lista de entradas hasta encontrar un terminador
|
|
25
|
+
topBitSetTerminatedArray - lista que termina cuando el bit más alto del
|
|
26
|
+
primer byte leído en una entrada NO está seteado
|
|
27
|
+
(patrón de listas LEB128-like)
|
|
28
|
+
|
|
29
|
+
Los tipos primitivos (varint, i32, bool, cstring, etc.) viven en
|
|
30
|
+
primitives.py y se resuelven por nombre.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from typing import Any, Callable
|
|
37
|
+
|
|
38
|
+
from .io import Reader, Writer
|
|
39
|
+
from .primitives import PRIMITIVES
|
|
40
|
+
from .conditions import eval_condition
|
|
41
|
+
from .errors import (
|
|
42
|
+
UnknownTypeError,
|
|
43
|
+
InvalidTypeDefinition,
|
|
44
|
+
SwitchCaseNotFound,
|
|
45
|
+
)
|
|
46
|
+
from .loader import load_protocol_dict
|
|
47
|
+
|
|
48
|
+
TypeDef = Any # str | [str, dict] -- no tipamos más estricto por simplicidad
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_field_path(path: Any, fields: dict, root: dict | None, parent: dict | None) -> Any:
|
|
52
|
+
"""
|
|
53
|
+
Resuelve un path de campo estilo node-protodef (utils.js:getField).
|
|
54
|
+
|
|
55
|
+
Soporta:
|
|
56
|
+
"nombre" -> fields["nombre"] (campo del container actual)
|
|
57
|
+
"../nombre" -> parent["nombre"] (un nivel arriba)
|
|
58
|
+
"/nombre" -> root["nombre"] (desde la raíz absoluta)
|
|
59
|
+
|
|
60
|
+
Además, paridad con node-protodef: `count`/`countFor` puede ser
|
|
61
|
+
directamente un entero literal (tamaño fijo), no solo un path de campo
|
|
62
|
+
-- p.ej. ["array", {"count": 4, "type": "i32"}]. En ese caso se
|
|
63
|
+
devuelve tal cual, sin tratarlo como string.
|
|
64
|
+
|
|
65
|
+
Nota: node-protodef sube niveles siguiendo un puntero '..' que cada
|
|
66
|
+
container encadena a su padre real, así que "../../x" puede subir N
|
|
67
|
+
niveles. Este port no mantiene esa cadena completa (solo fields/root/
|
|
68
|
+
parent explícitos), así que "../../x" y superior caen a "root" -- cubre
|
|
69
|
+
el caso real usado en protocolos existentes (como mucho un "../").
|
|
70
|
+
"""
|
|
71
|
+
if isinstance(path, int):
|
|
72
|
+
return path
|
|
73
|
+
|
|
74
|
+
if path.startswith("/"):
|
|
75
|
+
context = root if root is not None else fields
|
|
76
|
+
segments = [s for s in path.split("/") if s != ""]
|
|
77
|
+
elif path.startswith("../"):
|
|
78
|
+
up_count = 0
|
|
79
|
+
rest = path
|
|
80
|
+
while rest.startswith("../"):
|
|
81
|
+
up_count += 1
|
|
82
|
+
rest = rest[3:]
|
|
83
|
+
context = parent if up_count == 1 else (root if root is not None else parent)
|
|
84
|
+
segments = [s for s in rest.split("/") if s != ""]
|
|
85
|
+
else:
|
|
86
|
+
context = fields
|
|
87
|
+
segments = [s for s in path.split("/") if s != ""]
|
|
88
|
+
|
|
89
|
+
current = context
|
|
90
|
+
for seg in segments:
|
|
91
|
+
if current is None:
|
|
92
|
+
return None
|
|
93
|
+
current = current.get(seg) if isinstance(current, dict) else None
|
|
94
|
+
return current
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def substitute_type_args(type_def: Any, type_args: dict) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Sustituye placeholders "$nombre" dentro de una definición de tipo por el
|
|
100
|
+
valor correspondiente en `type_args`, recorriendo recursivamente dicts y
|
|
101
|
+
listas. Paridad con node-protodef (protodef.js: findArgs/setField/
|
|
102
|
+
produceArgs, usado por extendType).
|
|
103
|
+
|
|
104
|
+
Esto es lo que permite tipos nombrados "parametrizables" como:
|
|
105
|
+
|
|
106
|
+
entityMetadataItem:
|
|
107
|
+
switch:
|
|
108
|
+
compareTo: $compareTo
|
|
109
|
+
fields: {...}
|
|
110
|
+
|
|
111
|
+
invocados en el protocolo como:
|
|
112
|
+
|
|
113
|
+
type:
|
|
114
|
+
- entityMetadataItem
|
|
115
|
+
- compareTo: type
|
|
116
|
+
|
|
117
|
+
Al invocarse así, la definición default de entityMetadataItem (que tiene
|
|
118
|
+
el placeholder "$compareTo") se clona y el placeholder se reemplaza por
|
|
119
|
+
el valor real pasado en el punto de uso ("type" en este ejemplo) --
|
|
120
|
+
resultando en un switch efectivo con compareTo="type".
|
|
121
|
+
|
|
122
|
+
Un placeholder sin valor provisto en `type_args` (o `type_args=None`)
|
|
123
|
+
se deja tal cual el string "$nombre" -- igual que node-protodef, que
|
|
124
|
+
solo sustituye los args que sí vinieron en typeArgs.
|
|
125
|
+
"""
|
|
126
|
+
if isinstance(type_def, str):
|
|
127
|
+
if type_def.startswith("$") and isinstance(type_args, dict):
|
|
128
|
+
key = type_def[1:]
|
|
129
|
+
if key in type_args:
|
|
130
|
+
return type_args[key]
|
|
131
|
+
return type_def
|
|
132
|
+
if isinstance(type_def, list):
|
|
133
|
+
return [substitute_type_args(item, type_args) for item in type_def]
|
|
134
|
+
if isinstance(type_def, dict):
|
|
135
|
+
return {k: substitute_type_args(v, type_args) for k, v in type_def.items()}
|
|
136
|
+
return type_def
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class ParsedPacket:
|
|
141
|
+
name: str
|
|
142
|
+
params: dict[str, Any]
|
|
143
|
+
bytes_read: int
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class Scope:
|
|
148
|
+
"""Representa un 'state.direction' (p.ej. play.toClient) con sus tipos propios,
|
|
149
|
+
que tienen prioridad sobre los tipos globales del protocolo."""
|
|
150
|
+
|
|
151
|
+
types: dict[str, TypeDef] = field(default_factory=dict)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Protocol:
|
|
155
|
+
"""
|
|
156
|
+
Carga y representa un protocol.json completo.
|
|
157
|
+
|
|
158
|
+
Estructura esperada del dict de entrada (mismo formato que
|
|
159
|
+
node-minecraft-protocol / node-protodef):
|
|
160
|
+
|
|
161
|
+
{
|
|
162
|
+
"types": { "<nombre>": <typeDef>, ... },
|
|
163
|
+
"<state>": {
|
|
164
|
+
"toClient": { "types": { ... } },
|
|
165
|
+
"toServer": { "types": { ... } }
|
|
166
|
+
},
|
|
167
|
+
...
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Los nombres de tipo se resuelven primero contra el scope local
|
|
171
|
+
(state.direction.types) y si no aparecen ahí, contra los tipos globales.
|
|
172
|
+
|
|
173
|
+
`protocol_source` acepta:
|
|
174
|
+
- un dict ya parseado (formato node-protodef clásico)
|
|
175
|
+
- una ruta a archivo .json, .yml o .yaml
|
|
176
|
+
- un string con contenido JSON o YAML en memoria
|
|
177
|
+
|
|
178
|
+
Los archivos .yml/.yaml admiten una sintaxis "shorthand" más legible
|
|
179
|
+
(ver protodef/loader.py), que se traduce automáticamente al formato
|
|
180
|
+
interno ["tipo", opciones]. El JSON de minecraft-data funciona sin
|
|
181
|
+
cambios.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
def __init__(self, protocol_source: dict[str, Any] | str, *, fmt: str | None = None):
|
|
185
|
+
protocol_json = load_protocol_dict(protocol_source, fmt=fmt)
|
|
186
|
+
self.raw = protocol_json
|
|
187
|
+
self.global_types: dict[str, TypeDef] = protocol_json.get("types", {})
|
|
188
|
+
self._scopes: dict[tuple[str, str], Scope] = {}
|
|
189
|
+
|
|
190
|
+
for state_name, state_val in protocol_json.items():
|
|
191
|
+
if state_name == "types" or not isinstance(state_val, dict):
|
|
192
|
+
continue
|
|
193
|
+
for direction in ("toClient", "toServer"):
|
|
194
|
+
if direction in state_val:
|
|
195
|
+
self._scopes[(state_name, direction)] = Scope(
|
|
196
|
+
types=state_val[direction].get("types", {})
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
self._composite_handlers: dict[str, Callable] = {
|
|
200
|
+
"container": self._read_container,
|
|
201
|
+
"array": self._read_array,
|
|
202
|
+
"count": self._read_count,
|
|
203
|
+
"switch": self._read_switch,
|
|
204
|
+
"mapper": self._read_mapper,
|
|
205
|
+
"option": self._read_option,
|
|
206
|
+
"bitfield": self._read_bitfield,
|
|
207
|
+
"bitflags": self._read_bitflags,
|
|
208
|
+
"buffer": self._read_buffer,
|
|
209
|
+
"pstring": self._read_pstring,
|
|
210
|
+
"entityMetadataLoop": self._read_entity_metadata_loop,
|
|
211
|
+
"topBitSetTerminatedArray": self._read_top_bit_set_terminated_array,
|
|
212
|
+
"cstring": self._read_cstring_encoded,
|
|
213
|
+
}
|
|
214
|
+
self._composite_write_handlers: dict[str, Callable] = {
|
|
215
|
+
"container": self._write_container,
|
|
216
|
+
"array": self._write_array,
|
|
217
|
+
"count": self._write_count,
|
|
218
|
+
"switch": self._write_switch,
|
|
219
|
+
"mapper": self._write_mapper,
|
|
220
|
+
"option": self._write_option,
|
|
221
|
+
"bitfield": self._write_bitfield,
|
|
222
|
+
"bitflags": self._write_bitflags,
|
|
223
|
+
"buffer": self._write_buffer,
|
|
224
|
+
"pstring": self._write_pstring,
|
|
225
|
+
"entityMetadataLoop": self._write_entity_metadata_loop,
|
|
226
|
+
"topBitSetTerminatedArray": self._write_top_bit_set_terminated_array,
|
|
227
|
+
"cstring": self._write_cstring_encoded,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# -------------------------------------------------------------------
|
|
231
|
+
# Resolución de nombres de tipo
|
|
232
|
+
# -------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def get_scope(self, state: str, direction: str) -> Scope:
|
|
235
|
+
try:
|
|
236
|
+
return self._scopes[(state, direction)]
|
|
237
|
+
except KeyError:
|
|
238
|
+
raise UnknownTypeError(f"{state}.{direction} (state/direction no definido)")
|
|
239
|
+
|
|
240
|
+
def _resolve_named_type(self, name: str, scope: Scope | None) -> TypeDef | None:
|
|
241
|
+
if scope is not None and name in scope.types:
|
|
242
|
+
return scope.types[name]
|
|
243
|
+
if name in self.global_types:
|
|
244
|
+
return self.global_types[name]
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
# -------------------------------------------------------------------
|
|
248
|
+
# Lectura
|
|
249
|
+
# -------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def read_type(self, type_def: TypeDef, r: Reader, scope: Scope | None,
|
|
252
|
+
fields: dict[str, Any], root: dict | None = None,
|
|
253
|
+
parent: dict | None = None) -> Any:
|
|
254
|
+
if isinstance(type_def, str):
|
|
255
|
+
if type_def in PRIMITIVES:
|
|
256
|
+
return PRIMITIVES[type_def].read(r)
|
|
257
|
+
resolved = self._resolve_named_type(type_def, scope)
|
|
258
|
+
if resolved is None:
|
|
259
|
+
raise UnknownTypeError(type_def)
|
|
260
|
+
return self.read_type(resolved, r, scope, fields, root, parent)
|
|
261
|
+
|
|
262
|
+
if isinstance(type_def, list) and len(type_def) == 2:
|
|
263
|
+
base, opts = type_def
|
|
264
|
+
handler = self._composite_handlers.get(base)
|
|
265
|
+
if handler is not None:
|
|
266
|
+
return handler(opts, r, scope, fields, root, parent)
|
|
267
|
+
# `base` no es un tipo base compuesto (container/switch/array/...)
|
|
268
|
+
# -- puede ser un tipo NOMBRADO parametrizable, invocado como
|
|
269
|
+
# [nombre, typeArgs], p.ej. [entityMetadataItem, {compareTo: type}].
|
|
270
|
+
# Paridad con node-protodef (protodef.js: extendType/produceArgs):
|
|
271
|
+
# se resuelve la definición default del tipo nombrado y se
|
|
272
|
+
# sustituyen los placeholders "$arg" por los typeArgs dados acá.
|
|
273
|
+
named = self._resolve_named_type(base, scope)
|
|
274
|
+
if named is None:
|
|
275
|
+
raise UnknownTypeError(f"(tipo base compuesto) {base}")
|
|
276
|
+
substituted = substitute_type_args(named, opts if isinstance(opts, dict) else None)
|
|
277
|
+
return self.read_type(substituted, r, scope, fields, root, parent)
|
|
278
|
+
|
|
279
|
+
raise InvalidTypeDefinition(type_def)
|
|
280
|
+
|
|
281
|
+
# ---- container ------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def _read_container(self, opts: list[dict], r: Reader, scope: Scope,
|
|
284
|
+
fields: dict, root: dict | None, parent: dict | None,
|
|
285
|
+
*, push_level: bool = True) -> dict:
|
|
286
|
+
result: dict[str, Any] = {}
|
|
287
|
+
effective_root = root if root is not None else result
|
|
288
|
+
# `push_level` decide si ESTE container pasa a ser el `parent`
|
|
289
|
+
# ("..") que ven sus propios sub-campos. Un container normal
|
|
290
|
+
# (resuelto por nombre, campo con nombre, etc.) sí empuja nivel --
|
|
291
|
+
# así funciona node-protodef. La excepción es un container usado
|
|
292
|
+
# directamente como item_type inline de un array: ahí node-protodef
|
|
293
|
+
# NO empuja contexto (el array es transparente para ".."), así que
|
|
294
|
+
# "../campo" dentro de un item de array debe saltar ese item y
|
|
295
|
+
# resolver contra el parent real que el array mismo tenía (p.ej.
|
|
296
|
+
# el container que contiene el array, no el item con `uuid`).
|
|
297
|
+
child_parent = result if push_level else parent
|
|
298
|
+
for f in opts:
|
|
299
|
+
if "condition" in f:
|
|
300
|
+
if not eval_condition(f["condition"], result, effective_root, fields):
|
|
301
|
+
continue
|
|
302
|
+
value = self.read_type(f["type"], r, scope, result, effective_root, child_parent)
|
|
303
|
+
if f.get("anon"):
|
|
304
|
+
if isinstance(value, dict):
|
|
305
|
+
result.update(value)
|
|
306
|
+
else:
|
|
307
|
+
result[f["name"]] = value
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
def _write_container(self, opts: list[dict], value: dict, w: Writer, scope: Scope,
|
|
311
|
+
fields: dict, root: dict | None, parent: dict | None,
|
|
312
|
+
*, push_level: bool = True) -> None:
|
|
313
|
+
data = value or {}
|
|
314
|
+
effective_root = root if root is not None else data
|
|
315
|
+
child_parent = data if push_level else parent
|
|
316
|
+
for f in opts:
|
|
317
|
+
if "condition" in f:
|
|
318
|
+
if not eval_condition(f["condition"], data, effective_root, fields):
|
|
319
|
+
continue
|
|
320
|
+
if f.get("anon"):
|
|
321
|
+
self.write_type(f["type"], data, w, scope, data, effective_root, child_parent)
|
|
322
|
+
else:
|
|
323
|
+
self.write_type(f["type"], data.get(f["name"]), w, scope, data, effective_root, child_parent)
|
|
324
|
+
|
|
325
|
+
# ---- array ------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
def _read_array(self, opts: dict, r: Reader, scope: Scope,
|
|
328
|
+
fields: dict, root, parent) -> list:
|
|
329
|
+
if "count" in opts:
|
|
330
|
+
count = resolve_field_path(opts["count"], fields, root, parent)
|
|
331
|
+
if not isinstance(count, int) or count < 0:
|
|
332
|
+
raise InvalidTypeDefinition(
|
|
333
|
+
f"array: 'count' ({opts['count']!r}) resolvió a {count!r}, "
|
|
334
|
+
f"se esperaba un entero >= 0 -- revisá que el campo exista "
|
|
335
|
+
f"y que el path (../, /) apunte al nivel correcto"
|
|
336
|
+
)
|
|
337
|
+
elif "countType" in opts:
|
|
338
|
+
count = self.read_type(opts["countType"], r, scope, fields, root, parent)
|
|
339
|
+
else:
|
|
340
|
+
raise InvalidTypeDefinition("array requiere 'count' o 'countType'")
|
|
341
|
+
|
|
342
|
+
item_type = opts["type"]
|
|
343
|
+
return [self._read_array_item(item_type, r, scope, fields, root, parent) for _ in range(count)]
|
|
344
|
+
|
|
345
|
+
def _read_array_item(self, item_type, r: Reader, scope: Scope,
|
|
346
|
+
fields: dict, root, parent) -> Any:
|
|
347
|
+
# Un container inline (["container", opts], literal dentro del
|
|
348
|
+
# array.type -- no un tipo nombrado como "packet_player_info") no
|
|
349
|
+
# debe empujar su propio nivel de parent: ver comentario en
|
|
350
|
+
# _read_container. Cualquier otro item_type (tipo nombrado, switch,
|
|
351
|
+
# primitivo) se comporta exactamente igual que antes.
|
|
352
|
+
if isinstance(item_type, list) and len(item_type) == 2 and item_type[0] == "container":
|
|
353
|
+
return self._read_container(item_type[1], r, scope, fields, root, parent, push_level=False)
|
|
354
|
+
return self.read_type(item_type, r, scope, fields, root, parent)
|
|
355
|
+
|
|
356
|
+
def _write_array(self, opts: dict, value: list, w: Writer, scope: Scope,
|
|
357
|
+
fields: dict, root, parent) -> None:
|
|
358
|
+
items = value or []
|
|
359
|
+
if "countType" in opts:
|
|
360
|
+
self.write_type(opts["countType"], len(items), w, scope, fields, root, parent)
|
|
361
|
+
# si usa "count" (referencia a otro campo), el caller debe haber
|
|
362
|
+
# escrito ese campo antes (responsabilidad del container que arma 'fields')
|
|
363
|
+
item_type = opts["type"]
|
|
364
|
+
for item in items:
|
|
365
|
+
self._write_array_item(item_type, item, w, scope, fields, root, parent)
|
|
366
|
+
|
|
367
|
+
def _write_array_item(self, item_type, item, w: Writer, scope: Scope,
|
|
368
|
+
fields: dict, root, parent) -> None:
|
|
369
|
+
if isinstance(item_type, list) and len(item_type) == 2 and item_type[0] == "container":
|
|
370
|
+
self._write_container(item_type[1], item, w, scope, fields, root, parent, push_level=False)
|
|
371
|
+
return
|
|
372
|
+
self.write_type(item_type, item, w, scope, fields, root, parent)
|
|
373
|
+
|
|
374
|
+
# ---- count (length-prefix "separado", declarado como campo hermano) -------
|
|
375
|
+
# Uso típico: un container donde el prefijo de longitud de un array/buffer
|
|
376
|
+
# NO está pegado a ese array (caso normal de countType), sino que es un
|
|
377
|
+
# campo propio en otra posición del container, y el array referencia ese
|
|
378
|
+
# campo por nombre via su propio "count". El tipo `count` en sí, al leer,
|
|
379
|
+
# simplemente lee un entero (typeArgs.type); al escribir, IGNORA el valor
|
|
380
|
+
# que se le pase y escribe len(getField(countFor)) -- igual que
|
|
381
|
+
# node-protodef structures.js: readCount/writeCount.
|
|
382
|
+
#
|
|
383
|
+
# opts: {"type": "u8"|"varint"|..., "countFor": "<path al campo, admite ../ y />"}
|
|
384
|
+
|
|
385
|
+
def _read_count(self, opts: dict, r: Reader, scope: Scope,
|
|
386
|
+
fields: dict, root, parent) -> Any:
|
|
387
|
+
return self.read_type(opts["type"], r, scope, fields, root, parent)
|
|
388
|
+
|
|
389
|
+
def _write_count(self, opts: dict, value: Any, w: Writer, scope: Scope,
|
|
390
|
+
fields: dict, root, parent) -> None:
|
|
391
|
+
target = resolve_field_path(opts["countFor"], fields, root, parent)
|
|
392
|
+
length = len(target) if target is not None else 0
|
|
393
|
+
self.write_type(opts["type"], length, w, scope, fields, root, parent)
|
|
394
|
+
|
|
395
|
+
# ---- switch -------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
def _resolve_compare_value(self, opts: dict, fields: dict, root, parent) -> Any:
|
|
398
|
+
# node-protodef (compiler-conditional.js): el switch compara contra
|
|
399
|
+
# `compareTo` (path a un campo ya leído/escrito) O contra
|
|
400
|
+
# `compareToValue` (un literal fijo, sin indirección -- p.ej. útil
|
|
401
|
+
# para elegir tipo según el nombre del propio packet, que ya viene
|
|
402
|
+
# calculado desde afuera). Solo uno de los dos debería estar presente.
|
|
403
|
+
if "compareToValue" in opts:
|
|
404
|
+
return opts["compareToValue"]
|
|
405
|
+
compare_to = opts["compareTo"]
|
|
406
|
+
if compare_to.startswith("fields."):
|
|
407
|
+
return eval_condition(compare_to, fields, root, parent)
|
|
408
|
+
if compare_to in fields:
|
|
409
|
+
return fields[compare_to]
|
|
410
|
+
# Paths estilo node-protodef (utils.js:getField): "../campo" sube al
|
|
411
|
+
# container padre, "/campo" es absoluto desde la raíz. Antes esto
|
|
412
|
+
# caía a eval_condition, que solo entiende sintaxis "$parent.campo"
|
|
413
|
+
# -- nunca "../campo" -- así que un compareTo con ".." siempre
|
|
414
|
+
# resolvía a None (bug real detrás de SwitchCaseNotFound con
|
|
415
|
+
# compareTo="../algo"). resolve_field_path es quien sabe navegar
|
|
416
|
+
# "../" y "/", así que lo usamos primero para esos casos.
|
|
417
|
+
if compare_to.startswith("../") or compare_to.startswith("/"):
|
|
418
|
+
return resolve_field_path(compare_to, fields, root, parent)
|
|
419
|
+
try:
|
|
420
|
+
return eval_condition(compare_to, fields, root, parent)
|
|
421
|
+
except Exception:
|
|
422
|
+
return None
|
|
423
|
+
|
|
424
|
+
def _resolve_switch_case(self, opts: dict, compare_val: Any, root) -> TypeDef | None:
|
|
425
|
+
case_key = str(compare_val) if not isinstance(compare_val, str) else compare_val
|
|
426
|
+
case_type = opts["fields"].get(case_key, opts["fields"].get(compare_val))
|
|
427
|
+
if case_type is not None:
|
|
428
|
+
return case_type
|
|
429
|
+
# Keys que empiezan con "/" se resuelven contra el root context, no
|
|
430
|
+
# como comparación de string -- paridad con compiler-conditional.js:
|
|
431
|
+
# `if (val.startsWith('/')) val = 'ctx.' + val.slice(1)`. Recorremos
|
|
432
|
+
# las keys del switch buscando una cuyo valor-resuelto-en-root matchee.
|
|
433
|
+
for key, type_for_key in opts["fields"].items():
|
|
434
|
+
if key.startswith("/"):
|
|
435
|
+
root_val = resolve_field_path(key, {}, root, None)
|
|
436
|
+
if root_val == compare_val:
|
|
437
|
+
return type_for_key
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
def _identify_compare_ref(self, opts: dict) -> str:
|
|
441
|
+
return opts.get("compareToValue", opts.get("compareTo"))
|
|
442
|
+
|
|
443
|
+
def _read_switch(self, opts: dict, r: Reader, scope: Scope,
|
|
444
|
+
fields: dict, root, parent) -> Any:
|
|
445
|
+
compare_val = self._resolve_compare_value(opts, fields, root, parent)
|
|
446
|
+
case_type = self._resolve_switch_case(opts, compare_val, root)
|
|
447
|
+
if case_type is None:
|
|
448
|
+
if "default" in opts:
|
|
449
|
+
return self.read_type(opts["default"], r, scope, fields, root, parent)
|
|
450
|
+
raise SwitchCaseNotFound(self._identify_compare_ref(opts), compare_val)
|
|
451
|
+
return self.read_type(case_type, r, scope, fields, root, parent)
|
|
452
|
+
|
|
453
|
+
def _write_switch(self, opts: dict, value: Any, w: Writer, scope: Scope,
|
|
454
|
+
fields: dict, root, parent) -> None:
|
|
455
|
+
compare_val = self._resolve_compare_value(opts, fields, root, parent)
|
|
456
|
+
case_type = self._resolve_switch_case(opts, compare_val, root)
|
|
457
|
+
if case_type is None:
|
|
458
|
+
if "default" in opts:
|
|
459
|
+
return self.write_type(opts["default"], value, w, scope, fields, root, parent)
|
|
460
|
+
raise SwitchCaseNotFound(self._identify_compare_ref(opts), compare_val)
|
|
461
|
+
return self.write_type(case_type, value, w, scope, fields, root, parent)
|
|
462
|
+
|
|
463
|
+
# ---- mapper (entero <-> nombre simbólico) --------------------------------
|
|
464
|
+
|
|
465
|
+
def _read_mapper(self, opts: dict, r: Reader, scope: Scope,
|
|
466
|
+
fields: dict, root, parent) -> Any:
|
|
467
|
+
raw = self.read_type(opts["type"], r, scope, fields, root, parent)
|
|
468
|
+
mappings = opts["mappings"]
|
|
469
|
+
# normalizamos las keys del mapping (que pueden venir como "0x00",
|
|
470
|
+
# "0x1f", "31", etc.) a entero, para no depender del formato exacto
|
|
471
|
+
# con el que esté escrito el protocol.json
|
|
472
|
+
for key, mapped_name in mappings.items():
|
|
473
|
+
key_int = int(key, 16) if key.lower().startswith("0x") else int(key)
|
|
474
|
+
if key_int == raw:
|
|
475
|
+
return mapped_name
|
|
476
|
+
return raw # sin mapping conocido: se devuelve el valor crudo
|
|
477
|
+
|
|
478
|
+
def _write_mapper(self, opts: dict, value: Any, w: Writer, scope: Scope,
|
|
479
|
+
fields: dict, root, parent) -> None:
|
|
480
|
+
mappings = opts["mappings"]
|
|
481
|
+
if isinstance(value, str):
|
|
482
|
+
numeric = None
|
|
483
|
+
for k, v in mappings.items():
|
|
484
|
+
if v == value:
|
|
485
|
+
numeric = int(k, 16) if k.startswith("0x") else int(k)
|
|
486
|
+
break
|
|
487
|
+
if numeric is None:
|
|
488
|
+
raise InvalidTypeDefinition(
|
|
489
|
+
f"mapper: no se encontró mapping inverso para {value!r}"
|
|
490
|
+
)
|
|
491
|
+
else:
|
|
492
|
+
numeric = value
|
|
493
|
+
self.write_type(opts["type"], numeric, w, scope, fields, root, parent)
|
|
494
|
+
|
|
495
|
+
# ---- option (presente si un bool previo es true) -------------------------
|
|
496
|
+
|
|
497
|
+
def _read_option(self, opts: TypeDef, r: Reader, scope: Scope,
|
|
498
|
+
fields: dict, root, parent) -> Any:
|
|
499
|
+
present = PRIMITIVES["bool"].read(r)
|
|
500
|
+
if not present:
|
|
501
|
+
return None
|
|
502
|
+
return self.read_type(opts, r, scope, fields, root, parent)
|
|
503
|
+
|
|
504
|
+
def _write_option(self, opts: TypeDef, value: Any, w: Writer, scope: Scope,
|
|
505
|
+
fields: dict, root, parent) -> None:
|
|
506
|
+
present = value is not None
|
|
507
|
+
PRIMITIVES["bool"].write(present, w)
|
|
508
|
+
if present:
|
|
509
|
+
self.write_type(opts, value, w, scope, fields, root, parent)
|
|
510
|
+
|
|
511
|
+
# ---- bitfield (sub-campos empaquetados en N bits) -------------------------
|
|
512
|
+
|
|
513
|
+
def _read_bitfield(self, opts: list[dict], r: Reader, scope: Scope,
|
|
514
|
+
fields: dict, root, parent) -> dict:
|
|
515
|
+
# node-protodef (structures.js:readBitField) NO exige que la suma de
|
|
516
|
+
# bits sea múltiplo de 8: si sobran bits al final, se rellenan con
|
|
517
|
+
# padding hasta completar el último byte. Ya no lanzamos error acá;
|
|
518
|
+
# simplemente redondeamos hacia arriba al byte siguiente.
|
|
519
|
+
total_bits = sum(f["size"] for f in opts)
|
|
520
|
+
num_bytes = (total_bits + 7) // 8
|
|
521
|
+
pad_bits = num_bytes * 8 - total_bits
|
|
522
|
+
raw_bytes = r.read_bytes(num_bytes)
|
|
523
|
+
big = int.from_bytes(raw_bytes, "big")
|
|
524
|
+
|
|
525
|
+
result: dict[str, Any] = {}
|
|
526
|
+
bits_left = total_bits + pad_bits
|
|
527
|
+
for f in opts:
|
|
528
|
+
bits_left -= f["size"]
|
|
529
|
+
mask = (1 << f["size"]) - 1
|
|
530
|
+
val = (big >> bits_left) & mask
|
|
531
|
+
if f.get("signed") and val >= (1 << (f["size"] - 1)):
|
|
532
|
+
val -= 1 << f["size"]
|
|
533
|
+
result[f["name"]] = val
|
|
534
|
+
return result
|
|
535
|
+
|
|
536
|
+
def _write_bitfield(self, opts: list[dict], value: dict, w: Writer, scope: Scope,
|
|
537
|
+
fields: dict, root, parent) -> None:
|
|
538
|
+
# Mismo criterio que _read_bitfield: si el total de bits no completa
|
|
539
|
+
# un byte, se rellena (padding) desplazando a la izquierda el sobrante
|
|
540
|
+
# -- paridad con node-protodef: `buffer[offset++] = toWrite << (8 - bits)`
|
|
541
|
+
# en el último byte cuando quedan bits < 8 sin usar.
|
|
542
|
+
total_bits = sum(f["size"] for f in opts)
|
|
543
|
+
num_bytes = (total_bits + 7) // 8
|
|
544
|
+
pad_bits = num_bytes * 8 - total_bits
|
|
545
|
+
big = 0
|
|
546
|
+
for f in opts:
|
|
547
|
+
v = value.get(f["name"], 0) & ((1 << f["size"]) - 1)
|
|
548
|
+
big = (big << f["size"]) | v
|
|
549
|
+
big <<= pad_bits
|
|
550
|
+
w.write_bytes(big.to_bytes(num_bytes, "big"))
|
|
551
|
+
|
|
552
|
+
# ---- bitflags (entero como set de flags con nombre) ------------------------
|
|
553
|
+
|
|
554
|
+
def _read_bitflags(self, opts: dict, r: Reader, scope: Scope,
|
|
555
|
+
fields: dict, root, parent) -> dict[str, Any]:
|
|
556
|
+
raw = self.read_type(opts["type"], r, scope, fields, root, parent)
|
|
557
|
+
flags_def = opts["flags"]
|
|
558
|
+
shift = opts.get("shift", False)
|
|
559
|
+
|
|
560
|
+
# flags como dict: {"nombre": bitmask, ...} o, si shift=True,
|
|
561
|
+
# {"nombre": posiciónDeBit, ...} -- paridad con spec oficial
|
|
562
|
+
# (utils.md: "shift: Specify if flags is an object and holds bit
|
|
563
|
+
# positions as values opposed to a bitmask").
|
|
564
|
+
if isinstance(flags_def, dict):
|
|
565
|
+
result: dict[str, Any] = {}
|
|
566
|
+
for flag_name, raw_mask in flags_def.items():
|
|
567
|
+
mask = (1 << raw_mask) if shift else raw_mask
|
|
568
|
+
result[flag_name] = bool(raw & mask)
|
|
569
|
+
result["_value"] = raw
|
|
570
|
+
return result
|
|
571
|
+
|
|
572
|
+
# flags como lista posicional (cada uno ocupa 1 bit, LSB primero,
|
|
573
|
+
# o MSB primero si big=True) -- paridad con spec oficial.
|
|
574
|
+
flag_names: list[str] = flags_def
|
|
575
|
+
big_endian = opts.get("big", False)
|
|
576
|
+
result: dict[str, Any] = {}
|
|
577
|
+
names = list(reversed(flag_names)) if big_endian else flag_names
|
|
578
|
+
for i, flag_name in enumerate(names):
|
|
579
|
+
if flag_name is None:
|
|
580
|
+
continue
|
|
581
|
+
result[flag_name] = bool((raw >> i) & 1)
|
|
582
|
+
# Paridad con node-protodef: el valor crudo siempre viaja en '_value',
|
|
583
|
+
# además de cada flag individual con su propio nombre.
|
|
584
|
+
result["_value"] = raw
|
|
585
|
+
return result
|
|
586
|
+
|
|
587
|
+
def _write_bitflags(self, opts: dict, value: dict, w: Writer, scope: Scope,
|
|
588
|
+
fields: dict, root, parent) -> None:
|
|
589
|
+
flags_def = opts["flags"]
|
|
590
|
+
shift = opts.get("shift", False)
|
|
591
|
+
|
|
592
|
+
# Spec oficial: al escribir, el valor esperado viene envuelto como
|
|
593
|
+
# {"flags": {...}}. Por retrocompatibilidad con protocolos que ya
|
|
594
|
+
# dependían de este port pasando el dict de flags "pelado" (sin
|
|
595
|
+
# envolver), aceptamos ambas formas.
|
|
596
|
+
flag_values = value.get("flags", value) if isinstance(value, dict) else {}
|
|
597
|
+
|
|
598
|
+
if isinstance(flags_def, dict):
|
|
599
|
+
raw = 0
|
|
600
|
+
for flag_name, raw_mask in flags_def.items():
|
|
601
|
+
mask = (1 << raw_mask) if shift else raw_mask
|
|
602
|
+
if flag_values.get(flag_name):
|
|
603
|
+
raw |= mask
|
|
604
|
+
self.write_type(opts["type"], raw, w, scope, fields, root, parent)
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
flag_names: list[str] = flags_def
|
|
608
|
+
big_endian = opts.get("big", False)
|
|
609
|
+
names = list(reversed(flag_names)) if big_endian else flag_names
|
|
610
|
+
raw = 0
|
|
611
|
+
for i, flag_name in enumerate(names):
|
|
612
|
+
if flag_name is None:
|
|
613
|
+
continue
|
|
614
|
+
if flag_values.get(flag_name):
|
|
615
|
+
raw |= (1 << i)
|
|
616
|
+
self.write_type(opts["type"], raw, w, scope, fields, root, parent)
|
|
617
|
+
|
|
618
|
+
# ---- buffer (bytes crudos) ------------------------------------------------
|
|
619
|
+
|
|
620
|
+
def _read_buffer(self, opts: dict, r: Reader, scope: Scope,
|
|
621
|
+
fields: dict, root, parent) -> bytes:
|
|
622
|
+
if "count" in opts:
|
|
623
|
+
count = resolve_field_path(opts["count"], fields, root, parent)
|
|
624
|
+
if not isinstance(count, int) or count < 0:
|
|
625
|
+
raise InvalidTypeDefinition(
|
|
626
|
+
f"buffer: 'count' ({opts['count']!r}) resolvió a {count!r}, "
|
|
627
|
+
f"se esperaba un entero >= 0 -- revisá que el campo exista "
|
|
628
|
+
f"y que el path (../, /) apunte al nivel correcto"
|
|
629
|
+
)
|
|
630
|
+
elif "countType" in opts:
|
|
631
|
+
count = self.read_type(opts["countType"], r, scope, fields, root, parent)
|
|
632
|
+
elif opts.get("rest"):
|
|
633
|
+
count = r.remaining
|
|
634
|
+
else:
|
|
635
|
+
raise InvalidTypeDefinition("buffer requiere 'count', 'countType' o 'rest'")
|
|
636
|
+
return r.read_bytes(count)
|
|
637
|
+
|
|
638
|
+
def _write_buffer(self, opts: dict, value: bytes, w: Writer, scope: Scope,
|
|
639
|
+
fields: dict, root, parent) -> None:
|
|
640
|
+
data = value or b""
|
|
641
|
+
if "countType" in opts:
|
|
642
|
+
self.write_type(opts["countType"], len(data), w, scope, fields, root, parent)
|
|
643
|
+
elif "count" in opts and isinstance(opts["count"], int):
|
|
644
|
+
# count fijo (entero literal, no un path a otro campo): a
|
|
645
|
+
# diferencia de countType, acá NADIE escribe un largo antes
|
|
646
|
+
# -- el framing del protocolo asume que este campo ocupa
|
|
647
|
+
# EXACTAMENTE ese tanto de bytes. Si `data` viene más corto
|
|
648
|
+
# o más largo, escribir tal cual dejaría el siguiente campo
|
|
649
|
+
# desalineado en quien reciba el paquete, sin ningún error
|
|
650
|
+
# visible acá. Mejor fallar ruidoso que corromper el framing.
|
|
651
|
+
if len(data) != opts["count"]:
|
|
652
|
+
raise InvalidTypeDefinition(
|
|
653
|
+
f"buffer: se esperaban exactamente {opts['count']} bytes "
|
|
654
|
+
f"(count fijo), pero el valor tiene {len(data)} -- si el "
|
|
655
|
+
f"tamaño puede variar, generá un primitivo dedicado con "
|
|
656
|
+
f"padding explícito (ver make_fixed_buffer en "
|
|
657
|
+
f"primitives.py) en vez de 'buffer' genérico"
|
|
658
|
+
)
|
|
659
|
+
w.write_bytes(data)
|
|
660
|
+
|
|
661
|
+
# ---- pstring (string con longitud prefijada configurable) -----------------
|
|
662
|
+
|
|
663
|
+
def _read_pstring(self, opts: dict, r: Reader, scope: Scope,
|
|
664
|
+
fields: dict, root, parent) -> str:
|
|
665
|
+
count_type = opts.get("countType", "varint")
|
|
666
|
+
encoding = opts.get("encoding", "utf-8")
|
|
667
|
+
if "count" in opts:
|
|
668
|
+
length = resolve_field_path(opts["count"], fields, root, parent)
|
|
669
|
+
if not isinstance(length, int) or length < 0:
|
|
670
|
+
raise InvalidTypeDefinition(
|
|
671
|
+
f"pstring: 'count' ({opts['count']!r}) resolvió a {length!r}, "
|
|
672
|
+
f"se esperaba un entero >= 0 -- revisá que el campo exista "
|
|
673
|
+
f"y que el path (../, /) apunte al nivel correcto"
|
|
674
|
+
)
|
|
675
|
+
else:
|
|
676
|
+
length = self.read_type(count_type, r, scope, fields, root, parent)
|
|
677
|
+
data = r.read_bytes(length)
|
|
678
|
+
return data.decode(encoding)
|
|
679
|
+
|
|
680
|
+
def _write_pstring(self, opts: dict, value: str, w: Writer, scope: Scope,
|
|
681
|
+
fields: dict, root, parent) -> None:
|
|
682
|
+
count_type = opts.get("countType", "varint")
|
|
683
|
+
encoding = opts.get("encoding", "utf-8")
|
|
684
|
+
data = value.encode(encoding)
|
|
685
|
+
if "count" not in opts:
|
|
686
|
+
self.write_type(count_type, len(data), w, scope, fields, root, parent)
|
|
687
|
+
w.write_bytes(data)
|
|
688
|
+
|
|
689
|
+
# ---- entityMetadataLoop (lista hasta encontrar terminador) -----------------
|
|
690
|
+
|
|
691
|
+
def _read_entity_metadata_loop(self, opts: dict, r: Reader, scope: Scope,
|
|
692
|
+
fields: dict, root, parent) -> list:
|
|
693
|
+
"""
|
|
694
|
+
opts:
|
|
695
|
+
endVal: valor crudo (byte) que indica fin de la lista.
|
|
696
|
+
endType: tipo primitivo de 1 byte usado solo para el chequeo de
|
|
697
|
+
terminador (default 'u8'); NO se usa para decodificar
|
|
698
|
+
cada entrada real.
|
|
699
|
+
type: tipo completo de cada entrada (típicamente un container
|
|
700
|
+
que ya incluye, como campo anon, el bitfield real
|
|
701
|
+
type/key de Minecraft -- ver entityMetadata en el yml).
|
|
702
|
+
|
|
703
|
+
Bug real corregido acá: la versión anterior leía el primer byte de
|
|
704
|
+
cada entrada con `end_type` y lo usaba directo como "index" para
|
|
705
|
+
decidir el caso del switch -- pero ese byte es en realidad un
|
|
706
|
+
bitfield empaquetado `(type<<5)|key`, ya declarado como tal dentro
|
|
707
|
+
de `item_type` (el container en el yml). Al leerlo dos veces con
|
|
708
|
+
semánticas distintas (una vez acá como entero plano, descartado, y
|
|
709
|
+
de nuevo dentro del container como bitfield) el offset del reader
|
|
710
|
+
quedaba desalineado con el resto del paquete, produciendo
|
|
711
|
+
BufferUnderrun más adelante en la misma lectura.
|
|
712
|
+
|
|
713
|
+
La forma correcta (y genérica, sin hardcodear bits acá) es:
|
|
714
|
+
espiar el byte con peek_byte() SIN consumirlo para chequear si es
|
|
715
|
+
el terminador; si no lo es, dejar que `item_type` (el container
|
|
716
|
+
real, definido en el yml) lea la entrada completa -- incluyendo
|
|
717
|
+
ese mismo byte como parte de su propio bitfield.
|
|
718
|
+
"""
|
|
719
|
+
end_val = opts.get("endVal", 0xFF)
|
|
720
|
+
item_type = opts["type"]
|
|
721
|
+
|
|
722
|
+
result = []
|
|
723
|
+
while True:
|
|
724
|
+
if r.peek_byte() == end_val:
|
|
725
|
+
r.read_bytes(1) # consumir el terminador
|
|
726
|
+
break
|
|
727
|
+
entry = self.read_type(item_type, r, scope, fields, root, parent)
|
|
728
|
+
result.append(entry)
|
|
729
|
+
return result
|
|
730
|
+
|
|
731
|
+
def _is_container_type(self, type_def: TypeDef, scope: Scope) -> bool:
|
|
732
|
+
"""Resuelve (sin leer/escribir bytes) si un tipo es, en el fondo, un
|
|
733
|
+
container -- para saber si una entrada de entityMetadataLoop debe
|
|
734
|
+
pasarse como dict completo o desenvuelta en 'value'."""
|
|
735
|
+
seen: set[str] = set()
|
|
736
|
+
current = type_def
|
|
737
|
+
while isinstance(current, str):
|
|
738
|
+
if current in seen:
|
|
739
|
+
return False # ciclo raro, no asumimos container
|
|
740
|
+
seen.add(current)
|
|
741
|
+
if current in PRIMITIVES:
|
|
742
|
+
return False
|
|
743
|
+
resolved = self._resolve_named_type(current, scope)
|
|
744
|
+
if resolved is None:
|
|
745
|
+
return False
|
|
746
|
+
current = resolved
|
|
747
|
+
if isinstance(current, list) and len(current) == 2:
|
|
748
|
+
return current[0] == "container"
|
|
749
|
+
return False
|
|
750
|
+
|
|
751
|
+
def _write_entity_metadata_loop(self, opts: dict, value: list, w: Writer, scope: Scope,
|
|
752
|
+
fields: dict, root, parent) -> None:
|
|
753
|
+
"""
|
|
754
|
+
Simétrico al fix de _read_entity_metadata_loop: cada `entry` en
|
|
755
|
+
`value` es el dict completo tal como lo devuelve la lectura
|
|
756
|
+
(típicamente {'type':.., 'key':.., 'value':..}, producido por el
|
|
757
|
+
container real declarado en `item_type` -- bitfield anon type/key
|
|
758
|
+
+ campo nombrado value/switch). Se escribe con item_type
|
|
759
|
+
directamente, dejando que el container/bitfield/switch internos
|
|
760
|
+
empaqueten el byte type/key y el payload como corresponda. Al
|
|
761
|
+
final se escribe el byte terminador crudo (end_val) con end_type.
|
|
762
|
+
"""
|
|
763
|
+
end_val = opts.get("endVal", 0xFF)
|
|
764
|
+
end_type = opts.get("endType", "u8")
|
|
765
|
+
item_type = opts["type"]
|
|
766
|
+
|
|
767
|
+
for entry in (value or []):
|
|
768
|
+
self.write_type(item_type, entry, w, scope, fields, root, parent)
|
|
769
|
+
self.write_type(end_type, end_val, w, scope, fields, root, parent)
|
|
770
|
+
|
|
771
|
+
# ---- topBitSetTerminatedArray ------------------------------------------------
|
|
772
|
+
|
|
773
|
+
def _read_top_bit_set_terminated_array(self, opts: dict, r: Reader, scope: Scope,
|
|
774
|
+
fields: dict, root, parent) -> list:
|
|
775
|
+
"""
|
|
776
|
+
Lee entradas de `type` mientras el bit más alto (0x80) del PRIMER byte
|
|
777
|
+
de cada entrada esté seteado. Patrón típico de listas LEB128-like
|
|
778
|
+
en RakNet / formatos custom (cada entrada "anuncia" si hay otra después).
|
|
779
|
+
"""
|
|
780
|
+
item_type = opts["type"]
|
|
781
|
+
result = []
|
|
782
|
+
while True:
|
|
783
|
+
start_offset = r.offset
|
|
784
|
+
item = self.read_type(item_type, r, scope, fields, root, parent)
|
|
785
|
+
result.append(item)
|
|
786
|
+
first_byte = r.buffer[start_offset]
|
|
787
|
+
if not (first_byte & 0x80):
|
|
788
|
+
break
|
|
789
|
+
return result
|
|
790
|
+
|
|
791
|
+
def _write_top_bit_set_terminated_array(self, opts: dict, value: list, w: Writer, scope: Scope,
|
|
792
|
+
fields: dict, root, parent) -> None:
|
|
793
|
+
"""
|
|
794
|
+
Al escribir, el llamador es responsable de que cada item ya traiga el
|
|
795
|
+
bit alto seteado salvo el último (esto refleja el protocolo real: el
|
|
796
|
+
marcador de continuación suele ser parte de los datos del propio item,
|
|
797
|
+
no algo que este wrapper pueda inventar).
|
|
798
|
+
"""
|
|
799
|
+
item_type = opts["type"]
|
|
800
|
+
items = value or []
|
|
801
|
+
for item in items:
|
|
802
|
+
self.write_type(item_type, item, w, scope, fields, root, parent)
|
|
803
|
+
|
|
804
|
+
# -------------------------------------------------------------------
|
|
805
|
+
# Escritura
|
|
806
|
+
# -------------------------------------------------------------------
|
|
807
|
+
|
|
808
|
+
def write_type(self, type_def: TypeDef, value: Any, w: Writer, scope: Scope | None,
|
|
809
|
+
fields: dict[str, Any], root: dict | None = None,
|
|
810
|
+
parent: dict | None = None) -> None:
|
|
811
|
+
if isinstance(type_def, str):
|
|
812
|
+
if type_def in PRIMITIVES:
|
|
813
|
+
PRIMITIVES[type_def].write(value, w)
|
|
814
|
+
return
|
|
815
|
+
resolved = self._resolve_named_type(type_def, scope)
|
|
816
|
+
if resolved is None:
|
|
817
|
+
raise UnknownTypeError(type_def)
|
|
818
|
+
return self.write_type(resolved, value, w, scope, fields, root, parent)
|
|
819
|
+
|
|
820
|
+
if isinstance(type_def, list) and len(type_def) == 2:
|
|
821
|
+
base, opts = type_def
|
|
822
|
+
handler = self._composite_write_handlers.get(base)
|
|
823
|
+
if handler is not None:
|
|
824
|
+
return handler(opts, value, w, scope, fields, root, parent)
|
|
825
|
+
named = self._resolve_named_type(base, scope)
|
|
826
|
+
if named is None:
|
|
827
|
+
raise UnknownTypeError(f"(tipo base compuesto) {base}")
|
|
828
|
+
substituted = substitute_type_args(named, opts if isinstance(opts, dict) else None)
|
|
829
|
+
return self.write_type(substituted, value, w, scope, fields, root, parent)
|
|
830
|
+
|
|
831
|
+
raise InvalidTypeDefinition(type_def)
|
|
832
|
+
|
|
833
|
+
# ---- cstring compuesto (["cstring", {"encoding": "..."}]) -----------------
|
|
834
|
+
# El primitivo "cstring" (primitives.py) sigue existiendo tal cual, fuerza
|
|
835
|
+
# utf-8 y no se toca. Esto es un tipo COMPUESTO adicional, solo se activa
|
|
836
|
+
# si el protocol.json usa la forma ["cstring", {opts}] en vez del string
|
|
837
|
+
# "cstring" a secas -- así que es 100% aditivo, no cambia comportamiento
|
|
838
|
+
# existente. Paridad con node-protodef (src/datatypes/utils.js: readCString/
|
|
839
|
+
# writeCString aceptan typeArgs.encoding, default 'utf8').
|
|
840
|
+
|
|
841
|
+
def _read_cstring_encoded(self, opts: dict, r: Reader, scope: Scope,
|
|
842
|
+
fields: dict, root, parent) -> str:
|
|
843
|
+
encoding = (opts or {}).get("encoding", "utf-8")
|
|
844
|
+
out = bytearray()
|
|
845
|
+
while True:
|
|
846
|
+
b = r.read_bytes(1)
|
|
847
|
+
if b == b"\x00":
|
|
848
|
+
break
|
|
849
|
+
out += b
|
|
850
|
+
return out.decode(encoding)
|
|
851
|
+
|
|
852
|
+
def _write_cstring_encoded(self, opts: dict, value: str, w: Writer, scope: Scope,
|
|
853
|
+
fields: dict, root, parent) -> None:
|
|
854
|
+
encoding = (opts or {}).get("encoding", "utf-8")
|
|
855
|
+
w.write_bytes(value.encode(encoding) + b"\x00")
|
|
856
|
+
|
|
857
|
+
# -------------------------------------------------------------------
|
|
858
|
+
# API de alto nivel: paquetes completos
|
|
859
|
+
# -------------------------------------------------------------------
|
|
860
|
+
|
|
861
|
+
def parse_packet(self, state: str, direction: str, data: bytes) -> ParsedPacket:
|
|
862
|
+
scope = self.get_scope(state, direction)
|
|
863
|
+
r = Reader(data)
|
|
864
|
+
packet = self.read_type("packet", r, scope, {})
|
|
865
|
+
return ParsedPacket(name=packet["name"], params=packet["params"], bytes_read=r.offset)
|
|
866
|
+
|
|
867
|
+
def serialize_packet(self, state: str, direction: str, name: str, params: dict) -> bytes:
|
|
868
|
+
scope = self.get_scope(state, direction)
|
|
869
|
+
w = Writer()
|
|
870
|
+
self.write_type("packet", {"name": name, "params": params}, w, scope, {})
|
|
871
|
+
return w.result()
|
|
872
|
+
|
|
873
|
+
# acceso directo a un tipo nombrado (sin pasar por "packet"), útil para tests
|
|
874
|
+
# y para parsear/serializar sub-estructuras sueltas (p.ej. un slot, un NBT)
|
|
875
|
+
def read_named(self, state: str, direction: str, type_name: str, data: bytes) -> Any:
|
|
876
|
+
scope = self.get_scope(state, direction)
|
|
877
|
+
r = Reader(data)
|
|
878
|
+
return self.read_type(type_name, r, scope, {})
|
|
879
|
+
|
|
880
|
+
def write_named(self, state: str, direction: str, type_name: str, value: Any) -> bytes:
|
|
881
|
+
scope = self.get_scope(state, direction)
|
|
882
|
+
w = Writer()
|
|
883
|
+
self.write_type(type_name, value, w, scope, {})
|
|
884
|
+
return w.result()
|