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 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))