asnkitrt 1.0.1__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.
- asnkitrt/__init__.py +50 -0
- asnkitrt/codecs/__init__.py +520 -0
- asnkitrt/codecs/ber.py +2028 -0
- asnkitrt/codecs/codec.py +23 -0
- asnkitrt/codecs/compiler.py +1668 -0
- asnkitrt/codecs/constraints_checker.py +758 -0
- asnkitrt/codecs/der.py +733 -0
- asnkitrt/codecs/duration.py +315 -0
- asnkitrt/codecs/gser.py +550 -0
- asnkitrt/codecs/jer.py +744 -0
- asnkitrt/codecs/oer.py +1695 -0
- asnkitrt/codecs/per.py +2511 -0
- asnkitrt/codecs/permitted_alphabet.py +22 -0
- asnkitrt/codecs/type_checker.py +327 -0
- asnkitrt/codecs/uper.py +432 -0
- asnkitrt/codecs/xer.py +983 -0
- asnkitrt/constants.py +57 -0
- asnkitrt/errors.py +30 -0
- asnkitrt/py.typed +0 -0
- asnkitrt/runtime.py +423 -0
- asnkitrt/syntax/__init__.py +172 -0
- asnkitrt/syntax/__main__.py +88 -0
- asnkitrt/syntax/cbor.py +238 -0
- asnkitrt/syntax/container.py +867 -0
- asnkitrt/syntax/convert.py +1256 -0
- asnkitrt/syntax/nodes.py +711 -0
- asnkitrt/syntax/wire.py +536 -0
- asnkitrt/version.py +5 -0
- asnkitrt-1.0.1.dist-info/METADATA +63 -0
- asnkitrt-1.0.1.dist-info/RECORD +33 -0
- asnkitrt-1.0.1.dist-info/WHEEL +4 -0
- asnkitrt-1.0.1.dist-info/licenses/LICENSE-RUNTIMES +22 -0
- asnkitrt-1.0.1.dist-info/licenses/NOTICE +26 -0
asnkitrt/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2025 BoxTheta
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Encode and decode ASN.1 types of compiled specifications, which
|
|
6
|
+
are found in `.asni` files.
|
|
7
|
+
|
|
8
|
+
Nothing in this package parses ASN.1. That is what the compiler
|
|
9
|
+
`asnkitcc` does, which makes the `.asni` files.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
>>> import asnkitrt
|
|
13
|
+
>>> foo = asnkitrt.load_asni_file('foo.asni', 'uper')
|
|
14
|
+
>>> foo.encode('Question', {'id': 1, 'question': 'Is 1+1=3?'})
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from . import codecs, constants, errors, runtime, syntax
|
|
20
|
+
from .errors import (
|
|
21
|
+
CompileError,
|
|
22
|
+
ConstraintsError,
|
|
23
|
+
DecodeError,
|
|
24
|
+
EncodeError,
|
|
25
|
+
Error,
|
|
26
|
+
ParseError,
|
|
27
|
+
)
|
|
28
|
+
from .runtime import Specification, load, load_asni, load_asni_file
|
|
29
|
+
from .version import __version__
|
|
30
|
+
|
|
31
|
+
__author__ = "BoxTheta"
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CompileError",
|
|
35
|
+
"ConstraintsError",
|
|
36
|
+
"DecodeError",
|
|
37
|
+
"EncodeError",
|
|
38
|
+
"Error",
|
|
39
|
+
"ParseError",
|
|
40
|
+
"Specification",
|
|
41
|
+
"codecs",
|
|
42
|
+
"constants",
|
|
43
|
+
"errors",
|
|
44
|
+
"load",
|
|
45
|
+
"load_asni",
|
|
46
|
+
"load_asni_file",
|
|
47
|
+
"runtime",
|
|
48
|
+
"syntax",
|
|
49
|
+
"__version__",
|
|
50
|
+
]
|
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
# Copyright (c) 2025 BoxTheta
|
|
2
|
+
# Copyright (c) 2017-2019 Erik Moqvist
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
import importlib
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Any, List
|
|
10
|
+
|
|
11
|
+
from ..errors import ConstraintsError as _ConstraintsError
|
|
12
|
+
from ..errors import DecodeError as _DecodeError
|
|
13
|
+
from ..errors import EncodeError as _EncodeError
|
|
14
|
+
from ..errors import Error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseType(object):
|
|
18
|
+
"""
|
|
19
|
+
Base Type class containing common functionality between all codecs
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str, type_name: str) -> None:
|
|
23
|
+
self.name = name
|
|
24
|
+
self.type_name = type_name
|
|
25
|
+
self.optional = False
|
|
26
|
+
self.default: Any = None
|
|
27
|
+
|
|
28
|
+
def set_default(self, value: Any) -> None:
|
|
29
|
+
self.default = value
|
|
30
|
+
|
|
31
|
+
def get_default(self) -> Any:
|
|
32
|
+
return self.default
|
|
33
|
+
|
|
34
|
+
def has_default(self) -> bool:
|
|
35
|
+
return self.default is not None
|
|
36
|
+
|
|
37
|
+
def is_default(self, value: Any) -> bool:
|
|
38
|
+
return value == self.default
|
|
39
|
+
|
|
40
|
+
def encode(self, *args: Any, **kwargs: Any) -> Any:
|
|
41
|
+
raise NotImplementedError("To be implemented by subclasses.")
|
|
42
|
+
|
|
43
|
+
def decode(self, *args: Any, **kwargs: Any) -> Any:
|
|
44
|
+
raise NotImplementedError("To be implemented by subclasses.")
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return "{}({})".format(self.__class__.__name__, self.name)
|
|
48
|
+
|
|
49
|
+
def type_label(self) -> str:
|
|
50
|
+
return (
|
|
51
|
+
"{}({})".format(self.type_name, self.name) if self.name else self.type_name
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ErrorWithLocation(Exception):
|
|
56
|
+
"""
|
|
57
|
+
Mixin for Error classes which have location list
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, location: BaseType | None = None) -> None:
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
:param str message:
|
|
64
|
+
:param BaseType location: Type error occured for
|
|
65
|
+
"""
|
|
66
|
+
self.message = message
|
|
67
|
+
self.location: List[BaseType] = [location] if location else []
|
|
68
|
+
|
|
69
|
+
def add_location(self, element: BaseType) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Add location data for element
|
|
72
|
+
:param BaseType element:
|
|
73
|
+
:return:
|
|
74
|
+
"""
|
|
75
|
+
# Don't add name if it is blank (for SEQUENCE OF, SET OF etc)
|
|
76
|
+
if not getattr(element, "no_error_location", False) and (
|
|
77
|
+
not self.location or element != self.location[-1]
|
|
78
|
+
):
|
|
79
|
+
self.location.append(element)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def location_str(self) -> str:
|
|
83
|
+
return ".".join(loc.name for loc in self.location[::-1] if loc.name)
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
if self.location:
|
|
87
|
+
return "{}: {}".format(self.location_str, self.message)
|
|
88
|
+
else:
|
|
89
|
+
return self.message
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class EncodeError(ErrorWithLocation, _EncodeError):
|
|
93
|
+
"""General ASN.1 encode error."""
|
|
94
|
+
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class NamedNumbersMixin(object):
|
|
99
|
+
"""An INTEGER, which can be given by the name of any of its named
|
|
100
|
+
numbers when encoding. It is always a number when decoded.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
named_numbers: Any = None
|
|
105
|
+
|
|
106
|
+
def to_number(self, data: Any) -> Any:
|
|
107
|
+
if not isinstance(data, str):
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
return self.named_numbers[data]
|
|
112
|
+
except (KeyError, TypeError):
|
|
113
|
+
raise EncodeError(
|
|
114
|
+
"Expected an integer or a named number, but got '{}'.".format(data)
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def is_default(self, value: Any) -> bool:
|
|
118
|
+
return self.to_number(value) == self.default # type: ignore[attr-defined]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DecodeError(ErrorWithLocation, _DecodeError):
|
|
122
|
+
"""
|
|
123
|
+
General ASN.1 decode error with error location in the message.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
message: str,
|
|
129
|
+
offset: int | None = None,
|
|
130
|
+
location: BaseType | None = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
:param str message: Message for error
|
|
135
|
+
:param int offset: Data offset at which error occurred. Can be bits or bytes depending on codec
|
|
136
|
+
:param BaseType location: Element in which error occurred
|
|
137
|
+
"""
|
|
138
|
+
super(DecodeError, self).__init__(message, location=location)
|
|
139
|
+
self.offset = offset
|
|
140
|
+
|
|
141
|
+
def __str__(self) -> str:
|
|
142
|
+
if self.location:
|
|
143
|
+
_str = "{}: {}".format(self.location_str, self.message)
|
|
144
|
+
else:
|
|
145
|
+
_str = self.message
|
|
146
|
+
if self.offset is not None:
|
|
147
|
+
_str += self.get_offset_message()
|
|
148
|
+
return _str
|
|
149
|
+
|
|
150
|
+
def get_offset_message(self) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Get offset details to add to error message
|
|
153
|
+
:return:
|
|
154
|
+
"""
|
|
155
|
+
return " (At offset: {})".format(self.offset)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ConstraintsError(ErrorWithLocation, _ConstraintsError):
|
|
159
|
+
"""
|
|
160
|
+
General ASN.1 constraints error with error location in the message.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class OutOfDataError(DecodeError):
|
|
167
|
+
"""
|
|
168
|
+
Error for when trying to read data beyond length of type
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
def __init__(self, offset_bits: int, location: BaseType | None = None) -> None:
|
|
172
|
+
super(OutOfDataError, self).__init__(
|
|
173
|
+
"out of data", offset=offset_bits, location=location
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def get_offset_message(self) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Get offset details to add to error message
|
|
179
|
+
:return:
|
|
180
|
+
"""
|
|
181
|
+
return " (At bit offset: {})".format(self.offset)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# A GeneralizedTime: the date and the hour, then the minute and the
|
|
185
|
+
# second or not, a fraction of the last of them or not, and the offset
|
|
186
|
+
# or not (X.680 46.3).
|
|
187
|
+
_GENERALIZED_TIME = re.compile(
|
|
188
|
+
r"(\d{4})(\d{2})(\d{2})(\d{2})(?:(\d{2})(\d{2})?)?(?:[.,](\d+))?"
|
|
189
|
+
r"(Z|[+-]\d{2}(?:\d{2})?)?\Z",
|
|
190
|
+
re.ASCII,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _parse_generalized_time(string: str) -> datetime:
|
|
195
|
+
match = _GENERALIZED_TIME.match(string)
|
|
196
|
+
|
|
197
|
+
if match is None:
|
|
198
|
+
raise ValueError
|
|
199
|
+
|
|
200
|
+
year, month, day, hour, minute, second, fraction, offset = match.groups()
|
|
201
|
+
|
|
202
|
+
# A fraction is one of the hour, the minute or the second, as
|
|
203
|
+
# microseconds rounded down. The digits past the 20th are less than a
|
|
204
|
+
# microsecond, and are not looked at.
|
|
205
|
+
if minute is None:
|
|
206
|
+
unit = 3_600_000_000
|
|
207
|
+
elif second is None:
|
|
208
|
+
unit = 60_000_000
|
|
209
|
+
else:
|
|
210
|
+
unit = 1_000_000
|
|
211
|
+
|
|
212
|
+
if fraction is None:
|
|
213
|
+
microseconds = 0
|
|
214
|
+
else:
|
|
215
|
+
digits = fraction[:20]
|
|
216
|
+
microseconds = int(digits) * unit // 10 ** len(digits)
|
|
217
|
+
|
|
218
|
+
minutes = int(minute or 0) + microseconds // 60_000_000
|
|
219
|
+
microseconds %= 60_000_000
|
|
220
|
+
seconds = int(second or 0) + microseconds // 1_000_000
|
|
221
|
+
|
|
222
|
+
if offset is None:
|
|
223
|
+
tzinfo = None
|
|
224
|
+
elif offset == "Z":
|
|
225
|
+
tzinfo = timezone(timedelta(hours=0))
|
|
226
|
+
else:
|
|
227
|
+
hours = int(offset[1:3])
|
|
228
|
+
offset_minutes = int(offset[3:5] or 0)
|
|
229
|
+
|
|
230
|
+
if hours > 23 or offset_minutes > 59:
|
|
231
|
+
raise ValueError
|
|
232
|
+
|
|
233
|
+
sign = -1 if offset[0] == "-" else 1
|
|
234
|
+
tzinfo = timezone(timedelta(minutes=sign * (hours * 60 + offset_minutes)))
|
|
235
|
+
|
|
236
|
+
return datetime(
|
|
237
|
+
int(year),
|
|
238
|
+
int(month),
|
|
239
|
+
int(day),
|
|
240
|
+
int(hour),
|
|
241
|
+
minutes,
|
|
242
|
+
seconds,
|
|
243
|
+
microseconds % 1_000_000,
|
|
244
|
+
tzinfo=tzinfo,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def check_local_time(value: Any) -> None:
|
|
249
|
+
"""Check that `value`, a ``datetime.time`` or a ``datetime.datetime``,
|
|
250
|
+
is one of a TIME-OF-DAY or a DATE-TIME, which are a local time to the
|
|
251
|
+
second (X.680 38.4.2, 38.4.3).
|
|
252
|
+
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
if value.microsecond != 0 or value.tzinfo is not None:
|
|
256
|
+
raise EncodeError(
|
|
257
|
+
"A TIME-OF-DAY or a DATE-TIME has no fraction of a second and no offset."
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def format_or(items: List[Any]) -> str:
|
|
262
|
+
"""Return a string of comma separated items, with the last to items
|
|
263
|
+
separated by "or".
|
|
264
|
+
|
|
265
|
+
[1, 2, 3] -> "1, 2 or 3"
|
|
266
|
+
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
formatted_items = []
|
|
270
|
+
|
|
271
|
+
for item in items:
|
|
272
|
+
try:
|
|
273
|
+
item = "'" + item + "'"
|
|
274
|
+
except TypeError:
|
|
275
|
+
item = str(item)
|
|
276
|
+
|
|
277
|
+
formatted_items.append(item)
|
|
278
|
+
|
|
279
|
+
if len(items) == 1:
|
|
280
|
+
return formatted_items[0]
|
|
281
|
+
else:
|
|
282
|
+
return "{} or {}".format(", ".join(formatted_items[:-1]), formatted_items[-1])
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def utc_time_to_datetime(string: str) -> datetime:
|
|
286
|
+
"""Convert given ASN.1 UTC time string `string` to a
|
|
287
|
+
``datetime.datetime`` object.
|
|
288
|
+
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
length = len(string)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
if string[-1] == "Z":
|
|
295
|
+
if length == 11:
|
|
296
|
+
return datetime.strptime(string[:-1], "%y%m%d%H%M")
|
|
297
|
+
elif length == 13:
|
|
298
|
+
return datetime.strptime(string[:-1], "%y%m%d%H%M%S")
|
|
299
|
+
else:
|
|
300
|
+
raise ValueError
|
|
301
|
+
elif length == 15:
|
|
302
|
+
return datetime.strptime(string, "%y%m%d%H%M%z")
|
|
303
|
+
elif length == 17:
|
|
304
|
+
return datetime.strptime(string, "%y%m%d%H%M%S%z")
|
|
305
|
+
else:
|
|
306
|
+
raise ValueError
|
|
307
|
+
except (ValueError, IndexError):
|
|
308
|
+
raise Error("Expected a UTC time string, but got '{}'.".format(string))
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def utc_time_from_datetime(date: datetime) -> str:
|
|
312
|
+
"""Convert given ``datetime.datetime`` object `date` to an ASN.1 UTC
|
|
313
|
+
time string.
|
|
314
|
+
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
fmt = "%y%m%d%H%M"
|
|
318
|
+
|
|
319
|
+
if date.second > 0:
|
|
320
|
+
fmt += "%S"
|
|
321
|
+
|
|
322
|
+
if date.tzinfo is None:
|
|
323
|
+
fmt += "Z"
|
|
324
|
+
else:
|
|
325
|
+
fmt += "%z"
|
|
326
|
+
|
|
327
|
+
return date.strftime(fmt)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def restricted_utc_time_to_datetime(string: str) -> datetime:
|
|
331
|
+
"""Convert given restricted ASN.1 UTC time string `string` to a
|
|
332
|
+
``datetime.datetime`` object.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
if string[-1] != "Z":
|
|
338
|
+
raise ValueError
|
|
339
|
+
|
|
340
|
+
if len(string) != 13:
|
|
341
|
+
raise ValueError
|
|
342
|
+
|
|
343
|
+
return datetime.strptime(string[:-1], "%y%m%d%H%M%S")
|
|
344
|
+
except (ValueError, IndexError):
|
|
345
|
+
raise Error(
|
|
346
|
+
"Expected a restricted UTC time string, but got '{}'.".format(string)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def restricted_utc_time_from_datetime(date: datetime) -> str:
|
|
351
|
+
"""Convert given ``datetime.datetime`` object `date` to an restricted
|
|
352
|
+
ASN.1 UTC time string.
|
|
353
|
+
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
offset = date.utcoffset()
|
|
357
|
+
|
|
358
|
+
if offset is not None:
|
|
359
|
+
date -= offset
|
|
360
|
+
|
|
361
|
+
return date.strftime("%y%m%d%H%M%S") + "Z"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def generalized_time_to_datetime(string: str) -> datetime:
|
|
365
|
+
"""Convert given ASN.1 generalized time string `string` to a
|
|
366
|
+
``datetime.datetime`` object.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
return _parse_generalized_time(string)
|
|
372
|
+
except (ValueError, IndexError):
|
|
373
|
+
raise Error("Expected a generalized time string, but got '{}'.".format(string))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def generalized_time_from_datetime(date: datetime) -> str:
|
|
377
|
+
"""Convert given ``datetime.datetime`` object `date` to an ASN.1
|
|
378
|
+
generalized time string.
|
|
379
|
+
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
# A fraction after the minute would be one of a minute (X.680 46.3).
|
|
383
|
+
if date.microsecond > 0:
|
|
384
|
+
string = date.strftime("%Y%m%d%H%M%S.%f").rstrip("0")
|
|
385
|
+
elif date.second > 0:
|
|
386
|
+
string = date.strftime("%Y%m%d%H%M%S")
|
|
387
|
+
else:
|
|
388
|
+
string = date.strftime("%Y%m%d%H%M")
|
|
389
|
+
|
|
390
|
+
if date.tzinfo is not None:
|
|
391
|
+
if date.utcoffset():
|
|
392
|
+
string += date.strftime("%z")
|
|
393
|
+
else:
|
|
394
|
+
string += "Z"
|
|
395
|
+
|
|
396
|
+
return string
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def restricted_generalized_time_to_datetime(string: str) -> datetime:
|
|
400
|
+
"""Convert given restricted ASN.1 generalized time string `string` to
|
|
401
|
+
a ``datetime.datetime`` object.
|
|
402
|
+
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
if string[-1] != "Z":
|
|
407
|
+
raise ValueError
|
|
408
|
+
|
|
409
|
+
if "." in string:
|
|
410
|
+
if string[-2] == "0":
|
|
411
|
+
raise ValueError
|
|
412
|
+
|
|
413
|
+
if string[14] != ".":
|
|
414
|
+
raise ValueError
|
|
415
|
+
elif len(string) != 15:
|
|
416
|
+
raise ValueError
|
|
417
|
+
|
|
418
|
+
# Read as a time with no offset.
|
|
419
|
+
return _parse_generalized_time(string[:-1])
|
|
420
|
+
except (ValueError, IndexError):
|
|
421
|
+
raise Error(
|
|
422
|
+
"Expected a restricted generalized time string, but got '{}'.".format(
|
|
423
|
+
string
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def restricted_generalized_time_from_datetime(date: datetime) -> str:
|
|
429
|
+
"""Convert given ``datetime.datetime`` object `date` to an restricted
|
|
430
|
+
ASN.1 generalized time string.
|
|
431
|
+
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
offset = date.utcoffset()
|
|
435
|
+
|
|
436
|
+
if offset is not None:
|
|
437
|
+
date -= offset
|
|
438
|
+
|
|
439
|
+
if date.microsecond > 0:
|
|
440
|
+
string = date.strftime("%Y%m%d%H%M%S.%f").rstrip("0")
|
|
441
|
+
else:
|
|
442
|
+
string = date.strftime("%Y%m%d%H%M%S")
|
|
443
|
+
|
|
444
|
+
return string + "Z"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def bmp_string_encode(text: str) -> bytes:
|
|
448
|
+
"""A BMPString in the 2-octet form, of which each character is in the
|
|
449
|
+
BMP (X.690 8.23.8, X.696 27.4).
|
|
450
|
+
|
|
451
|
+
"""
|
|
452
|
+
|
|
453
|
+
for character in text:
|
|
454
|
+
if ord(character) > 0xFFFF:
|
|
455
|
+
raise EncodeError(
|
|
456
|
+
"Expected a character in the BMP, but got U+{:04X}.".format(
|
|
457
|
+
ord(character)
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return text.encode("utf-16-be")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def bmp_string_decode(data: bytes | bytearray) -> str:
|
|
465
|
+
"""The text of a BMPString in the 2-octet form. A surrogate is not a
|
|
466
|
+
character.
|
|
467
|
+
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
if len(data) % 2 != 0:
|
|
471
|
+
raise DecodeError("Expected a BMPString of an even number of octets.")
|
|
472
|
+
|
|
473
|
+
units = [
|
|
474
|
+
int.from_bytes(data[offset : offset + 2], "big")
|
|
475
|
+
for offset in range(0, len(data), 2)
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
for unit in units:
|
|
479
|
+
if 0xD800 <= unit <= 0xDFFF:
|
|
480
|
+
raise DecodeError(
|
|
481
|
+
"Expected a character in the BMP, but got the surrogate "
|
|
482
|
+
"0x{:04X}.".format(unit)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return "".join(map(chr, units))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def format_bytes(tag: bytes | bytearray) -> str:
|
|
489
|
+
"""
|
|
490
|
+
Format tag as hex string
|
|
491
|
+
:param bytes tag:
|
|
492
|
+
:return:
|
|
493
|
+
"""
|
|
494
|
+
return tag.hex()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# A codec is imported when first used, by name or as an attribute of
|
|
498
|
+
# this package.
|
|
499
|
+
_SUBMODULES = [
|
|
500
|
+
"ber",
|
|
501
|
+
"codec",
|
|
502
|
+
"compiler",
|
|
503
|
+
"constraints_checker",
|
|
504
|
+
"der",
|
|
505
|
+
"gser",
|
|
506
|
+
"jer",
|
|
507
|
+
"oer",
|
|
508
|
+
"per",
|
|
509
|
+
"permitted_alphabet",
|
|
510
|
+
"type_checker",
|
|
511
|
+
"uper",
|
|
512
|
+
"xer",
|
|
513
|
+
]
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def __getattr__(name: str) -> Any:
|
|
517
|
+
if name in _SUBMODULES:
|
|
518
|
+
return importlib.import_module("." + name, __name__)
|
|
519
|
+
|
|
520
|
+
raise AttributeError("module '{}' has no attribute '{}'".format(__name__, name))
|