]> Cypherpunks.ru repositories - pyderasn.git/blobdiff - pyderasn.py
Limitations section
[pyderasn.git] / pyderasn.py
index cc33073279913c78a25e69d2d5681dde0d995d10..172c1f6474705de88d59918be509c1658a79e0aa 100755 (executable)
@@ -463,6 +463,10 @@ NumericString
 _____________
 .. autoclass:: pyderasn.NumericString
 
+PrintableString
+_______________
+.. autoclass:: pyderasn.PrintableString
+
 UTCTime
 _______
 .. autoclass:: pyderasn.UTCTime
@@ -555,6 +559,8 @@ from six import indexbytes
 from six import int2byte
 from six import integer_types
 from six import iterbytes
+from six import iteritems
+from six import itervalues
 from six import PY2
 from six import string_types
 from six import text_type
@@ -1549,10 +1555,10 @@ class Boolean(Obj):
                 self._value = default
 
     def _value_sanitize(self, value):
-        if issubclass(value.__class__, Boolean):
-            return value._value
         if isinstance(value, bool):
             return value
+        if issubclass(value.__class__, Boolean):
+            return value._value
         raise InvalidValueType((self.__class__, bool))
 
     @property
@@ -1798,10 +1804,10 @@ class Integer(Obj):
                 self._value = default
 
     def _value_sanitize(self, value):
-        if issubclass(value.__class__, Integer):
-            value = value._value
-        elif isinstance(value, integer_types):
+        if isinstance(value, integer_types):
             pass
+        elif issubclass(value.__class__, Integer):
+            value = value._value
         elif isinstance(value, str):
             value = self.specs.get(value)
             if value is None:
@@ -1861,7 +1867,7 @@ class Integer(Obj):
 
     @property
     def named(self):
-        for name, value in self.specs.items():
+        for name, value in iteritems(self.specs):
             if value == self._value:
                 return name
 
@@ -2045,6 +2051,9 @@ class Integer(Obj):
             yield pp
 
 
+SET01 = frozenset(("0", "1"))
+
+
 class BitString(Obj):
     """``BIT STRING`` bit string type
 
@@ -2150,8 +2159,6 @@ class BitString(Obj):
         return bit_len, bytes(octets)
 
     def _value_sanitize(self, value):
-        if issubclass(value.__class__, BitString):
-            return value._value
         if isinstance(value, (string_types, binary_type)):
             if (
                     isinstance(value, string_types) and
@@ -2159,7 +2166,7 @@ class BitString(Obj):
             ):
                 if value.endswith("'B"):
                     value = value[1:-2]
-                    if not set(value) <= set(("0", "1")):
+                    if not frozenset(value) <= SET01:
                         raise ValueError("B's coding contains unacceptable chars")
                     return self._bits2octets(value)
                 elif value.endswith("'H"):
@@ -2187,11 +2194,13 @@ class BitString(Obj):
                 bits.append(bit)
             if len(bits) == 0:
                 return self._bits2octets("")
-            bits = set(bits)
+            bits = frozenset(bits)
             return self._bits2octets("".join(
                 ("1" if bit in bits else "0")
                 for bit in six_xrange(max(bits) + 1)
             ))
+        if issubclass(value.__class__, BitString):
+            return value._value
         raise InvalidValueType((self.__class__, binary_type, string_types))
 
     @property
@@ -2243,7 +2252,7 @@ class BitString(Obj):
 
     @property
     def named(self):
-        return [name for name, bit in self.specs.items() if self[bit]]
+        return [name for name, bit in iteritems(self.specs) if self[bit]]
 
     def __call__(
             self,
@@ -2600,10 +2609,10 @@ class OctetString(Obj):
         )
 
     def _value_sanitize(self, value):
-        if issubclass(value.__class__, OctetString):
-            value = value._value
-        elif isinstance(value, binary_type):
+        if isinstance(value, binary_type):
             pass
+        elif issubclass(value.__class__, OctetString):
+            value = value._value
         else:
             raise InvalidValueType((self.__class__, bytes))
         if not self._bound_min <= len(value) <= self._bound_max:
@@ -3355,7 +3364,7 @@ class Enumerated(Integer):
         if isinstance(value, self.__class__):
             value = value._value
         elif isinstance(value, integer_types):
-            for _value in self.specs.values():
+            for _value in itervalues(self.specs):
                 if _value == value:
                     break
             else:
@@ -3564,7 +3573,7 @@ class AllowableCharsMixin(object):
     def allowable_chars(self):
         if PY2:
             return self._allowable_chars
-        return set(six_unichr(c) for c in self._allowable_chars)
+        return frozenset(six_unichr(c) for c in self._allowable_chars)
 
 
 class NumericString(AllowableCharsMixin, CommonString):
@@ -3574,17 +3583,17 @@ class NumericString(AllowableCharsMixin, CommonString):
     be stored.
 
     >>> NumericString().allowable_chars
-    set(['3', '4', '7', '5', '1', '0', '8', '9', ' ', '6', '2'])
+    frozenset(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' '])
     """
     __slots__ = ()
     tag_default = tag_encode(18)
     encoding = "ascii"
     asn1_type_name = "NumericString"
