smithy-cbor 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.
- smithy_cbor/__init__.py +40 -0
- smithy_cbor/_private/__init__.py +2 -0
- smithy_cbor/_private/deserializers.py +431 -0
- smithy_cbor/_private/generic.py +152 -0
- smithy_cbor/_private/schema_extensions.py +115 -0
- smithy_cbor/_private/serializers.py +357 -0
- smithy_cbor/py.typed +1 -0
- smithy_cbor/settings.py +12 -0
- smithy_cbor-0.1.0.dist-info/METADATA +35 -0
- smithy_cbor-0.1.0.dist-info/RECORD +12 -0
- smithy_cbor-0.1.0.dist-info/WHEEL +4 -0
- smithy_cbor-0.1.0.dist-info/licenses/NOTICE +1 -0
smithy_cbor/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
|
|
5
|
+
from smithy_core.codecs import Codec
|
|
6
|
+
from smithy_core.deserializers import ShapeDeserializer
|
|
7
|
+
from smithy_core.interfaces import BytesReader, BytesWriter
|
|
8
|
+
from smithy_core.serializers import ShapeSerializer
|
|
9
|
+
|
|
10
|
+
from ._private.deserializers import CBORShapeDeserializer as _CBORShapeDeserializer
|
|
11
|
+
from ._private.generic import loads as loads
|
|
12
|
+
from ._private.serializers import CBORShapeSerializer as _CBORShapeSerializer
|
|
13
|
+
from .settings import CBORSettings
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
__all__ = ("CBORCodec", "CBORSettings", "loads")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CBORCodec(Codec):
|
|
20
|
+
"""Targets the subset of CBOR used by the ``smithy.protocols#rpcv2Cbor`` protocol."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
default_namespace: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self._settings = CBORSettings(
|
|
27
|
+
default_namespace=default_namespace,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def media_type(self) -> str:
|
|
32
|
+
return "application/cbor"
|
|
33
|
+
|
|
34
|
+
def create_serializer(self, sink: BytesWriter) -> "ShapeSerializer":
|
|
35
|
+
return _CBORShapeSerializer(sink, settings=self._settings)
|
|
36
|
+
|
|
37
|
+
def create_deserializer(self, source: bytes | BytesReader) -> "ShapeDeserializer":
|
|
38
|
+
if isinstance(source, bytes):
|
|
39
|
+
source = BytesIO(source)
|
|
40
|
+
return _CBORShapeDeserializer(source, settings=self._settings)
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""CBOR shape deserializer."""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import struct as _struct
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
from typing import override
|
|
10
|
+
|
|
11
|
+
from smithy_core.deserializers import ShapeDeserializer
|
|
12
|
+
from smithy_core.documents import Document
|
|
13
|
+
from smithy_core.exceptions import SmithyError
|
|
14
|
+
from smithy_core.interfaces import BytesReader
|
|
15
|
+
from smithy_core.schemas import Schema
|
|
16
|
+
|
|
17
|
+
from ..settings import CBORSettings
|
|
18
|
+
|
|
19
|
+
# Major type occupies the high 3 bits; the low 5 bits are the "additional information".
|
|
20
|
+
_MAJOR_UINT = 0
|
|
21
|
+
_MAJOR_NEGINT = 1
|
|
22
|
+
_MAJOR_BYTES = 2
|
|
23
|
+
_MAJOR_TEXT = 3
|
|
24
|
+
_MAJOR_ARRAY = 4
|
|
25
|
+
_MAJOR_MAP = 5
|
|
26
|
+
_MAJOR_TAG = 6
|
|
27
|
+
_MAJOR_SIMPLE = 7
|
|
28
|
+
|
|
29
|
+
# Additional-information 24-27: the following value occupies 1, 2, 4, or 8 bytes.
|
|
30
|
+
_W_1BYTE = 24
|
|
31
|
+
_W_2BYTE = 25
|
|
32
|
+
_W_4BYTE = 26
|
|
33
|
+
_W_8BYTE = 27
|
|
34
|
+
_INDEFINITE = 31
|
|
35
|
+
|
|
36
|
+
_SIMPLE_FALSE = 20
|
|
37
|
+
_SIMPLE_TRUE = 21
|
|
38
|
+
_SIMPLE_NULL = 22
|
|
39
|
+
|
|
40
|
+
_TAG_EPOCH = 1
|
|
41
|
+
_TAG_UNSIGNED_BIGNUM = 2
|
|
42
|
+
_TAG_NEGATIVE_BIGNUM = 3
|
|
43
|
+
_TAG_DECIMAL_FRACTION = 4
|
|
44
|
+
|
|
45
|
+
# ends indefinite item
|
|
46
|
+
_BREAK = 0xFF
|
|
47
|
+
|
|
48
|
+
# Initial byte of a CBOR null (major 7, simple value 22).
|
|
49
|
+
_NULL_BYTE = (_MAJOR_SIMPLE << 5) | _SIMPLE_NULL
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class CBORDecodeError(SmithyError):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CBORShapeDeserializer(ShapeDeserializer):
|
|
57
|
+
def __init__(self, source: BytesReader, settings: CBORSettings) -> None:
|
|
58
|
+
self._settings = settings
|
|
59
|
+
self._data = source.read()
|
|
60
|
+
self._len: int = len(self._data)
|
|
61
|
+
self._pos: int = 0
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
def read_struct(
|
|
65
|
+
self,
|
|
66
|
+
schema: Schema,
|
|
67
|
+
consumer: Callable[[Schema, ShapeDeserializer], None],
|
|
68
|
+
) -> None:
|
|
69
|
+
count = self._expect(_MAJOR_MAP)
|
|
70
|
+
members_get = schema.members.get
|
|
71
|
+
cursor = self
|
|
72
|
+
read_text = self._read_text
|
|
73
|
+
skip_value = self._skip_value
|
|
74
|
+
read_null = self.read_null
|
|
75
|
+
|
|
76
|
+
indefinite = count == -1
|
|
77
|
+
remaining = count
|
|
78
|
+
while True:
|
|
79
|
+
if indefinite:
|
|
80
|
+
if cursor._at_break():
|
|
81
|
+
cursor._consume_break()
|
|
82
|
+
break
|
|
83
|
+
else:
|
|
84
|
+
if remaining <= 0:
|
|
85
|
+
break
|
|
86
|
+
remaining -= 1
|
|
87
|
+
key = read_text()
|
|
88
|
+
member = members_get(key)
|
|
89
|
+
if member is None:
|
|
90
|
+
skip_value()
|
|
91
|
+
elif cursor._at_null():
|
|
92
|
+
read_null()
|
|
93
|
+
else:
|
|
94
|
+
consumer(member, self)
|
|
95
|
+
|
|
96
|
+
@override
|
|
97
|
+
def read_list(
|
|
98
|
+
self, schema: Schema, consumer: Callable[[ShapeDeserializer], None]
|
|
99
|
+
) -> None:
|
|
100
|
+
count = self._expect(_MAJOR_ARRAY)
|
|
101
|
+
if count == -1:
|
|
102
|
+
while not self._at_break():
|
|
103
|
+
consumer(self)
|
|
104
|
+
self._consume_break()
|
|
105
|
+
else:
|
|
106
|
+
for _ in range(count):
|
|
107
|
+
consumer(self)
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
def read_map(
|
|
111
|
+
self,
|
|
112
|
+
schema: Schema,
|
|
113
|
+
consumer: Callable[[str, ShapeDeserializer], None],
|
|
114
|
+
) -> None:
|
|
115
|
+
count = self._expect(_MAJOR_MAP)
|
|
116
|
+
if count == -1:
|
|
117
|
+
while not self._at_break():
|
|
118
|
+
key = self._read_text()
|
|
119
|
+
consumer(key, self)
|
|
120
|
+
self._consume_break()
|
|
121
|
+
else:
|
|
122
|
+
for _ in range(count):
|
|
123
|
+
key = self._read_text()
|
|
124
|
+
consumer(key, self)
|
|
125
|
+
|
|
126
|
+
@override
|
|
127
|
+
def is_null(self) -> bool:
|
|
128
|
+
initial = self._peek_byte()
|
|
129
|
+
return initial == ((_MAJOR_SIMPLE << 5) | _SIMPLE_NULL)
|
|
130
|
+
|
|
131
|
+
@override
|
|
132
|
+
def read_null(self) -> None:
|
|
133
|
+
major, arg = self._read_head()
|
|
134
|
+
if major != _MAJOR_SIMPLE or arg != _SIMPLE_NULL:
|
|
135
|
+
raise CBORDecodeError("Expected CBOR null.")
|
|
136
|
+
|
|
137
|
+
@override
|
|
138
|
+
def read_boolean(self, schema: Schema) -> bool:
|
|
139
|
+
major, arg = self._read_head()
|
|
140
|
+
if major != _MAJOR_SIMPLE or arg not in (_SIMPLE_FALSE, _SIMPLE_TRUE):
|
|
141
|
+
raise CBORDecodeError("Expected CBOR boolean.")
|
|
142
|
+
return arg == _SIMPLE_TRUE
|
|
143
|
+
|
|
144
|
+
@override
|
|
145
|
+
def read_blob(self, schema: Schema) -> bytes:
|
|
146
|
+
return self._read_string_bytes(_MAJOR_BYTES)
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
def read_integer(self, schema: Schema) -> int:
|
|
150
|
+
major, arg = self._read_head()
|
|
151
|
+
if major == _MAJOR_UINT:
|
|
152
|
+
return arg
|
|
153
|
+
if major == _MAJOR_NEGINT:
|
|
154
|
+
return -1 - arg
|
|
155
|
+
raise CBORDecodeError(f"Expected CBOR integer, found major type {major}.")
|
|
156
|
+
|
|
157
|
+
@override
|
|
158
|
+
def read_float(self, schema: Schema) -> float:
|
|
159
|
+
major, add_info, arg = self._read_head_add_info()
|
|
160
|
+
if major != _MAJOR_SIMPLE:
|
|
161
|
+
raise CBORDecodeError("Expected CBOR float.")
|
|
162
|
+
if add_info == _W_8BYTE: # double
|
|
163
|
+
return _struct.unpack(">d", arg.to_bytes(8, "big"))[0]
|
|
164
|
+
if add_info == _W_4BYTE: # single
|
|
165
|
+
return _struct.unpack(">f", arg.to_bytes(4, "big"))[0]
|
|
166
|
+
if add_info == _W_2BYTE: # half
|
|
167
|
+
return _decode_half(arg.to_bytes(2, "big"))
|
|
168
|
+
raise CBORDecodeError(f"Unexpected float additional-info: {add_info}.")
|
|
169
|
+
|
|
170
|
+
@override
|
|
171
|
+
def read_big_integer(self, schema: Schema) -> int:
|
|
172
|
+
major, tag = self._read_head()
|
|
173
|
+
if major != _MAJOR_TAG or tag not in (
|
|
174
|
+
_TAG_UNSIGNED_BIGNUM,
|
|
175
|
+
_TAG_NEGATIVE_BIGNUM,
|
|
176
|
+
):
|
|
177
|
+
raise CBORDecodeError("Expected CBOR bignum tag.")
|
|
178
|
+
magnitude = int.from_bytes(self.read_blob(schema), "big")
|
|
179
|
+
return magnitude if tag == _TAG_UNSIGNED_BIGNUM else -1 - magnitude
|
|
180
|
+
|
|
181
|
+
@override
|
|
182
|
+
def read_big_decimal(self, schema: Schema) -> Decimal:
|
|
183
|
+
major, tag = self._read_head()
|
|
184
|
+
if major != _MAJOR_TAG or tag != _TAG_DECIMAL_FRACTION:
|
|
185
|
+
raise CBORDecodeError("Expected CBOR decimal-fraction tag.")
|
|
186
|
+
count = self._expect(_MAJOR_ARRAY)
|
|
187
|
+
if count != 2:
|
|
188
|
+
raise CBORDecodeError("Decimal fraction must be a 2-element array.")
|
|
189
|
+
exponent = self.read_integer(schema)
|
|
190
|
+
mantissa = self.read_integer(schema)
|
|
191
|
+
return Decimal(mantissa).scaleb(exponent)
|
|
192
|
+
|
|
193
|
+
@override
|
|
194
|
+
def read_string(self, schema: Schema) -> str:
|
|
195
|
+
return self._read_text()
|
|
196
|
+
|
|
197
|
+
@override
|
|
198
|
+
def read_document(self, schema: Schema) -> Document:
|
|
199
|
+
raise NotImplementedError(
|
|
200
|
+
"This version of the Smithy RPCv2 CBOR protocol does not support document types."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@override
|
|
204
|
+
def read_timestamp(self, schema: Schema) -> datetime.datetime:
|
|
205
|
+
major, tag = self._read_head()
|
|
206
|
+
if major != _MAJOR_TAG or tag != _TAG_EPOCH:
|
|
207
|
+
raise CBORDecodeError("Expected CBOR epoch-timestamp tag.")
|
|
208
|
+
# The tagged value is an int or float number of epoch seconds.
|
|
209
|
+
peek_major = self._peek_byte() >> 5
|
|
210
|
+
if peek_major in (_MAJOR_UINT, _MAJOR_NEGINT):
|
|
211
|
+
seconds: float = self.read_integer(schema)
|
|
212
|
+
else:
|
|
213
|
+
seconds = self.read_float(schema)
|
|
214
|
+
return datetime.datetime.fromtimestamp(seconds, tz=datetime.UTC)
|
|
215
|
+
|
|
216
|
+
def _read_text(self) -> str:
|
|
217
|
+
text = self._try_read_text()
|
|
218
|
+
if text is not None:
|
|
219
|
+
return text
|
|
220
|
+
return self._read_string_bytes(_MAJOR_TEXT).decode("utf-8")
|
|
221
|
+
|
|
222
|
+
def _read_string_bytes(self, major: int) -> bytes:
|
|
223
|
+
"""An indefinite-length string (RFC 8949 §3.2.3) is a sequence of definite-length
|
|
224
|
+
chunks of the same major type, terminated by a break.
|
|
225
|
+
"""
|
|
226
|
+
found_major, arg = self._read_head()
|
|
227
|
+
while found_major == _MAJOR_TAG:
|
|
228
|
+
found_major, arg = self._read_head()
|
|
229
|
+
if found_major != major:
|
|
230
|
+
raise CBORDecodeError(
|
|
231
|
+
f"Expected CBOR major type {major}, found {found_major}."
|
|
232
|
+
)
|
|
233
|
+
if arg != -1:
|
|
234
|
+
return self._take(arg)
|
|
235
|
+
chunks = bytearray()
|
|
236
|
+
while not self._at_break():
|
|
237
|
+
chunk_major, chunk_len = self._read_head()
|
|
238
|
+
if chunk_major != major:
|
|
239
|
+
raise CBORDecodeError(
|
|
240
|
+
"Indefinite-length string chunk has mismatched major type."
|
|
241
|
+
)
|
|
242
|
+
chunks += self._take(chunk_len)
|
|
243
|
+
self._consume_break()
|
|
244
|
+
return bytes(chunks)
|
|
245
|
+
|
|
246
|
+
def _skip_value(self) -> None:
|
|
247
|
+
"""e.g. unrecognized member."""
|
|
248
|
+
major, arg = self._read_head()
|
|
249
|
+
if major in (_MAJOR_UINT, _MAJOR_NEGINT):
|
|
250
|
+
return
|
|
251
|
+
if major in (_MAJOR_BYTES, _MAJOR_TEXT):
|
|
252
|
+
self._take(arg)
|
|
253
|
+
elif major == _MAJOR_ARRAY:
|
|
254
|
+
if arg == -1:
|
|
255
|
+
while not self._at_break():
|
|
256
|
+
self._skip_value()
|
|
257
|
+
self._consume_break()
|
|
258
|
+
else:
|
|
259
|
+
for _ in range(arg):
|
|
260
|
+
self._skip_value()
|
|
261
|
+
elif major == _MAJOR_MAP:
|
|
262
|
+
if arg == -1:
|
|
263
|
+
while not self._at_break():
|
|
264
|
+
self._skip_value()
|
|
265
|
+
self._skip_value()
|
|
266
|
+
self._consume_break()
|
|
267
|
+
else:
|
|
268
|
+
for _ in range(arg):
|
|
269
|
+
self._skip_value()
|
|
270
|
+
self._skip_value()
|
|
271
|
+
elif major == _MAJOR_TAG:
|
|
272
|
+
self._skip_value()
|
|
273
|
+
elif major == _MAJOR_SIMPLE:
|
|
274
|
+
if arg == _W_2BYTE:
|
|
275
|
+
self._take(2)
|
|
276
|
+
elif arg == _W_4BYTE:
|
|
277
|
+
self._take(4)
|
|
278
|
+
elif arg == _W_8BYTE:
|
|
279
|
+
self._take(8)
|
|
280
|
+
|
|
281
|
+
def _expect(self, major: int) -> int:
|
|
282
|
+
found_major, arg = self._read_head()
|
|
283
|
+
while found_major == _MAJOR_TAG:
|
|
284
|
+
found_major, arg = self._read_head()
|
|
285
|
+
if found_major != major:
|
|
286
|
+
raise CBORDecodeError(
|
|
287
|
+
f"Expected CBOR major type {major}, found {found_major}."
|
|
288
|
+
)
|
|
289
|
+
return arg
|
|
290
|
+
|
|
291
|
+
def _take(self, n: int) -> bytes:
|
|
292
|
+
end = self._pos + n
|
|
293
|
+
if end > self._len:
|
|
294
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
295
|
+
chunk = self._data[self._pos : end]
|
|
296
|
+
self._pos = end
|
|
297
|
+
return chunk
|
|
298
|
+
|
|
299
|
+
def _peek_byte(self) -> int:
|
|
300
|
+
pos = self._pos
|
|
301
|
+
if pos >= self._len:
|
|
302
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
303
|
+
return self._data[pos]
|
|
304
|
+
|
|
305
|
+
def _read_head(self) -> tuple[int, int]:
|
|
306
|
+
"""For indefinite-length items the argument is returned as ``-1``."""
|
|
307
|
+
data = self._data
|
|
308
|
+
pos = self._pos
|
|
309
|
+
if pos >= self._len:
|
|
310
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
311
|
+
initial = data[pos]
|
|
312
|
+
major = initial >> 5
|
|
313
|
+
add_info = initial & 0x1F
|
|
314
|
+
if add_info < _W_1BYTE:
|
|
315
|
+
self._pos = pos + 1
|
|
316
|
+
return major, add_info
|
|
317
|
+
if add_info == _W_1BYTE:
|
|
318
|
+
end = pos + 2
|
|
319
|
+
if end > self._len:
|
|
320
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
321
|
+
self._pos = end
|
|
322
|
+
return major, data[pos + 1]
|
|
323
|
+
if add_info == _W_2BYTE:
|
|
324
|
+
width = 2
|
|
325
|
+
elif add_info == _W_4BYTE:
|
|
326
|
+
width = 4
|
|
327
|
+
elif add_info == _W_8BYTE:
|
|
328
|
+
width = 8
|
|
329
|
+
elif add_info == _INDEFINITE:
|
|
330
|
+
self._pos = pos + 1
|
|
331
|
+
return major, -1
|
|
332
|
+
else:
|
|
333
|
+
raise CBORDecodeError(f"Reserved additional-information value: {add_info}")
|
|
334
|
+
start = pos + 1
|
|
335
|
+
end = start + width
|
|
336
|
+
if end > self._len:
|
|
337
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
338
|
+
self._pos = end
|
|
339
|
+
return major, int.from_bytes(data[start:end], "big")
|
|
340
|
+
|
|
341
|
+
def _read_head_add_info(self) -> tuple[int, int, int]:
|
|
342
|
+
"""Returns (major, add_info, arg): major type, raw additional-info bits, and the
|
|
343
|
+
decoded argument. add_info is kept to recover a float's width, which arg alone
|
|
344
|
+
loses; arg is -1 for an indefinite-length item.
|
|
345
|
+
"""
|
|
346
|
+
data = self._data
|
|
347
|
+
pos = self._pos
|
|
348
|
+
if pos >= self._len:
|
|
349
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
350
|
+
initial = data[pos]
|
|
351
|
+
major = initial >> 5
|
|
352
|
+
add_info = initial & 0x1F
|
|
353
|
+
if add_info < _W_1BYTE:
|
|
354
|
+
self._pos = pos + 1
|
|
355
|
+
return major, add_info, add_info
|
|
356
|
+
if add_info == _W_1BYTE:
|
|
357
|
+
end = pos + 2
|
|
358
|
+
if end > self._len:
|
|
359
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
360
|
+
self._pos = end
|
|
361
|
+
return major, add_info, data[pos + 1]
|
|
362
|
+
if add_info == _W_2BYTE:
|
|
363
|
+
width = 2
|
|
364
|
+
elif add_info == _W_4BYTE:
|
|
365
|
+
width = 4
|
|
366
|
+
elif add_info == _W_8BYTE:
|
|
367
|
+
width = 8
|
|
368
|
+
elif add_info == _INDEFINITE:
|
|
369
|
+
self._pos = pos + 1
|
|
370
|
+
return major, add_info, -1
|
|
371
|
+
else:
|
|
372
|
+
raise CBORDecodeError(f"Reserved additional-information value: {add_info}")
|
|
373
|
+
start = pos + 1
|
|
374
|
+
end = start + width
|
|
375
|
+
if end > self._len:
|
|
376
|
+
raise CBORDecodeError("Unexpected end of CBOR input.")
|
|
377
|
+
self._pos = end
|
|
378
|
+
return major, add_info, int.from_bytes(data[start:end], "big")
|
|
379
|
+
|
|
380
|
+
def _at_break(self) -> bool:
|
|
381
|
+
pos = self._pos
|
|
382
|
+
return pos < self._len and self._data[pos] == _BREAK
|
|
383
|
+
|
|
384
|
+
def _consume_break(self) -> None:
|
|
385
|
+
self._take(1)
|
|
386
|
+
|
|
387
|
+
def _try_read_text(self) -> str | None:
|
|
388
|
+
"""Fast path for a definite-length text string (major 3, no tag prefix), which
|
|
389
|
+
is every struct key and every string value in practice. Returns ``None`` when
|
|
390
|
+
the next item is not such a string, so the caller falls back to the general path.
|
|
391
|
+
"""
|
|
392
|
+
data = self._data
|
|
393
|
+
pos = self._pos
|
|
394
|
+
if pos >= self._len:
|
|
395
|
+
return None
|
|
396
|
+
initial = data[pos]
|
|
397
|
+
if (initial >> 5) != _MAJOR_TEXT:
|
|
398
|
+
return None
|
|
399
|
+
add_info = initial & 0x1F
|
|
400
|
+
if add_info < _W_1BYTE:
|
|
401
|
+
start = pos + 1
|
|
402
|
+
end = start + add_info
|
|
403
|
+
elif add_info == _W_1BYTE and pos + 1 < self._len:
|
|
404
|
+
length = data[pos + 1]
|
|
405
|
+
start = pos + 2
|
|
406
|
+
end = start + length
|
|
407
|
+
else:
|
|
408
|
+
return None
|
|
409
|
+
if end > self._len:
|
|
410
|
+
return None
|
|
411
|
+
self._pos = end
|
|
412
|
+
return data[start:end].decode("utf-8")
|
|
413
|
+
|
|
414
|
+
def _at_null(self) -> bool:
|
|
415
|
+
pos = self._pos
|
|
416
|
+
return pos < self._len and self._data[pos] == _NULL_BYTE
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _decode_half(data: bytes) -> float:
|
|
420
|
+
"""Decodes an IEEE 754 half-precision float, implementing RFC 8949 Appendix D."""
|
|
421
|
+
(bits,) = _struct.unpack(">H", data)
|
|
422
|
+
sign = (bits >> 15) & 0x1
|
|
423
|
+
exp = (bits >> 10) & 0x1F
|
|
424
|
+
mant = bits & 0x3FF
|
|
425
|
+
if exp == 0:
|
|
426
|
+
value = (mant / 1024.0) * (2.0**-14)
|
|
427
|
+
elif exp == 0x1F:
|
|
428
|
+
value = float("inf") if mant == 0 else float("nan")
|
|
429
|
+
else:
|
|
430
|
+
value = (1.0 + mant / 1024.0) * (2.0 ** (exp - 15))
|
|
431
|
+
return -value if sign else value
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Generic, schema-free CBOR decoding."""
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import struct as _struct
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
_TAG_EPOCH = 1
|
|
11
|
+
_TAG_UNSIGNED_BIGNUM = 2
|
|
12
|
+
_TAG_NEGATIVE_BIGNUM = 3
|
|
13
|
+
_TAG_DECIMAL_FRACTION = 4
|
|
14
|
+
|
|
15
|
+
# Additional-information 24-27: the following value occupies 1, 2, 4, or 8 bytes.
|
|
16
|
+
_W_1BYTE = 24
|
|
17
|
+
_W_2BYTE = 25
|
|
18
|
+
_W_4BYTE = 26
|
|
19
|
+
_W_8BYTE = 27
|
|
20
|
+
_INDEFINITE = 31
|
|
21
|
+
|
|
22
|
+
_BREAK = 0xFF
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def loads(data: bytes) -> Any:
|
|
26
|
+
return _decode(data, 0)[0]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _read_head(data: bytes, pos: int) -> tuple[int, int, int, int]:
|
|
30
|
+
"""Indefinite arg is -1."""
|
|
31
|
+
initial = data[pos]
|
|
32
|
+
major = initial >> 5
|
|
33
|
+
ai = initial & 0x1F
|
|
34
|
+
pos += 1
|
|
35
|
+
if ai < _W_1BYTE:
|
|
36
|
+
return major, ai, ai, pos
|
|
37
|
+
if ai == _W_1BYTE:
|
|
38
|
+
return major, ai, data[pos], pos + 1
|
|
39
|
+
if ai == _W_2BYTE:
|
|
40
|
+
return major, ai, int.from_bytes(data[pos : pos + 2], "big"), pos + 2
|
|
41
|
+
if ai == _W_4BYTE:
|
|
42
|
+
return major, ai, int.from_bytes(data[pos : pos + 4], "big"), pos + 4
|
|
43
|
+
if ai == _W_8BYTE:
|
|
44
|
+
return major, ai, int.from_bytes(data[pos : pos + 8], "big"), pos + 8
|
|
45
|
+
if ai == _INDEFINITE:
|
|
46
|
+
return major, ai, -1, pos
|
|
47
|
+
raise ValueError(f"Reserved CBOR additional-information value: {ai}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _decode(data: bytes, pos: int) -> tuple[Any, int]:
|
|
51
|
+
major, ai, arg, pos = _read_head(data, pos)
|
|
52
|
+
if major == 0:
|
|
53
|
+
return arg, pos
|
|
54
|
+
if major == 1:
|
|
55
|
+
return -1 - arg, pos
|
|
56
|
+
if major == 2:
|
|
57
|
+
return _decode_bytes(data, pos, arg, major)
|
|
58
|
+
if major == 3:
|
|
59
|
+
raw, pos = _decode_bytes(data, pos, arg, major)
|
|
60
|
+
return raw.decode("utf-8"), pos
|
|
61
|
+
if major == 4:
|
|
62
|
+
return _decode_array(data, pos, arg)
|
|
63
|
+
if major == 5:
|
|
64
|
+
return _decode_map(data, pos, arg)
|
|
65
|
+
if major == 6:
|
|
66
|
+
return _decode_tagged(data, pos, arg)
|
|
67
|
+
return _decode_simple(data, ai, arg, pos)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _decode_bytes(data: bytes, pos: int, arg: int, major: int) -> tuple[bytes, int]:
|
|
71
|
+
if arg != -1:
|
|
72
|
+
return data[pos : pos + arg], pos + arg
|
|
73
|
+
# Indefinite-length string: concatenation of definite-length chunks until break.
|
|
74
|
+
chunks = bytearray()
|
|
75
|
+
while data[pos] != _BREAK:
|
|
76
|
+
_cmajor, _cai, clen, pos = _read_head(data, pos)
|
|
77
|
+
chunks += data[pos : pos + clen]
|
|
78
|
+
pos += clen
|
|
79
|
+
return bytes(chunks), pos + 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _decode_array(data: bytes, pos: int, arg: int) -> tuple[list[Any], int]:
|
|
83
|
+
items: list[Any] = []
|
|
84
|
+
if arg == -1:
|
|
85
|
+
while data[pos] != _BREAK:
|
|
86
|
+
value, pos = _decode(data, pos)
|
|
87
|
+
items.append(value)
|
|
88
|
+
return items, pos + 1
|
|
89
|
+
for _ in range(arg):
|
|
90
|
+
value, pos = _decode(data, pos)
|
|
91
|
+
items.append(value)
|
|
92
|
+
return items, pos
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _decode_map(data: bytes, pos: int, arg: int) -> tuple[dict[Any, Any], int]:
|
|
96
|
+
result: dict[Any, Any] = {}
|
|
97
|
+
if arg == -1:
|
|
98
|
+
while data[pos] != _BREAK:
|
|
99
|
+
key, pos = _decode(data, pos)
|
|
100
|
+
value, pos = _decode(data, pos)
|
|
101
|
+
result[key] = value
|
|
102
|
+
return result, pos + 1
|
|
103
|
+
for _ in range(arg):
|
|
104
|
+
key, pos = _decode(data, pos)
|
|
105
|
+
value, pos = _decode(data, pos)
|
|
106
|
+
result[key] = value
|
|
107
|
+
return result, pos
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _decode_tagged(data: bytes, pos: int, tag: int) -> tuple[Any, int]:
|
|
111
|
+
if tag in (_TAG_UNSIGNED_BIGNUM, _TAG_NEGATIVE_BIGNUM):
|
|
112
|
+
magnitude_bytes, pos = _decode(data, pos)
|
|
113
|
+
magnitude = int.from_bytes(magnitude_bytes, "big")
|
|
114
|
+
return (magnitude if tag == _TAG_UNSIGNED_BIGNUM else -1 - magnitude), pos
|
|
115
|
+
if tag == _TAG_DECIMAL_FRACTION:
|
|
116
|
+
array, pos = _decode(data, pos)
|
|
117
|
+
exponent, mantissa = array
|
|
118
|
+
return Decimal(mantissa).scaleb(exponent), pos
|
|
119
|
+
if tag == _TAG_EPOCH:
|
|
120
|
+
seconds, pos = _decode(data, pos)
|
|
121
|
+
return datetime.datetime.fromtimestamp(seconds, tz=datetime.UTC), pos
|
|
122
|
+
# Unknown tag: return the tagged content unchanged.
|
|
123
|
+
return _decode(data, pos)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _decode_simple(data: bytes, ai: int, arg: int, pos: int) -> tuple[Any, int]:
|
|
127
|
+
if ai == 20:
|
|
128
|
+
return False, pos
|
|
129
|
+
if ai == 21:
|
|
130
|
+
return True, pos
|
|
131
|
+
if ai in (22, 23):
|
|
132
|
+
return None, pos
|
|
133
|
+
if ai == 25:
|
|
134
|
+
return _decode_half(arg), pos
|
|
135
|
+
if ai == 26:
|
|
136
|
+
return _struct.unpack(">f", arg.to_bytes(4, "big"))[0], pos
|
|
137
|
+
if ai == 27:
|
|
138
|
+
return _struct.unpack(">d", arg.to_bytes(8, "big"))[0], pos
|
|
139
|
+
return arg, pos
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _decode_half(bits: int) -> float:
|
|
143
|
+
sign = (bits >> 15) & 0x1
|
|
144
|
+
exp = (bits >> 10) & 0x1F
|
|
145
|
+
mant = bits & 0x3FF
|
|
146
|
+
if exp == 0:
|
|
147
|
+
value = (mant / 1024.0) * (2.0**-14)
|
|
148
|
+
elif exp == 0x1F:
|
|
149
|
+
value = float("inf") if mant == 0 else float("nan")
|
|
150
|
+
else:
|
|
151
|
+
value = (1.0 + mant / 1024.0) * (2.0 ** (exp - 15))
|
|
152
|
+
return -value if sign else value
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Cached schema metadata used by the CBOR codec."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from smithy_core.schemas import Schema, SchemaExtension
|
|
8
|
+
from smithy_core.shapes import ShapeType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class CBORMemberLookup:
|
|
13
|
+
"""Matches raw UTF-8 CBOR field names to member schemas."""
|
|
14
|
+
|
|
15
|
+
ordered_schemas: tuple[Schema, ...]
|
|
16
|
+
ordered_name_bytes: tuple[bytes, ...]
|
|
17
|
+
entries_by_name: dict[bytes, Schema]
|
|
18
|
+
|
|
19
|
+
def lookup(
|
|
20
|
+
self,
|
|
21
|
+
data: bytes,
|
|
22
|
+
start: int,
|
|
23
|
+
end: int,
|
|
24
|
+
expected_next: int,
|
|
25
|
+
) -> Schema | None:
|
|
26
|
+
"""Look up a member without decoding its field name to a string."""
|
|
27
|
+
name_length = end - start
|
|
28
|
+
if 0 <= expected_next < len(self.ordered_name_bytes):
|
|
29
|
+
expected = self.ordered_name_bytes[expected_next]
|
|
30
|
+
if len(expected) == name_length and data.startswith(expected, start, end):
|
|
31
|
+
return self.ordered_schemas[expected_next]
|
|
32
|
+
|
|
33
|
+
return self.entries_by_name.get(data[start:end])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class CBORSchemaMetadata:
|
|
38
|
+
"""Precomputed CBOR data stored on a schema."""
|
|
39
|
+
|
|
40
|
+
member_name_bytes: bytes | None = None
|
|
41
|
+
member_lookup: CBORMemberLookup | None = None
|
|
42
|
+
field_name_table: tuple[bytes | None, ...] = ()
|
|
43
|
+
member_count: int = 0
|
|
44
|
+
struct_header_width: int = 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _encode_member_name(name: str) -> bytes:
|
|
48
|
+
encoded = name.encode("utf-8")
|
|
49
|
+
length = len(encoded)
|
|
50
|
+
if length < 24:
|
|
51
|
+
return bytes((0x60 | length,)) + encoded
|
|
52
|
+
if length < 0x100:
|
|
53
|
+
return b"\x78" + bytes((length,)) + encoded
|
|
54
|
+
if length < 0x10000:
|
|
55
|
+
return b"\x79" + length.to_bytes(2, "big") + encoded
|
|
56
|
+
if length < 0x100000000:
|
|
57
|
+
return b"\x7a" + length.to_bytes(4, "big") + encoded
|
|
58
|
+
return b"\x7b" + length.to_bytes(8, "big") + encoded
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _head_width(arg: int) -> int:
|
|
62
|
+
if arg < 24:
|
|
63
|
+
return 1
|
|
64
|
+
if arg < 0x100:
|
|
65
|
+
return 2
|
|
66
|
+
if arg < 0x10000:
|
|
67
|
+
return 3
|
|
68
|
+
if arg < 0x100000000:
|
|
69
|
+
return 5
|
|
70
|
+
return 9
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_cbor_schema_metadata(schema: Schema) -> CBORSchemaMetadata | None:
|
|
74
|
+
if schema.member_target is not None:
|
|
75
|
+
return CBORSchemaMetadata(
|
|
76
|
+
member_name_bytes=_encode_member_name(schema.expect_member_name())
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if schema.shape_type not in (ShapeType.STRUCTURE, ShapeType.UNION):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
members = tuple(
|
|
83
|
+
sorted(schema.members.values(), key=lambda member: member.expect_member_index())
|
|
84
|
+
)
|
|
85
|
+
member_count = len(members)
|
|
86
|
+
if not members:
|
|
87
|
+
return CBORSchemaMetadata()
|
|
88
|
+
|
|
89
|
+
max_index = max(member.expect_member_index() for member in members)
|
|
90
|
+
field_name_table: list[bytes | None] = [None] * (max_index + 1)
|
|
91
|
+
ordered_name_bytes: list[bytes] = []
|
|
92
|
+
entries_by_name: dict[bytes, Schema] = {}
|
|
93
|
+
|
|
94
|
+
for member in members:
|
|
95
|
+
raw_name = member.expect_member_name().encode("utf-8")
|
|
96
|
+
ordered_name_bytes.append(raw_name)
|
|
97
|
+
entries_by_name[raw_name] = member
|
|
98
|
+
field_name_table[member.expect_member_index()] = _encode_member_name(
|
|
99
|
+
member.expect_member_name()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return CBORSchemaMetadata(
|
|
103
|
+
member_lookup=CBORMemberLookup(
|
|
104
|
+
ordered_schemas=members,
|
|
105
|
+
ordered_name_bytes=tuple(ordered_name_bytes),
|
|
106
|
+
entries_by_name=entries_by_name,
|
|
107
|
+
),
|
|
108
|
+
field_name_table=tuple(field_name_table),
|
|
109
|
+
member_count=member_count,
|
|
110
|
+
struct_header_width=_head_width(member_count),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
CBOR_SCHEMA_EXTENSION = SchemaExtension(_build_cbor_schema_metadata)
|
|
115
|
+
"""Shared descriptor for lazily cached CBOR schema metadata."""
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""CBOR shape serializer.
|
|
4
|
+
|
|
5
|
+
Aggregates are definite-length: lists and maps take their element count up front, and
|
|
6
|
+
structs reserve a header slot sized for the schema's max member count, then patch the
|
|
7
|
+
actual present-member count into it on close.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import datetime
|
|
11
|
+
import struct as _struct
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from contextlib import AbstractContextManager, nullcontext
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
from io import BytesIO
|
|
16
|
+
from types import TracebackType
|
|
17
|
+
from typing import Self, override
|
|
18
|
+
|
|
19
|
+
from smithy_core.documents import Document
|
|
20
|
+
from smithy_core.interfaces import BytesWriter
|
|
21
|
+
from smithy_core.schemas import Schema
|
|
22
|
+
from smithy_core.serializers import MapSerializer, ShapeSerializer
|
|
23
|
+
from smithy_core.shapes import ShapeType
|
|
24
|
+
from smithy_core.utils import ensure_utc
|
|
25
|
+
|
|
26
|
+
from ..settings import CBORSettings
|
|
27
|
+
|
|
28
|
+
# CBOR major types, shifted into the high 3 bits of the initial byte.
|
|
29
|
+
_MAJOR_UINT = 0 << 5
|
|
30
|
+
_MAJOR_NEGINT = 1 << 5
|
|
31
|
+
_MAJOR_BYTES = 2 << 5
|
|
32
|
+
_MAJOR_TEXT = 3 << 5
|
|
33
|
+
_MAJOR_ARRAY = 4 << 5
|
|
34
|
+
_MAJOR_MAP = 5 << 5
|
|
35
|
+
_MAJOR_TAG = 6 << 5
|
|
36
|
+
_MAJOR_SIMPLE = 7 << 5
|
|
37
|
+
|
|
38
|
+
# Tags used by rpcv2Cbor.
|
|
39
|
+
_TAG_EPOCH = 1
|
|
40
|
+
_TAG_UNSIGNED_BIGNUM = 2
|
|
41
|
+
_TAG_NEGATIVE_BIGNUM = 3
|
|
42
|
+
_TAG_DECIMAL_FRACTION = 4
|
|
43
|
+
|
|
44
|
+
# Simple values (major type 7).
|
|
45
|
+
_SIMPLE_FALSE = 20
|
|
46
|
+
_SIMPLE_TRUE = 21
|
|
47
|
+
_SIMPLE_NULL = 22
|
|
48
|
+
|
|
49
|
+
# Additional-information 24-27: the following value occupies 1, 2, 4, or 8 bytes.
|
|
50
|
+
_W_1BYTE = 24
|
|
51
|
+
_W_2BYTE = 25
|
|
52
|
+
_W_4BYTE = 26
|
|
53
|
+
_W_8BYTE = 27
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CBORShapeSerializer(ShapeSerializer):
|
|
57
|
+
def __init__(self, sink: BytesWriter, settings: CBORSettings) -> None:
|
|
58
|
+
self._sink = sink
|
|
59
|
+
self.settings = settings
|
|
60
|
+
|
|
61
|
+
def write(self, data: bytes) -> None:
|
|
62
|
+
self._sink.write(data)
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
def begin_struct(self, schema: Schema) -> AbstractContextManager[ShapeSerializer]:
|
|
66
|
+
return _CBORStructSerializer(self, self._sink, schema)
|
|
67
|
+
|
|
68
|
+
@override
|
|
69
|
+
def begin_list(
|
|
70
|
+
self, schema: Schema, size: int
|
|
71
|
+
) -> AbstractContextManager[ShapeSerializer]:
|
|
72
|
+
# A definite-length list needs no per-element framing or close step, and a value
|
|
73
|
+
# encodes identically inside a collection or not, so elements reuse this same
|
|
74
|
+
# serializer; only the header is context-specific.
|
|
75
|
+
self.write(_encode_head(_MAJOR_ARRAY, size))
|
|
76
|
+
return nullcontext(self)
|
|
77
|
+
|
|
78
|
+
@override
|
|
79
|
+
def begin_map(
|
|
80
|
+
self, schema: Schema, size: int
|
|
81
|
+
) -> AbstractContextManager[MapSerializer]:
|
|
82
|
+
self.write(_encode_head(_MAJOR_MAP, size))
|
|
83
|
+
return nullcontext(_CBORMapSerializer(self))
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
def write_null(self, schema: Schema) -> None:
|
|
87
|
+
self.write(bytes((_MAJOR_SIMPLE | _SIMPLE_NULL,)))
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
def write_boolean(self, schema: Schema, value: bool) -> None:
|
|
91
|
+
self.write(bytes((_MAJOR_SIMPLE | (_SIMPLE_TRUE if value else _SIMPLE_FALSE),)))
|
|
92
|
+
|
|
93
|
+
@override
|
|
94
|
+
def write_integer(self, schema: Schema, value: int) -> None:
|
|
95
|
+
self.write(_encode_int(value))
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def write_float(self, schema: Schema, value: float) -> None:
|
|
99
|
+
# A `float` shape encodes as IEEE single (major 7, add_info 26); `double` as
|
|
100
|
+
# IEEE double (add_info 27). Half-precision is not emitted. NaN/Infinity
|
|
101
|
+
# round-trip through struct.pack unchanged.
|
|
102
|
+
if schema.shape_type is ShapeType.FLOAT:
|
|
103
|
+
self.write(bytes((_MAJOR_SIMPLE | 26,)) + _struct.pack(">f", value))
|
|
104
|
+
else:
|
|
105
|
+
self.write(bytes((_MAJOR_SIMPLE | 27,)) + _struct.pack(">d", value))
|
|
106
|
+
|
|
107
|
+
@override
|
|
108
|
+
def write_big_integer(self, schema: Schema, value: int) -> None:
|
|
109
|
+
if value >= 0:
|
|
110
|
+
tag, magnitude = _TAG_UNSIGNED_BIGNUM, value
|
|
111
|
+
else:
|
|
112
|
+
tag, magnitude = _TAG_NEGATIVE_BIGNUM, -1 - value
|
|
113
|
+
length = (magnitude.bit_length() + 7) // 8
|
|
114
|
+
payload = magnitude.to_bytes(length, "big")
|
|
115
|
+
self.write(_encode_head(_MAJOR_TAG, tag))
|
|
116
|
+
self.write(_encode_head(_MAJOR_BYTES, len(payload)))
|
|
117
|
+
self.write(payload)
|
|
118
|
+
|
|
119
|
+
@override
|
|
120
|
+
def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
|
|
121
|
+
sign, digits, exponent = value.as_tuple()
|
|
122
|
+
if not isinstance(exponent, int): # "n"/"N"/"F" for special values
|
|
123
|
+
raise ValueError(f"Cannot CBOR-encode non-finite Decimal: {value!r}")
|
|
124
|
+
mantissa = int("".join(map(str, digits)) or "0")
|
|
125
|
+
if sign:
|
|
126
|
+
mantissa = -mantissa
|
|
127
|
+
self.write(_encode_head(_MAJOR_TAG, _TAG_DECIMAL_FRACTION))
|
|
128
|
+
self.write(_encode_head(_MAJOR_ARRAY, 2))
|
|
129
|
+
self.write(_encode_int(exponent))
|
|
130
|
+
self.write(_encode_int(mantissa))
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
134
|
+
encoded = value.encode("utf-8")
|
|
135
|
+
self.write(_encode_head(_MAJOR_TEXT, len(encoded)))
|
|
136
|
+
self.write(encoded)
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
def write_blob(self, schema: Schema, value: bytes) -> None:
|
|
140
|
+
self.write(_encode_head(_MAJOR_BYTES, len(value)))
|
|
141
|
+
self.write(value)
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
def write_timestamp(self, schema: Schema, value: datetime.datetime) -> None:
|
|
145
|
+
# timestampFormat MUST NOT be respected.
|
|
146
|
+
self.write(_encode_head(_MAJOR_TAG, _TAG_EPOCH))
|
|
147
|
+
self.write_float(schema, ensure_utc(value).timestamp())
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def write_document(self, schema: Schema, value: Document) -> None:
|
|
151
|
+
raise NotImplementedError(
|
|
152
|
+
"The rpcv2Cbor protocol does not support document types."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@override
|
|
156
|
+
def flush(self) -> None:
|
|
157
|
+
# BytesWriter has no flush; the sink is written to directly.
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _encode_head(major: int, arg: int) -> bytes:
|
|
162
|
+
"""Uses the smallest of the 1/2/4/8-byte length forms per RFC 8949 §3."""
|
|
163
|
+
if arg < _W_1BYTE:
|
|
164
|
+
return bytes((major | arg,))
|
|
165
|
+
if arg < 0x100:
|
|
166
|
+
return bytes((major | _W_1BYTE, arg))
|
|
167
|
+
if arg < 0x10000:
|
|
168
|
+
return bytes((major | _W_2BYTE,)) + arg.to_bytes(2, "big")
|
|
169
|
+
if arg < 0x100000000:
|
|
170
|
+
return bytes((major | _W_4BYTE,)) + arg.to_bytes(4, "big")
|
|
171
|
+
return bytes((major | _W_8BYTE,)) + arg.to_bytes(8, "big")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _head_width(arg: int) -> int:
|
|
175
|
+
"""Bytes _encode_head needs for arg. A struct reserves this for its MAX member count
|
|
176
|
+
up front, then patches the actual (<=) count into that width on close.
|
|
177
|
+
"""
|
|
178
|
+
if arg < _W_1BYTE:
|
|
179
|
+
return 1
|
|
180
|
+
if arg < 0x100:
|
|
181
|
+
return 2
|
|
182
|
+
if arg < 0x10000:
|
|
183
|
+
return 3
|
|
184
|
+
if arg < 0x100000000:
|
|
185
|
+
return 5
|
|
186
|
+
return 9
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _write_head_fixed(
|
|
190
|
+
buf: memoryview, offset: int, major: int, arg: int, width: int
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Patches a definite header of exactly `width` bytes into `buf` at `offset`.
|
|
193
|
+
|
|
194
|
+
CBOR allows a non-minimal length form, so an over-reserved width just holds the same
|
|
195
|
+
count in a wider encoding (e.g. 5 as 0xb8 0x05, not the minimal 0xa5) -- no gap.
|
|
196
|
+
"""
|
|
197
|
+
if width == 1:
|
|
198
|
+
buf[offset] = major | arg
|
|
199
|
+
return
|
|
200
|
+
add_info = {2: _W_1BYTE, 3: _W_2BYTE, 5: _W_4BYTE, 9: _W_8BYTE}[width]
|
|
201
|
+
buf[offset] = major | add_info
|
|
202
|
+
buf[offset + 1 : offset + width] = arg.to_bytes(width - 1, "big")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _encode_int(value: int) -> bytes:
|
|
206
|
+
if value >= 0:
|
|
207
|
+
return _encode_head(_MAJOR_UINT, value)
|
|
208
|
+
# Negative integers encode -1 - n as an unsigned argument.
|
|
209
|
+
return _encode_head(_MAJOR_NEGINT, -1 - value)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# Attribute name under which a member schema's pre-encoded CBOR key (text header +
|
|
213
|
+
# UTF-8 name) is memoized. Member schemas are long-lived and shared, so encoding the
|
|
214
|
+
# key once per schema removes a per-serialization encode + head allocation.
|
|
215
|
+
_CBOR_KEY_ATTR = "_cbor_encoded_key"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _encoded_member_key(schema: Schema) -> bytes:
|
|
219
|
+
key = getattr(schema, _CBOR_KEY_ATTR, None)
|
|
220
|
+
if key is None:
|
|
221
|
+
encoded = schema.expect_member_name().encode("utf-8")
|
|
222
|
+
key = _encode_head(_MAJOR_TEXT, len(encoded)) + encoded
|
|
223
|
+
# Schema is a frozen dataclass; object.__setattr__ is its own escape hatch.
|
|
224
|
+
object.__setattr__(schema, _CBOR_KEY_ATTR, key)
|
|
225
|
+
return key
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class _CBORStructSerializer(ShapeSerializer):
|
|
229
|
+
"""Emits definite-length map. Reserves the full memberCount worth of byte-width
|
|
230
|
+
and writes the actual member count back to that position when completing the struct.
|
|
231
|
+
This may waste up to 1 byte in edge-case conditions, but minimizes overall work.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
def __init__(
|
|
235
|
+
self, parent: CBORShapeSerializer, sink: BytesWriter, schema: Schema
|
|
236
|
+
) -> None:
|
|
237
|
+
self._parent = parent
|
|
238
|
+
# Reserve-and-patch needs a seekable buffer; Codec.serialize always supplies a
|
|
239
|
+
# BytesIO, and rpcv2Cbor has no streaming-serialize path that would not.
|
|
240
|
+
if not isinstance(sink, BytesIO):
|
|
241
|
+
raise TypeError(
|
|
242
|
+
"CBOR struct serialization requires a seekable BytesIO sink."
|
|
243
|
+
)
|
|
244
|
+
self._sink: BytesIO = sink
|
|
245
|
+
self._width = _head_width(len(schema.members))
|
|
246
|
+
self._offset = sink.tell()
|
|
247
|
+
sink.write(bytes(self._width)) # reserved header slot, patched on close
|
|
248
|
+
self._count = 0
|
|
249
|
+
|
|
250
|
+
def __enter__(self) -> Self:
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def __exit__(
|
|
254
|
+
self,
|
|
255
|
+
exc_type: type[BaseException] | None,
|
|
256
|
+
exc_value: BaseException | None,
|
|
257
|
+
traceback: TracebackType | None,
|
|
258
|
+
) -> None:
|
|
259
|
+
if exc_value is not None:
|
|
260
|
+
return
|
|
261
|
+
buf = self._sink.getbuffer()
|
|
262
|
+
_write_head_fixed(buf, self._offset, _MAJOR_MAP, self._count, self._width)
|
|
263
|
+
buf.release()
|
|
264
|
+
|
|
265
|
+
@override
|
|
266
|
+
def begin_struct(self, schema: Schema) -> AbstractContextManager[ShapeSerializer]:
|
|
267
|
+
self._write_key(schema)
|
|
268
|
+
return self._parent.begin_struct(schema)
|
|
269
|
+
|
|
270
|
+
@override
|
|
271
|
+
def begin_list(
|
|
272
|
+
self, schema: Schema, size: int
|
|
273
|
+
) -> AbstractContextManager[ShapeSerializer]:
|
|
274
|
+
self._write_key(schema)
|
|
275
|
+
return self._parent.begin_list(schema, size)
|
|
276
|
+
|
|
277
|
+
@override
|
|
278
|
+
def begin_map(
|
|
279
|
+
self, schema: Schema, size: int
|
|
280
|
+
) -> AbstractContextManager[MapSerializer]:
|
|
281
|
+
self._write_key(schema)
|
|
282
|
+
return self._parent.begin_map(schema, size)
|
|
283
|
+
|
|
284
|
+
@override
|
|
285
|
+
def write_null(self, schema: Schema) -> None:
|
|
286
|
+
self._write_key(schema)
|
|
287
|
+
self._parent.write_null(schema)
|
|
288
|
+
|
|
289
|
+
@override
|
|
290
|
+
def write_boolean(self, schema: Schema, value: bool) -> None:
|
|
291
|
+
self._write_key(schema)
|
|
292
|
+
self._parent.write_boolean(schema, value)
|
|
293
|
+
|
|
294
|
+
@override
|
|
295
|
+
def write_integer(self, schema: Schema, value: int) -> None:
|
|
296
|
+
self._write_key(schema)
|
|
297
|
+
self._parent.write_integer(schema, value)
|
|
298
|
+
|
|
299
|
+
@override
|
|
300
|
+
def write_float(self, schema: Schema, value: float) -> None:
|
|
301
|
+
self._write_key(schema)
|
|
302
|
+
self._parent.write_float(schema, value)
|
|
303
|
+
|
|
304
|
+
@override
|
|
305
|
+
def write_big_integer(self, schema: Schema, value: int) -> None:
|
|
306
|
+
self._write_key(schema)
|
|
307
|
+
self._parent.write_big_integer(schema, value)
|
|
308
|
+
|
|
309
|
+
@override
|
|
310
|
+
def write_big_decimal(self, schema: Schema, value: Decimal) -> None:
|
|
311
|
+
self._write_key(schema)
|
|
312
|
+
self._parent.write_big_decimal(schema, value)
|
|
313
|
+
|
|
314
|
+
@override
|
|
315
|
+
def write_string(self, schema: Schema, value: str) -> None:
|
|
316
|
+
self._write_key(schema)
|
|
317
|
+
self._parent.write_string(schema, value)
|
|
318
|
+
|
|
319
|
+
@override
|
|
320
|
+
def write_blob(self, schema: Schema, value: bytes) -> None:
|
|
321
|
+
self._write_key(schema)
|
|
322
|
+
self._parent.write_blob(schema, value)
|
|
323
|
+
|
|
324
|
+
@override
|
|
325
|
+
def write_timestamp(self, schema: Schema, value: datetime.datetime) -> None:
|
|
326
|
+
self._write_key(schema)
|
|
327
|
+
self._parent.write_timestamp(schema, value)
|
|
328
|
+
|
|
329
|
+
@override
|
|
330
|
+
def write_document(self, schema: Schema, value: Document) -> None:
|
|
331
|
+
raise NotImplementedError(
|
|
332
|
+
"The rpcv2Cbor protocol does not support document types."
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
@override
|
|
336
|
+
def flush(self) -> None:
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
def _write_key(self, schema: Schema) -> None:
|
|
340
|
+
self._count += 1
|
|
341
|
+
self._sink.write(_encoded_member_key(schema))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
class _CBORMapSerializer(MapSerializer):
|
|
345
|
+
"""The definite-length map header (from `size`) is written by `begin_map` before
|
|
346
|
+
this is constructed; entries stream straight to the sink.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
def __init__(self, parent: CBORShapeSerializer) -> None:
|
|
350
|
+
self._parent = parent
|
|
351
|
+
|
|
352
|
+
@override
|
|
353
|
+
def entry(self, key: str, value_writer: Callable[[ShapeSerializer], None]) -> None:
|
|
354
|
+
encoded = key.encode("utf-8")
|
|
355
|
+
self._parent.write(_encode_head(_MAJOR_TEXT, len(encoded)))
|
|
356
|
+
self._parent.write(encoded)
|
|
357
|
+
value_writer(self._parent)
|
smithy_cbor/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Marker
|
smithy_cbor/settings.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class CBORSettings:
|
|
8
|
+
"""Configuration for the CBOR codec."""
|
|
9
|
+
|
|
10
|
+
default_namespace: str | None = None
|
|
11
|
+
"""Default namespace for resolving a document's discriminator. Not yet consumed:
|
|
12
|
+
the codec does not implement document types."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: smithy-cbor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CBOR serialization and deserialization support for Smithy tooling.
|
|
5
|
+
Project-URL: Changelog, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-cbor/CHANGELOG.md
|
|
6
|
+
Project-URL: Code, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-cbor/
|
|
7
|
+
Project-URL: Issue tracker, https://github.com/smithy-lang/smithy-python/issues
|
|
8
|
+
Author: Amazon Web Services
|
|
9
|
+
License: Apache License 2.0
|
|
10
|
+
License-File: NOTICE
|
|
11
|
+
Keywords: cbor,sdk,smithy
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
25
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
27
|
+
Requires-Python: >=3.12
|
|
28
|
+
Requires-Dist: smithy-core
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# smithy-cbor
|
|
32
|
+
|
|
33
|
+
This package provides generic CBOR ([RFC 8949](https://www.rfc-editor.org/rfc/rfc8949))
|
|
34
|
+
serialization and deserialization support for Smithy clients and servers, targeting
|
|
35
|
+
the subset of CBOR used by the `smithy.protocols#rpcv2Cbor` protocol.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
smithy_cbor/__init__.py,sha256=-tG-gheJRSxBPnmuiVZnUsiUg4AX0K7_qnlxs0b4AKU,1418
|
|
2
|
+
smithy_cbor/py.typed,sha256=E84IaZyFwfLqvXjOVW4LS6WH7QOaKEFpNh9TFyzHNQc,6
|
|
3
|
+
smithy_cbor/settings.py,sha256=UnEwQRd9kuv1-Pxey5EgD4D-9hUofMN4WS7HUlGHsHo,411
|
|
4
|
+
smithy_cbor/_private/__init__.py,sha256=5idTbB990emq5l8fB_u7RfQB5qGATEBtb4PxBU2IIhs,109
|
|
5
|
+
smithy_cbor/_private/deserializers.py,sha256=GhT0RQTTwXEI1k1agC7Vcc6QM2AD_LVeKdL0tgPToiE,14589
|
|
6
|
+
smithy_cbor/_private/generic.py,sha256=y_NJFIlyPCBT_6kXfQpGqXdK5V5ciCXGw8-M_0CgC5Q,4795
|
|
7
|
+
smithy_cbor/_private/schema_extensions.py,sha256=xODPgafw0GXSmenDnOCWx3tyKBs9f_l_3dExgalFJyo,3714
|
|
8
|
+
smithy_cbor/_private/serializers.py,sha256=tjS3bsm6KCcfYauEtQeKqeoN6Y_5PlV_nWn_qYGtedI,12458
|
|
9
|
+
smithy_cbor-0.1.0.dist-info/METADATA,sha256=m9BG-7PXYeY-M5KdooobFfk39q8EUihHsXhe2nsUhQI,1675
|
|
10
|
+
smithy_cbor-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
11
|
+
smithy_cbor-0.1.0.dist-info/licenses/NOTICE,sha256=1CkO1kwu3Q_OHYTj-d-yiBJA_lNN73a4zSntavaD4oc,67
|
|
12
|
+
smithy_cbor-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|