macula-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- macula_py/__init__.py +3 -0
- macula_py/blake3_hash.py +34 -0
- macula_py/bolt4.py +80 -0
- macula_py/cbor.py +304 -0
- macula_py/connection.py +456 -0
- macula_py/content.py +150 -0
- macula_py/frame.py +789 -0
- macula_py/identity.py +188 -0
- macula_py/manifest.py +266 -0
- macula_py-0.1.0.dist-info/METADATA +238 -0
- macula_py-0.1.0.dist-info/RECORD +14 -0
- macula_py-0.1.0.dist-info/WHEEL +5 -0
- macula_py-0.1.0.dist-info/licenses/LICENSE +190 -0
- macula_py-0.1.0.dist-info/top_level.txt +1 -0
macula_py/__init__.py
ADDED
macula_py/blake3_hash.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""BLAKE3 content-addressing.
|
|
2
|
+
|
|
3
|
+
Ports the real algorithm ``macula_blake3_nif`` uses (a Rust NIF over the
|
|
4
|
+
``blake3`` crate), NOT its pure-Erlang fallback path -- that fallback is
|
|
5
|
+
explicitly documented in its own source as "NOT cryptographically
|
|
6
|
+
equivalent to BLAKE3" (a SHA-256-based stand-in used only when the NIF
|
|
7
|
+
fails to load) and produces output that matches neither real BLAKE3 nor
|
|
8
|
+
plain SHA-256. Verified directly against the real NIF, not assumed
|
|
9
|
+
correct because the package name matches: ``blake3.blake3(b"macula")``
|
|
10
|
+
produces the identical 32-byte digest ``macula_blake3_nif:hash(<<"macula">>)``
|
|
11
|
+
does when run against the actual compiled NIF (see
|
|
12
|
+
``tests/test_blake3_hash.py``'s golden vectors).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import blake3 as _blake3
|
|
18
|
+
|
|
19
|
+
DIGEST_SIZE = 32
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def hash_bytes(data: bytes) -> bytes:
|
|
23
|
+
"""BLAKE3(`data`) -- a 32-byte digest."""
|
|
24
|
+
return _blake3.blake3(data).digest()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def hash_hex(data: bytes) -> str:
|
|
28
|
+
"""BLAKE3(`data`), lowercase hex-encoded."""
|
|
29
|
+
return _blake3.blake3(data).hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def verify(data: bytes, expected_hash: bytes) -> bool:
|
|
33
|
+
"""Whether `data` hashes to `expected_hash` under BLAKE3."""
|
|
34
|
+
return hash_bytes(data) == expected_hash
|
macula_py/bolt4.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""BOLT#4-style error taxonomy for CALL failures.
|
|
2
|
+
|
|
3
|
+
Ports the complete 17-entry table from ``macula_bolt4.erl`` (macula-io/macula,
|
|
4
|
+
src/peering/) -- adapted from Lightning Network's BOLT#4 onion-failure
|
|
5
|
+
codes. Every CALL ERROR frame carries one of these codes; `name` is
|
|
6
|
+
derived from `code` on the wire (see :mod:`macula_py.frame`'s ERROR builder),
|
|
7
|
+
never sent independently.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
RetryPolicy = str # one of the RETRY_POLICIES values below, kept as plain str (no enum) to match this module's simple lookup-table shape
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class Bolt4Info:
|
|
19
|
+
code: int
|
|
20
|
+
name: str
|
|
21
|
+
retry: RetryPolicy
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_TABLE: list[Bolt4Info] = [
|
|
25
|
+
Bolt4Info(0x00, "ok", "none"),
|
|
26
|
+
Bolt4Info(0x01, "unknown_next_peer", "different_path"),
|
|
27
|
+
Bolt4Info(0x02, "temporary_relay_failure", "same_path_after_backoff"),
|
|
28
|
+
Bolt4Info(0x03, "relay_disabled", "different_path"),
|
|
29
|
+
Bolt4Info(0x04, "node_not_found_at_target_relay", "caller_recompute_with_lookup"),
|
|
30
|
+
Bolt4Info(0x05, "target_realm_refused", "application"),
|
|
31
|
+
Bolt4Info(0x06, "loop_detected", "caller_recompute"),
|
|
32
|
+
Bolt4Info(0x07, "expiry_too_soon", "caller_extends_deadline"),
|
|
33
|
+
Bolt4Info(0x08, "upstream_congestion", "exponential_backoff"),
|
|
34
|
+
Bolt4Info(0x09, "invalid_path_header", "caller_recompute"),
|
|
35
|
+
Bolt4Info(0x0A, "crypto_puzzle_invalid", "crypto_drop"),
|
|
36
|
+
Bolt4Info(0x0B, "realm_not_authoritative_here", "caller_recompute_with_lookup"),
|
|
37
|
+
Bolt4Info(0x0C, "tombstoned", "application"),
|
|
38
|
+
Bolt4Info(0x0D, "payload_too_large", "application"),
|
|
39
|
+
Bolt4Info(0x0E, "signature_invalid", "crypto_drop"),
|
|
40
|
+
Bolt4Info(0x0F, "unknown_error", "log_and_caution"),
|
|
41
|
+
# A gated provider refused: the caller lacked a valid capability
|
|
42
|
+
# (UCAN) for this procedure. Not retryable as-is -- the caller must
|
|
43
|
+
# present valid authorization, so this is an application concern.
|
|
44
|
+
Bolt4Info(0x10, "unauthorized", "application"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
BY_CODE: dict[int, Bolt4Info] = {entry.code: entry for entry in _TABLE}
|
|
48
|
+
BY_NAME: dict[str, Bolt4Info] = {entry.name: entry for entry in _TABLE}
|
|
49
|
+
|
|
50
|
+
# Convenience constants for the codes this SDK's own code actually raises/checks.
|
|
51
|
+
UNKNOWN_NEXT_PEER = BY_NAME["unknown_next_peer"].code
|
|
52
|
+
UNAUTHORIZED = BY_NAME["unauthorized"].code
|
|
53
|
+
UNKNOWN_ERROR = BY_NAME["unknown_error"].code
|
|
54
|
+
TEMPORARY_RELAY_FAILURE = BY_NAME["temporary_relay_failure"].code
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UnknownBolt4CodeError(Exception):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def name_for_code(code: int) -> str:
|
|
62
|
+
entry = BY_CODE.get(code)
|
|
63
|
+
if entry is None:
|
|
64
|
+
raise UnknownBolt4CodeError(f"no BOLT#4 entry for code {code}")
|
|
65
|
+
return entry.name
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def code_for_name(name: str) -> int:
|
|
69
|
+
entry = BY_NAME.get(name)
|
|
70
|
+
if entry is None:
|
|
71
|
+
raise UnknownBolt4CodeError(f"no BOLT#4 entry named {name!r}")
|
|
72
|
+
return entry.code
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_NON_RETRYABLE = {"none", "application", "crypto_drop"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_retryable(name_or_code: str | int) -> bool:
|
|
79
|
+
entry = BY_CODE[name_or_code] if isinstance(name_or_code, int) else BY_NAME[name_or_code]
|
|
80
|
+
return entry.retry not in _NON_RETRYABLE
|
macula_py/cbor.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""Deterministic CBOR encoder/decoder.
|
|
2
|
+
|
|
3
|
+
Ports ``macula_record_cbor.erl`` (macula-io/macula, src/record/) exactly --
|
|
4
|
+
NOT a generic CBOR library (Python's own ``cbor2``/stdlib would diverge:
|
|
5
|
+
shortest-float-width canonicalization per RFC 8949 Appendix, boolean simple
|
|
6
|
+
values, indefinite-length items). Macula's wire format enforces its own,
|
|
7
|
+
narrower deterministic subset:
|
|
8
|
+
|
|
9
|
+
- Definite lengths only, smallest length-prefix encoding (RFC 8949 4.2.1).
|
|
10
|
+
- Map keys sorted by BYTEWISE order of their own encoded bytes, not by the
|
|
11
|
+
key's own natural ordering.
|
|
12
|
+
- Floats ALWAYS emitted as IEEE 754 binary64 (major 7, additional info 27)
|
|
13
|
+
-- never the shorter half/single forms, even when a value would round-trip
|
|
14
|
+
through them. Determinism requires one canonical width per value, not the
|
|
15
|
+
shortest one that happens to work.
|
|
16
|
+
- NO boolean simple values (CBOR major 7, additional info 20/21) exist on
|
|
17
|
+
this wire at all -- the station's own decoder has no clause for them.
|
|
18
|
+
Every sibling Macula SDK's convention is the same: encode true/false as
|
|
19
|
+
the integers 1/0. This module doesn't special-case Python `bool` at all;
|
|
20
|
+
it falls through the plain integer branch naturally (`bool` is an `int`
|
|
21
|
+
subclass in Python), which already produces exactly that encoding.
|
|
22
|
+
|
|
23
|
+
Python's own type system maps onto Macula's wire types with no wrapper
|
|
24
|
+
class needed, unlike Erlang (which needs a `{text, binary()}` tag to tell a
|
|
25
|
+
text string apart from a byte string -- both are just `binary()`
|
|
26
|
+
otherwise): `bytes`/`bytearray` -> byte string (major 2), `str` -> UTF-8
|
|
27
|
+
text string (major 3), each already a distinct native Python type.
|
|
28
|
+
|
|
29
|
+
Two deliberate, documented divergences from the Erlang reference, both
|
|
30
|
+
narrower (stricter) than it, never looser:
|
|
31
|
+
|
|
32
|
+
- Decoding an invalid-UTF-8 text string raises `DecodeError`. Erlang's own
|
|
33
|
+
decoder has no such check -- `{text, <<255>>}` decodes "successfully"
|
|
34
|
+
there with un-decodable bytes inside. Python has no equivalent of a
|
|
35
|
+
"text string that isn't really text," so this codec refuses it instead
|
|
36
|
+
of silently handing a caller something that looks like `str` but isn't.
|
|
37
|
+
- A CBOR map key that isn't hashable in Python (e.g. a decoded array or
|
|
38
|
+
map used as a key -- legal but rare in Erlang, where any term can be a
|
|
39
|
+
map key) raises `DecodeError` rather than crashing with `TypeError`.
|
|
40
|
+
Erlang maps have no such restriction; Python `dict` does. Relatedly,
|
|
41
|
+
Python's own `1 == 1.0` and `hash(1) == hash(1.0)` mean a map with both
|
|
42
|
+
integer key `1` and float key `1.0` collapses to one entry after
|
|
43
|
+
decoding, where Erlang would keep both distinct -- there is no way to
|
|
44
|
+
represent that map faithfully as a Python `dict`. No real Macula frame
|
|
45
|
+
is expected to construct either case (every real field name is an atom,
|
|
46
|
+
encoding as a scalar text-string key), so this is a documented
|
|
47
|
+
representational gap versus a Python `dict`'s own limits, not something
|
|
48
|
+
this codec works around.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import math
|
|
54
|
+
import struct
|
|
55
|
+
|
|
56
|
+
MAX_UINT64 = 0xFFFFFFFFFFFFFFFF
|
|
57
|
+
# Deliberately -2**64, not -2**63: mirrors macula_record_cbor.erl's own
|
|
58
|
+
# asymmetric bound exactly (`-(?MAX_UINT64 + 1)`), not a signed-64-bit
|
|
59
|
+
# integer's natural range. The name pairs with MAX_UINT64 (this wire's
|
|
60
|
+
# actual admissibility bound), not with a two's-complement width.
|
|
61
|
+
MIN_INT64 = -(MAX_UINT64 + 1)
|
|
62
|
+
|
|
63
|
+
# Bounds recursion for decode_one's own descent into nested arrays/maps --
|
|
64
|
+
# adversarial or corrupt input (e.g. ~1500 levels of single-element nested
|
|
65
|
+
# arrays) would otherwise blow Python's call stack with a bare
|
|
66
|
+
# RecursionError instead of the DecodeError this module's contract promises.
|
|
67
|
+
# Far beyond anything a real macula frame nests.
|
|
68
|
+
_MAX_DECODE_DEPTH = 64
|
|
69
|
+
|
|
70
|
+
Value = None | bool | int | float | bytes | bytearray | str | list | tuple | dict
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DecodeError(Exception):
|
|
74
|
+
"""Raised when a buffer isn't a valid deterministic-CBOR encoding of a supported value."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_encodable_int(n: int) -> bool:
|
|
78
|
+
"""Can this integer be rendered as major 0 / major 1 on this wire?
|
|
79
|
+
|
|
80
|
+
Exported so a caller that must decide admissibility BEFORE encoding
|
|
81
|
+
(payload validation) can ask rather than restate the bound.
|
|
82
|
+
"""
|
|
83
|
+
# isinstance, not a bare comparison: a bool passes (it legitimately
|
|
84
|
+
# encodes via the plain-integer path, see _encode_into), but a float
|
|
85
|
+
# like 1.5 must NOT report itself encodable here even though it
|
|
86
|
+
# numerically satisfies the bound -- floats take a completely
|
|
87
|
+
# different wire encoding (always binary64), never major 0/1.
|
|
88
|
+
return isinstance(n, int) and MIN_INT64 <= n <= MAX_UINT64
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def encode(value: Value) -> bytes:
|
|
92
|
+
"""Encode `value` as deterministic CBOR. Raises TypeError/ValueError for anything this wire can't carry."""
|
|
93
|
+
out = bytearray()
|
|
94
|
+
_encode_into(value, out)
|
|
95
|
+
return bytes(out)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _encode_into(value: Value, out: bytearray) -> None:
|
|
99
|
+
if value is None:
|
|
100
|
+
out.append(0xF6) # major 7, simple value 22 (null)
|
|
101
|
+
elif isinstance(value, int):
|
|
102
|
+
# `bool` is an `int` subclass in Python, so True/False fall
|
|
103
|
+
# through here too -- isinstance(True, int) is True, and
|
|
104
|
+
# True >= 0 / int(True) == 1 both hold. That is deliberate: this
|
|
105
|
+
# wire has no CBOR-boolean simple value at all (see module doc),
|
|
106
|
+
# and encoding True/False as the plain integers 1/0 is exactly
|
|
107
|
+
# the convention every sibling Macula SDK follows. Do not add a
|
|
108
|
+
# dedicated `isinstance(value, bool)` branch above this one.
|
|
109
|
+
if not is_encodable_int(value):
|
|
110
|
+
raise ValueError(f"integer {value} does not fit this wire's 64-bit signed/unsigned range")
|
|
111
|
+
if value >= 0:
|
|
112
|
+
_encode_head_into(0, value, out)
|
|
113
|
+
else:
|
|
114
|
+
_encode_head_into(1, -1 - value, out)
|
|
115
|
+
elif isinstance(value, float):
|
|
116
|
+
# Erlang arithmetic structurally cannot produce NaN or an
|
|
117
|
+
# infinity (it raises badarith instead), so macula_record_cbor.erl
|
|
118
|
+
# never had to reject them -- but Python can construct both
|
|
119
|
+
# directly (float('nan')/float('inf')), and the station's decoder
|
|
120
|
+
# has no clause that accepts them (confirmed live: sending either
|
|
121
|
+
# gets a bad_frame, not a value back). Reject here rather than
|
|
122
|
+
# silently emitting bytes the peer will only ever drop.
|
|
123
|
+
if not math.isfinite(value):
|
|
124
|
+
raise ValueError(f"{value!r} has no representation on this wire (Erlang floats are always finite)")
|
|
125
|
+
# ALWAYS binary64 -- see module doc. struct.pack('>d', ...) gives
|
|
126
|
+
# the big-endian IEEE 754 binary64 bytes RFC 8949 major 7 wants.
|
|
127
|
+
out.append((7 << 5) | 27)
|
|
128
|
+
out.extend(struct.pack(">d", value))
|
|
129
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
130
|
+
_encode_head_into(2, len(value), out)
|
|
131
|
+
out.extend(value)
|
|
132
|
+
elif isinstance(value, str):
|
|
133
|
+
encoded = value.encode("utf-8")
|
|
134
|
+
_encode_head_into(3, len(encoded), out)
|
|
135
|
+
out.extend(encoded)
|
|
136
|
+
elif isinstance(value, (list, tuple)):
|
|
137
|
+
_encode_head_into(4, len(value), out)
|
|
138
|
+
for item in value:
|
|
139
|
+
_encode_into(item, out)
|
|
140
|
+
elif isinstance(value, dict):
|
|
141
|
+
_encode_map_into(value, out)
|
|
142
|
+
else:
|
|
143
|
+
raise TypeError(f"value of type {type(value).__name__} cannot be encoded on this wire")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _encode_map_into(value: dict, out: bytearray) -> None:
|
|
147
|
+
# Encode each key/value independently, then sort pairs by the KEY'S
|
|
148
|
+
# OWN ENCODED BYTES (bytewise comparison) -- exactly what the
|
|
149
|
+
# deterministic-CBOR spec requires, matching macula_record_cbor.erl's
|
|
150
|
+
# own `lists:sort/1` over `{encode(K), encode(V)}` pairs.
|
|
151
|
+
pairs = [(encode(k), encode(v)) for k, v in value.items()]
|
|
152
|
+
pairs.sort(key=lambda pair: pair[0])
|
|
153
|
+
_encode_head_into(5, len(value), out)
|
|
154
|
+
for k_bytes, v_bytes in pairs:
|
|
155
|
+
out.extend(k_bytes)
|
|
156
|
+
out.extend(v_bytes)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _encode_head_into(major_type: int, n: int, out: bytearray) -> None:
|
|
160
|
+
mt = major_type << 5
|
|
161
|
+
if n <= 23:
|
|
162
|
+
out.append(mt | n)
|
|
163
|
+
elif n <= 0xFF:
|
|
164
|
+
out.append(mt | 24)
|
|
165
|
+
out.append(n)
|
|
166
|
+
elif n <= 0xFFFF:
|
|
167
|
+
out.append(mt | 25)
|
|
168
|
+
out.extend(struct.pack(">H", n))
|
|
169
|
+
elif n <= 0xFFFFFFFF:
|
|
170
|
+
out.append(mt | 26)
|
|
171
|
+
out.extend(struct.pack(">I", n))
|
|
172
|
+
elif n <= MAX_UINT64:
|
|
173
|
+
out.append(mt | 27)
|
|
174
|
+
out.extend(struct.pack(">Q", n))
|
|
175
|
+
else:
|
|
176
|
+
raise ValueError(f"length/count {n} exceeds 64 bits")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def decode(data: bytes) -> Value:
|
|
180
|
+
"""Decode a single deterministic-CBOR value. The ENTIRE buffer must be one value -- trailing bytes raise DecodeError."""
|
|
181
|
+
value, consumed = decode_one(data)
|
|
182
|
+
if consumed != len(data):
|
|
183
|
+
raise DecodeError(f"{len(data) - consumed} trailing byte(s) after a complete value")
|
|
184
|
+
return value
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def decode_one(data: bytes, offset: int = 0, _depth: int = 0) -> tuple[Value, int]:
|
|
188
|
+
"""Decode exactly one value starting at `offset`. Returns (value, new_offset) -- new_offset is where the NEXT value would start, not a count.
|
|
189
|
+
|
|
190
|
+
Exposed (not just `decode`) so a frame-stream reader can parse one
|
|
191
|
+
length-prefixed frame's body without first knowing exactly where it
|
|
192
|
+
ends. `_depth` is an internal recursion guard, not part of the public
|
|
193
|
+
signature -- callers should never pass it.
|
|
194
|
+
"""
|
|
195
|
+
if _depth > _MAX_DECODE_DEPTH:
|
|
196
|
+
raise DecodeError(f"nesting exceeds {_MAX_DECODE_DEPTH} levels")
|
|
197
|
+
if offset >= len(data):
|
|
198
|
+
raise DecodeError("unexpected end of buffer")
|
|
199
|
+
first = data[offset]
|
|
200
|
+
major = first >> 5
|
|
201
|
+
ai = first & 0x1F
|
|
202
|
+
offset += 1
|
|
203
|
+
|
|
204
|
+
if major == 7:
|
|
205
|
+
if ai == 22:
|
|
206
|
+
return None, offset
|
|
207
|
+
if ai == 25:
|
|
208
|
+
return _decode_half_float(data, offset)
|
|
209
|
+
if ai == 26:
|
|
210
|
+
return _decode_struct(data, offset, ">f", 4)
|
|
211
|
+
if ai == 27:
|
|
212
|
+
return _decode_struct(data, offset, ">d", 8)
|
|
213
|
+
raise DecodeError(f"unsupported major-7 additional info {ai} (no boolean/undefined/indefinite on this wire)")
|
|
214
|
+
|
|
215
|
+
count, offset = _decode_count(ai, data, offset)
|
|
216
|
+
|
|
217
|
+
if major == 0:
|
|
218
|
+
return count, offset
|
|
219
|
+
if major == 1:
|
|
220
|
+
return -1 - count, offset
|
|
221
|
+
if major == 2:
|
|
222
|
+
_require(data, offset, count)
|
|
223
|
+
return bytes(data[offset : offset + count]), offset + count
|
|
224
|
+
if major == 3:
|
|
225
|
+
_require(data, offset, count)
|
|
226
|
+
raw = data[offset : offset + count]
|
|
227
|
+
try:
|
|
228
|
+
text = raw.decode("utf-8")
|
|
229
|
+
except UnicodeDecodeError as e:
|
|
230
|
+
raise DecodeError(f"text string is not valid UTF-8: {e}") from e
|
|
231
|
+
return text, offset + count
|
|
232
|
+
if major == 4:
|
|
233
|
+
items = []
|
|
234
|
+
for _ in range(count):
|
|
235
|
+
item, offset = decode_one(data, offset, _depth + 1)
|
|
236
|
+
items.append(item)
|
|
237
|
+
return items, offset
|
|
238
|
+
if major == 5:
|
|
239
|
+
result: dict = {}
|
|
240
|
+
for _ in range(count):
|
|
241
|
+
key, offset = decode_one(data, offset, _depth + 1)
|
|
242
|
+
val, offset = decode_one(data, offset, _depth + 1)
|
|
243
|
+
try:
|
|
244
|
+
result[key] = val
|
|
245
|
+
except TypeError as e:
|
|
246
|
+
# A decoded array or map used as a map key -- legal in
|
|
247
|
+
# Erlang (any term can be a map key), unrepresentable as a
|
|
248
|
+
# Python dict key. See module doc's divergence note.
|
|
249
|
+
raise DecodeError(f"map key of type {type(key).__name__} is not usable as a Python dict key") from e
|
|
250
|
+
return result, offset
|
|
251
|
+
|
|
252
|
+
raise DecodeError(f"unsupported major type {major}")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _decode_count(ai: int, data: bytes, offset: int) -> tuple[int, int]:
|
|
256
|
+
if ai <= 23:
|
|
257
|
+
return ai, offset
|
|
258
|
+
if ai == 24:
|
|
259
|
+
_require(data, offset, 1)
|
|
260
|
+
return data[offset], offset + 1
|
|
261
|
+
if ai == 25:
|
|
262
|
+
_require(data, offset, 2)
|
|
263
|
+
return struct.unpack_from(">H", data, offset)[0], offset + 2
|
|
264
|
+
if ai == 26:
|
|
265
|
+
_require(data, offset, 4)
|
|
266
|
+
return struct.unpack_from(">I", data, offset)[0], offset + 4
|
|
267
|
+
if ai == 27:
|
|
268
|
+
_require(data, offset, 8)
|
|
269
|
+
return struct.unpack_from(">Q", data, offset)[0], offset + 8
|
|
270
|
+
raise DecodeError(f"unsupported additional info {ai} (indefinite-length items are not on this wire)")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _decode_struct(data: bytes, offset: int, fmt: str, size: int) -> tuple[float, int]:
|
|
274
|
+
_require(data, offset, size)
|
|
275
|
+
value = struct.unpack_from(fmt, data, offset)[0]
|
|
276
|
+
if not math.isfinite(value):
|
|
277
|
+
# Symmetric with the encode-side rejection: Erlang can never
|
|
278
|
+
# produce or accept a NaN/infinity float, so a peer claiming to
|
|
279
|
+
# send one is sending something no real macula peer ever would.
|
|
280
|
+
raise DecodeError(f"{value!r} has no representation this wire's own encoder could have produced")
|
|
281
|
+
return value, offset + size
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _decode_half_float(data: bytes, offset: int) -> tuple[float, int]:
|
|
285
|
+
# We only ever EMIT binary64, but a conforming peer may send the
|
|
286
|
+
# shorter forms, so this codec accepts them on decode. Python's
|
|
287
|
+
# struct module has no native IEEE 754 binary16, so unpack by hand.
|
|
288
|
+
_require(data, offset, 2)
|
|
289
|
+
bits = struct.unpack_from(">H", data, offset)[0]
|
|
290
|
+
sign = -1.0 if (bits & 0x8000) else 1.0
|
|
291
|
+
exponent = (bits >> 10) & 0x1F
|
|
292
|
+
fraction = bits & 0x3FF
|
|
293
|
+
if exponent == 0:
|
|
294
|
+
value = sign * (2.0**-14) * (fraction / 1024.0)
|
|
295
|
+
elif exponent < 31:
|
|
296
|
+
value = sign * (2.0 ** (exponent - 15)) * (1 + fraction / 1024.0)
|
|
297
|
+
else:
|
|
298
|
+
raise DecodeError("half-float NaN/infinity has no representation this codec accepts")
|
|
299
|
+
return value, offset + 2
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _require(data: bytes, offset: int, n: int) -> None:
|
|
303
|
+
if offset + n > len(data):
|
|
304
|
+
raise DecodeError("unexpected end of buffer")
|