-    _allowable_chars = set(digits.encode("ascii") + b" ")
+    _allowable_chars = frozenset(digits.encode("ascii") + b" ")
 
     def _value_sanitize(self, value):
         value = super(NumericString, self)._value_sanitize(value)
-        if not set(value) <= self._allowable_chars:
+        if not frozenset(value) <= self._allowable_chars:
             raise DecodeError("non-numeric value")
         return value
 
@@ -3595,19 +3604,19 @@ class PrintableString(AllowableCharsMixin, CommonString):
     Its value is properly sanitized: see X.680 41.4 table 10.
 
     >>> PrintableString().allowable_chars
-    >>> set([' ', "'", ..., 'z'])
+    frozenset([' ', "'", ..., 'z'])
     """
     __slots__ = ()
     tag_default = tag_encode(19)
     encoding = "ascii"
     asn1_type_name = "PrintableString"
-    _allowable_chars = set(
+    _allowable_chars = frozenset(
         (ascii_letters + digits + " '()+,-./:=?").encode("ascii")
     )
 
     def _value_sanitize(self, value):
         value = super(PrintableString, self)._value_sanitize(value)
-        if not set(value) <= self._allowable_chars:
+        if not frozenset(value) <= self._allowable_chars:
             raise DecodeError("non-printable value")
         return value
 
@@ -3656,14 +3665,16 @@ class UTCTime(CommonString):
     datetime.datetime(2017, 9, 30, 22, 7, 50)
     >>> UTCTime(datetime(2057, 9, 30, 22, 7, 50)).todatetime()
     datetime.datetime(1957, 9, 30, 22, 7, 50)
+
+    .. warning::
+
+       BER encoding is unsupported.
     """
     __slots__ = ()
     tag_default = tag_encode(23)
     encoding = "ascii"
     asn1_type_name = "UTCTime"
 
