protolib 0.2.2__tar.gz

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.
@@ -0,0 +1,593 @@
1
+ Metadata-Version: 2.4
2
+ Name: protolib
3
+ Version: 0.2.2
4
+ Summary: Pure-Python, from-scratch declarative binary protocol (de)serializer — node-protodef style, for Minecraft, ClassiCube and any other binary protocol.
5
+ Author: R
6
+ License: MIT
7
+ Keywords: protocol,binary,serialization,minecraft,classicube,protodef,networking,packet
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Networking
18
+ Classifier: Topic :: Games/Entertainment
19
+ Requires-Python: >=3.9
20
+ Description-Content-Type: text/markdown
21
+ Provides-Extra: yaml
22
+ Requires-Dist: pyyaml>=6.0; extra == "yaml"
23
+
24
+ # protolib (Python)
25
+
26
+ A from-scratch, pure-Python implementation of a
27
+ [node-protodef](https://github.com/ProtoDef-io/ProtoDef)-style system:
28
+ you describe the shape of a binary protocol's packets in a declarative
29
+ file (`.yml` or `.json`), and the library takes care of parsing raw
30
+ bytes into a Python `dict` and serializing a `dict` back into bytes —
31
+ without you having to hand-write a parser for every single packet.
32
+
33
+ Already tested against two real protocols, each with its own `.yml`
34
+ (hand-written/maintained) and its equivalent `.json` (the native
35
+ `["type", opts]` form, generated by the loader itself — so you can see
36
+ exactly what the engine understands internally, and it's useful for
37
+ consuming the protocol from something that doesn't want to parse YAML):
38
+ - **Minecraft Java 1.8.9** (`examples/example_protocol.yml` /
39
+ `.json`) — advanced patterns: parametrizable switch, relative
40
+ `compareTo`, bitfield.
41
+ - **Minecraft Classic / ClassiCube 0x07 + CPE**
42
+ (`examples/classicube_protocol.yml` / `.json`) — a complete
43
+ fixed-size protocol, no varints, with CPE extensions.
44
+
45
+ ---
46
+
47
+ ## 1. The idea in 30 seconds
48
+
49
+ ```python
50
+ from protolib.core import Protocol
51
+
52
+ proto = Protocol("my_protocol.yml")
53
+
54
+ # bytes -> dict
55
+ parsed = proto.parse_packet("play", "toClient", raw_data)
56
+ print(parsed.name, parsed.params, parsed.bytes_read)
57
+
58
+ # dict -> bytes
59
+ data = proto.serialize_packet("play", "toClient", "spawn_player", {
60
+ "playerId": 5, "x": 320, "y": 2080, "z": 320,
61
+ })
62
+ ```
63
+
64
+ Everything else in this document is about how to declare
65
+ `my_protocol.yml` so that `parse_packet`/`serialize_packet` know which
66
+ fields to expect.
67
+
68
+ ---
69
+
70
+ ## 2. Install / use
71
+
72
+ No dependencies beyond `pyyaml` (for the `.yml` format). Used as a
73
+ local package, no need to install it:
74
+
75
+ ```python
76
+ import sys
77
+ sys.path.insert(0, "path/to/the/folder/containing/protolib")
78
+ from protolib.core import Protocol
79
+ ```
80
+
81
+ `Protocol(...)` accepts:
82
+ - a path to a `.json`, `.yml`, or `.yaml` file
83
+ - an in-memory string with JSON or YAML content
84
+ - an already-parsed `dict` (the "native" node-protodef format)
85
+
86
+ ---
87
+
88
+ ## 3. Structure of a protocol file
89
+
90
+ ```yaml
91
+ types:
92
+ # types valid across the ENTIRE protocol, regardless of state/direction
93
+ varint: native
94
+ myCustomType:
95
+ container:
96
+ - name: x
97
+ type: varint
98
+
99
+ <state_name>: # e.g.: "play", "login", "handshaking"...
100
+ toClient:
101
+ types:
102
+ # types that only exist for this direction/state, take
103
+ # PRIORITY over the ones above if there's a name collision
104
+ packet: ...
105
+ toServer:
106
+ types:
107
+ packet: ...
108
+ ```
109
+
110
+ - **global `types:`** — shared types (primitives, common structs).
111
+ - **`<state>.toClient.types` / `<state>.toServer.types`** — the real
112
+ entry point to parse a packet is always a type named `packet` inside
113
+ one of these blocks. `parse_packet(state, direction, data)` looks for
114
+ that `packet`, reads it, and returns `{name, params}` based on how
115
+ you built it (see the "packet container with mapper+switch" pattern
116
+ in section 7).
117
+
118
+ > **Important:** `Protocol.parse_packet`/`serialize_packet` expect the
119
+ > `packet` type to ALWAYS have exactly two fields named `name` and
120
+ > `params` — that's what builds the `ParsedPacket(name=...,
121
+ > params=...)` you get back. The standard pattern (used in both
122
+ > example protocols) is:
123
+ >
124
+ > ```yaml
125
+ > packet:
126
+ > container:
127
+ > - name: name
128
+ > type:
129
+ > mapper: # wire integer -> packet name
130
+ > type: u8 # (or varint, depending on your protocol)
131
+ > mappings:
132
+ > '0x00': identification
133
+ > - name: params
134
+ > type:
135
+ > switch: # packet name -> its payload type
136
+ > compareTo: name
137
+ > fields:
138
+ > identification: packet_identification
139
+ > ```
140
+ >
141
+ > If your `packet` doesn't follow this exact shape, `parse_packet` will
142
+ > fail with `KeyError: 'name'` — it's not a weird limitation of your
143
+ > protocol, it's literally how this library knows what to return.
144
+
145
+ `state` and `direction` don't have to be called that — they're simply
146
+ the two keys you'll later use to call
147
+ `parse_packet`/`serialize_packet`. A protocol without real "states"
148
+ (like ClassiCube) still declares a single `play:` just to have
149
+ somewhere to hang `toClient`/`toServer`.
150
+
151
+ ---
152
+
153
+ ## 4. The native `.json` format (without the YAML shorthand)
154
+
155
+ Everything in section 3 can be written directly in JSON, without going
156
+ through YAML — it's the format already used by `minecraft-data` and
157
+ the original `node-protodef`, and this library understands it with no
158
+ translation needed. The difference with the `.yml` shorthand is one of
159
+ **shape**, not semantics: each composite type (`container`, `switch`,
160
+ `array`, `bitfield`, etc.) is written as a 2-element list instead of a
161
+ single-key mapping:
162
+
163
+ ```json
164
+ ["<base_type_name>", <options>]
165
+ ```
166
+
167
+ The same `switch` from section 6, in YAML shorthand:
168
+
169
+ ```yaml
170
+ type:
171
+ switch:
172
+ compareTo: name
173
+ fields:
174
+ identification: packet_identification
175
+ ```
176
+
177
+ ...is this in native JSON:
178
+
179
+ ```json
180
+ {
181
+ "type": [
182
+ "switch",
183
+ {
184
+ "compareTo": "name",
185
+ "fields": { "identification": "packet_identification" }
186
+ }
187
+ ]
188
+ }
189
+ ```
190
+
191
+ And a `container` (which carries a list of fields instead of an
192
+ options-dict) follows the same `["container", [...]]` pattern:
193
+
194
+ ```json
195
+ "packet_identification": [
196
+ "container",
197
+ [
198
+ { "name": "protocolVersion", "type": "u8" },
199
+ { "name": "name", "type": "string64" }
200
+ ]
201
+ ]
202
+ ```
203
+
204
+ **Why would you still want to write `.yml`?** Because by hand, several
205
+ levels of nested 2-element lists are error-prone and hard to read (does
206
+ that `]` close the `container` or the outer `switch`?). The loader
207
+ (`protolib/loader.py`) automatically translates the `.yml` shorthand
208
+ into this native form before `Protocol` uses it — that's why
209
+ `example_protocol.json` and `example_protocol.yml` (or
210
+ `classicube_protocol.json`/`.yml`) are **exactly equivalent**: they
211
+ serialize and parse byte-for-byte the same, one is just more
212
+ convenient to write/maintain by hand than the other.
213
+
214
+ **So when should you use the raw `.json` directly?**
215
+ - You already have a protocol from `minecraft-data` or another
216
+ `node-protodef` project in JSON and want to reuse it as-is, without
217
+ rewriting it.
218
+ - You want to generate the protocol programmatically from another
219
+ language or script that has no business knowing the shorthand
220
+ syntax.
221
+ - You want to see "what the engine actually understands internally"
222
+ with no ambiguity — useful for debugging a `.yml` that isn't loading
223
+ the way you expected: generate its `.json` (see below) and check if
224
+ the translation came out as you thought.
225
+
226
+ **Generating `.json` from an existing `.yml`**, using the loader
227
+ itself (this guarantees it's faithful to what `Protocol` will actually
228
+ read, not a separately hand-made translation):
229
+
230
+ ```python
231
+ import json
232
+ from protolib.loader import load_protocol_dict
233
+
234
+ d = load_protocol_dict("my_protocol.yml")
235
+ with open("my_protocol.json", "w", encoding="utf-8") as f:
236
+ json.dump(d, f, indent=2, ensure_ascii=False)
237
+ ```
238
+
239
+ This is how `examples/classicube_protocol.json` was generated, and
240
+ `examples/example_protocol.json` already existed in this project.
241
+
242
+ ---
243
+
244
+ ## 5. `native`: how primitive types get registered
245
+
246
+ `native` in the `.yml` means "don't define this here, look it up by
247
+ name in `protolib/primitives.py`". Already registered:
248
+
249
+ | Name | Size | Notes |
250
+ |---|---|---|
251
+ | `u8`/`i8` | 1 byte | unsigned/signed |
252
+ | `u16`/`i16` | 2 bytes BE | |
253
+ | `u32`/`i32` | 4 bytes BE | |
254
+ | `u64`/`i64` | 8 bytes BE | |
255
+ | `f32`/`f64` | 4/8 bytes BE | float/double |
256
+ | `lu16`…`lf64` | same, little-endian | `l` prefix |
257
+ | `varint`/`varlong` | variable | signed zigzag LEB128 — no, see `zigzag32`/`zigzag64` for those |
258
+ | `bool` | 1 byte | |
259
+ | `void` | 0 bytes | useful as an empty case in a `switch` |
260
+ | `cstring` | variable | `\0`-terminated |
261
+ | `UUID` | 16 bytes | |
262
+ | `nbt`/`optionalNbt` | variable | Minecraft's NBT format |
263
+ | `string64` | 64 fixed bytes | CP437, space-padded (ClassiCube) |
264
+ | `buffer1024`| 1024 fixed bytes | raw, `0x00`-padded (ClassiCube) |
265
+ | `fixedCoord`/`fixedCoordDelta` | 2/1 bytes | fixed-point *32 (ClassiCube) |
266
+
267
+ **If your protocol needs a new type that isn't a composition of the
268
+ ones above** (say, a fixed-length string with a different encoding, or
269
+ a buffer of another fixed size), add it in Python following the
270
+ pattern of `make_fixed_cp437_string`/`make_fixed_buffer` in
271
+ `primitives.py`, and register it in the `PRIMITIVES` dict at the end
272
+ of the file. After that it's available as `native` in any `.yml`, for
273
+ you and for anyone else using this library.
274
+
275
+ Non-native types (`pstring`, `container`, `switch`, `array`,
276
+ `bitfield`, `bitflags`, `buffer`, `mapper`, `option`, `count`) are
277
+ resolved by the Python engine (`core.py`) according to their own logic
278
+ — explained below with examples.
279
+
280
+ ---
281
+
282
+ ## 6. Composite types, one by one
283
+
284
+ ### `container` — groups named fields, in order
285
+
286
+ ```yaml
287
+ packet_login:
288
+ container:
289
+ - name: protocolVersion
290
+ type: varint
291
+ - name: username
292
+ type: string
293
+ ```
294
+
295
+ Reads as `{"protocolVersion": ..., "username": ...}`.
296
+
297
+ **`anon: true` field**: instead of being stored under its own name, its
298
+ sub-fields get merged directly into the parent container's `dict`.
299
+ Useful for "breaking up" a bitfield or a switch into several loose
300
+ fields at the same level (see bitfield below).
301
+
302
+ **`condition` field**: the field is only read/written if the condition
303
+ (an expression over already-read fields) is true. Syntax: same path
304
+ language as `compareTo` (section 7).
305
+
306
+ ### `array` — list of N elements of the same type
307
+
308
+ ```yaml
309
+ type:
310
+ array:
311
+ countType: varint # reads a varint first, that's the length
312
+ # count: otherField # alternative: uses the VALUE of another already-read field
313
+ type: myItemType
314
+ ```
315
+
316
+ `countType` prepends an integer that's read/written automatically.
317
+ `count`, on the other hand, references a sibling field already present
318
+ (typically built with the `count` type, see section 9) — it doesn't
319
+ write anything new, it just reads that existing value as the array's
320
+ length.
321
+
322
+ ### `switch` — picks the field's type based on another field's value
323
+
324
+ ```yaml
325
+ type:
326
+ switch:
327
+ compareTo: name # path to an already-read field (see section 7)
328
+ fields:
329
+ valueA: typeForA
330
+ valueB: typeForB
331
+ default: void # optional, if nothing matches
332
+ ```
333
+
334
+ This is the central piece for "the `params` field depends on what the
335
+ `name` field says" — the pattern of every network packet with an
336
+ id/name.
337
+
338
+ ### `mapper` — integer ⟷ symbolic name
339
+
340
+ ```yaml
341
+ type:
342
+ mapper:
343
+ type: varint
344
+ mappings:
345
+ '0x00': handshake
346
+ '0x01': status_request
347
+ ```
348
+
349
+ When reading, a `0x00` in the stream becomes the string `"handshake"`
350
+ in the resulting `dict` (and vice versa when writing). This way the
351
+ `switch` above can compare against readable names (`compareTo: name`)
352
+ instead of the raw packet id integer.
353
+
354
+ ### `bitfield` — several fields packed into less than 1 byte each
355
+
356
+ ```yaml
357
+ type:
358
+ bitfield:
359
+ - name: type
360
+ size: 3
361
+ signed: false
362
+ - name: index
363
+ size: 5
364
+ signed: false
365
+ ```
366
+
367
+ Declares sub-fields that each take up `size` bits, MSB first, within
368
+ the minimum necessary bytes (if they don't fill an exact byte, padding
369
+ is added). Almost always combined with `anon: true` on the container
370
+ that holds it, so that `type`/`index` end up as loose fields instead of
371
+ nested under an extra name.
372
+
373
+ ### `bitflags` — an integer as a named set of booleans
374
+
375
+ ```yaml
376
+ type:
377
+ bitflags:
378
+ type: u8
379
+ flags: [air, water, lava, null, fire] # position = bit number
380
+ # or, as a dict: {air: 0x01, water: 0x02, ...}
381
+ ```
382
+
383
+ Returns `{"air": true, "water": false, ..., "_value": <raw integer>}`.
384
+
385
+ ### `buffer` — raw bytes (uninterpreted)
386
+
387
+ ```yaml
388
+ type:
389
+ buffer:
390
+ count: 1024 # fixed size, or a reference to a field
391
+ # countType: varint # alternative: prefixed length
392
+ # rest: true # alternative: "whatever's left in the frame"
393
+ ```
394
+
395
+ ### `option` — present only if a preceding boolean flag says so
396
+
397
+ ```yaml
398
+ type:
399
+ option: myType
400
+ ```
401
+
402
+ Reads a `bool` first; if `true`, reads `myType` right after; if
403
+ `false`, the value is `None` and nothing else is read.
404
+
405
+ ### `pstring` — string with a prefixed length
406
+
407
+ ```yaml
408
+ type:
409
+ pstring:
410
+ countType: varint # how many length-bytes come before it
411
+ # encoding: utf-8 # default
412
+ ```
413
+
414
+ The `string:` shortcut already ships built with this
415
+ (`countType: varint`, used in Minecraft Java). For ClassiCube,
416
+ `string64` is used instead (ALWAYS fixed size, no prefix — see
417
+ section 5), since that protocol doesn't prefix a length.
418
+
419
+ ---
420
+
421
+ ## 7. `compareTo` and field paths (`../`, `/`)
422
+
423
+ `compareTo` (in a `switch`) and `condition` (on a `container` field)
424
+ use the same tiny path language to refer to "a field that's already
425
+ been read":
426
+
427
+ | Syntax | Means |
428
+ |---|---|
429
+ | `fieldName` | sibling field, same container |
430
+ | `../fieldName` | goes up one level: the container that holds this one |
431
+ | `../../fieldName` | goes up two levels |
432
+ | `/fieldName` | absolute, from the root of the whole packet |
433
+
434
+ **Important gotcha, already documented with a full example in
435
+ `examples/README.md`:** `../` only goes up one real level when the
436
+ container is written **inline** (literally inside `array.type` or
437
+ another container). If instead you reference a type **by name**
438
+ (`type: myNamedType`), that named type always adds its own nesting
439
+ level — so a `../field` inside it reaches, at most, the named
440
+ container itself, not the real parent further up. If your switch needs
441
+ to see a field higher up in the hierarchy, declare the container
442
+ inline at the point of use instead of moving it into a separately
443
+ named type.
444
+
445
+ ---
446
+
447
+ ## 8. Parametrizable types (`$arg`)
448
+
449
+ A named type can declare a `$`-prefixed placeholder instead of a fixed
450
+ value, and whoever uses it decides the real value at that point:
451
+
452
+ ```yaml
453
+ types:
454
+ itemByType:
455
+ switch:
456
+ compareTo: $compareTo # placeholder -- the name after
457
+ fields: # the $ is arbitrary, you choose it
458
+ '0': i8
459
+ '1': varint
460
+
461
+ myContainer:
462
+ container:
463
+ - name: type
464
+ type: u8
465
+ - name: value
466
+ type:
467
+ - itemByType # referenced as [name, options]
468
+ - compareTo: type # here the real $compareTo gets resolved
469
+ ```
470
+
471
+ This defines a "generic" type once and reuses it with a different
472
+ comparison field depending on context — it's exactly how
473
+ `minecraft-data` defines `entityMetadataItem`.
474
+
475
+ ---
476
+
477
+ ## 9. `count` — a length-prefix declared apart from the array/buffer
478
+
479
+ For the (rare, but real in some protocols) case where an array's
480
+ length prefix isn't attached to that array but is instead its own
481
+ field elsewhere in the container:
482
+
483
+ ```yaml
484
+ container:
485
+ - name: itemCount
486
+ type:
487
+ count:
488
+ type: varint
489
+ countFor: items # when WRITING, ignores the given value and
490
+ - name: items # writes len(items) automatically
491
+ type:
492
+ array:
493
+ count: itemCount # when READING, references that already-read field
494
+ type: myItem
495
+ ```
496
+
497
+ ---
498
+
499
+ ## 10. The 4 advanced Minecraft Java patterns
500
+
501
+ Covered with working, commented examples in
502
+ [`examples/README.md`](examples/README.md) +
503
+ [`examples/example_protocol.yml`](examples/example_protocol.yml)
504
+ (its `.json` equivalent, already translated to the native `["type",
505
+ opts]` form, lives in
506
+ [`examples/example_protocol.json`](examples/example_protocol.json)):
507
+
508
+ 1. Parametrizable named type with `$arg` (section 8 of this doc).
509
+ 2. `compareTo: ../field` and the inline-vs-named container gotcha
510
+ (section 7).
511
+ 3. Array of containers with a `switch` inside that looks at a field of
512
+ the array's parent container.
513
+ 4. `bitfield` packed into 1 byte + `switch` that uses that field to
514
+ pick the type of another sibling field (real pattern from
515
+ `entity_metadata` in Minecraft 1.8.9-1.16).
516
+
517
+ Run `python examples/demo.py` to see them in action.
518
+
519
+ ---
520
+
521
+ ## 11. Full reference protocol: ClassiCube
522
+
523
+ [`examples/classicube_protocol.yml`](examples/classicube_protocol.yml)
524
+ (equivalent in
525
+ [`examples/classicube_protocol.json`](examples/classicube_protocol.json),
526
+ byte-for-byte identical when serializing/parsing — generated by the
527
+ loader itself, not hand-written)
528
+ has the complete 0x07 protocol (Identification, Level Init/Data/
529
+ Finalize, Set Block, Spawn/Position/Despawn Player, Message,
530
+ Disconnect, Update User Type) plus several CPE extensions (ExtInfo/
531
+ ExtEntry, ClickDistance, CustomBlockSupportLevel, HeldBlock,
532
+ ExtPlayerList) — a good example of a protocol **with no varints**, all
533
+ fixed size, useful if your use case doesn't look like Minecraft Java's.
534
+
535
+ `examples/servidor.py` is a real, minimal server (login + generates a
536
+ world and sends it) built entirely on top of `Protocol.parse_packet` /
537
+ `serialize_packet` — it's an example of how to integrate this library
538
+ with real sockets (a `recv()` loop accumulating bytes until
539
+ `parse_packet` stops throwing `BufferUnderrun`).
540
+
541
+ ```bash
542
+ cd examples
543
+ python servidor.py # caves, port 25565
544
+ python servidor.py --modo llano # flat world
545
+ ```
546
+
547
+ ---
548
+
549
+ ## 12. Errors and what they mean
550
+
551
+ All of them live in `protolib/errors.py`:
552
+
553
+ - **`UnknownTypeError`** — a type name isn't in `native`
554
+ (`primitives.py`) nor defined in `types:` (global or scoped). Usually
555
+ a typo, or you forgot to declare it.
556
+ - **`SwitchCaseNotFound`** — a `switch` has no `fields` entry for the
557
+ value `compareTo` produced, and there's no `default` either. Check
558
+ that the comparison value resolves to what you expect (does the
559
+ `mapper` building that field convert it to a string? does
560
+ `compareTo` point to the right field using the syntax from section
561
+ 7?).
562
+ - **`InvalidTypeDefinition`** — a composite type is missing a required
563
+ option (`array` without `count`/`countType`, `buffer` without
564
+ `count`/`countType`/`rest`, etc.).
565
+ - **`BufferUnderrun`** (in `protolib/io.py`, not `errors.py`) — an
566
+ attempt was made to read more bytes than are available. In a real
567
+ server this is NOT necessarily a protocol error: it means "the full
568
+ packet hasn't arrived on the socket yet," and the correct pattern is
569
+ to catch it and wait for more bytes (see the loop in `servidor.py`).
570
+
571
+ ## Changelog
572
+
573
+ ### 0.2.1
574
+ - New primitive `buffer64`: fixed 64-byte raw bytes with automatic
575
+ `\x00` padding (same semantics as `buffer1024`, built with the same
576
+ `make_fixed_buffer` mold). Added while migrating `PocketNet` to a
577
+ single `protocol.yml` — the `data` field of `PluginMessagePacket`
578
+ (0x35) needed exactly this behavior, and the generic `buffer` with a
579
+ fixed `count` is strict (fails if the value isn't exactly N bytes
580
+ long instead of truncating/padding).
581
+ - New example: `examples/pocketnet_protocol.yml`, the complete
582
+ Minecraft Classic 0.30 + CPE protocol (55 packets) migrated from the
583
+ "one class per packet" style into a single declarative file. Tested
584
+ with byte-exact round-trips against the original `encode()`/
585
+ `decode()` on the most complex cases (packed bitfields, nested
586
+ arrays of containers, padded buffers).
587
+
588
+ ### 0.2.0
589
+ - Base version: primitives, composites (`container`, `array`,
590
+ `switch`, `mapper`, `option`, `bitfield`, `bitflags`, `buffer`,
591
+ `pstring`, `count`, `entityMetadataLoop`,
592
+ `topBitSetTerminatedArray`), JSON and YAML support, native NBT and
593
+ UUID.