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/huffman.py ADDED
@@ -0,0 +1,212 @@
1
+ """`huffman-canonical-v1` decoder (WEFT-AI-CODEC §5.3.1).
2
+
3
+ The serialized form is::
4
+
5
+ u8 code_lengths[256] # 0 = symbol absent
6
+ || bitstream # MSB-first, zero-padded to a byte boundary
7
+
8
+ The decoder rebuilds the canonical table from the 256 lengths and rejects, in
9
+ this order:
10
+
11
+ 1. a length greater than 24 -> ``NonCanonical``;
12
+ 2. an over-subscribed or incomplete prefix table (Kraft sum, 24-bit
13
+ resolution, not exactly ``2**24``) -> ``NonCanonical``, except the
14
+ single-symbol form (one used symbol with length 1);
15
+ 3. a truncated bitstream -> ``Corrupt``;
16
+ 4. extra trailing bytes, or padding bits that are not zero -> ``Corrupt``;
17
+ 5. an output length other than the declared ``element_count`` -> ``Corrupt``.
18
+
19
+ ``decode`` consumes the whole slice; ``decode_prefix`` consumes exactly the
20
+ 256-byte table plus this stream's own padded bitstream and returns that length
21
+ so callers can concatenate independently coded streams (modes 11/12/13/14/15/
22
+ 16).
23
+
24
+ This is a deliberately straightforward canonical decoder: performance is not
25
+ the goal, exactness and identical error classes are.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from .errors import Corrupt, NonCanonical
31
+
32
+ TABLE_BYTES = 256
33
+ MAX_CODE_BITS = 24
34
+
35
+
36
+ class _Canonical:
37
+ """Canonical tables for one 256-byte code-length header."""
38
+
39
+ __slots__ = ("counts", "first_code", "first_index", "symbols", "max_len")
40
+
41
+ def __init__(self) -> None:
42
+ self.counts = [0] * (MAX_CODE_BITS + 1)
43
+ self.first_code = [0] * (MAX_CODE_BITS + 1)
44
+ self.first_index = [0] * (MAX_CODE_BITS + 1)
45
+ self.symbols = []
46
+ self.max_len = 0
47
+
48
+
49
+ def _build_table(lengths: bytes, expected_len: int) -> "tuple[_Canonical | None, bytes | None, int]":
50
+ """Return ``(tables, single_symbol_output, used)``.
51
+
52
+ ``tables`` is ``None`` for the empty and single-symbol forms. The second
53
+ element is the pre-filled output for a single-symbol table.
54
+ """
55
+ if len(lengths) < TABLE_BYTES:
56
+ raise Corrupt(
57
+ "huffman-canonical-v1 stream is %d bytes, shorter than its %d-byte "
58
+ "code-length table" % (len(lengths), TABLE_BYTES)
59
+ )
60
+ lengths = lengths[:TABLE_BYTES]
61
+ counts = [0] * (MAX_CODE_BITS + 1)
62
+ used = 0
63
+ max_len = 0
64
+ for symbol, length in enumerate(lengths):
65
+ if length > MAX_CODE_BITS:
66
+ raise NonCanonical(
67
+ "huffman-canonical-v1 symbol %d has code length %d > %d"
68
+ % (symbol, length, MAX_CODE_BITS)
69
+ )
70
+ if length:
71
+ used += 1
72
+ counts[length] += 1
73
+ if length > max_len:
74
+ max_len = length
75
+
76
+ if used == 0:
77
+ if expected_len != 0:
78
+ raise Corrupt(
79
+ "huffman-canonical-v1 table has no symbols but the descriptor "
80
+ "declares a non-empty plane"
81
+ )
82
+ return None, b"", 0
83
+
84
+ if used == 1:
85
+ if max_len != 1:
86
+ raise NonCanonical(
87
+ "huffman-canonical-v1 single-symbol table must use code length 1"
88
+ )
89
+ if expected_len == 0:
90
+ raise NonCanonical(
91
+ "huffman-canonical-v1 non-empty table for an empty plane"
92
+ )
93
+ symbol = next(index for index, length in enumerate(lengths) if length)
94
+ return None, bytes([symbol]) * expected_len, 1
95
+
96
+ if expected_len == 0:
97
+ raise NonCanonical(
98
+ "huffman-canonical-v1 non-empty table for an empty plane"
99
+ )
100
+
101
+ # Kraft equality proves a complete prefix code without building a table.
102
+ space = 1 << MAX_CODE_BITS
103
+ used_space = 0
104
+ for length in range(1, MAX_CODE_BITS + 1):
105
+ used_space += counts[length] << (MAX_CODE_BITS - length)
106
+ if used_space > space:
107
+ raise NonCanonical(
108
+ "huffman-canonical-v1 code lengths are over-subscribed"
109
+ )
110
+ if used_space != space:
111
+ raise NonCanonical("huffman-canonical-v1 code lengths are incomplete")
112
+
113
+ table = _Canonical()
114
+ table.counts = counts
115
+ table.max_len = max_len
116
+ # Canonical assignment: sort used symbols by (code length, symbol).
117
+ order = sorted(
118
+ (length, symbol) for symbol, length in enumerate(lengths) if length
119
+ )
120
+ code = 0
121
+ previous = 0
122
+ assigned = [0] * (MAX_CODE_BITS + 1)
123
+ symbols = []
124
+ for length, symbol in order:
125
+ code <<= length - previous
126
+ if assigned[length] == 0:
127
+ table.first_code[length] = code
128
+ table.first_index[length] = len(symbols)
129
+ assigned[length] += 1
130
+ symbols.append(symbol)
131
+ code += 1
132
+ previous = length
133
+ table.symbols = symbols
134
+ return table, None, used
135
+
136
+
137
+ def _decode_prefix_tables(
138
+ table: _Canonical, stream: bytes, expected_len: int
139
+ ) -> "tuple[bytes, int]":
140
+ out = bytearray()
141
+ bit = 0
142
+ total_bits = len(stream) * 8
143
+ current_code = 0
144
+ current_len = 0
145
+ counts = table.counts
146
+ first_code = table.first_code
147
+ first_index = table.first_index
148
+ symbols = table.symbols
149
+ max_len = table.max_len
150
+
151
+ while len(out) < expected_len:
152
+ if bit >= total_bits:
153
+ raise Corrupt(
154
+ "huffman-canonical-v1 bitstream ended after %d of %d bytes"
155
+ % (len(out), expected_len)
156
+ )
157
+ byte = stream[bit >> 3]
158
+ current_code = (current_code << 1) | ((byte >> (7 - (bit & 7))) & 1)
159
+ current_len += 1
160
+ bit += 1
161
+
162
+ slot = current_len
163
+ if slot <= max_len:
164
+ first = first_code[slot]
165
+ if current_code >= first:
166
+ offset = current_code - first
167
+ if offset < counts[slot]:
168
+ out.append(symbols[first_index[slot] + offset])
169
+ current_code = 0
170
+ current_len = 0
171
+ continue
172
+ if current_len >= max_len:
173
+ raise Corrupt(
174
+ "huffman-canonical-v1 bitstream contains an invalid code"
175
+ )
176
+
177
+ consumed_stream = (bit + 7) // 8
178
+ used_bits_in_last = bit % 8
179
+ if used_bits_in_last:
180
+ last = stream[consumed_stream - 1]
181
+ mask = (1 << (8 - used_bits_in_last)) - 1
182
+ if last & mask:
183
+ raise Corrupt("huffman-canonical-v1 padding bits must be zero")
184
+ return bytes(out), TABLE_BYTES + consumed_stream
185
+
186
+
187
+ def decode_prefix(encoded: bytes, expected_len: int):
188
+ """Decode one stream and return ``(bytes, consumed)``.
189
+
190
+ ``consumed`` is the 256-byte table plus this stream's own padded bitstream.
191
+ Bytes after that prefix belong to the caller.
192
+ """
193
+ if len(encoded) < TABLE_BYTES:
194
+ raise Corrupt(
195
+ "huffman-canonical-v1 stream is %d bytes, shorter than its %d-byte "
196
+ "code-length table" % (len(encoded), TABLE_BYTES)
197
+ )
198
+ table, single, _used = _build_table(encoded[:TABLE_BYTES], expected_len)
199
+ if table is None:
200
+ return single, TABLE_BYTES
201
+ return _decode_prefix_tables(table, encoded[TABLE_BYTES:], expected_len)
202
+
203
+
204
+ def decode(encoded: bytes, expected_len: int) -> bytes:
205
+ """Decode exactly one complete stream; any trailing byte is ``Corrupt``."""
206
+ out, consumed = decode_prefix(encoded, expected_len)
207
+ if consumed != len(encoded):
208
+ raise Corrupt(
209
+ "huffman-canonical-v1 bitstream has trailing bytes after the "
210
+ "declared output"
211
+ )
212
+ return out
weftcodec/identity.py ADDED
@@ -0,0 +1,141 @@
1
+ """Identity helpers: domain-separated BLAKE3 and frame verification."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import asdict, dataclass
7
+
8
+ from .dependency import Resolver
9
+ from .frame import Frame, parse
10
+ from .hashing import (
11
+ DOMAIN_CHUNK,
12
+ HASH_LEN,
13
+ content_root,
14
+ content_root_hex,
15
+ hash_domain,
16
+ hash_domain_hex,
17
+ )
18
+ from .limits import Limits
19
+ from .modes import decode_frame
20
+
21
+ __all__ = [
22
+ "DOMAIN_CHUNK",
23
+ "HASH_LEN",
24
+ "VerifyResult",
25
+ "content_root",
26
+ "content_root_hex",
27
+ "hash_domain",
28
+ "hash_domain_hex",
29
+ "verify_bytes",
30
+ "verify_frame",
31
+ ]
32
+
33
+
34
+ @dataclass
35
+ class VerifyResult:
36
+ """Structured result of parsing and decoding one frame."""
37
+
38
+ ok: bool
39
+ mode: "str | None" = None
40
+ mode_value: "int | None" = None
41
+ domain: "int | None" = None
42
+ flags: "list[str] | None" = None
43
+ features: "list[str] | None" = None
44
+ decoded_len: "int | None" = None
45
+ body_len: "int | None" = None
46
+ chunk_count: "int | None" = None
47
+ dependency_count: "int | None" = None
48
+ level: "int | None" = None
49
+ content_hash: "str | None" = None
50
+ computed_content_hash: "str | None" = None
51
+ error_class: "str | None" = None
52
+ error: "str | None" = None
53
+
54
+ def to_dict(self) -> dict:
55
+ return asdict(self)
56
+
57
+
58
+ def _fill_from_header(result: VerifyResult, frame: Frame) -> None:
59
+ header = frame.header
60
+ result.mode = header.mode_name
61
+ result.mode_value = header.mode
62
+ result.domain = header.domain
63
+ result.flags = header.flag_names()
64
+ result.features = header.feature_names()
65
+ result.decoded_len = header.decoded_len
66
+ result.body_len = header.body_len
67
+ result.chunk_count = header.chunk_count
68
+ result.dependency_count = header.dep_count
69
+ result.level = header.level
70
+ result.content_hash = header.content_hash.hex()
71
+
72
+
73
+ def verify_bytes(
74
+ data: bytes,
75
+ resolver: "Resolver | None" = None,
76
+ limits: "Limits | None" = None,
77
+ base_frame: "bytes | None" = None,
78
+ ) -> VerifyResult:
79
+ """Parse, decode, and verify every hash in ``data``."""
80
+ limits = limits or Limits()
81
+ result = VerifyResult(ok=False)
82
+ try:
83
+ frame = parse(data, limits)
84
+ except Exception as exc: # typed Weft errors plus no unexpected swallowing
85
+ return _failure(result, exc)
86
+ _fill_from_header(result, frame)
87
+ try:
88
+ plaintext = decode_frame(frame, resolver, limits, base_frame)
89
+ except Exception as exc:
90
+ return _failure(result, exc, keep_header=True)
91
+ result.computed_content_hash = content_root_hex(plaintext)
92
+ result.ok = True
93
+ return result
94
+
95
+
96
+ def verify_frame(
97
+ path: "str | os.PathLike[str]",
98
+ resolver: "Resolver | None" = None,
99
+ limits: "Limits | None" = None,
100
+ base_frame: "bytes | None" = None,
101
+ ) -> VerifyResult:
102
+ """Read ``path``, parse, decode, and verify it. Never raises for codec errors."""
103
+ limits = limits or Limits()
104
+ result = VerifyResult(ok=False)
105
+ try:
106
+ size = os.path.getsize(path)
107
+ except OSError as exc:
108
+ from .errors import Io
109
+
110
+ return _failure(result, Io("cannot stat %s: %s" % (path, exc)))
111
+ if size > limits.max_frame_bytes:
112
+ from .errors import LimitExceeded
113
+
114
+ return _failure(
115
+ result,
116
+ LimitExceeded(
117
+ "frame length %d exceeds limit %d" % (size, limits.max_frame_bytes)
118
+ ),
119
+ )
120
+ try:
121
+ with open(path, "rb") as handle:
122
+ data = handle.read()
123
+ except OSError as exc:
124
+ from .errors import Io
125
+
126
+ return _failure(result, Io("cannot read %s: %s" % (path, exc)))
127
+ return verify_bytes(data, resolver, limits, base_frame)
128
+
129
+
130
+ def _failure(
131
+ result: VerifyResult, exc: Exception, keep_header: bool = False
132
+ ) -> VerifyResult:
133
+ error_class = getattr(exc, "error_class", None)
134
+ if error_class is None:
135
+ raise exc
136
+ result.ok = False
137
+ result.error_class = error_class
138
+ result.error = str(exc)
139
+ if not keep_header:
140
+ result.mode = None
141
+ return result
weftcodec/limits.py ADDED
@@ -0,0 +1,58 @@
1
+ """Decoder/encoder resource limits (WEFTCODEC-FORMAT §10).
2
+
3
+ Defaults are the normative v1 values. Every field can be lowered; nothing in
4
+ this package raises a limit above the frozen default.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, fields, replace
10
+
11
+ DEFAULT_MAX_FRAME_BYTES = 1 << 30
12
+ DEFAULT_MAX_DECODED_BYTES = 4 << 30
13
+ DEFAULT_MAX_DEPENDENCY_DEPTH = 8
14
+ DEFAULT_MAX_DEPENDENCY_COUNT = 64
15
+ DEFAULT_MAX_TOTAL_FETCHED_BYTES = 8 << 30
16
+ DEFAULT_MAX_CHUNKS = 1_048_576
17
+ DEFAULT_MAX_DICTIONARY_BYTES = 64 << 20
18
+ DEFAULT_MAX_STRUCTURED_DEPTH = 64
19
+ DEFAULT_MAX_STRUCTURED_ITEMS = 1_000_000
20
+ DEFAULT_MAX_STRING_BYTES = 256 << 20
21
+ DEFAULT_MAX_DELTA_OPS = 16_777_216
22
+ DEFAULT_MAX_TENSOR_ELEMENTS = 1 << 32
23
+ DEFAULT_MAX_TENSOR_RANK = 8
24
+ DEFAULT_MAX_TOKENS = 1 << 24
25
+ DEFAULT_MAX_SPARSE_RUNS = 1 << 24
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class Limits:
30
+ max_frame_bytes: int = DEFAULT_MAX_FRAME_BYTES
31
+ max_decoded_bytes: int = DEFAULT_MAX_DECODED_BYTES
32
+ max_dependency_depth: int = DEFAULT_MAX_DEPENDENCY_DEPTH
33
+ max_dependency_count: int = DEFAULT_MAX_DEPENDENCY_COUNT
34
+ max_total_fetched_bytes: int = DEFAULT_MAX_TOTAL_FETCHED_BYTES
35
+ max_chunks: int = DEFAULT_MAX_CHUNKS
36
+ max_dictionary_bytes: int = DEFAULT_MAX_DICTIONARY_BYTES
37
+ max_structured_depth: int = DEFAULT_MAX_STRUCTURED_DEPTH
38
+ max_structured_items: int = DEFAULT_MAX_STRUCTURED_ITEMS
39
+ max_string_bytes: int = DEFAULT_MAX_STRING_BYTES
40
+ max_delta_ops: int = DEFAULT_MAX_DELTA_OPS
41
+ max_tensor_elements: int = DEFAULT_MAX_TENSOR_ELEMENTS
42
+ max_tensor_rank: int = DEFAULT_MAX_TENSOR_RANK
43
+ max_tokens: int = DEFAULT_MAX_TOKENS
44
+ max_sparse_runs: int = DEFAULT_MAX_SPARSE_RUNS
45
+
46
+ @classmethod
47
+ def from_overrides(cls, overrides: "dict[str, int] | None") -> "Limits":
48
+ """Build limits from a vector's ``limits`` object.
49
+
50
+ Unknown keys are rejected so a typo can never silently widen a bound.
51
+ """
52
+ if not overrides:
53
+ return cls()
54
+ known = {f.name for f in fields(cls)}
55
+ unknown = set(overrides) - known
56
+ if unknown:
57
+ raise ValueError("unknown limit(s): %s" % ", ".join(sorted(unknown)))
58
+ return replace(cls(), **{k: int(v) for k, v in overrides.items()})