-    fmt = "%y%m%d%H%M%SZ"
-
     def __init__(
             self,
             value=None,
@@ -3702,24 +3713,36 @@ class UTCTime(CommonString):
             if self._value is None:
                 self._value = default
 
+    def _strptime(self, value):
+        # datetime.strptime's format: %y%m%d%H%M%SZ
+        if len(value) != LEN_YYMMDDHHMMSSZ:
+            raise ValueError("invalid UTCTime length")
+        if value[-1] != "Z":
+            raise ValueError("non UTC timezone")
+        return datetime(
+            2000 + int(value[:2]),  # %y
+            int(value[2:4]),  # %m
+            int(value[4:6]),  # %d
+            int(value[6:8]),  # %H
+            int(value[8:10]),  # %M
+            int(value[10:12]),  # %S
+        )
+
     def _value_sanitize(self, value):
-        if isinstance(value, self.__class__):
-            return value._value
-        if isinstance(value, datetime):
-            return value.strftime(self.fmt).encode("ascii")
         if isinstance(value, binary_type):
             try:
                 value_decoded = value.decode("ascii")
             except (UnicodeEncodeError, UnicodeDecodeError) as err:
                 raise DecodeError("invalid UTCTime encoding")
-            if len(value_decoded) == LEN_YYMMDDHHMMSSZ:
-                try:
-                    datetime.strptime(value_decoded, self.fmt)
-                except (TypeError, ValueError):
-                    raise DecodeError("invalid UTCTime format")
-                return value
-            else:
-                raise DecodeError("invalid UTCTime length")
+            try:
+                self._strptime(value_decoded)
+            except (TypeError, ValueError) as err:
+                raise DecodeError("invalid UTCTime format: %r" % err)
+            return value
+        if isinstance(value, self.__class__):
+            return value._value
+        if isinstance(value, datetime):
+            return value.strftime("%y%m%d%H%M%SZ").encode("ascii")
         raise InvalidValueType((self.__class__, datetime))
 
     def __eq__(self, their):
@@ -3744,7 +3767,7 @@ class UTCTime(CommonString):
         having < 50 years are treated as 20xx, 19xx otherwise, according
         to X.509 recomendation.
         """
-        value = datetime.strptime(self._value.decode("ascii"), self.fmt)
+        value = self._strptime(self._value.decode("ascii"))
         year = value.year % 100
         return datetime(
             year=(2000 + year) if year < 50 else (1900 + year),
@@ -3796,54 +3819,85 @@ class GeneralizedTime(UTCTime):
     '20170930220750.000123Z'
     >>> t = GeneralizedTime(datetime(2057, 9, 30, 22, 7, 50))
     GeneralizedTime GeneralizedTime 2057-09-30T22:07:50
+
+    .. warning::
+
+       BER encoding is unsupported.
+
+    .. warning::
+
+       Only microsecond fractions are supported.
+       :py:exc:`pyderasn.DecodeError` will be raised during decoding of
+       higher precision values.
     """
     __slots__ = ()
     tag_default = tag_encode(24)
     asn1_type_name = "GeneralizedTime"
 
-    fmt = "%Y%m%d%H%M%SZ"
-    fmt_ms = "%Y%m%d%H%M%S.%fZ"
+    def _strptime(self, value):
+        l = len(value)
+        if l == LEN_YYYYMMDDHHMMSSZ:
+            # datetime.strptime's format: %y%m%d%H%M%SZ
+            if value[-1] != "Z":
+                raise ValueError("non UTC timezone")
+            return datetime(
+                int(value[:4]),  # %Y
+                int(value[4:6]),  # %m
+                int(value[6:8]),  # %d
+                int(value[8:10]),  # %H
+                int(value[10:12]),  # %M
+                int(value[12:14]),  # %S
+            )
+        if l >= LEN_YYYYMMDDHHMMSSDMZ:
+            # datetime.strptime's format: %Y%m%d%H%M%S.%fZ
+            if value[-1] != "Z":
+                raise ValueError("non UTC timezone")
+            if value[14] != ".":
+                raise ValueError("no fractions separator")
+            us = value[15:-1]
+            if us[-1] == "0":
+                raise ValueError("trailing zero")
+            us_len = len(us)
+            if us_len > 6:
+                raise ValueError("only microsecond fractions are supported")
+            us = int(us + ("0" * (6 - us_len)))
+            decoded = datetime(
+                int(value[:4]),  # %Y
+                int(value[4:6]),  # %m
+                int(value[6:8]),  # %d
+                int(value[8:10]),  # %H
+                int(value[10:12]),  # %M
+                int(value[12:14]),  # %S
+                us,  # %f
+            )
+            return decoded
+        raise ValueError("invalid GeneralizedTime length")
 
     def _value_sanitize(self, value):
-        if isinstance(value, self.__class__):
-            return value._value
-        if isinstance(value, datetime):
-            return value.strftime(
-                self.fmt_ms if value.microsecond > 0 else self.fmt
-            ).encode("ascii")
         if isinstance(value, binary_type):
             try:
                 value_decoded = value.decode("ascii")
             except (UnicodeEncodeError, UnicodeDecodeError) as err:
                 raise DecodeError("invalid GeneralizedTime encoding")
-            if len(value_decoded) == LEN_YYYYMMDDHHMMSSZ:
-                try:
-                    datetime.strptime(value_decoded, self.fmt)
-                except (TypeError, ValueError):
-                    raise DecodeError(
-                        "invalid GeneralizedTime (without ms) format",
-                    )
-                return value
-            elif len(value_decoded) >= LEN_YYYYMMDDHHMMSSDMZ:
-                try:
-                    datetime.strptime(value_decoded, self.fmt_ms)
-                except (TypeError, ValueError):
-                    raise DecodeError(
-                        "invalid GeneralizedTime (with ms) format",
-                    )
-                return value
-            else:
+            try:
+                self._strptime(value_decoded)
+            except (TypeError, ValueError) as err:
                 raise DecodeError(
-                    "invalid GeneralizedTime length",
+                    "invalid GeneralizedTime format: %r" % err,
                     klass=self.__class__,
                 )
+            return value
+        if isinstance(value, self.__class__):
+            return value._value
+        if isinstance(value, datetime):
+            encoded = value.strftime("%Y%m%d%H%M%S")
+            if value.microsecond > 0:
+                encoded = encoded + (".%06d" % value.microsecond).rstrip("0")
+            return (encoded + "Z").encode("ascii")
         raise InvalidValueType((self.__class__, datetime))
 
     def todatetime(self):
-        value = self._value.decode("ascii")
-        if len(value) == LEN_YYYYMMDDHHMMSSZ:
-            return datetime.strptime(value, self.fmt)
-        return datetime.strptime(value, self.fmt_ms)
+        return self._strptime(self._value.decode("ascii"))
 
 
 class GraphicString(CommonString):
@@ -3962,8 +4016,6 @@ class Choice(Obj):
                 self._value = default_obj.copy()._value
 
     def _value_sanitize(self, value):
-        if isinstance(value, self.__class__):
-            return value._value
         if isinstance(value, tuple) and len(value) == 2:
             choice, obj = value
             spec = self.specs.get(choice)
@@ -3972,6 +4024,8 @@ class Choice(Obj):
             if not isinstance(obj, spec.__class__):
                 raise InvalidValueType((spec,))
             return (choice, spec(obj))
+        if isinstance(value, self.__class__):
+            return value._value
         raise InvalidValueType((self.__class__, tuple))
 
     @property
@@ -4067,7 +4121,7 @@ class Choice(Obj):
         return self._value[1].encode()
 
     def _decode(self, tlv, offset, decode_path, ctx, tag_only):
-        for choice, spec in self.specs.items():
+        for choice, spec in iteritems(self.specs):
             sub_decode_path = decode_path + (choice,)
             try:
                 spec.decode(
@@ -4207,12 +4261,12 @@ class Any(Obj):
         self.defined = None
 
     def _value_sanitize(self, value):
+        if isinstance(value, binary_type):
+            return value
         if isinstance(value, self.__class__):
             return value._value
         if isinstance(value, Obj):
             return value.encode()
-        if isinstance(value, binary_type):
-            return value
         raise InvalidValueType((self.__class__, Obj, binary_type))
 
     @property
@@ -4552,7 +4606,7 @@ class Sequence(Obj):
 
     @property
     def ready(self):
-        for name, spec in self.specs.items():
+        for name, spec in iteritems(self.specs):
             value = self._value.get(name)
             if value is None:
                 if spec.optional:
@@ -4567,7 +4621,7 @@ class Sequence(Obj):
     def bered(self):
         if self.expl_lenindef or self.lenindef or self.ber_encoded:
             return True
-        return any(value.bered for value in self._value.values())
+        return any(value.bered for value in itervalues(self._value))
 
     def copy(self):
         obj = self.__class__(schema=self.specs)
@@ -4581,7 +4635,7 @@ class Sequence(Obj):
         obj.expl_lenindef = self.expl_lenindef
         obj.lenindef = self.lenindef
         obj.ber_encoded = self.ber_encoded
-        obj._value = {k: v.copy() for k, v in self._value.items()}
+        obj._value = {k: v.copy() for k, v in iteritems(self._value)}
         return obj
 
     def __eq__(self, their):
@@ -4642,7 +4696,7 @@ class Sequence(Obj):
 
     def _encoded_values(self):
         raws = []
-        for name, spec in self.specs.items():
+        for name, spec in iteritems(self.specs):
             value = self._value.get(name)
             if value is None:
                 if spec.optional:
@@ -4708,7 +4762,7 @@ class Sequence(Obj):
         values = {}
         ber_encoded = False
         ctx_allow_default_values = ctx.get("allow_default_values", False)
-        for name, spec in self.specs.items():
+        for name, spec in iteritems(self.specs):
             if spec.optional and (
                     (lenindef and v[:EOC_LEN].tobytes() == EOC) or
                     len(v) == 0
@@ -4902,6 +4956,9 @@ class Set(Sequence):
         v = b"".join(raws)
         return b"".join((self.tag, len_encode(len(v)), v))
 
+    def _specs_items(self):
+        return iteritems(self.specs)
+
     def _decode(self, tlv, offset, decode_path, ctx, tag_only):
         try:
             t, tlen, lv = tag_strip(tlv)
@@ -4956,11 +5013,11 @@ class Set(Sequence):
         ctx_allow_default_values = ctx.get("allow_default_values", False)
         ctx_allow_unordered_set = ctx.get("allow_unordered_set", False)
         value_prev = memoryview(v[:0])
-        specs_items = self.specs.items
+
         while len(v) > 0:
             if lenindef and v[:EOC_LEN].tobytes() == EOC:
                 break
-            for name, spec in specs_items():
+            for name, spec in self._specs_items():
                 sub_decode_path = decode_path + (name,)
                 try:
                     spec.decode(