weftcodec 1.2.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.
- weftcodec/__init__.py +62 -0
- weftcodec/__main__.py +6 -0
- weftcodec/ai_modes.py +3360 -0
- weftcodec/cli.py +252 -0
- weftcodec/crc32c.py +33 -0
- weftcodec/dcp.py +2108 -0
- weftcodec/dependency.py +224 -0
- weftcodec/errors.py +119 -0
- weftcodec/frame.py +455 -0
- weftcodec/hashing.py +56 -0
- weftcodec/huffman.py +212 -0
- weftcodec/identity.py +141 -0
- weftcodec/limits.py +58 -0
- weftcodec/modes.py +718 -0
- weftcodec/orbax.py +1734 -0
- weftcodec/service.py +959 -0
- weftcodec/torch.py +94 -0
- weftcodec/varint.py +88 -0
- weftcodec/wcb.py +310 -0
- weftcodec-1.2.0.dist-info/METADATA +257 -0
- weftcodec-1.2.0.dist-info/RECORD +24 -0
- weftcodec-1.2.0.dist-info/WHEEL +5 -0
- weftcodec-1.2.0.dist-info/entry_points.txt +2 -0
- weftcodec-1.2.0.dist-info/top_level.txt +1 -0
weftcodec/torch.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""PyTorch-facing dataset over a mounted WeftFS directory.
|
|
2
|
+
|
|
3
|
+
``torch`` is an optional dependency and is never imported at module import time.
|
|
4
|
+
Plain ``bytes`` access works without it; requesting a tensor raises a clear
|
|
5
|
+
``ImportError`` when PyTorch is absent.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _torch_module():
|
|
16
|
+
try:
|
|
17
|
+
return importlib.import_module("torch")
|
|
18
|
+
except ImportError as exc: # pragma: no cover - exercised without torch
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"weftcodec.torch requires PyTorch; install it with "
|
|
21
|
+
"`pip install weftcodec[torch]` (or `pip install torch`)"
|
|
22
|
+
) from exc
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WeftMapDataset:
|
|
26
|
+
"""Deterministic map-style dataset over files in a WeftFS mount.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
root:
|
|
31
|
+
Directory served by a WeftFS mount (or any directory).
|
|
32
|
+
transform:
|
|
33
|
+
Optional callable applied to the raw ``bytes`` before the item is
|
|
34
|
+
returned or converted to a tensor.
|
|
35
|
+
dtype:
|
|
36
|
+
When set (for example ``torch.uint8`` or ``torch.float32``), items are
|
|
37
|
+
returned as ``torch.frombuffer`` tensors instead of ``bytes``.
|
|
38
|
+
shape:
|
|
39
|
+
Optional shape used to reshape a returned tensor. ``-1`` can be used
|
|
40
|
+
for one inferred dimension.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
root: "str | os.PathLike[str]",
|
|
46
|
+
transform=None,
|
|
47
|
+
*,
|
|
48
|
+
dtype=None,
|
|
49
|
+
shape: "tuple[int, ...] | None" = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.root = Path(root)
|
|
52
|
+
if not self.root.is_dir():
|
|
53
|
+
raise NotADirectoryError("dataset root is not a directory: %s" % self.root)
|
|
54
|
+
self.transform = transform
|
|
55
|
+
self.dtype = dtype
|
|
56
|
+
self.shape = tuple(shape) if shape is not None else None
|
|
57
|
+
self._entries = self._walk()
|
|
58
|
+
|
|
59
|
+
def _walk(self):
|
|
60
|
+
entries = []
|
|
61
|
+
for dirpath, dirnames, filenames in os.walk(self.root):
|
|
62
|
+
dirnames.sort()
|
|
63
|
+
for name in sorted(filenames):
|
|
64
|
+
full = Path(dirpath) / name
|
|
65
|
+
if full.is_file():
|
|
66
|
+
relative = full.relative_to(self.root).as_posix()
|
|
67
|
+
entries.append((relative, full))
|
|
68
|
+
entries.sort(key=lambda entry: entry[0])
|
|
69
|
+
return entries
|
|
70
|
+
|
|
71
|
+
def __len__(self) -> int:
|
|
72
|
+
return len(self._entries)
|
|
73
|
+
|
|
74
|
+
def paths(self) -> "list[str]":
|
|
75
|
+
return [relative for relative, _ in self._entries]
|
|
76
|
+
|
|
77
|
+
def __getitem__(self, index: int):
|
|
78
|
+
if index < 0:
|
|
79
|
+
index += len(self._entries)
|
|
80
|
+
if index < 0 or index >= len(self._entries):
|
|
81
|
+
raise IndexError("dataset index %d out of range" % index)
|
|
82
|
+
relative, full = self._entries[index]
|
|
83
|
+
with open(full, "rb") as handle:
|
|
84
|
+
data = handle.read()
|
|
85
|
+
if self.transform is not None:
|
|
86
|
+
data = self.transform(data)
|
|
87
|
+
if self.dtype is None:
|
|
88
|
+
return relative, data
|
|
89
|
+
torch = _torch_module()
|
|
90
|
+
# `bytes` is read-only; torch.frombuffer needs a writable buffer.
|
|
91
|
+
tensor = torch.frombuffer(bytearray(data), dtype=self.dtype)
|
|
92
|
+
if self.shape is not None:
|
|
93
|
+
tensor = tensor.reshape(self.shape)
|
|
94
|
+
return relative, tensor
|
weftcodec/varint.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Minimal unsigned LEB128 varints (WEFTCODEC-FORMAT §1).
|
|
2
|
+
|
|
3
|
+
The decoder mirrors the Rust implementation exactly:
|
|
4
|
+
|
|
5
|
+
* at most 10 bytes;
|
|
6
|
+
* a multi-byte encoding must not end in ``0x00`` (``NonCanonical``);
|
|
7
|
+
* the tenth byte must be ``0x00`` or ``0x01`` (``Malformed`` otherwise);
|
|
8
|
+
* truncated input is ``Malformed``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from .errors import Malformed, NonCanonical
|
|
14
|
+
|
|
15
|
+
MAX_VARINT_LEN = 10
|
|
16
|
+
U64_MAX = (1 << 64) - 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def encoded_len(value: int) -> int:
|
|
20
|
+
if value < 0 or value > U64_MAX:
|
|
21
|
+
raise Malformed("varint value is outside u64")
|
|
22
|
+
n = 1
|
|
23
|
+
while value >= 0x80:
|
|
24
|
+
value >>= 7
|
|
25
|
+
n += 1
|
|
26
|
+
return n
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encode(value: int, out: bytearray) -> None:
|
|
30
|
+
"""Append the minimal LEB128 encoding of ``value`` to ``out``."""
|
|
31
|
+
if value < 0 or value > U64_MAX:
|
|
32
|
+
raise Malformed("varint value is outside u64")
|
|
33
|
+
v = value
|
|
34
|
+
while True:
|
|
35
|
+
byte = v & 0x7F
|
|
36
|
+
v >>= 7
|
|
37
|
+
if v == 0:
|
|
38
|
+
out.append(byte)
|
|
39
|
+
return
|
|
40
|
+
out.append(byte | 0x80)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def encode_bytes(value: int) -> bytes:
|
|
44
|
+
out = bytearray()
|
|
45
|
+
encode(value, out)
|
|
46
|
+
return bytes(out)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def decode(data: bytes, pos: int) -> "tuple[int, int]":
|
|
50
|
+
"""Decode one varint at ``pos``; return ``(value, next_pos)``."""
|
|
51
|
+
value = 0
|
|
52
|
+
shift = 0
|
|
53
|
+
offset = pos
|
|
54
|
+
count = 0
|
|
55
|
+
while True:
|
|
56
|
+
if offset >= len(data):
|
|
57
|
+
raise Malformed("truncated varint")
|
|
58
|
+
byte = data[offset]
|
|
59
|
+
offset += 1
|
|
60
|
+
count += 1
|
|
61
|
+
if count > MAX_VARINT_LEN:
|
|
62
|
+
raise Malformed("varint longer than 10 bytes")
|
|
63
|
+
if count == MAX_VARINT_LEN and byte > 0x01:
|
|
64
|
+
raise Malformed("varint overflows u64")
|
|
65
|
+
low = byte & 0x7F
|
|
66
|
+
if shift < 64:
|
|
67
|
+
value |= low << shift
|
|
68
|
+
elif low != 0:
|
|
69
|
+
raise Malformed("varint overflows u64")
|
|
70
|
+
if byte & 0x80 == 0:
|
|
71
|
+
if count > 1 and byte == 0x00:
|
|
72
|
+
raise NonCanonical("non-minimal varint")
|
|
73
|
+
return value, offset
|
|
74
|
+
shift += 7
|
|
75
|
+
if shift > 63:
|
|
76
|
+
raise Malformed("varint exceeds 10 bytes")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def zigzag_encode(value: int) -> int:
|
|
80
|
+
"""``(v << 1) ^ (v >> 63)`` on a 64-bit two's-complement pattern."""
|
|
81
|
+
if value < -(1 << 63) or value > (1 << 63) - 1:
|
|
82
|
+
raise Malformed("signed value is outside i64")
|
|
83
|
+
return ((value << 1) ^ (value >> 63)) & U64_MAX
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def zigzag_decode(value: int) -> int:
|
|
87
|
+
"""Inverse of :func:`zigzag_encode`."""
|
|
88
|
+
return (value >> 1) ^ -(value & 1)
|
weftcodec/wcb.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Canonical WCB bytes (WEFTCODEC-FORMAT §8).
|
|
2
|
+
|
|
3
|
+
WCB is a strict, explicit subset of deterministic CBOR. Python values map as:
|
|
4
|
+
|
|
5
|
+
=============== ==========================================
|
|
6
|
+
Python WCB
|
|
7
|
+
=============== ==========================================
|
|
8
|
+
``None`` null (tag 0x00)
|
|
9
|
+
``False`` false (0x01)
|
|
10
|
+
``True`` true (0x02)
|
|
11
|
+
``U64(n)`` unsigned int (0x03)
|
|
12
|
+
``I64(n)`` signed int (0x04)
|
|
13
|
+
``str`` text (0x05), UTF-8
|
|
14
|
+
``bytes`` bytes (0x06)
|
|
15
|
+
``list``/tuple array (0x07)
|
|
16
|
+
``dict`` map (0x08), text keys only
|
|
17
|
+
``Timestamp(n)`` timestamp (0x09), i64 nanoseconds since the epoch
|
|
18
|
+
=============== ==========================================
|
|
19
|
+
|
|
20
|
+
Tag ``0x0A..0xFF`` is ``NonCanonical`` in v1. Map keys are sorted by their
|
|
21
|
+
full encoded bytes, duplicates are rejected, and varints are minimal.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
from . import varint
|
|
29
|
+
from .errors import LimitExceeded, Malformed, NonCanonical
|
|
30
|
+
from .limits import Limits
|
|
31
|
+
|
|
32
|
+
TAG_NULL = 0x00
|
|
33
|
+
TAG_FALSE = 0x01
|
|
34
|
+
TAG_TRUE = 0x02
|
|
35
|
+
TAG_U64 = 0x03
|
|
36
|
+
TAG_I64 = 0x04
|
|
37
|
+
TAG_TEXT = 0x05
|
|
38
|
+
TAG_BYTES = 0x06
|
|
39
|
+
TAG_ARRAY = 0x07
|
|
40
|
+
TAG_MAP = 0x08
|
|
41
|
+
TAG_TIMESTAMP = 0x09
|
|
42
|
+
|
|
43
|
+
I64_MIN = -(1 << 63)
|
|
44
|
+
I64_MAX = (1 << 63) - 1
|
|
45
|
+
U64_MAX = (1 << 64) - 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class U64:
|
|
50
|
+
"""Unsigned 64-bit integer."""
|
|
51
|
+
|
|
52
|
+
value: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class I64:
|
|
57
|
+
"""Signed 64-bit integer."""
|
|
58
|
+
|
|
59
|
+
value: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class Timestamp:
|
|
64
|
+
"""Signed 64-bit nanoseconds since the Unix epoch, UTC."""
|
|
65
|
+
|
|
66
|
+
nanos: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def encode(value, limits: "Limits | None" = None) -> bytes:
|
|
70
|
+
"""Encode ``value`` to canonical WCB bytes."""
|
|
71
|
+
encoder = _Encoder(limits or Limits())
|
|
72
|
+
out = bytearray()
|
|
73
|
+
encoder.value(value, 0, out)
|
|
74
|
+
return bytes(out)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def decode(data: bytes, limits: "Limits | None" = None):
|
|
78
|
+
"""Decode canonical WCB bytes, rejecting every non-canonical form."""
|
|
79
|
+
if not data:
|
|
80
|
+
raise Malformed("empty WCB value")
|
|
81
|
+
decoder = _Decoder(data, limits or Limits())
|
|
82
|
+
value = decoder.value(0)
|
|
83
|
+
if decoder.pos != len(data):
|
|
84
|
+
raise NonCanonical(
|
|
85
|
+
"trailing bytes after WCB value: %d of %d unconsumed"
|
|
86
|
+
% (len(data) - decoder.pos, len(data))
|
|
87
|
+
)
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class _Encoder:
|
|
92
|
+
__slots__ = ("limits", "items")
|
|
93
|
+
|
|
94
|
+
def __init__(self, limits: Limits) -> None:
|
|
95
|
+
self.limits = limits
|
|
96
|
+
self.items = 0
|
|
97
|
+
|
|
98
|
+
def _bump_item(self) -> None:
|
|
99
|
+
self.items += 1
|
|
100
|
+
if self.items > self.limits.max_structured_items:
|
|
101
|
+
raise LimitExceeded(
|
|
102
|
+
"WCB item count %d exceeds limit %d"
|
|
103
|
+
% (self.items, self.limits.max_structured_items)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _check_text(self, length: int, what: str) -> None:
|
|
107
|
+
if length > self.limits.max_string_bytes:
|
|
108
|
+
raise LimitExceeded(
|
|
109
|
+
"%s length %d exceeds limit %d"
|
|
110
|
+
% (what, length, self.limits.max_string_bytes)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _enter(self, depth: int) -> int:
|
|
114
|
+
nxt = depth + 1
|
|
115
|
+
if nxt > self.limits.max_structured_depth:
|
|
116
|
+
raise LimitExceeded(
|
|
117
|
+
"WCB nesting depth %d exceeds limit %d"
|
|
118
|
+
% (nxt, self.limits.max_structured_depth)
|
|
119
|
+
)
|
|
120
|
+
return nxt
|
|
121
|
+
|
|
122
|
+
def value(self, value, depth: int, out: bytearray) -> None:
|
|
123
|
+
self._bump_item()
|
|
124
|
+
if value is None:
|
|
125
|
+
out.append(TAG_NULL)
|
|
126
|
+
elif value is False:
|
|
127
|
+
out.append(TAG_FALSE)
|
|
128
|
+
elif value is True:
|
|
129
|
+
out.append(TAG_TRUE)
|
|
130
|
+
elif isinstance(value, U64):
|
|
131
|
+
if value.value < 0 or value.value > U64_MAX:
|
|
132
|
+
raise Malformed("U64 value is outside u64")
|
|
133
|
+
out.append(TAG_U64)
|
|
134
|
+
varint.encode(value.value, out)
|
|
135
|
+
elif isinstance(value, I64):
|
|
136
|
+
if value.value < I64_MIN or value.value > I64_MAX:
|
|
137
|
+
raise Malformed("I64 value is outside i64")
|
|
138
|
+
out.append(TAG_I64)
|
|
139
|
+
varint.encode(varint.zigzag_encode(value.value), out)
|
|
140
|
+
elif isinstance(value, Timestamp):
|
|
141
|
+
if value.nanos < I64_MIN or value.nanos > I64_MAX:
|
|
142
|
+
raise Malformed("Timestamp value is outside i64")
|
|
143
|
+
out.append(TAG_TIMESTAMP)
|
|
144
|
+
varint.encode(varint.zigzag_encode(value.nanos), out)
|
|
145
|
+
elif isinstance(value, str):
|
|
146
|
+
raw = value.encode("utf-8")
|
|
147
|
+
self._check_text(len(raw), "text")
|
|
148
|
+
out.append(TAG_TEXT)
|
|
149
|
+
varint.encode(len(raw), out)
|
|
150
|
+
out.extend(raw)
|
|
151
|
+
elif isinstance(value, (bytes, bytearray, memoryview)):
|
|
152
|
+
raw = bytes(value)
|
|
153
|
+
self._check_text(len(raw), "bytes")
|
|
154
|
+
out.append(TAG_BYTES)
|
|
155
|
+
varint.encode(len(raw), out)
|
|
156
|
+
out.extend(raw)
|
|
157
|
+
elif isinstance(value, (list, tuple)):
|
|
158
|
+
child = self._enter(depth)
|
|
159
|
+
if len(value) > self.limits.max_structured_items:
|
|
160
|
+
raise LimitExceeded("WCB array too large")
|
|
161
|
+
out.append(TAG_ARRAY)
|
|
162
|
+
varint.encode(len(value), out)
|
|
163
|
+
for item in value:
|
|
164
|
+
self.value(item, child, out)
|
|
165
|
+
elif isinstance(value, dict):
|
|
166
|
+
child = self._enter(depth)
|
|
167
|
+
if len(value) > self.limits.max_structured_items:
|
|
168
|
+
raise LimitExceeded("WCB map too large")
|
|
169
|
+
entries = []
|
|
170
|
+
for key in value:
|
|
171
|
+
if not isinstance(key, str):
|
|
172
|
+
raise Malformed("WCB map keys must be text")
|
|
173
|
+
raw = key.encode("utf-8")
|
|
174
|
+
self._check_text(len(raw), "map key")
|
|
175
|
+
encoded_key = bytearray()
|
|
176
|
+
encoded_key.append(TAG_TEXT)
|
|
177
|
+
varint.encode(len(raw), encoded_key)
|
|
178
|
+
encoded_key.extend(raw)
|
|
179
|
+
entries.append((bytes(encoded_key), key))
|
|
180
|
+
entries.sort(key=lambda entry: entry[0])
|
|
181
|
+
out.append(TAG_MAP)
|
|
182
|
+
varint.encode(len(entries), out)
|
|
183
|
+
for encoded_key, key in entries:
|
|
184
|
+
out.extend(encoded_key)
|
|
185
|
+
self.value(value[key], child, out)
|
|
186
|
+
else:
|
|
187
|
+
raise Malformed("value of type %s is not representable in WCB" % type(value).__name__)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class _Decoder:
|
|
191
|
+
__slots__ = ("input", "pos", "limits", "items")
|
|
192
|
+
|
|
193
|
+
def __init__(self, data: bytes, limits: Limits) -> None:
|
|
194
|
+
self.input = data
|
|
195
|
+
self.pos = 0
|
|
196
|
+
self.limits = limits
|
|
197
|
+
self.items = 0
|
|
198
|
+
|
|
199
|
+
def _bump_item(self) -> None:
|
|
200
|
+
self.items += 1
|
|
201
|
+
if self.items > self.limits.max_structured_items:
|
|
202
|
+
raise LimitExceeded(
|
|
203
|
+
"WCB item count %d exceeds limit %d"
|
|
204
|
+
% (self.items, self.limits.max_structured_items)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _read_u8(self) -> int:
|
|
208
|
+
if self.pos >= len(self.input):
|
|
209
|
+
raise Malformed("truncated WCB value")
|
|
210
|
+
byte = self.input[self.pos]
|
|
211
|
+
self.pos += 1
|
|
212
|
+
return byte
|
|
213
|
+
|
|
214
|
+
def _read_varint(self) -> int:
|
|
215
|
+
value, self.pos = varint.decode(self.input, self.pos)
|
|
216
|
+
return value
|
|
217
|
+
|
|
218
|
+
def _remaining(self) -> int:
|
|
219
|
+
return len(self.input) - self.pos
|
|
220
|
+
|
|
221
|
+
def _take(self, length: int, what: str) -> bytes:
|
|
222
|
+
if length > self.limits.max_string_bytes:
|
|
223
|
+
raise LimitExceeded(
|
|
224
|
+
"%s length %d exceeds limit %d"
|
|
225
|
+
% (what, length, self.limits.max_string_bytes)
|
|
226
|
+
)
|
|
227
|
+
if length > self._remaining():
|
|
228
|
+
raise Malformed("truncated WCB %s" % what)
|
|
229
|
+
start = self.pos
|
|
230
|
+
self.pos += length
|
|
231
|
+
return self.input[start : self.pos]
|
|
232
|
+
|
|
233
|
+
def _enter(self, depth: int) -> int:
|
|
234
|
+
nxt = depth + 1
|
|
235
|
+
if nxt > self.limits.max_structured_depth:
|
|
236
|
+
raise LimitExceeded(
|
|
237
|
+
"WCB nesting depth %d exceeds limit %d"
|
|
238
|
+
% (nxt, self.limits.max_structured_depth)
|
|
239
|
+
)
|
|
240
|
+
return nxt
|
|
241
|
+
|
|
242
|
+
def _check_count(self, count: int, min_bytes_each: int, what: str) -> int:
|
|
243
|
+
if count > self.limits.max_structured_items:
|
|
244
|
+
raise LimitExceeded(
|
|
245
|
+
"WCB %s count %d exceeds limit %d"
|
|
246
|
+
% (what, count, self.limits.max_structured_items)
|
|
247
|
+
)
|
|
248
|
+
if count * min_bytes_each > self._remaining():
|
|
249
|
+
raise Malformed(
|
|
250
|
+
"truncated WCB %s: count %d cannot fit in %d remaining bytes"
|
|
251
|
+
% (what, count, self._remaining())
|
|
252
|
+
)
|
|
253
|
+
return count
|
|
254
|
+
|
|
255
|
+
def value(self, depth: int):
|
|
256
|
+
self._bump_item()
|
|
257
|
+
tag = self._read_u8()
|
|
258
|
+
if tag == TAG_NULL:
|
|
259
|
+
return None
|
|
260
|
+
if tag == TAG_FALSE:
|
|
261
|
+
return False
|
|
262
|
+
if tag == TAG_TRUE:
|
|
263
|
+
return True
|
|
264
|
+
if tag == TAG_U64:
|
|
265
|
+
return U64(self._read_varint())
|
|
266
|
+
if tag == TAG_I64:
|
|
267
|
+
return I64(varint.zigzag_decode(self._read_varint()))
|
|
268
|
+
if tag == TAG_TIMESTAMP:
|
|
269
|
+
return Timestamp(varint.zigzag_decode(self._read_varint()))
|
|
270
|
+
if tag == TAG_TEXT:
|
|
271
|
+
raw = self._take(self._read_varint(), "text")
|
|
272
|
+
try:
|
|
273
|
+
return raw.decode("utf-8")
|
|
274
|
+
except UnicodeDecodeError:
|
|
275
|
+
raise NonCanonical("WCB text is not valid UTF-8")
|
|
276
|
+
if tag == TAG_BYTES:
|
|
277
|
+
return self._take(self._read_varint(), "bytes")
|
|
278
|
+
if tag == TAG_ARRAY:
|
|
279
|
+
child = self._enter(depth)
|
|
280
|
+
count = self._check_count(self._read_varint(), 1, "array")
|
|
281
|
+
return [self.value(child) for _ in range(count)]
|
|
282
|
+
if tag == TAG_MAP:
|
|
283
|
+
child = self._enter(depth)
|
|
284
|
+
count = self._check_count(self._read_varint(), 2, "map")
|
|
285
|
+
result = {}
|
|
286
|
+
previous_key = None
|
|
287
|
+
for _ in range(count):
|
|
288
|
+
key_start = self.pos
|
|
289
|
+
key_tag = self._read_u8()
|
|
290
|
+
if key_tag != TAG_TEXT:
|
|
291
|
+
raise NonCanonical("WCB map key is not a text value (tag 0x05)")
|
|
292
|
+
raw = self._take(self._read_varint(), "map key")
|
|
293
|
+
key_end = self.pos
|
|
294
|
+
if previous_key is not None:
|
|
295
|
+
previous = self.input[previous_key[0] : previous_key[1]]
|
|
296
|
+
current = self.input[key_start:key_end]
|
|
297
|
+
if current <= previous:
|
|
298
|
+
raise NonCanonical(
|
|
299
|
+
"WCB map keys are duplicated or not sorted by encoded key bytes"
|
|
300
|
+
)
|
|
301
|
+
previous_key = (key_start, key_end)
|
|
302
|
+
try:
|
|
303
|
+
key = raw.decode("utf-8")
|
|
304
|
+
except UnicodeDecodeError:
|
|
305
|
+
raise NonCanonical("WCB map key is not valid UTF-8")
|
|
306
|
+
if key in result:
|
|
307
|
+
raise NonCanonical("duplicate WCB map key")
|
|
308
|
+
result[key] = self.value(child)
|
|
309
|
+
return result
|
|
310
|
+
raise NonCanonical("unknown WCB tag 0x%02x" % tag)
|